响应为什么要流式输出
逐个 token 地送达不是界面上的小花招,而是生成本来的形态。它对感知延迟的改变,超过任何一项优化。
文字一个词一个词地冒出来,看起来像是个刻意的设计选择——用打字机效果显得在认真思考。
恰恰相反。流式输出(streaming)就是生成过程真实的样子。等完整回复出来再给,才是后来加上去的那一步。
它是从机制里自然长出来的
生成是一个循环:预测一个 token,把它接上去,再预测下一个。每个 token 在被采样出来的那一刻就存在了,比整条回复结束早几百毫秒。
于是有两种做法。把每个 token 都攒着,等全部生成完再一次性发出去。或者出来一个就发一个。
流式输出是第二种,而且它不需要额外的机械——它就是「不做缓冲」。
两个延迟数字
流式输出把延迟拆成了两个行为不同的测量值。
首 token 延迟(time to first token)衡量的是预填充(prefill)——在生成开始前把整个 prompt 处理一遍。它随 prompt 长度增长,而且长 prompt 下涨得很陡,因为注意力的开销是平方级的。
每秒 token 数衡量的是解码循环。每个 token 的开销大致恒定,取决于模型大小和负载。
总时间等于第一个数字加上(输出长度 ÷ 第二个数字)。该优化哪个数字取决于你的处境:长 prompt 配短回答,瓶颈在预填充,缩短 prompt 就是解药。短 prompt 配长回答,瓶颈在解码,只有换更小的模型或减少输出才有用。
不做流式输出,你根本无法区分这两者,性能优化就只能靠猜。
为什么它对界面很重要
一次耗时 8 秒的响应,在沉默中显得像坏了;文字不断到达时,就变得可以接受。
原因在于阅读速度。多数人读得比模型生成得慢,所以一旦输出开始流动,剩下的生成就藏在你的阅读背后悄悄完成了。感知到的等待是首 token 延迟,不是总时间——这就是为什么流式输出对体感性能的改善,超过大多数真实的优化。
它还让提前退出成为可能。用户看到答案正在跑偏,可以把它停掉,省下 token 也省下自己的时间。
实现细节
服务商通过服务器发送事件(server-sent events)或类似协议传输流。你会收到一串小事件,每个携带一小段文本,最后以一个带停止原因和 token 计数的事件收尾。
有三件事要处理:
片段不是单词。 事件携带的是 token,可能是半个单词,也可能只是一个标点。拼接起来就好,永远不要假设边界在哪。
错误可能在流中途到达。 一个成功开始的请求可能中途失败。要处理「已经输出了一部分之后才失败」的情况——如果你已经把文字显示给用户了,这确实很棘手。
token 计数最后才来。 成本核算发生在最后一个事件里。
对浏览器应用来说,这个流必须经由你的后端中转,因为 API key 不能出现在客户端代码里。你的服务器消费服务商的流,再转发给客户端。
什么时候不要流式输出
结构化输出。 残缺的 JSON 不是合法的 JSON。如果你要解析结果,就等它完整——既然没有任何东西能增量展示,流式输出什么也换不来。
管线中的环节。 如果输出直接喂给下一次调用,就没有观察者在受益。
批处理。 没人在等。
展示前的后处理。 如果你要先校验或重排格式再显示,那反正需要完整回复。
规则是:有人在盯着文字出现时才流式。其他情况下,它只是白添的复杂度。
在 agent 循环里
agent 是另一种情况。单个模型调用未必值得流式——真正有价值的输出是工具调用,不是散文。
但循环本身应该把进度流式输出。十二步的沉默是不能接受的,展示当前在跑哪个工具能让整次运行可读。要在循环这一层流式输出,而不是 token 这一层。
要点回顾
- 流式输出是生成天然的形态;缓冲才是加上去的那一步。
- 它把首 token 延迟(预填充,随 prompt 长度增长)和每秒 token 数(解码,大致恒定)分开了。
- 感知延迟主要由首 token 延迟决定,因为阅读比生成慢。
- 事件携带的是 token 片段,错误可能在流中途到达,token 计数最后才来。
- 结构化输出、管线环节和批处理都别用流式——agent 该流式输出的是进度,不是 token。
下一篇: 砍掉 API 账单