AI 流式聊天接口实现(WebFlux+SSE)
在基于RAG的问答Agent系统中,流式返回是提升用户体验的关键------用户无需等待完整回答生成,就能实时看到内容滚动展示。以下是Spring Boot环境下,基于WebFlux+SSE(Server-Sent Events)实现的AI流式聊天接口完整实现、代码解析及落地注意事项,适配RedisSearch+ES检索上下文、大模型异步流式调用的核心场景。
一、SSE 核心定义
SSE 全称 Server-Sent Events(服务器推送事件),是一种基于HTTP的"单向通信协议",核心作用是:服务器主动、持续地向客户端(浏览器/前端)推送数据,客户端无需反复请求。
简单类比:就像你订阅了一个公众号,公众号有新文章时,会主动推送给你,不用你每天打开公众号刷新------SSE就是"服务器推、客户端收"的单向流,适合AI流式回答、实时通知、监控数据推送等场景(比如当前接口的AI打字机效果)。
SSE的核心特点
- 基于HTTP协议,无需额外搭建WebSocket等复杂协议,前端适配简单;
- 单向通信:只有服务器推送给客户端,客户端不能主动向服务器发送数据(刚好适配AI问答"用户问一次,服务器持续返回回答"的场景);
- 文本流传输:推送的数据是文本格式(如当前接口的AI回答片段),无需复杂序列化;
- 自动重连:客户端与服务器断开连接后,会自动尝试重连,保证流式传输的稳定性。
二、SSE 与 WebFlux 的核心联系
SSE 是「通信协议/技术规范」,WebFlux 是「Spring的响应式开发框架」,两者不是"替代关系",而是「适配关系」------WebFlux 是实现 SSE 流式推送的"优秀工具",尤其适合高并发场景。
1. 核心关联:WebFlux 天然适配 SSE 的流式特性
SSE 需要服务器"持续、非阻塞"地向客户端推送数据,而 WebFlux 基于响应式编程(Reactor框架),核心优势就是「非阻塞、高并发、流式处理」,刚好与 SSE 的需求完美契合:
- WebFlux 的 Flux 组件:表示"多个异步结果的流",可以持续输出数据(如当前接口的AI回答片段),与 SSE 的"持续推送"需求完全匹配;
- 非阻塞特性:WebFlux 不会因为一个客户端的流式请求,阻塞其他客户端的请求(比如千人同时请求AI流式回答,接口依然流畅),这是实现高并发 SSE 推送的关键;
- 标准封装:WebFlux 提供了 ServerSentEvent 类,直接封装了 SSE 的消息格式(event事件、data数据、id消息ID),无需开发者手动封装SSE格式(避免原生开发的繁琐)。
2. 为什么选择 WebFlux?(核心优势,适配企业高并发场景)
在AI流式问答、高频并发查询等场景(如德邦数万名员工查询SOP),WebFlux相比传统Spring MVC(基于Servlet)有不可替代的核心优势,这也是我们当前接口选择WebFlux的核心原因:
- 突破"一请求一线程"的阻塞瓶颈:传统Spring MVC采用"一请求一线程"模型,若大模型生成一个回答需要10秒,这10秒内该线程会被完全阻塞,无法处理其他请求。而WebFlux基于事件驱动,当后端等待Qwen-Max返回下一个Token时,线程会被立即释放,去处理其他用户的检索或流式请求,彻底解决阻塞问题。
- 原生支持非阻塞 I/O(Non-blocking I/O):WebFlux底层基于Netty运行,依托非阻塞I/O模型,无需为每个请求长期占用线程。这对于德邦这类拥有数万名员工、高频并发查询SOP的场景至关重要,能在高并发下保持接口流畅响应。
- 内置背压(Backpressure)支持:流式传输中可能出现"大模型吐字太快,前端渲染或网络链路跟不上"的问题,WebFlux的Flux流能自动调节数据发送速率,避免数据堆积导致的内存溢出,保障系统稳定性。
- 极致的资源利用率,降低服务器成本:在相同的硬件配置下,WebFlux能支持的并发连接数通常是Spring MVC的10倍以上,无需增加服务器部署数量,就能承载更高的并发请求,极大降低企业的服务器运维成本。
简单来说,WebFlux的核心优势的是"非阻塞、高并发、省资源",完美匹配AI流式问答、高频企业查询等场景的需求,也是我们当前RAG+SSE接口选型WebFlux的核心逻辑。
3. 关键区别:别混淆"协议"和"框架"
很多开发者会把两者搞混,这里明确区分:
| 维度 | SSE | WebFlux |
|---|---|---|
| 本质 | 通信协议/技术规范(规定服务器如何向客户端推流) | Spring 响应式开发框架(用于开发非阻塞、高并发接口) |
| 作用 | 实现"服务器推、客户端收"的单向流通信 | 提供非阻塞编程能力,适配流式、高并发场景 |
| 关系 | 可以用多种框架实现(WebFlux、Spring MVC、原生Servlet) | 是实现 SSE 的"优选框架",尤其适合高并发场景 |
4. 结合当前接口的实际应用
当前接口中,SSE 与 WebFlux 的配合逻辑的是:
- 接口通过 produces = MediaType.TEXT_EVENT_STREAM_VALUE 声明"响应类型为SSE",遵循SSE协议;
- 用 WebFlux 的 Flux<ServerSentEvent> 作为返回值,实现"持续输出SSE消息";
- 通过 WebFlux 的 flatMapMany、map 等响应式方法,将大模型的流式输出(Flux),转换为 SSE 消息流,推送给前端。
简单总结:SSE 是"要做什么"(实现服务器推流),WebFlux 是"怎么做好"(非阻塞、高并发地实现SSE推流)。
三、代码示例
java
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* AI流式聊天接口(基于RAG检索+SSE流式返回)
* 核心逻辑:检索上下文 → 注入Prompt → 大模型流式调用 → SSE推送
*/
@RestController
public class AIChatController {
private final RagService ragService;
private final QwenAsyncClient qwenAsyncClient;
// 构造器注入(推荐,替代@Autowired,提升可测试性)
public AIChatController(RagService ragService, QwenAsyncClient qwenAsyncClient) {
this.ragService = ragService;
this.qwenAsyncClient = qwenAsyncClient;
}
/**
* 流式聊天接口
* @param question 用户提问
* @return Flux<ServerSentEvent<String>> SSE流式响应
*/
@GetMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(@RequestParam String question) {
// 1. 检索上下文(RedisSearch + ES 联合检索,结合当前用户权限过滤)
// Mono:单个异步结果,fromCallable用于包裹同步方法,避免阻塞事件循环
Mono<String> contextMono = Mono.fromCallable(() ->
ragService.retrieve(question) // 内部需实现:权限校验 + 关键词检索 + 上下文拼接
).onErrorResume(e -> {
// 检索异常处理:返回空上下文,避免流程中断
log.error("RAG检索上下文失败,原因:{}", e.getMessage(), e);
return Mono.just("");
});
// 2. 上下文注入Prompt + 大模型异步流式调用
// flatMapMany:将单个Mono(上下文)转换为Flux(流式响应)
return contextMono.flatMapMany(context ->
// 调用大模型流式接口,返回Flux<String>(逐块生成的文本)
qwenAsyncClient.streamCall(question, context)
// 每一块文本封装为SSE消息,事件类型为"message"
.map(chunk -> ServerSentEvent.<String>builder()
.data(chunk) // 流式返回的文本片段(如单个字、短语)
.event("message") // 事件标识,前端可根据event区分消息类型
.id(String.valueOf(System.currentTimeMillis())) // 消息ID,用于前端去重/排序
.build())
// 拼接结束标识:当大模型流式调用完成后,推送"end"事件
.concatWith(Mono.just(ServerSentEvent.<String>builder()
.event("end")
.data("") // 结束标识可不带内容,前端监听"end"事件即可
.build()))
// 异常处理:当大模型调用失败时,推送"error"事件,返回错误信息
.onErrorResume(e -> {
log.error("大模型流式调用失败,原因:{}", e.getMessage(), e);
return Mono.just(ServerSentEvent.<String>builder()
.event("error")
.data("服务异常,请稍后重试:" + e.getMessage())
.build());
})
);
}
}
四、核心代码解析(关键细节)
1. 接口注解与响应类型
- @GetMapping(value = "/ai/chat/stream"):接口路径,对应前端流式聊天请求;
- produces = MediaType.TEXT_EVENT_STREAM_VALUE:核心注解,声明响应类型为SSE(文本事件流),告诉浏览器以流式方式接收响应,而非等待完整响应;
- 返回值 Flux<ServerSentEvent>:Flux表示"多个异步结果的流",ServerSentEvent是SSE的标准封装,包含消息数据、事件类型、消息ID等
2. 大模型流式调用与SSE封装
- flatMapMany(context -> qwenAsyncClient.streamCall(question, context)):将检索到的上下文注入大模型,调用异步流式接口,返回Flux(大模型逐块生成的文本,如"你""好"",""我""是""AI");
- map(chunk -> ServerSentEvent.builder()):将每一块文本封装为SSE消息,指定事件类型为"message",前端监听该事件,实时渲染文本;
- concatWith(Mono.just(...)):在大模型流式调用完成后,拼接一个"end"事件,告诉前端"回答已生成完毕",可用于隐藏加载动画;
- onErrorResume:大模型调用失败(如超时、接口异常)时,推送"error"事件,返回友好错误提示,提升用户体验。
五、落地注意事项(避坑关键)
1. 响应式编程规范(避免阻塞)
当前接口基于Spring WebFlux实现,核心注意:
- 禁止在Mono.fromCallable中编写耗时同步代码(如无缓冲的ES检索),若ragService.retrieve是同步方法,建议优化为异步方法(返回Mono);
- 避免使用block()(阻塞方法),否则会破坏响应式编程的非阻塞特性,导致接口性能下降。
2. 大模型流式调用的兼容性
确保qwenAsyncClient.streamCall方法返回的Flux是"冷流"(即调用时才开始请求大模型),避免提前触发大模型调用,造成资源浪费;同时确保返回的文本块无乱码、无重复,提升用户体验。
六、企业开发中,SSE流式开发常用技术选型
解答:一般企业开发,是否用WebFlux做SSE流式开发?
1. 核心结论
不是"必须用WebFlux",但WebFlux是企业级SSE开发的主流选择之一,尤其适配高并发、低延迟的场景(比如你当前的RAG+AI问答接口);小型场景/传统项目,也可用Spring MVC实现SSE。
简单说:WebFlux做SSE更优雅、非阻塞,适合高并发;Spring MVC做SSE更简单、成本低,适合并发量不大的场景。
2. 企业开发中SSE的3种常用实现方式(按主流度排序)
方式1:Spring WebFlux(最主流,推荐)
就是你当前接口的实现方式,核心优势:
- 非阻塞、高并发,支持海量用户同时流式接收(比如千人同时问AI,接口不卡顿);
- 天然适配响应式编程,和你代码里的Flux完美契合;
- 适合:AI问答、实时监控、高并发客服等场景(比如你当前的RAG+AI接口)。
方式2:Spring MVC(传统方案,简单易用)
无需引入WebFlux,用普通Spring MVC就能实现,核心是通过ResponseWriter持续输出流;
- 优势:开发成本低、无需切换响应式框架,团队无需学习WebFlux;
- 缺点:同步阻塞,并发量高时容易卡顿,适合并发量≤1000QPS的场景(比如小型内部系统)。
方式3:原生Java Servlet(底层方案)
直接操作Servlet的OutputStream,手动封装SSE格式(event:xxx\ndata:xxx\n\n);
- 优势:完全自主控制,无框架依赖;
- 缺点:开发繁琐,需自己处理连接保活、异常关闭,企业很少直接用(除非无框架依赖场景)。
3. 企业怎么选?
- 如果是AI问答、实时监控、高并发客服(比如你当前的RAG+AI接口)→ 用 Spring WebFlux(和你现有代码一致,适配高并发);
- 如果是小型内部系统、并发量低(比如内部办公通知、小体量问答)→ 用 Spring MVC(简单快,不用学WebFlux);
- 如果是无框架依赖、底层开发 → 用 原生Servlet。
总结
- SSE是基于HTTP的单向推流协议,WebFlux是实现SSE的优选框架(非阻塞、高并发),二者是"做什么"和"怎么做"的关系;
- 企业级AI流式问答接口优先选择WebFlux+SSE方案,小型系统可选择Spring MVC实现SSE;