SSE 流式协议
昨天(Day 71)把 agent runner 的网络边界写成了契约:RPC 风格 POST /chat + 4 类 typed error code。但契约里的「响应」还是一次性 body——用户要盯着空白屏等整段生成完。
阶段: B8 · 真 API + 流式 + Docker(Day 71-80) 标签: #sse #streaming #streamable-http #mcp
今日导引(由浅入深)
昨天(Day 71)把 agent runner 的网络边界写成了契约:RPC 风格 POST /chat + 4 类 typed error code。但契约里的「响应」还是一次性 body——用户要盯着空白屏等整段生成完。
今天在 B1→B18 曲线上前进一格:把响应从「一次性返回」改成「token 增量推送」。这是任何 agent 产品「感觉快」的物理基础。今天先用纯协议把管子打通(硬编码 3 帧),明天(Day 73)再灌真模型的 token。
最小可判定产出:一个返回 text/event-stream 的 route,curl / 浏览器能抓到 3 个 SSE 帧(2 帧 token delta + 1 帧 [DONE])。
1. 机理精读
1.1 SSE 是单向 server→client 的 text/event-stream
它建立在普通 HTTP 之上:客户端发一个普通 GET/POST,服务端不关闭连接、持续往里写文本帧。相比 WebSocket:
- 不需要协议升级握手(
Upgrade: websocket)。 - 不需要双向帧管理。
- 天然走 HTTP 基础设施(代理、CDN、HTTP/2 多路复用)。
对「server 持续吐、client 只读」的 token 流式场景,SSE 是恰到好处的最小工具;WebSocket 的双向能力在这里是冗余的复杂度。一句话判断:单向推送选 SSE,需要双向实时(聊天室、协同编辑)才选 WebSocket。
1.2 SSE 的代价与补偿
代价:
- SSE 单向意味着客户端无法在同一连接上回传(要回传得另开请求)。
- 浏览器对同域 SSE 连接数有上限(HTTP/1.1 下约 6 个),HTTP/2 多路复用缓解了这点。
补偿——「重连友好」:
- SSE 协议内建
Last-Event-ID头与retry:字段。 - 浏览器原生
EventSource断线会自动带上最后 ID 重连,服务端可据此续传。
对长生成的 agent 流,这种内建重连是 WebSocket 要手写一堆代码才有的能力。(注意:模型生成本身通常不可重放,所以「续传」对 LLM 流多是「干净重开」而非「续上断点」——见 Day 74 的流中断处理。)
1.3 帧格式:event: + data: 行,空行分隔,哨兵帧终止
一帧 SSE 由若干 field: value 行组成,常用字段:
event::事件类型,缺省为message。data::载荷,可多行(多个data:行会被换行拼接)。id::事件 ID(配合Last-Event-ID续传)。retry::重连毫秒。
一帧以一个空行结束——这是解析的关键边界。流没有内建「结束」信号,所以约定一个哨兵帧(OpenAI 风格的 data: [DONE])告诉客户端「生成完了,可以关连接」。本日就硬编码这种结构:2 帧 data: 装 token,1 帧 data: [DONE] 收尾。
1.4 对照 MCP 2026-07-28 spec 的 Streamable HTTP transport
旧的 MCP transport 是「HTTP + SSE 双端点」设计——一个端点收请求、另一个 SSE 端点推事件,两条连接要对齐很麻烦。Streamable HTTP 把它统一成单个 HTTP 端点:
- 客户端 POST 一个 JSON-RPC 请求。
- 服务端可以选择用
text/event-stream在同一个响应里流式回多帧。
本仓的 src/agent/mcp/server.ts 走的就是这个新范式——startHttpServer() 在 /mcp 上用 StreamableHTTPServerTransport,stateless 风格每请求新建 server+transport。本日硬编码的 SSE route 要对齐这个帧边界思路,而不是去实现废弃的双端点旧 transport。
2. 推导 / 手算 / 代码走读
2.1 手算一个最小合法 SSE 响应体
(\n 代表换行,空行是帧分隔符)
data: Hello\n
\n
data: world\n
\n
data: [DONE]\n
\n
逐步解析(站在客户端 EventSource / curl 视角):
- 读到
data: Hello,遇到空行 → 触发一个message事件,payload=Hello。 - 读到
data: world(注意data:后第一个空格是分隔符、会被去掉,所以 payload 是world带一个前导空格)→ 第二个事件。 - 读到
data: [DONE]→ 第三个事件,客户端识别哨兵、关闭连接。
2.2 走读 src/agent/mcp/server.ts 对齐真实帧边界
本日 route 要模仿其传输层意图:
startHttpServer(port):createServer里只处理req.url.startsWith('/mcp'),否则res.writeHead(404).end('not found')——本日 route 是/api/chat,是独立的硬编码验证端点,不复用此路径。- 它把 body 累积到
chunks(req.on('data', ...)),req.on('end')后整体 parse——MCP 是「一次请求、流式响应」,请求体非流式、响应体才流式,本日 SSE 帧对应的正是「响应体流式」那一侧。 new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }):sessionIdGenerator: undefined即 stateless(每请求新 transport),是 2026-07-28 风格。enableJsonResponse: true让简单调用走 JSON 而非强制 SSE——说明 SSE 是「需要流式时才升级」的可选模式,不是所有响应都得 SSE。
res.on('close', () => { void transport.close(); void server.close() }):连接关闭时清理 transport + server,对应 stateless「用完即弃」。
结论:本仓真实流式由 MCP server 承载;本日 /api/chat 是一个硬编码 3 帧的验证步骤,用来在产品 route 层把 SSE 帧格式跑通,不是常驻服务。
3. 今日实战
- 在 Next.js 起一个 route
/api/chat(POST),响应头设Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive。 - 用
ReadableStream硬编码写 3 帧:两帧data: <token>\n\n的 token delta + 一帧data: [DONE]\n\n哨兵。 - 用 curl 验证:
curl -N -X POST localhost:3000/api/chat(-N关闭缓冲),确认能逐帧抓到 3 个 SSE 帧。 - 帧边界思路对齐
src/agent/mcp/server.ts的 Streamable HTTP 处理,不实现废弃的双端点 transport。 - 把 curl 抓到的 transcript 存成文件归档(产出物,非常驻服务)。
4. 今日实测 / 产出
- 待跑(需起 route)——curl 抓到 3 个 SSE 帧的 transcript。
- 诚实标注:standalone
POST /chatSSE app route 在本仓库未单独构建,真实流式能力由 MCP server(src/agent/mcp/server.ts)承载;本日为硬编码验证步骤,产出 transcript 文件而非常驻服务。
5. 常见误区 / 陷阱
- 忘了空行分帧:SSE 一帧必须以空行(
\n\n)结束,少一个换行客户端会把多帧粘成一帧或永远不触发事件。 - 没关响应缓冲就 curl:不加
-N(或代理 / Nginx 开了 buffering)会让所有帧攒到最后一次性吐出,看起来「没流式」其实是被缓冲了。 - 重新实现废弃的 HTTP+SSE 双端点:MCP 已统一到单端点 Streamable HTTP,照旧设计就是给自己挖坑。
- 以为 SSE 能双向:SSE 只能 server→client,要客户端回传得另开请求或换 WebSocket,别在 SSE 连接上找回传通道。
6. 学习资源(每条带 YYYY-MM)
- MDN, "Server-sent events / Using server-sent events"(持续更新,2026 现行;SSE 帧格式与
EventSource权威说明)。 - WHATWG HTML Living Standard, "Server-Sent Events" 章节(持续更新;
text/event-stream规范来源)。 - Model Context Protocol, "Streamable HTTP transport" 规范(MCP spec,2026-07-28 定稿前为草案,须当周复验)。
- 本仓代码:
src/agent/mcp/server.ts(B6 真实 MCP server,Streamable HTTP stateless 实现)。
SOTA检查 (2026-06 更新)
- 当前主流:SSE 仍是 LLM token 流式的主力传输(OpenAI / Anthropic / DeepSeek 流式 API 均走
text/event-stream+ 哨兵帧)。MCP Streamable HTTP 是当前传输方案。 - 是否仍 SOTA:是,但有版本前提——MCP Streamable HTTP 的最终规范 2026-07-28 才定稿,server 构建应排在其后,当周重验帧约定。
- 过时黑名单:
- 避免实现已废弃的「HTTP+SSE 双端点」旧 transport。
- 勿假设 SSE 双向。
- 下次复查点:2026-07-28 MCP 最终规范发布后,复验 Streamable HTTP 帧约定与 stateless 风格是否有改动;Day 73 接真模型流式时复验 DeepSeek SSE delta 帧结构。
衔接
- 昨天:Day 71 — HTTP API 边界设计(RPC 风格
POST /chat+ 4 类 typed error 契约)。 - 今天:用最轻量的 SSE 协议把「响应流」管子打通,硬编码 3 帧验证
text/event-stream可读。 - 明天:Day 73 — 模型流式接入(把硬编码的 token 帧换成 DeepSeek 真流式,测 TTFT)。