Appearance
SSE 流式传输、网络中断恢复与大模型打字机打点
回到总览:AI 时代的工程师能力
相关模块:Prompt 工程、Tool Calling 与 Agent 记忆机制 (Memory Workflow) · API 幂等性、超时重试与移动端数据最终一致性
一句话定义
SSE(Server-Sent Events,服务器发送事件)是大语言模型(LLM)实现流式文本输出(Streaming)的标准 HTTP 轻量级协议;结合移动端网络中断自动恢复(Last-Event-ID)与 Markdown 增量解析,构成了 AI 打字机渲染体验的核心技术链路。
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
| SSE 流式 + 中断恢复 | sse-agent-orchestration | server.js · client.js |
| SSE + Tool Calling + 最小 RAG 流式前端 | tool-calling-rag-streaming-demo | server.js · client.js |
为什么需要
- 为什么 LLM 输出代码或长文本时必须采用 SSE 流式传输,而不是像传统 API 那样等完全生成后再一次性返回?
- 一句话答:LLM 生成长文本需要数秒甚至数十秒,一次性返回会导致 TTFB(首字节时间)极长,用户会感觉页面假死;通过 SSE 按 Token 逐字流式下发,可以在几十毫秒内向用户展示首字,极大地降低了感知延迟。
- 为什么移动端 SSE 客户端必须实现
Last-Event-ID重连机制?- 一句话答:移动网络(Wi‑Fi / 5G 切换)极易发生长连接断开;利用 SSE 协议标准的
Last-Event-ID报头,客户端在断网重连后可以告知服务端从中断处的那个 Token 接着推,避免文本重复或从头打印。
- 一句话答:移动网络(Wi‑Fi / 5G 切换)极易发生长连接断开;利用 SSE 协议标准的
- SSE 与 WebSocket 在 LLM 打字机场景中如何选型?
- 一句话答:LLM 打字机是单向接收流式文本,基于标准 HTTP 的 SSE 协议更轻量且原生支持断线自动重连;只有需要双向高频交互的场景才选 WebSocket。
底层机制
1. SSE (Server-Sent Events) HTTP 协议流流转
对应 Lab:sse-agent-orchestration
SSE 报头标准规则
Content-Type:text/event-streamCache-Control:no-cache(禁止中间 CDN 节点缓存)Connection:keep-alive
2. 网络中断重连与 Last-Event-ID 机制
当网络瞬断重连时: 客户端向服务端发送 Last-Event-ID: 2 报头,服务端直接从 id: 3 的 Token 开始接着推送,实现无缝增量恢复。
3. 最小流式前端 demo 要关心的不是“页面好不好看”,而是事件边界
真实 AI 产品里,前端并不只是接收一串 token。更常见的是同一条 SSE 流里混着:
tool_call:展示“模型正在查资料/调工具”tool_result:展示工具结果摘要token:真正的打字机内容done:收尾与引用来源
本仓库的最小 demo 直接把这四类事件拆开,CLI 客户端按事件类型分别处理:
js
const event = lines.find((line) => line.startsWith('event: '))?.slice(7) || 'message';
const payload = JSON.parse(dataLine.slice(6));
if (event === 'token') {
process.stdout.write(payload.token);
} else {
process.stdout.write(`
[${event}] ${JSON.stringify(payload, null, 2)}
`);
}- 可能执行顺序
- 建立
/api/chatSSE 连接。 - 服务端先发
tool_call。 - 再发
tool_result。 - 随后连续发
token。 - 最后发
done。
- 建立
- 可能输出text
[tool_call] {...} [tool_result] {...} 问题:为什么 RAG 需要 chunk overlap? 这个最小 demo 展示了一条最短 AI 工程闭环... [done] {"references":["docs/08-ai-engineering/04-rag-and-knowledge-retrieval.md#chunk-6"]} - 预期现象:前端可以区分“正在调工具”和“正在打印答案”,而不是把所有事件都混成一段普通文本。
- 观察重点:这正是“流式前端 demo”的最小价值——先把事件协议定稳,再谈浏览器 UI 和 Markdown 增量渲染。
Android / Flutter / Web / Backend 对照
| 维度 | SSE (Server-Sent Events) | WebSocket | HTTP 长轮询 (Long-Polling) |
|---|---|---|---|
| 数据流向 | 单向 (Server → Client) | 双向 (Full-Duplex) | 双向 (多次 HTTP 模拟) |
| 协议复杂度 | 极低 (基于标准 HTTP 协议) | 较复杂 (需要独立握手与帧) | 低 |
| 自动重连 | 原生支持 (Last-Event-ID) | 需要自定义心跳与重连逻辑 | 需要客户端再次发请求 |
| 适合场景 | LLM 流式打字机、新闻直播 | 实时聊天室、多人游戏 | 简单状态轮询 |
常见场景与代码实现
1. OkHttp 处理 SSE 流式事件流
java
Request request = new Request.Builder()
.url("https://api.openai.com/v1/chat/completions")
.header("Accept", "text/event-stream")
.header("Last-Event-ID", lastEventId) // 中断恢复!
.post(body)
.build();
RealServerSentEvent sse = new RealServerSentEvent(request, new EventSourceListener() {
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
if ("[DONE]".equals(data)) {
// 结束流
return;
}
String token = parseTokenFromJson(data);
// 切回主线程追加渲染 Markdown 打字机
mainHandler.post(() -> ui.appendToken(token));
}
});常见误配、事故后果与排障
1. 事故:代理服务器(Nginx)开启了 Buffer 导致 SSE 流式打字机失效变“一次性吐出”
- 误配原因:后端的 Nginx 反向代理未关闭
proxy_buffering,Nginx 缓存了所有 SSE 块,等 LLM 完全生成后才一次性发给客户端。 - 后果:前端完全丧失了流式打字机效果,TTFB 延迟暴增。
- 排障与修法:在 Nginx 配置中增加
proxy_buffering off;,或在服务端 HTTP Header 中返回X-Accel-Buffering: no。
2. 误配:把工具事件、token 事件、done 事件混成同一种 data
- 后果:前端很难区分“正在查工具”还是“正在输出正文”,恢复/重放时也无法按阶段回放。
- 排障与修法:显式使用
event:字段区分事件类型;最小 demo 里就是tool_call/tool_result/token/done四类事件分流。
与相近概念对比
| 传输机制 | 协议 | 连通性开销 | 为什么 LLM 首选 SSE |
|---|---|---|---|
| SSE | HTTP | 极低 | LLM 是单向推 Token 场景,无需 WebSocket 复杂双向握手 |
| WebSocket | WS/WSS | 高 | 适合聊天室等双向高频交互 |
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| sse-agent-orchestration | SSE 流式推送 + 任务状态机 + 中断恢复(纯 Node,可跑可停) | server.js · client.js |
| tool-calling-rag-streaming-demo | SSE 事件分流:工具调用、检索结果、token、done(与文首「代码索引」一致) | server.js · client.js |
复习检查题
在 LLM 大模型文本生成场景中,为什么 SSE (Server-Sent Events) 比 WebSocket 更适合作为传输协议?
答:因为 LLM 文本生成是一个典型的“客户端发起一次请求,服务端单向连续推送 Token”的单向流式场景。SSE 直接基于标准的 HTTP 协议,实现极其简单,天然支持网络中断后基于
Last-Event-ID的自动重连;而 WebSocket 是双向全双工协议,需要额外的握手、心跳包与断线重连逻辑,在 LLM 打字机场景中显得过于繁重。为什么 Nginx 反向代理配置不当会导致 SSE 流式打字机效果失效?如何解决?
答:因为 Nginx 默认开启了
proxy_buffering(代理缓冲区),会将服务端推过来的数据先暂存在 Nginx 的 Buffer 中,凑够一定大小后才一次性发送给客户端。这破坏了 SSE 逐字下发的实时性。解决办法是在 Nginx 配置中对该 API 路径设置proxy_buffering off;,或者让服务端返回 HeaderX-Accel-Buffering: no。
速记
- SSE 协议头:
text/event-stream+Cache-Control: no-cache+Connection: keep-alive。 - 中断恢复:利用
Last-Event-ID报头实现断网重连后的增量续推。 - 事件分流:工具调用、token、done 最好分事件类型,不要混成一坨文本。
- Nginx 防坑:服务端/代理必须关闭 Buffer (
proxy_buffering off) 保障实时流式。