第十篇:SpringAI 实战 10|全模型流式输出(Streaming)实战:实现打字机效果
导读:在上一章中,我们成功构建了多模型共存的底层架构。但在实际体验中,如果调用大模型生成一篇长文,传统的同步请求需要等待几十秒模型完全生成完毕后,才能一次性返回结果。这种“干等”的体验在 AI 应用中是灾难性的。
真正的 AI 应用(如 ChatGPT)都是“边思考边输出”,即流式响应(Streaming Response)。本章我们将基于上一章的多模型架构,引入 Spring WebFlux 的响应式编程,利用 SSE(Server-Sent Events)协议,用极少的代码为 OpenAI、通义千问、DeepSeek 和 Ollama 实现丝滑的“打字机效果”。
一、环境前置说明
运行前提:电脑安装 Ollama客户端,提前拉取开源模型文件
- JDK:21
- Gradle:8.8
- SpringBoot:3.5.14
- SpringAI:1.1.7
- IDEA:2023 社区版
(本章代码是在上一篇的基础上新增/修改的)
二、 核心原理:SSE 与 Flux 数据流
要实现流式输出,我们需要理解两个核心技术点:
- SSE(Server-Sent Events)协议:这是一种基于 HTTP 的单向通信协议。服务端可以主动向客户端推送数据,非常适合大模型这种“服务端持续生成,客户端持续渲染”的场景。
- Reactor 的 Flux 类型:Spring WebFlux 提供了 Flux 响应式流类型。Spring AI 的 ChatClient 原生支持响应式编程,只需将同步的 .call() 替换为 .stream(),底层就会自动将大模型生成的增量 Token 封装为 SSE 数据流推送给前端。
三、 后端改造:一行代码开启流式输出
得益于 Spring AI 的高度抽象,我们无需修改上一章的 MultiModelConfig 配置类,只需在 Controller 层新增流式接口即可。
- 引入 WebFlux 依赖
确保你的 build.gradle 中包含 WebFlux 依赖(Spring AI 的流式响应依赖它):
implementation'org.springframework.boot:spring-boot-starter-webflux'- 新增流式 Controller
我们在上一章的 MultiModelController 中新增一个流式接口。注意 produces 必须设置为 text/event-stream:
/** * 全模型流式输出接口 */@GetMapping(value="/stream/{provider}",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<String>streamChat(@PathVariableStringprovider,@RequestParam(defaultValue="你好,请介绍一下你自己")Stringmsg){ChatClientchatclient=getClientByProvider(provider);// 核心:使用 .stream() 替代 .call(),并调用 .content() 仅返回文本内容returnchatclient.prompt().user(msg).stream().content();}/** * 根据路径参数获取对应的 Client */privateChatClientgetClientByProvider(Stringprovider){returnswitch(provider.toLowerCase()){case"openai"->openaiClient;case"ollama"->ollamaClient;case"qwen"->qwenClient;case"deepseek"->deepseekClient;default->thrownewIllegalArgumentException("Unsupported provider: "+provider);};}代码解析:Spring WebFlux 检测到返回值是 Flux 且 produces = text/event-stream 时,会自动启用 ServerSentEventHttpMessageWriter。每当大模型生成一个词,Spring 就会自动将其包装成 data: 词语\n\n 的 SSE 格式推送到前端
四、 前端实战:极简 HTML 实现打字机
在 resources/static 目录下新建 stream-test.html文件,代码如下:
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>Spring AI 流式输出测试</title><style>#output{border:1px solid #ccc;padding:15px;min-height:150px;white-space:pre-wrap;font-family:monospace;}button{padding:8px 16px;margin:5px;cursor:pointer;}</style></head><body><h2>多模型流式对话测试</h2><inputtype="text"id="msgInput"value="用五句话聊一聊苏轼"style="width:300px;"><buttononclick="startStream('openai')">OpenAI</button><buttononclick="startStream('qwen')">通义千问</button><buttononclick="startStream('deepseek')">DeepSeek</button><buttononclick="startStream('ollama')">Ollama</button><divid="output">等待输入...</div><script>letcurrentEventSource=null;functionstartStream(provider){constmsg=document.getElementById('msgInput').value;constoutputDiv=document.getElementById('output');// 1. 关闭上一次的连接,防止流冲突if(currentEventSource)currentEventSource.close();outputDiv.innerHTML='';// 2. 建立 SSE 连接consturl=`/ai/stream/${provider}?msg=${encodeURIComponent(msg)}`;currentEventSource=newEventSource(url);// 3. 监听消息,实现打字机追加效果currentEventSource.onmessage=(event)=>{outputDiv.innerHTML+=event.data;// 自动滚动到底部outputDiv.scrollTop=outputDiv.scrollHeight;};// 4. 监听完成或错误currentEventSource.onerror=()=>{currentEventSource.close();};}</script></body></html>五、 运行与验证
- 启动 Spring Boot 应用。
- 使用浏览器访问 http://localhost:8080/stream-test.html
- 点击不同的模型按钮,你会看到文字像真人打字一样逐字出现在屏幕上。
六、 本章总结
通过本章的实战,我们仅用 .stream().content() 这一行核心代码,就打通了从大模型到前端的流式数据链路。
对后端而言:响应式编程避免了长文本生成时的线程阻塞,单台服务器即可支撑成千上万个并发流式连接。
对前端而言:浏览器原生的 EventSource API 完美契合 SSE 协议,无需引入任何第三方 WebSocket 库。
至此,我们的 AI 应用已经具备了“多模型路由”与“丝滑流式输出”两大核心能力。
六、 参考文献
- SpringAI官方文档
