Spring AI 流式对话实现
ChatClient 调用链路
Spring AI 调用大模型的完整链路:
ChatClient(客户端入口)
→ Advisor 链(拦截器,可插入日志、RAG等)
→ ChatModel(模型抽象层,如 DashScopeChatModel)
→ 内部 HTTP 请求 → 大模型 API(如阿里百炼)
ChatClient 是核心客户端,相当于调用大模型的"RestTemplate"。ChatModel 是模型抽象层,屏蔽不同大模型的 API 差异,你配了哪个厂商就自动用对应的实现类,不需要自己管。
两种调用方式
// 非流式:等大模型全部生成完,一次性返回
String answer = chatClient.prompt()
.user("你好")
.call()
.content();
// 流式:大模型每生成一点就推一点过来
Flux<String> stream = chatClient.prompt()
.user("你好")
.stream()
.content();
.call() 是同步等待完整结果,.stream() 是响应式数据流,适合"逐字蹦出来"的聊天效果。
流式调用的完整链路
return this.chatClient.prompt()
.system(promptSystem -> promptSystem
.text(this.systemPromptConfig.getChatSystemMessage().get()) // 系统提示词
.param("now", DateUtil.now()) // 模板变量
)
.user(question)
.stream()
.chatResponse()
.map(chatResponse -> {
String text = chatResponse.getResult().getOutput().getText();
return ChatEventVO.builder()
.eventData(text)
.eventType(ChatEventTypeEnum.DATA.getValue())
.build();
})
.concatWith(Flux.just(ChatEventVO.builder()
.eventType(ChatEventTypeEnum.STOP.getValue())
.build()));
逐方法拆解:
| 方法 | 所属 | 作用 |
|---|---|---|
| prompt() | ChatClient | 创建一次请求 |
| system(...) | ChatClient | 设置系统提示词 |
| user(question) | ChatClient | 设置用户问题 |
| stream() | ChatClient | 发起流式调用,返回 Flux |
| chatResponse() | ChatClient | 取出完整响应对象(含元数据) |
| map(...) | Flux (Reactor) | 逐段转换数据类型 |
| concatWith(...) | Flux (Reactor) | 在流末尾追加数据 |
| Flux.just(...) | Flux (Reactor) | 把单个对象包装成流 |
注意区分:prompt() 到 chatResponse() 是 ChatClient 的方法,从 map() 开始都是 Reactor 框架的 Flux 方法。ChatClient 负责调大模型,Flux 负责处理数据流。
Lambda 表达式在这里的作用
.system(promptSystem -> promptSystem.text("...").param("now", DateUtil.now()))
-> 是 Java 的 Lambda 语法。用 Lambda 是因为对象不直接给你用。如果框架直接把对象给你,你就不需要 Lambda,直接操作就行。正因为框架内部创建对象、内部管理,才通过 Lambda 回调的方式让你"借用"一下配置。
跟 StringBuilder 类似,都是构建器模式,但 StringBuilder 是你自己拿着对象操作,Lambda 方式是你把操作逻辑交给框架。
参数名随便取,promptSystem、s、p 都一样。
为什么封装成 JSON 对象而不是纯文本
// 简单写法:返回纯文本
Flux<String> stream = chatClient.prompt().user("你好").stream().content();
// 前端收到:你好
// 项目写法:返回 JSON 对象
Flux<ChatEventVO> stream = ...
// 前端收到:{"eventData":"你好","eventType":"DATA"}
封装成 JSON 是行业通用做法。好处是前端能通过 eventType 区分数据类型:
{"eventData":"你好","eventType":"DATA"} ← 渲染文字
{"eventData":",我是","eventType":"DATA"} ← 渲染文字
{"eventData":"AI助手","eventType":"DATA"} ← 渲染文字
{"eventData":null,"eventType":"STOP"} ← 输出结束
后续要扩展也方便,比如加 THINKING(思考中)、ERROR(出错)等类型,直接加枚举值就行,不用改协议。
Java 对象怎么变成 JSON 的
代码里全程操作的都是 Java 对象(ChatEventVO),没有写任何 JSON 转换代码。Spring 看到返回类型是对象,自动用 Jackson 序列化成 JSON 发出去:
ChatEventVO 对象 → Spring 自动序列化 → {"eventData":"你好","eventType":"DATA"} → HTTP 发给前端
如果返回 Flux<String>,Spring 就直接发纯文本,不走 JSON 序列化。
响应式编程(Reactor)
Flux 是 Reactor 框架的核心类,代表一个持续产出数据的数据流。响应式编程的思想是:数据不是一次性给你,而是像水流一样持续推过来,你定义"收到数据时怎么处理"。
常用操作符:
.map(x -> ...) // 转换流中的每个元素
.concatWith(...) // 在流末尾拼接另一个流
Flux.just(...) // 创建只有一个元素的流
AI 流式对话场景天然适合响应式编程,因为大模型生成内容本身就是逐步产出的。不需要专门系统学 Reactor,项目里用到什么记什么,够用。
前后端配合
流式接口需要前后端约定:
- 传输协议:Spring WebFlux 的 Flux 默认用 SSE(Server-Sent Events)推送,前端用 EventSource 接收
- 事件格式:前端要知道 ChatEventVO 的字段名和含义
- 结束标记:前端收到 STOP 事件就知道输出结束,可以关闭连接
系统提示词
系统提示词是大模型收到的第一条消息,定义大模型的角色和行为规范,不会显示给用户。对回答质量和风格影响很大。
.system(s -> s.text("你是一个专业的Java老师"))
.user("什么是多态?")
大模型实际收到的消息结构:
[系统] 你是一个专业的Java老师 [用户] 什么是多态? [助手] 多态是指..
更多推荐




所有评论(0)