答案一个字一个字蹦出来流式生成的完整链路
开篇引言
上一篇把 Prompt 组装的全过程拆完了——RAGPromptService.buildStructuredMessages() 根据场景(KB_ONLY / MCP_ONLY / MIXED)选模板,把 System Prompt、对话历史、检索证据、用户问题按固定骨架拼成一个 messages 数组,再加上 temperature、topP、是否开启深度思考,封装成一个 ChatRequest。
到这一步,所有调用参数都就位了。但 把 ChatRequest 发给大模型,不是一个简单的 HTTP 请求-响应——大模型不会一口气把答案全吐出来,而是一个 token 一个 token 地往外蹦。这些 token 要穿过好几层,才能最终到达用户的浏览器。
打个比方,这有点像快递的中转流程。你在网上下了单(发出请求),商家一件一件地打包(模型逐 token 生成),然后包裹经过分拣中心、中转站、末端网点(各层处理),最后一件一件送到你家门口(浏览器渲染)。每一层只管自己那段路,接力完成整个投递。
Ragent 的流式生成也是这样,从 Controller 到大模型再到浏览器,中间经过五层接力。本篇就把这条链路从头到尾拆开讲。
全景图:五层接力
1. 一张图看全链路
先上一张时序图,把流式生成的完整数据流画出来。从用户发起请求,到 token 一个个蹦到浏览器,中间经过了哪些参与者、发生了哪些关键事件,一目了然:

2. 五层各自负责什么
整条链路分为五层,每层职责清晰,通过 StreamCallback 接口串联:
| 层次 | 关键类 | 职责 | 输入 → 输出 |
|---|---|---|---|
| 入口层 | RAGChatController / RAGChatServiceImpl | 创建 SSE 连接、生成 ID、创建回调实例 | HTTP 请求 → SseEmitter + StreamChatContext |
| 流水线层 | StreamChatPipeline | 前置阶段 + 组装 Prompt + 调 LLM | StreamChatContext → ChatRequest |
| 调度层 | RoutingLLMService / ProbeStreamBridge / ModelHealthStore | 选模型、首包探测、容错切换、断路器 | ChatRequest → 确定可用模型并启动流式调用 |
| 客户端层 | AbstractOpenAIStyleChatClient / OpenAIStyleSseParser | OkHttp 读流、SSE 解析、delta 提取、触发回调 | HTTP SSE 流 → StreamCallback 回调 |
| 推送层 | StreamChatEventHandler / SseEmitterSender | 缓冲分块、SSE 推送、消息持久化、连接关闭 | 回调事件 → 浏览器 SSE 事件 |
这五层的串联方式很朴素——StreamCallback 接口只有四个方法:
public interface StreamCallback {
void onContent(String content);
default void onThinking(String content) {
}
void onComplete();
void onError(Throwable error);
}
onContent(String):正式回答的增量内容,模型吐一段就回调一次onThinking(String):深度思考模式下的思考过程,和回答内容分开传输,默认空实现onComplete():整个推理完成,触发持久化、发事件、关连接onError(Throwable):出错了,通知前端、释放资源
客户端层读到 token 后调 onContent(),推送层在 onContent() 里把 token 推到浏览器——中间不需要队列、不需要消息总线,一个接口方法调用就完成了层与层的衔接。