推理模型把流式输出的复杂度抬高了一档。普通对话模型只吐一种东西:正文 token。推理模型吐两种,甚至三种:内部推理(thinking / reasoning_content)、最终正文、工具调用参数。SSE 协议那一层没变,但客户端要判断的边界多了很多。这两周我先后接了通义千问和智谱的流式接口,遇到的问题基本都在“怎么识别阶段切换”上,而不是“怎么收 chunk”。
SSE 这一层没变,变的是 chunk 里的东西
协议层面大家都知道:设置 stream=True,服务端建一条持久 HTTP 连接,每生成一个文本块就用 data: 推一行,最后用 data: [DONE] 收尾。差别在字段结构。非流式调用返回一个完整 JSON,正文落在 choices[0].message.content;流式则是逐块返回 choices[0].delta.content,由客户端拼接。阿里云百炼的文档把这件事写得很直白,同时提醒了一句容易被忽略的话:Qwen3 开源版、QwQ 商业版与开源版、QVQ,以及 Qwen3.5-Omni 这些模型只支持流式调用,非流式根本不给你走。
所以流式对推理模型不是优化项,是唯一通路。这意味着客户端第一行代码就得按流式写,不能先做同步版本再补流式。
用量统计也会变。OpenAI 兼容协议默认不在流里返回 token 消耗,得显式开 stream_options={"include_usage": true},开启后最后一个 chunk 不带 choices、只带 usage。写代码时如果只判 chunk.choices[0].delta.content,会在最后一个包上抛 IndexError。这是很常见的线上崩溃点,真正出问题的时候一般是在空回复或提前中断的边界上。
推理块和正文块的区分,是控制点
普通模型的流式,前端拿到 delta.content 直接追加进 DOM 就完事。推理模型不行,因为思考过程和答案的性质完全不同:思考过程可能很长,可能中途反悔,可能包含不该展示给终端用户的试探性语句;而答案是用户要的最终交付物。把两者混在一个缓冲区里,用户看到的是一大段自问自答然后突然收敛,体验很差,也没法做折叠。
工程上的做法是维护两个缓冲区,按字段名分流。推理内容通常另起一个字段返回(不同厂商叫法不同,很多实现用 reasoning_content),正文走 delta.content。只要每个 chunk 判断它带哪个字段,往对应的缓冲区追加即可。不要按“先后顺序”猜阶段,认为“先来的都是思考,后来的都是答案”——工具调用、多轮插入都可能打乱这个假设。字段名是唯一可靠的信号。
这里有个具体的取舍:思考块要不要实时推送。我的建议是推,但节流。推理模型的思考 token 生成速度可能比正文快,逐 token 推给前端会把渲染线程打满,尤其移动端。折中方案是服务端按 50 到 100 毫秒聚合一次再下发,把 SSE 的消息数降一到两个数量级,而用户几乎感知不到差别。代价是首字延迟多一点点,这个延迟对“思考中”这种状态展示无所谓。
中途停止是流式最大的隐性收益
文档里有一句容易略过但很实用的话:请求中断时,只计算服务端收到终止请求前已生成的部分。放进推理模型场景,这意味着用户看到“这个思路不对,它在往奇怪方向跑”时可以立刻点停,已经烧掉的思考 token 不再继续堆。长推理任务里这个操作能省掉大量成本,而如果非流式调用,你必须等它全部跑完才有机会断。
要做这件事,客户端得能中断底层连接(AbortController 之类),并且服务端要能感知断开。自己做网关的话,务必不要在客户端断开后还在后台把流读完再转存——见过这种实现,省成本的初衷完全被抵消了。终止时记得把两个缓冲区都持久化,半截思考过程对排查有用,但对用户展示要明确标成“已停止”,不要让半截内容看起来像完整答案。
LazyLLM、TextStreamer 这些小工具的定位
如果不在云 API,而是在本地拿 transformers 跑模型,好处是控制权全在自己。transformers 自带 TextStreamer,把每个新 token 的回调接管掉自己做出站就行;模型部署框架 vLLM、TGI 也提供现成的流式接口。LazyLLM 的思路不太一样:它把任务塞进带线程池的执行器,通过一个文件系统队列轮询取输出块,future.done() 作为终止条件。这个设计在单机调试时好用,可以同时兼容在线大模型和本地模型,stream=True 一开,OnlineChatModule 和 TrainableModule 的调用方式一致。但要注意它的轮询模型意味着延迟取决于轮询间隔,以及队列实现是多进程还是单进程,高并发下要先压一次。
几个容易踩的细节
- 拼接方式。百炼的示例用的是列表暂存片段、最后
"".join(),而不是循环里content += chunk。单请求无所谓,量级上来之后字符串反复拷贝是有成本的,照抄示例就行。 - curl 要加
--no-buffer。不加的话中间层会用标准输出缓冲,你会以为服务端没流,其实只是终端攒着。排查“为什么流式卡住了”第一步先确认这点。 - 结束信号与 business 结束信号要分开。
data: [DONE]是传输层的结束;一个推理任务逻辑上的完成,可能要结合finish_reason和是否有工具调用。只有前者就渲染“回答完毕”按钮,遇到多轮工具调用会误判。 - 计费口径确认清楚。百炼写的是按输入、输出 token 计费,流式与非流式规则相同。但思考 token 算不算输出,各家定义不一样,接新模型时先看账单再写成本估算模型,别按经验猜。
我对这件事的判断是:推理模型的流式控制,真正的难点不在协议,而在阶段化的事件建模。如果把 adapter 设计成一个只吐字符串的回调,后面迟早要被思考块、工具调用、usage 这三类信息逼着改接口。一开始就把事件定义成“阶段 + 内容块 + 元信息”的结构,上层 UI 想折叠就折叠、想隐藏就隐藏,后面换模型只改字段映射这一层,不动渲染逻辑。
转载请注明出处: 皮哈皮阿
本文的链接地址: https://blog.phpat.com/ai-engineering/618.html
微信或手机浏览器扫一扫,随时随地随心阅读与分享
-
2026 年 AI 应用评测:从单轮打分到多轮轨迹评估
一个 Agent 的评测分数从 72% 涨到 81%,团队很高兴,直到发现它学会了在失败时直接结束任务,把“没做完”算成了“无需回答”。评测设计错了,指标越好看越危险。
5天前
-
推理成本下降之后:模型路由、KV Cache 与端侧推理
上个月一个团队拿着账单来找我:模型单价降了,费用却涨了三倍。看完调用链就明白了,问题不在单价,在用法。单位 token 便宜,不等于每次任务便宜。
2026/10/10
-
长度惩罚参数调优:常见参数名字容易混淆
长度惩罚在主流 API 里并不存在。真正管长度的是 max_tokens 和 stop sequences,frequency、presence、repetition 惩罚管的是用词重复和新词探索。把这些旋钮分开,能避免输出被截断、术语被替换成同义词两类故障。
1天前
-
推理模型的流式输出:thinking 块怎么流
推理模型的流式输出不只是逐字渲染:思考块、正文、工具调用参数混杂在 SSE 流里,客户端要按字段分流、独立缓冲、支持中途中断。记录接通义千问与智谱过程中的判断和踩坑。
2小时前
京公网安备11011402053654 
@muffin:链接已更新,分类图像功能...
AI网址导航模版图标库更换指南
作者大大,图标库邀请链接失效了,麻烦更新...
AI网址导航模版图标库更换指南
太好看了,快点更新!
友情链接
@戴墨镜的刘备:哈喽,路过点赞。
vue.js组件之手写签名板vue-signature-simple
太好看了,快点更新!太棒了!👍真的很赞!
vue.js组件之手写签名板vue-signature-simple
你写得非常清晰明了,让我很容易理解你的观...
Emlog插件RSS订阅器发布