说明:本篇笔记基于真实企业级 Spring AI 流式聊天接口整理,采用 SSE 长连接 + Spring WebFlux(Flux/FluxSink)架构。内容会先讲整体项目架构与代码结构,再讲 Flux 核心知识点,最后解答开发中最容易踩坑的疑问,全程图文结合,新手也能看懂。
一、整体架构说明(先看懂全貌)
我们这套服务是标准 SSE 流式服务端架构,用于实现 AI 回答实时逐字输出,核心技术栈为 Spring WebFlux(Flux/FluxSink)+ SSE,整体采用"Controller + Service + Agent 服务"的分层架构,所有代码均围绕这套架构运行。
1. 整体分层架构
前端请求
↓
【 Controller 层 】SSE 接口入口,返回 SseEmitter
↓
【 Service 层 】业务编排 + Flux 数据流创建 + FluxSink 生产数据
↓
【 Agent 服务层 】各类 AI 能力实现,返回 Flux 流式数据
↓
数据通过 Flux 传输
↓
【 Controller 层 】doOnNext 监听 → SSE 推送给前端
2. 核心代码结构(极简版)
① 对外接口:SSE Controller
/**
* SSE 流式聊天接口(对外提供服务)
* 前端通过 POST 请求建立 SSE 连接,实时接收 AI 流式响应
*/
@PostMapping("/agent/call/stream")
public SseEmitter agentChatStream(@Validated @RequestBody ChatRequestDto chatRequestDto) {
// 调用 Service 层方法,获取流式处理结果(内部封装 Flux 数据流)
return chatService.managerChatStream(chatRequestDto);
}
作用:接收前端请求,建立 SSE 长连接,返回流式响应。
② 业务层:Service(返回 Flux)
// 核心 Service 方法,返回 Flux 数据流,供 Controller 层监听
public Flux<StreamResponse> processUserMessageStream(UserInputMessage userInputMessage) {
// 手动创建 Flux 数据流,通过 FluxSink 生产和转发数据
return Flux.create(sink -> {
try {
// 1. 发送处理进度事件
sink.next(StreamResponse.progress(new HashMap<String, String>(){{
put("sessionId", userInputMessage.getSessionId());
put("content","开始处理您的请求..." );
}}));
// 2. 特殊逻辑处理,发送 content 和 complete 事件
if (特殊业务类型) {
sink.next(StreamResponse.builder().eventType(EventType.CONTENT.getName()).build());
sink.next(StreamResponse.builder().eventType(EventType.COMPLETE.getName()).build());
sink.complete(); // 终止流
return;
}
// 3. 意图分类,调用对应 Agent 服务(返回 Flux 数据流)
Flux<StreamResponse> agentFlux = faqChatStreamService.chatFAQAgentStream(userInputMessage);
// 订阅 Agent 流,转发数据到当前 Flux
agentFlux
.doOnNext(streamResponse -> sink.next(streamResponse))
.doOnComplete(() -> sink.complete())
.doOnError(error -> sink.error(error))
.subscribe();
} catch (Exception e) {
// 异常处理,发送错误事件
sink.next(StreamResponse.error(...));
sink.complete();
}
});
}
③ SSE 管理方法
public SseEmitter managerChatStream(ChatRequestDto chatRequestDto) {
// 1. 创建 SSE 连接,设置超时时间(避免连接异常)
SseEmitter emitter = new SseEmitter(30_000L);
// 2. 转换请求参数,调用核心方法获取 Flux 数据流
UserInputMessage userInputMessage = convertToUserInput(chatRequestDto);
Flux<StreamResponse> responseFlux = aiChatManagerService.processUserMessageStream(userInputMessage);
// 3. 监听 Flux 数据流,推送给前端
responseFlux
.doOnNext(streamResponse -> {
// 每接收一条数据,通过 SseEmitter 推送给前端
String jsonData = objectMapper.writeValueAsString(streamResponse);
emitter.send(SseEmitter.event().name(eventName).data(jsonData));
})
.doOnComplete(() -> {
// 数据流结束,关闭 SSE 连接
emitter.complete();
})
.doOnError(error -> {
// 异常处理,发送错误信息并关闭连接
emitter.send(StreamResponse.error(...));
emitter.complete();
})
.subscribe(); // 启动 Flux 流,否则所有逻辑不执行
return emitter;
}
④ 业务层:Agent 服务层(具体业务实现)
核心作用:根据 Service 层的意图分类结果(FAQ、订单、商品等),提供具体的 AI 流式响应逻辑,返回 Flux<StreamResponse> 数据流,供 Service 层转发。
示例(自身代码中 Agent 服务):
-
faqChatStreamService.chatFAQAgentStream:FAQ 意图对应的流式服务
-
orderChatStreamService.chatOrderAgentStream:订单意图对应的流式服务
-
productChatStreamService.chatProductAgentStream:商品意图对应的流式服务
3. 核心技术关联(Flux + SSE 如何协同工作)
整套服务的核心是"Flux 数据流 + SSE 长连接",两者协同实现"实时流式响应",核心关联逻辑:Flux 负责"异步生产、传输数据",SseEmitter 负责"维护长连接、推送数据给前端",Service 层封装业务逻辑生产数据,Controller 层监听数据并推送,形成完整的流式响应链路。
二、完整流程图(经典版)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 【 Spring AI 流式完整流程图 】 │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────┐ ┌─────────────────────────────────────┐
│ 【生产端:业务 Service】 │ │ │
│ Flux.create(sink -> { ... })│───────▶│ FluxSink 【水龙头】 │
│ │ │ │
│ 作用:创建一条数据流 │ │ • sink.next(数据) 发一条消息 │
└───────────────────────────────┘ │ • sink.complete() 正常结束 │
│ • sink.error(异常) 异常报错 │
└───────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Flux │
│ 【 数据流 / 管道 】 │
│ │
│ 特点:异步、被动、只传输、不执行、不订阅就不动 │
└───────────────────────────────┬─────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┬───────────────────┐
│ │ │ │
▼ ▼ ▼ │
┌────────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ doOnNext │ │ doOnComplete │ │ doOnError │ │
│ 每条数据来就执行 │ │ 流结束时执行 │ │ 流异常时执行 │ │
└────────────────────┘ └────────────────┘ └────────────────┘ │
│ │ │ │
└───────────────────┼───────────────────┴───────────────────┘
│
▼
┌───────────────────────────────┐
│ .subscribe() │
│ 【 🔥 点火开关 】 │
│ 不调用 → 前面所有代码不执行 │
└───────────────┬───────────────┘
│
▼
┌─────────────────────────────────────┐
│ SseEmitter.send() 推送给前端 │
└─────────────────────────────────────┘
三、核心疑问前置(对应自身代码困惑)
结合上面的整体架构和代码结构,下面梳理开发中最常遇到的疑问,后续将结合知识点和代码逐一解答:
-
疑问1:代码中 doOnNext 里调用 emitter.send(),会不会重复发送消息?
-
疑问2:subscribe() 到底有什么作用?为什么必须调用?
-
疑问3:Flux 和 FluxSink 是什么关系?在代码中各自扮演什么角色?
-
疑问4:代码中 if ("content") {send; return;},return 能阻止后面的兜底 send 吗?
-
疑问5:双层 Flux(Service 层 Flux.create + Controller 层 responseFlux)为什么要嵌套?
-
疑问6:SSE Controller 接口如何与 Flux 结合,实现前端实时接收流式响应?
四、Flux 核心知识点(必记)
1. 核心概念(对应自身代码)
-
Flux:异步数据流容器,可发送 0~N 条数据、1 个完成信号、1 个错误信号,仅负责"传输数据",不生产、不主动执行,没人订阅就处于"休眠"状态。
-
自身代码中:`Flux<StreamResponse> responseFlux = aiChatManagerService.processUserMessageStream(userInputMessage);` 就是一个 Flux,用于传输 AI 流式响应数据。
-
FluxSink:手动生产 Flux 数据的工具,通过 `Flux.create(sink -> { ... })` 获取,是 Flux 的"数据生产者"。
-
自身代码中:Service 层 `Flux.create(sink -> { ... })` 里的 sink 就是 FluxSink,通过 `sink.next()` 发送进度、内容、完成等事件。
-
doOnNext:Flux 的事件监听器,每有一条数据从 Flux 中流过,就执行一次,仅监听、不修改数据,不影响数据流本身。
-
自身代码中:Controller 层通过 doOnNext 监听 AI 流式数据,调用 `emitter.send()` 推送给前端 SSE。
-
subscribe():Flux 的"点火开关",只有调用 subscribe(),Flux 才会启动,数据流才会开始传输,否则前面所有逻辑(doOnNext、FluxSink 生产数据)都不执行。
-
自身代码中:Controller 层最后 `.subscribe()` 启动外层流,Service 层内部调用 Agent 流后 `.subscribe()` 启动内层流,双层流都需启动。
2. Flux 核心方法(高频使用,结合自身代码)
|--------------------------------------------|-----------------------------------------------|---------------------------------------------------------------------------------------|
| 方法 | 作用 | 自身代码中的使用场景 |
| Flux.create(sink -> {}) | 手动创建 Flux 数据流,获取 FluxSink 用于生产数据 | Service 层 `processUserMessageStream` 方法中,创建内层流,生产进度、内容等事件 |
| flux.doOnNext(Cons<T> consumer) | 监听 Flux 中的每一条数据,数据流过时执行一次 consumer 逻辑 | 1. Controller 层监听 Flux 数据,调用 emitter.send() 推送给前端;2. Service 层监听 Agent 流数据,转发到自身 Flux |
| flux.doOnComplete(Runnable runnable) | Flux 数据流正常结束时,执行一次 runnable 逻辑 | 1. Controller 层流结束时,关闭 SSE 连接;2. Service 层转发 Agent 流结束信号,终止自身 Flux |
| flux.doOnError(Cons<Throwable> consumer) | Flux 数据流异常时,执行一次 consumer 逻辑,处理异常 | 1. Controller 层异常时,发送错误信息并关闭 SSE 连接;2. Service 层转发 Agent 流异常信号,处理错误 |
| flux.subscribe() | 启动 Flux 数据流,触发所有监听逻辑(doOnNext/doOnComplete 等) | 1. Controller 层启动外层流;2. Service 层启动 Agent 流,确保数据正常生产和转发 |
| sink.next(T data) | FluxSink 生产一条数据,发送到 Flux 管道中 | Service 层发送进度、内容、完成等事件,Agent 流生产数据后也通过该方法发送 |
| sink.complete() | 发送 Flux 数据流结束信号,终止数据流传输 | Service 层特殊逻辑处理完成、Agent 流结束后,终止自身 Flux 流 |
| sink.error(Throwable error) | 发送 Flux 数据流异常信号,终止数据流并抛出异常 | Service 层初始化失败、转发 Agent 流异常时,发送异常信号 |
3. 关键区别(避免混淆)
-
Flux vs FluxSink:Flux 是"管道"(传输),FluxSink 是"水龙头"(生产数据),没有 FluxSink,Flux 就是空管道;没有 Flux,FluxSink 生产的数据无处传输。
-
doOnNext vs map:doOnNext 只监听、不修改数据;map 用于修改、转换数据(自身代码中未用到 map,仅用 doOnNext 监听推送)。
-
subscribe() vs doOnNext:subscribe() 负责启动流,doOnNext 负责监听流中的数据,没有 subscribe(),doOnNext 永远不执行。
五、自身代码中 Flux 的实际用法(核心流程)
1. 代码整体架构(双层 Flux 嵌套)
结合开篇的架构说明,自身代码采用"双层 Flux"架构,对应 AI 流式响应的生产、传输、消费全流程,流程拆解(对应代码):
-
Controller 层:接收前端请求,调用 Service 层方法,获取 Flux<StreamResponse>(外层流)。
-
Service 层:通过 `Flux.create(sink -> { ... })` 创建内层流,用 FluxSink 生产数据(进度、内容、完成等)。
-
Service 层:根据意图分类,调用 FAQ/订单/商品等 Agent 流(也是 Flux),订阅 Agent 流后,将其数据转发到内层流(sink.next())。
-
Controller 层:给外层流绑定 doOnNext(监听数据)、doOnComplete(流结束)、doOnError(异常),调用 subscribe() 启动外层流。
-
Controller 层:doOnNext 中通过 SseEmitter 将数据推送给前端,完成流式输出。
2. 核心代码片段解析(Flux 相关)
(1)Service 层:Flux.create 手动创建流 + FluxSink 生产数据
// 自身代码 Service 层核心方法
public Flux<StreamResponse> processUserMessageStream(UserInputMessage userInputMessage) {
// 手动创建 Flux,获取 FluxSink(sink)
return Flux.create(sink -> {
try {
// 1. 用 FluxSink 发送进度事件(生产数据)
sink.next(StreamResponse.progress(new HashMap<String, String>(){{
put("sessionId", userInputMessage.getSessionId());
put("content","开始处理您的请求..." );
}}));
// 2. 特殊逻辑处理,发送 content 和 complete 事件
if (特殊业务类型) {
sink.next(StreamResponse.builder().eventType(EventType.CONTENT.getName()).build());
sink.next(StreamResponse.builder().eventType(EventType.COMPLETE.getName()).build());
sink.complete(); // 终止流
return;
}
// 3. 调用 Agent 流(内层嵌套的 Flux)
Flux<StreamResponse> responseFlux = faqChatStreamService.chatFAQAgentStream(userInputMessage);
// 订阅 Agent 流,将其数据转发到当前 Flux(sink.next())
responseFlux
.doOnNext(streamResponse -> sink.next(streamResponse)) // 转发数据
.doOnComplete(() -> sink.complete()) // 转发结束信号
.doOnError(error -> sink.error(error)) // 转发异常信号
.subscribe(); // 启动 Agent 流
} catch (Exception e) {
// 异常时,用 FluxSink 发送错误事件
sink.next(StreamResponse.error(...));
sink.complete();
}
});
}
(2)Controller 层:监听 Flux 数据 + 启动流
// 自身代码 Controller 层核心逻辑(managerChatStream 方法)
public SseEmitter managerChatStream(ChatRequestDto chatRequestDto) {
// 1. 创建 SSE 连接,设置超时时间
SseEmitter emitter = new SseEmitter(30_000L);
// 2. 转换请求参数
UserInputMessage userInputMessage = convertToUserInput(chatRequestDto);
// 3. 获取 Service 层返回的 Flux(外层流)
Flux<StreamResponse> responseFlux = aiChatManagerService.processUserMessageStream(userInputMessage);
// 4. 监听 Flux 数据,推送给前端 SSE
responseFlux
.doOnNext(streamResponse -> {
// 监听每一条数据,发送给前端
String jsonData = objectMapper.writeValueAsString(streamResponse);
emitter.send(SseEmitter.event().name(eventName).data(jsonData));
})
.doOnComplete(() -> {
// 流结束时,关闭 SSE 连接
emitter.complete();
})
.doOnError(error -> {
// 异常时,发送错误信息并关闭连接
emitter.send(StreamResponse.error(...));
emitter.complete();
})
.subscribe(); // 启动外层流,否则所有逻辑不执行
return emitter;
}
六、核心疑问解答(结合代码 + 原理)
疑问1:doOnNext 里调用 emitter.send(),会不会重复发送消息?
答:不会!核心原因:1 条数据 → Flux 传输 → 触发 1 次 doOnNext → 执行 1 次 emitter.send(),一对一执行,不会重复。
自身代码中,通过 return 截断逻辑,进一步避免重复。补充:自身代码逻辑完全正确,仅"其他未匹配事件"走兜底 send,content/progress/complete 都会被 return 截断,不会走到兜底 send。
疑问2:subscribe() 到底有什么作用?为什么必须调用?
答:subscribe() 是 Flux 的"点火开关",Flux 是"声明式"数据流,仅定义流程(生产、传输、监听),不主动执行,必须调用 subscribe() 才能启动整个流程。
自身代码中,需要两次 subscribe():
-
Controller 层 subscribe():启动外层流,让 Service 层的 Flux 开始生产数据。
-
Service 层 subscribe():启动 Agent 流,让 Agent 开始生产数据,才能转发到外层流。
类比:Flux 是铺好的水管,FluxSink 是水龙头,subscribe() 是拧开水龙头的动作,不拧开水龙头,水管里永远没有水。
疑问3:Flux 和 FluxSink 是什么关系?
答:互补关系,Flux 是"管道",FluxSink 是"水龙头"。自身代码中:FluxSink 生产的数据(progress、content 等),必须通过 Flux 管道传输,才能被 Controller 层的 doOnNext 监听并推送给前端。
疑问4:if ("content") {send; return;},return 能阻止后面的兜底 send 吗?
答:能!return 会直接退出当前 doOnNext 方法,后面的所有代码(包括兜底 send)都不会执行。
验证(对应自身代码):
-
当 eventName = content:进入 if 分支,send 后 return → 方法结束,兜底 send 不执行。
-
当 eventName = progress:进入 if 分支,直接 return → 方法结束,兜底 send 不执行。
-
当 eventName = complete:进入 if 分支,send 后 return → 方法结束,兜底 send 不执行。
-
其他事件:不进入任何 if 分支,执行兜底 send → 仅发送 1 次。
疑问5:双层 Flux 为什么要嵌套?
答:为了实现"业务逻辑封装 + 数据转发",符合分层开发思想:
-
内层 Flux(Service 层):封装 AI 意图分类、特殊逻辑处理、Agent 流调用,负责生产和转发核心业务数据。
-
外层 Flux(Controller 层):负责监听数据、推送前端 SSE、处理连接生命周期(结束、异常),与前端交互。
好处:业务逻辑与前端交互解耦,便于维护和扩展(如修改 Agent 流,不影响 Controller 层的 SSE 推送逻辑)。
疑问6:SSE Controller 接口如何与 Flux 结合,实现前端实时接收流式响应?
答:核心是"Flux 生产传输数据 + SseEmitter 维护长连接推送数据",结合自身代码的完整流程:
-
前端发送 POST 请求到 /agent/call/stream,Controller 层创建 SseEmitter 对象(维护长连接)。
-
Controller 层调用 Service 层方法,获取 Flux<StreamResponse> 数据流(外层流)。
-
给 Flux 绑定 doOnNext 监听器,每有一条数据流过,就通过 SseEmitter.send() 推送给前端。
-
调用 subscribe() 启动 Flux 流,Service 层开始生产数据(FluxSink.next()),数据通过 Flux 传输到 Controller 层。
-
流结束(doOnComplete)或异常(doOnError)时,关闭 SseEmitter 连接,完成整个流式响应。
七、实战注意事项(结合自身代码)
-
- 双层 Flux 必须都调用 subscribe(),否则数据流不会启动(自身代码已实现,无需修改)。
-
- doOnNext 中需先判断 closed.get(),避免连接关闭后继续发送数据,导致异常(自身代码已实现)。
-
- FluxSink.send() 后,需及时调用 sink.complete(),避免流一直处于"活跃"状态,造成资源泄露(自身代码特殊逻辑中已处理)。
-
- 异常处理需完整:doOnError 中需发送错误事件并关闭 SSE 连接,避免前端一直等待(自身代码已实现,可补充心跳机制优化)。
-
- 避免重复判断 closed.get():自身代码中 doOnNext 里两次判断 closed.get(),可精简为 1 次(不影响功能,仅优化代码整洁度)。
-
- SseEmitter 需设置超时时间,避免连接长期闲置导致异常(自身代码可补充 `emitter.setTimeout(30_000L)`)。
八、终极总结(口诀 + 核心流程)
1. 核心口诀(背会即通透)
-
Flux = 数据流管道,只传不产不执行
-
FluxSink = 水龙头,手动生产数据
-
doOnNext = 监听器,来一条处理一条
-
subscribe = 点火开关,不调用不执行
-
return 能截断,不会多发消息
-
SSE + Flux = 实时流式响应
2. 自身代码核心流程(一句话概括)
前端请求 SSE Controller 接口 → Controller 层创建 SseEmitter 并调用 Service 层 → Service 层通过 Flux.create() 创建流,用 FluxSink 生产数据并转发 Agent 流数据 → Controller 层监听 Flux 数据,通过 SseEmitter 推送给前端 → subscribe() 启动整个流程,实现 AI 流式响应的实时接收。