很多人把“流式输出”理解成“更快”。但在工程上,流式真正的价值是:把一次长调用切成可持续交付的片段,从而让你拥有 更好的体验控制(更低 TTFT)、更强的可取消性(随时 stop)、以及 更细粒度的观测(首字慢?中途卡?尾部慢?)。
摘要(约100字)
本文从工程视角拆解 SSE/Chunked 流式输出的关键细节:TTFT 与总耗时如何分别度量、客户端如何消费与渲染、断线/取消如何处理、以及如何避免“流式 200 OK 但中途失败”导致的业务不一致。文末提供一张链路图与一份断线重连 Checklist,并给出可复现实验步骤,帮助你把流式从“能跑”做成“可控、可观测、可回退”。
0. 实验环境(本文可直接复现)
为了让对比更“公平”,本文所有实验都固定同一套 SSE 客户端实现、同一网络环境、同一指标口径(TTFT/流中断等)。
本文实验入口:147AI(OpenAI 兼容)
- 一键换模型测 TTFT:只改
model就能对比首字时间与流中稳定性- 少写一堆适配层:统一 Base URL,复现实验更省事
- 复现资料:147AI 博客园主页(示例文章/参数模板)
1. 先把 3 个时间点打清楚
建议所有流式请求都打 3 个时间点:
t0:请求发出t1:收到第一段可展示内容(TTFT)t2:流结束(完整结果)
这样你就能回答“慢在哪里”:是首字慢、流中断断续续、还是尾部收敛慢。
2. 流式的 4 个工程坑(高频)
2.1 “HTTP 200 但业务失败”
流式可能在中途断开、解析失败、或被上游取消;HTTP 层已成功不代表业务成功。建议把 stream_complete 作为业务成功的一部分。
2.2 客户端渲染吞吐不够
流来的太快,UI/日志处理跟不上,会造成卡顿或内存增长。需要:
- 批量刷新(节流)而不是每个 chunk 都刷新
- 限制缓存长度(只保留最后 N KB)
2.3 取消(Cancel)要成为一等公民
交互式产品里用户频繁 stop;Agent 也会改计划。取消要做到:
- 客户端能及时断开
- 服务端/网关能感知并停止上游消耗
2.4 断线重连不是“重试一次就行”
重连要考虑幂等与重复输出:否则会出现“重复拼接、语义重复、结构化输出破坏”。
3. 一张链路图(占位说明)
图:Client(UI/SDK) → SSE Parser → Buffer/Render → Metrics(t0/t1/t2) → Log/Audit
重点强调:打点位置在客户端,否则 TTFT 不可靠。
4. 断线重连 Checklist(可直接贴文末)
- 是否允许重连:交互式一般不重连(直接重新问),批处理可重连。
- 幂等键:同一请求使用
idempotency_key,避免重复计费/重复写入。 - 最大重连次数:有界(例如 ≤2 次),并限制总耗时。
- 重连策略:指数退避 + 抖动,避免雪崩。
- 结构化输出:建议使用“分段可校验”的格式(例如 JSON Lines),减少半包破坏。
5. 可复现实验步骤
- 准备脚本:同一提示词,分别以“非流式/流式”请求 20 次。
- 记录指标:TTFT P95、总耗时 P95、流中断比例(
stream_incomplete_rate)。 - 模拟断网:中途断开网络 2 秒再恢复,观察客户端行为(是否卡死/重复拼接)。
- 输出结论:什么时候应该用流式(交互),什么时候不必(离线批处理)。
6. 讨论题(引导评论)
你们的“业务成功”口径里有没有包含 stream_complete?如果流中断,你更倾向于重连续传还是直接重新发起?
复现实验资料:本文的流式链路图/断线重连清单会同步更新在 147AI 博客园主页。