流式输出与中间结果。
流式输出并不会让模型变快——它让等待变得可见,而对于一个要思考九十秒的智能体来说,这就是「一个产品」与「一个卡死的页面」之间的差别。代价是你已经把收不回的文字交了出去:令牌一旦出现在用户屏幕上,你就无法再校验它、重排它,也无法悄悄重试;于是你所有的护栏要么前移到流的最前端,要么就得承认它们现在跑得太晚了。
要看两个数字,而不是一个。
「延迟」这个说法掩盖了唯一真正决定体感速度的区分:
- 首令牌时间(TTFT)——用户盯着空白屏幕的时长。主要由提示词处理决定,因此随输入长度增长,并在提示词缓存命中时骤降。
- 令牌间延迟——之后的「滴水速率」,对同一模型与硬件大致恒定。
总时长等于 TTFT 加上「速率 × 长度」,流式输出对此毫无改变。它改变的是:用户从 TTFT 时刻就开始阅读,而不是等到最后。一个生成耗时 20 秒、但 400 毫秒就开始出字的回复让人觉得跟手;同样的回复在第 20 秒整块吐出来,就让人觉得坏了。这也说明两个抓手是两件事:靠缓存与精简输入压缩 TTFT,靠少生成令牌压缩总时长。
人的阅读速度大约是每秒 5–8 个令牌。多数托管模型的吐字速度都快于此,这意味着超过某个速率之后,再快也换不来用户可感知的收益——而 TTFT 是每一轮对话都会被感知到的。请优化人们真正体验得到的那个数字。
智能体流出来的不只是散文。
聊天的流式输出只有一个文本字段。智能体的流是一串带类型的事件,把它当成字符串处理,正是智能体界面难做的根源:
- 文本增量——可见的答案。
- 思考增量——在推理模型上,思考过程先于任何答案到达,且可能是摘要而非逐字原文。即便流瞬间就打开了,答案的 TTFT 仍可能是几十秒。
- 工具调用增量——参数以不完整的 JSON 字符串形式陆续到达。在调用完整之前你无法据此行动;世上不存在「半个工具调用」。
- 生命周期事件——消息开始/结束、内容块边界、停止原因与用量。停止原因这个字段,决定了你是自然收尾还是撞上了令牌上限。
实用规则:文本增量立即渲染,工具调用增量缓冲到完整为止,并把工具活动做成独立的界面元素(「检索中…」「运行测试中…」)而不是正文文字。用户看着智能体干活时,真正让人安心的是看到它在做什么;他们并不需要原始参数。
流式输出会悄悄弄坏的东西。
下面每一条都是真实的生产事故,而它们同源——你在做完之前就已经发布了:
- 输出侧护栏跑得太晚。对完整回复做内容审核或个人信息检查,收不回已经渲染出去的内容。要么在缓冲区上增量扫描并接受 TTFT 变长,要么承认你的输出过滤器如今只是「建议性」的。输入侧的管控与护栏没有这个问题,这也是应当更倚重它们的又一个理由。
- 重试变得可见。非流式下,失败的调用重试一次,没人会知道。流到一半时,你已经展示了半个答案,重试要么重复、要么自相矛盾。请提前决定:整块替换,还是带着可见的错误继续。
- 不完整的 JSON 不是 JSON。流式传输的结构化输出,在最后一个令牌到达之前,对解析器而言全程都是非法的。要么用容忍流式的解析器,要么干脆不要流式返回结构化结果。
- 断连是无声的。浏览器标签页关掉了,服务端的生成并不会停;你还在为没人会读的令牌付费。请检测连接断开并主动取消。
- 截断看起来和完成一模一样。因撞上最大令牌上限而结束的流,与正常收尾的流看上去毫无区别——除非你检查停止原因。请务必检查。
传输方式,以及它强加的形态。
Server-Sent Events 成为默认选择自有其道理:它是单向的(服务端推送,而这正是「生成」的本质),能穿过企业代理与 HTTP/1.1 基础设施,并在协议层自动重连。WebSocket 换来的是你在纯文本场景很少用得上的双向能力——涉及音频时再考虑它,那里的实时模型确实需要一条全双工通道。
- 会缓冲的中间层会让它失效。任何缓冲响应体的代理、CDN 或 Serverless 平台,都会把你的流重新变回一次性投递,而这在本地测试时看起来完全正常。请在真实生产设施上验证,而不是在 localhost 上。
- 长时间生成会超过请求超时。许多 Serverless 的默认超时低于智能体思考所需的时间。这正是「流式智能体在开发环境好好的、上了生产就 504」最常见的原因。
- 可恢复性是一个设计决定,而不是白送的特性。如果你希望用户能合上笔记本、回头再看,那么「生成」就必须是一个带持久状态的任务,而不是一个攥着套接字的请求。
对人流式输出,对程序不要。如果消费方是另一个服务、一个队列或一套评测框架,请取用完整回复——你换来的是校验能力、干净的重试和简单得多的代码,而你什么都没损失,因为没有任何机器在乎第一个令牌来得有多快。流式输出是一种恰好跑在你 API 之上的用户体验机制。
延伸阅读:成本、质量与延迟说明它所处的权衡关系,智能体可观测性讲当回复是分片到达时该记录什么,智能体循环讲工具事件落在哪里。