Spring AI Alibaba 流式对话实现

发布时间:2026-08-25 21:54  浏览量:2

上一篇文章里,聊天 Agent 用的是 call,做的是同步对话——模型一次性把完整回复吐出来,用户只能干等着。但你回想一下用 ChatGPT 或者通义千问网页版的感受:回复是一个字一个字往外蹦的,像打字机一样。

这个"打字机效果"就是流式输出。对聊天场景来说,它不只是好看,而是实实在在的体验提升。Spring AI Alibaba 里实现流式,核心改动其实只有一个词:

把 call 换成 stream

这篇文章就围绕这个词,把流式对话从头到尾跑通,包括怎么推给前端。

做这件事的理由很直接:Gartner 预测到 2028 年,33% 的企业软件将内嵌 Agentic AI 能力,而对话类 Agent 几乎都会用到流式输出。对用户来说,一个"几秒没反应"的 Agent 和一个"立刻开始打字"的 Agent,体验差距是决定性的——流式不是锦上添花,而是聊天类产品的及格线。

先看同步和流式的本质区别。

维度同步 call流式 stream返回类型StringFlux首字延迟高(等全部生成完)低(生成一个字就返回一个字)用户感受干等几秒打字机式逐字出现适用场景短问答、批处理聊天、长文本、Agent 对话

关键在于:大模型本来就是逐 token 生成内容的。

同步调用只是"等它全部生成完再一次性给你",而流式调用是"每生成一点就推给你一点"。流式没有改变模型的生成方式,只是把生成过程实时暴露了出来。

对用户来说,首字延迟从"几秒"降到"几百毫秒",心理感受天差地别。这也是为什么几乎所有对话产品都做成了流式。

首字延迟为什么这么关键?人对"系统有没有在响应"的感知,取决于第一个反馈出现的时间。同步模式下,哪怕整个回复只需要 3 秒,这 3 秒的空白也会让人怀疑"是不是卡了";流式模式下,几百毫秒内第一段文字就出现,用户立刻知道"它在干活了"。同样的内容,体验能差出一个量级。

改动小到可以忽略。上一篇文章的同步代码:

String reply = chatClient.prompt .user(message) .call .content;

换成流式,就是:

Fluxreply = chatClient.prompt .user(message) .stream .content;返回类型从 String 变成了 Flux。

Flux 是 Reactor 里的响应式流,代表"一串陆续到来的数据"

,每个元素就是模型生成的一个片段。拿到这个 Flux,就拿到了整条"流"。

Flux 这个概念值得多说一句:它不是一个"已经算好的结果",而是一个"结果的管道"。订阅(subscribe)它,数据就一段一段从管道里流过来。这也意味着流式调用是异步的——stream 返回的瞬间,模型还没生成完,后续内容会在订阅的回调里陆续到来。

到这里,流式调用本身已经完成了。难的不是这一步,而是下一步:怎么把这串 Flux 推给浏览器。

SSE 值得单独说一下它的定位。HTTP 普通的请求-响应是一次性的,浏览器问一句、服务器答一句就结束;SSE 则是服务器"持续答",一条连接上可以源源不断地发数据,直到服务器主动关闭。它天然适合大模型这种"边生成边输出"的场景,所以成了流式对话的标准推送方式。

浏览器能接收服务端"持续推送"的协议,叫

SSE(Server-Sent Events)

,本质是一条只从服务端到客户端的长连接。Spring 生态里实现 SSE,有两条路。

路线一:SseEmitter(Spring MVC)

,手动把每个片段发出去:

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public SseEmitter stream(@RequestParam String message) { SseEmitter emitter = new SseEmitter; chatClient.prompt .user(message) .stream .content .subscribe( chunk -> { try { emitter.send(chunk); } catch (IOException e) { emitter.completeWithError(e); } }, emitter::completeWithError, emitter::complete ); return emitter;}@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Fluxstream(@RequestParam String message) { return chatClient.prompt .user(message) .stream .content;}

两条路的区别在技术栈:传统 Spring MVC 用 SseEmitter 手动订阅,WebFlux 则让 Flux 一路透传到浏览器。

如果你的项目是传统的 MVC,用路线一;如果已经上了响应式栈,路线二几乎不用写什么。

前端消费 SSE,用的是浏览器原生的 EventSource:

const es = new EventSource('/chat/stream?message=你好');es.onmessage = (event) => { document.getElementById('reply').innerText += event.data;};

每收到一个片段,就往页面上追加一段,打字机效果就出来了。

前面是"流式聊天",那"Agent 流式对话"呢?差别在于要不要工具调用。好消息是:

两者完全兼容,流式不影响工具调用循环。

return chatClient.prompt .user(message) .tools(new WeatherTools) .stream .content;

这段代码的行为是这样的:Agent 先在内部走完工具调用循环(比如判断要查天气、调用工具、拿到结果),

这个"思考+动手"的阶段是拿不到中间输出的

,等它开始组织最终回答时,回复才会一个字一个字地流出来。

所以 Agent 的流式,流的是"最终回答",而不是"思考过程"。这一点和市面上那些会把"思考过程"也展示出来的产品不同,Spring AI 默认只流最终答案。

还有一个细节:工具调用阶段的耗时,用户是感知不到的。如果 Agent 要查 3 个工具才回答,那"查工具"的这几秒里,前端是没有任何输出的。所以工具调用越多、越慢,流式带来的体验改善就越明显——至少最终回复出来的时候,用户不用再等一段完整的"空白期"。

把依赖、配置、接口、前端串起来,完整形态如下。依赖和配置和上一篇一样,用阿里官方 starter:

com.alibaba.cloud.aispring-ai-alibaba-starter-dashscope1.0.0.2

接口层(以 WebFlux 路线为例,最简洁):

@RestControllerpublic class StreamAgentController { private final ChatClient chatClient; public StreamAgentController(ChatClient.Builder builder) { this.chatClient = builder .defaultAdvisors( new MessageChatMemoryAdvisor(new InMemoryChatMemory) ) .build; } @GetMapping(value = "/agent/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Fluxstream(@RequestParam String message) { return chatClient.prompt .user(message) .tools(new WeatherTools) .stream .content; }}

记忆(Advisor)、工具(tools)、流式(stream)三者叠加在一起,就是一个"有上下文、会动手、打字机式回复"的 Agent。

这段代码里,记忆、工具、流式三者是"叠加"而非"冲突"的关系:Advisor 负责记忆,tools 负责动手能力,stream 负责输出方式。三者各管一摊,互不干扰,这正是 Spring AI 的 Advisor 架构带来的好处——加一个能力,不需要改别的代码。

流式对话的代码不难,但真正上线时,有几个坑要提前想清楚:

超时

:SseEmitter 默认超时时间有限,长对话要主动调大超时,否则连接会中途断开。

断线重连

:EventSource 掉线后会自动重连,但重连会丢失之前的上下文,需要服务端配合恢复。

异常处理

:流式过程中的异常要正确收尾(completeWithError),否则前端会一直挂着等不到结束。

上下文成本

:记忆 + 流式,每一轮都会把历史重新发给模型,token 成本会随对话变长而增长。

把这些问题处理掉,一个能落地的流式 Agent 就成型了。

核心始终没变:把 call 换成 stream,再让 Flux 顺着 SSE 一路流到用户的屏幕上。

剩下的,都是工程细节。

如果还想更进一步,可以研究流式的进阶玩法:把工具调用的中间状态也推给前端(展示"正在查询天气…"),或者用 WebFlux 的背压机制控制推送节奏。但对大多数场景来说,把这篇文章的"call 换 stream + SSE 推送"跑通,就已经足够交付一个体验合格的流式 Agent 了。

最后,把这篇和上一篇连起来看,会发现一条清晰的递进线:先用 call 跑通最小骨架(对话、记忆、工具),再用 stream 把它升级成打字机式的流式体验。技术栈没变,依赖没变,变的只是输出方式——这正是 Spring AI 这类框架最舒服的地方:核心能力是稳定的,体验升级是叠加的。

从同步到流式,从一次问答到带记忆带工具的 Agent,Spring AI Alibaba 这条路的每一步,都踩在 Spring 开发者熟悉的节奏上。