流式输出的体验账,和它带来的麻烦
同样的总耗时,流式和非流式在用户感受上完全是两回事。6 秒后一次性吐出 500 字,用户会觉得"卡住了";1 秒开始出字、6 秒出完,用户会觉得"挺快"。
这是纯粹的感知问题,但值得做。
流式的三个工程代价
一、错误处理变复杂
非流式的时候,失败就是失败,返回一个错误码,前端弹个提示。流式开始输出之后中途断了,用户已经看到半截内容,这时候弹"生成失败"很尴尬。
我们的处理是:已经输出的内容保留,在末尾追加一句"(生成中断,可点击重试)",同时把这次请求标记为不完整,不计入成功统计。
二、不能做后置校验
非流式可以等全文出来,跑一遍敏感词和格式校验,不合格就拦掉。流式做不到——字已经给用户了。
只能改成前置约束加边流边检:每积累 20 个字跑一次检查,命中敏感规则立即中断。这会带来误伤(有些词只有结合后文才不敏感),所以要定期看误伤率。
三、代理和网关会缓冲
这一条坑了我们很久。本地开发流式正常,部署到线上就变成"一次性吐出来"。查了半天发现是中间的网关默认开了响应缓冲。
如果你的流式上线后不"流"了,先查这几处:Nginx 的 proxy_buffering、CDN 的缓冲配置、以及应用层的压缩中间件。有些 gzip 实现会把小包攒起来一起发。
首字延迟怎么压
首字延迟主要由三部分组成:网络往返、模型排队、模型生成第一个 token。
- 网络往返:把服务部署到离模型 API 近的地域,能省几十到上百毫秒
- 模型排队:高峰期明显,可以准备一个备用模型,首字超过 3 秒就切换
- 生成第一个 token:提示词越短越快,尤其是系统提示词,能砍就砍
我们做过一次对比:系统提示词从 1200 token 砍到 400 之后,首字延迟中位数从 1.8 秒降到 1.1 秒。
什么场景不适合流式
结构化输出(比如返回 JSON 给下游系统用)不适合流式,因为下游要的是完整对象,流式反而增加拼接成本。批量任务也不适合,用户根本不在屏幕前等。
一句话:有人在盯着屏幕等,就用流式;没有,就别折腾。