当前位置: 首页 > news >正文

Spring AI实现对话聊天-流式输出

目录

1.版本选择

2.完整代码实现

3.效果


1.版本选择

当前Spring AI 最新正式版本为1.1.2,我们使用这个版本,对应的springboot版本Spring Boot >= 3.5.0 and < 4.0.0

2.完整代码实现

这里我们使用ollama部署的本地模型,ollama部署可以参考之前的文章:(二)1.1 ollama本地快速部署deepseek

后端:

pom.xml

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.9</version> </parent> <groupId>com.haylee</groupId> <artifactId>spring-ai-agent</artifactId> <version>1.0-SNAPSHOT</version> <name>spring-ai-agent</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.release>17</maven.compiler.release> <spring-ai-version>1.1.2</spring-ai-version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai-version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> </dependencies> <build> </build> </project>

application.yml

spring: thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html encoding: UTF-8 ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen3:4b temperature: 0.6 # 值越小,会降低随机性,保证一致性 init: # 不自动下载模型 pull-model-strategy: never

IndexController:

package com.haylee.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class IndexController { @GetMapping("/") public String streamIndexPage() { return "stream-index"; // 返回模板名称 } }

OllamaChatController:

package com.haylee.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController @RequestMapping("/ollama") public class OllamaChatController { private Logger logger = LoggerFactory.getLogger(OllamaChatController.class); @Autowired private OllamaChatModel ollamaChatModel; /** * 模型 * @param prompt * @return */ @GetMapping("/call") public String call(@RequestParam("prompt") String prompt) { Prompt pt = new Prompt(prompt, OllamaChatOptions.builder() .enableThinking() .build()); ChatResponse response = ollamaChatModel.call(pt); String thinking = response.getResult().getMetadata().get("thinking"); logger.info("[Thinking] " + thinking); String answer = response.getResult().getOutput().getText(); logger.info("[Response] " + answer); return answer; } /** * 模型stream+springboot reactive stream * @param prompt * @return */ @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> stream(@RequestParam("prompt") String prompt) { Prompt pt = new Prompt(prompt, OllamaChatOptions.builder() .enableThinking() .build()); Flux<ChatResponse> result = ollamaChatModel.stream(pt); // result.subscribe(response -> { // String thinking = response.getResult().getMetadata().get("thinking"); // String content = response.getResult().getOutput().getText(); // if (thinking != null && !thinking.isEmpty()) { // System.out.println("[Thinking] " + thinking); // } // if (content != null && !content.isEmpty()) { // System.out.println("[Response] " + content); // } // }); return result.map(response -> response.getResult().getOutput().getText() ). concatWith(Flux.just("[DONE]")). doOnComplete(() -> logger.info("Stream completed")); } }

前端:

resources/templates/stream-index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Spring AI 流式输出</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .input-section { margin-bottom: 20px; } #prompt-input { width: 70%; padding: 10px; font-size: 16px; } button { padding: 10px 20px; font-size: 16px; margin-left: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:hover { background-color: #0056b3; } button:disabled { background-color: #6c757d; cursor: not-allowed; } #response-container { border: 1px solid #ddd; padding: 15px; min-height: 200px; max-height: 400px; overflow-y: auto; background-color: #f9f9f9; white-space: pre-wrap; font-family: monospace; line-height: 1.5; } .thinking { color: #666; font-style: italic; } .output { color: #000; } .status { margin-top: 10px; padding: 5px; color: #28a745; } </style> </head> <body> <h1>Spring AI 流式输出</h1> <div class="input-section"> <input type="text" id="prompt-input" placeholder="请输入您的问题..." /> <button id="send-btn">发送</button> <button id="clear-btn">清空</button> </div> <div id="response-container">等待输入...</div> <div id="status" class="status"></div> <script> document.addEventListener('DOMContentLoaded', function() { const promptInput = document.getElementById('prompt-input'); const sendBtn = document.getElementById('send-btn'); const clearBtn = document.getElementById('clear-btn'); const responseContainer = document.getElementById('response-container'); const statusDiv = document.getElementById('status'); sendBtn.addEventListener('click', function() { const prompt = promptInput.value.trim(); if (prompt) { responseContainer.innerHTML = ''; // 创建新的 EventSource const eventSource = new EventSource('/ollama/stream?prompt=' + encodeURIComponent(prompt)); sendBtn.disabled = true; sendBtn.textContent = '响应中...'; eventSource.onmessage = function(event) { if (event.data === '[DONE]') { eventSource.close(); // 关闭连接 sendBtn.disabled = false; sendBtn.textContent = '发送'; return; } responseContainer.textContent += event.data; responseContainer.scrollTop = responseContainer.scrollHeight; }; // 监听错误事件并关闭连接 eventSource.onerror = function(err) { console.error('SSE Error:', err); eventSource.close(); // 关闭连接 sendBtn.disabled = false; sendBtn.textContent = '发送'; }; // 监听完成事件(需要服务器发送完成信号) eventSource.addEventListener('complete', function() { eventSource.close(); // 手动关闭连接 sendBtn.disabled = false; sendBtn.textContent = '发送'; }); } else { alert('请输入提示内容'); } }); // 支持回车键发送 promptInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { sendBtn.click(); } }); // 清空按钮 clearBtn.addEventListener('click', function() { responseContainer.textContent = '等待输入...'; promptInput.value = ''; statusDiv.textContent = ''; }); }); </script> </body> </html>

3.效果

*****************想要spring AI 完整代码(12306mcp、自定义mcp、rag等)关注顶部公众号,发送 sai免费领取*****************

这里使用MCP服务工具:参考AI大模型:(三)3.2 Spring AI实现Agent

大模型相关课程:

11.大模型的发展与局限性
21.1 ollama本地快速部署deepseek
31.2 linux本地部署deepseek千问蒸馏版+web对话聊天
41.3 linux本地部署通义万相2.1+deepseek视频生成
51.4 Qwen2.5-Omni全模态大模型部署
61.5 Stable Diffusion中文文生图模型部署
71.6 DeepSeek-OCR部署尝鲜
82.1 从零训练自己的大模型概述
92.2 分词器
102.3 预训练自己的模型
112.4 微调自己的模型
122.5 人类对齐训练自己的模型
133.1 微调训练详解
143.2 Llama-Factory微调训练deepseek-r1实践
153.3 transform+LoRA代码微调deepseek实践
164.1 文生图(Text-to-Image)模型发展史
174.2 文生图GUI训练实践-真人写实生成
184.3 文生图代码训练实践-真人写实生成
195.1 文生视频(Text-to-Video)模型发展史
205.2 文生视频(Text-to-Video)模型训练实践
216.1 目标检测模型的发展史
226.2 YOLO模型训练实践及目标跟踪
231.1 Dify介绍
241.2 Dify安装
251.3 Dify文本生成快速搭建旅游助手
261.4 Dify聊天助手快速搭建智能淘宝店小二
271.5 Dify agent快速搭建爬虫助手
281.6 Dify工作流快速搭建数据可视化助手
291.7 Dify chatflow快速搭建数据查询智能助手
302.1 RAG介绍
312.2 Spring AI-手动实现RAG
322.3 Spring AI-开箱即用完整实践RAG
332.4 LlamaIndex实现RAG
342.5 LlamaIndex构建RAG优化与实践
352.6 LangChain实现RAG企业知识问答助手
362.7 LangChain构建RAG企业知识问答助手实践
373.1 agent核心功能与概念
383.2 Spring AI实现Agent
http://www.gsyq.cn/news/1647528.html

相关文章:

  • go-stock终极指南:三步解锁AI股票分析的核心能力
  • 重塑音乐体验:foobox-cn带你解锁foobar2000的视觉革命
  • 从三维网格到方块世界:ObjToSchematic如何实现3D模型到Minecraft的智能转换
  • 三大解码引擎:QRazyBox如何重构破损二维码的数据世界
  • Beagle内存取证实战指南:企业级安全事件响应与可视化分析方案
  • HEIF Utility:解决Windows用户iPhone照片查看难题的终极方案
  • 《深入理解 RFC 793:传输控制协议(TCP)》
  • Flare-Game终极指南:如何快速上手这款免费2D动作RPG游戏
  • 3步破解模型部署难题:BitNet转换工具实战指南
  • Oracle TNS监听器远程投毒漏洞CVE-2012-1675复现与防御指南
  • 【Atlas】 Atlas 启动时常见的端口有哪些(如 21000)?
  • 【Claude Code】入门
  • HOScrcpy:鸿蒙开发者必备的远程真机投屏终极解决方案
  • Anki记忆革命:7天掌握终身受益的学习系统
  • 终极3个漫画阅读管理方案:为Android用户打造的纯净开源阅读体验
  • Java基础及入门
  • 内网渗透核心技术解析:从信息收集到横向移动的实战攻防
  • LunaTV终极容器化部署指南:5分钟构建个人影视中心
  • Halcon 标定板像素当量实战:单图计算XY方向精度,误差控制在0.01mm内
  • 使用Metasploit生成Windows后门:从原理到实战控制
  • OpenCV 4.8 与 PIL.Image 图像转换:BGR/RGB 通道陷阱与 3 种正确转换方法
  • 实战p5.js:3步构建惊艳的可视化项目与创意编程应用
  • 如何为洛雪音乐配置完美音源:新手3分钟快速上手指南
  • 十七.读写区块索引(上)
  • 2026年暑期 AI 简历工具推荐:毕业生求职必备
  • 别被坑了!2026实测靠谱的AI写作辅助平台|省心版
  • linux的dd命令详解
  • 如何为QQNT桌面客户端部署LiteLoader插件加载器
  • 五大神经网络模型原理与实战:CNN、RNN、GAN、Transformer、GNN选型指南
  • 高性能异步网络架构与多线程下载管理:EhViewer技术实现深度解析