流式工具调用最容易踩的坑,不是分片拼接,而是把参数当成了一个完整 JSON 去校验。分片还没到齐的时候,那个左花括号一直在,任何 json.loads 都会失败;等它成功时,参数可能已经被下游执行完了。这块必须换一种校验思路:校验的对象从“一个完整文档”变成“一条逐步生长的字节或字符流”。
先接受一个事实:大部分分片不是合法 JSON
在 OpenAI 风格的 tool call 流里,函数名通常出现在第一个 delta,arguments 字段以后续增量片段的形式一点点追加。参考实现里可以看到,累积状态从 {'name': 'Multiply', 'args': {}, 'id': 'call_xyz1'},随着 a、b 进入,慢慢变成 {'a': 3, 'b': 12}(智能体开发者社区那篇流式上下文处理工具调用的笔记给出的输出示例)。注意,中途的 args 之所以是空的,不是解析失败,而是参数还没到。这意味着对每个增量都跑一次完整解析是浪费,而且几乎必然报错。
暴力补花括号、给缺引号补引号能提升单次解析成功率,但埋下两类风险:一是括号数量、转义状态靠正则估算,复杂嵌套下会判错;二是补全过程中一旦给出“看起来合法”的参数,下游可能提前执行不完整的调用。增量校验要盯的恰恰是“这条参数流现在还欠什么”,而非“能否通过完整性校验”。
把“能不能解析”降级成分类判断
比较实用的做法,是每收到一个片段先做低成本的词法扫描,把当前缓冲归类:
- 引号是否闭合、转义是否在尾部断掉(即上一个字符是未配对的
\); - 嵌套深度花括号与方括号的配平差额;
- 当前是否处在键名阶段、冒号前、值起点;
- 值是否处于纯数字中途,比如只拿到了
3.还没到3.12。
这四件事比完整解析便宜得多,因为扫描速度与增量长度成线性关系,不必回溯整条缓冲(除非刚好断在转义符或数字尾部)。有了这些分类,我可以定义一条“可执行门槛”:只有当所有引号闭合、嵌套归零、且底层解析器恰好能接受时,才把这个调用置为 ready 状态。中间状态不触发任何工具执行,只更新一个结构化的 tool_call_state 对象。
校验本身用二次判定跳过大部分无效重试:buffer 长度没越过上一次校验的版本号,且新增字符并非结构性字符(引号、花括号、反斜杠、数字结尾),就快速跳过本次解析。这样在做高并发对话时,一个很耗 CPU 的完整解析变成只在关键边界触发。
def step(buf, frag):
buf += frag
scan = lex_hint(buf)
if not scan.closed_quotes: return PENDING
if scan.brackets > 0: return PENDING
try:
obj = json.loads(buf)
except ValueError as e:
return REPAIR_AND_REJECT if scan.tail_backslash else INVALID
return READY(obj)三道具体的失败情况
分片断在转义和 Unicode 上
如果契约生成方会在两个分片之间断开 \\ 或包在代理对里的一个 emoji,拼接之后就可能得到一个孤立的 UTF-8 半截字节。接受中文字段的参数时,工程上应把缓冲区按字节流处理,解码交给事务边界处的解码层,而不是每片 string += 后再去判断(在 Python 层,字符串通过 append 拼接本身没问题,问题出在提前以 `String` 签名切分的数据包)。我见过把参数直接塞进 tool call callback 里做正则替换的写法,中文全角引号一旦拆分,会被误判成未闭合引号,然后工具调用就被复位,用户只能重说一遍。
幂等性被提前执行破坏
有些工具本身可重入(查天气、查订单),有些不行,比如扣款、下单、发消息。对前者,参数读全之前执行的风险低;对后者,必须等到前面说的 ready 门槛把参数喂进去,而这里的关键往往是中间参数暂存与调用参数的解耦:如果参数 key A 是分片到的某个值,需要确定的是“最终态”是否已经进入,不是在每一个片段时推算 next arg。增量校验在这里承担的是判断“可以开始”还是“继续等”,而不是“现在能部分执行不”的责任。
服务端切片与模型端不一致
各个 API 的服务端切片粒度和中断位置并不一致。同一段参数,有的平台按 token 边界切,有的会把 {"city": 切到单独一块,然后在下一 chunk 里发 "Beijing"}。这让“只看刚到的这一片”的写法很危险,处理必须带上累积缓冲,而且校验逻辑本身要在长流上保持稳定,不致于在某一种奇怪的 split 面前失效。单元测试只按服务端帮助文档里的那套格式写是不够的,至少应该收集几家不同供应商(或者同供应商单态、FIM 等多种写法)的真实字节流回放。
超长参数与大参数
如果工具参数包含整段代码或长文档,缓冲区规模有上限,重解析一整块 buffer 的开销会越来越昂贵。这时候就可以做一个演进:持续累积识别到的结构和值,每块有效内容增量写进一个小状态器,只在遇到结构性不确定状态(断引号、断转义、断数字)才做一次带限制的回退,最多回退到上一个安全解析点,并且记录这个接受位点,并把接受过的片段转交工具侧消费它需要的参数键。这样不会因为大块参数而卡住其它并发调用。
该不该做校验之外的“参数重写”
一个值得提前想清楚的选择:增量阶段只做语法校验和状态集中,参数规范化(删除尾部逗号、给字符串补上单双引号、数字变成字符串等)应当尽可能放在执行前的窄口里一次做掉,而且要留下改写日志。一次错误的重写与随后的工具执行叠加,比单单死锁更伤;留痕后回放异常时能直接定位是分片边界问题还是模型本身输出的问题。
回到一件事
增量参数校验的目标没变:在参数完整可执行之前,拦下一切把它抛到下游的举动。方法本身用的是缓冲累积、词法自述完成度、约束的解析、成本有界的校验和最终的一次性重写;所有设计都围绕一个事实——流式工具调用的数据在绝大多数时间里不是 JSON,而是一段正在生成的字符串。
转载请注明出处: 皮哈皮阿
本文的链接地址: https://blog.phpat.com/ai-engineering/638.html
微信或手机浏览器扫一扫,随时随地随心阅读与分享
-
推理成本下降之后:模型路由、KV Cache 与端侧推理
上个月一个团队拿着账单来找我:模型单价降了,费用却涨了三倍。看完调用链就明白了,问题不在单价,在用法。单位 token 便宜,不等于每次任务便宜。
2026/10/10
-
2026 年 AI 应用评测:从单轮打分到多轮轨迹评估
一个 Agent 的评测分数从 72% 涨到 81%,团队很高兴,直到发现它学会了在失败时直接结束任务,把“没做完”算成了“无需回答”。评测设计错了,指标越好看越危险。
2026/10/19
-
长度惩罚参数调优:常见参数名字容易混淆
长度惩罚在主流 API 里并不存在。真正管长度的是 max_tokens 和 stop sequences,frequency、presence、repetition 惩罚管的是用词重复和新词探索。把这些旋钮分开,能避免输出被截断、术语被替换成同义词两类故障。
2026/10/23
-
动态拼批不是开个开关:线上拼批的取舍与门禁
在线服务里的动态拼批和离线批量推理是两码事。窗口时长、KV Cache 水位、批内参数对齐共同决定拼接能否成立。上线前该盯的是 p99 和显存峰值,而不只是吞吐。
5天前
-
推理模型的流式输出:thinking 块怎么流
推理模型的流式输出不只是逐字渲染:思考块、正文、工具调用参数混杂在 SSE 流里,客户端要按字段分流、独立缓冲、支持中途中断。记录接通义千问与智谱过程中的判断和踩坑。
6天前
-
推理服务排队延迟的分层观测:从请求数到Token账本
盯 QPS 和 P99 看不到排队延迟长在哪。把这件事拆成入口→上游准入队列→后端等待→batch 解码四层,用 vLLM 暴露的排队与 KV 缓存指标做层级归因,讨论分层之后仍然看不见的部分与取舍。
4天前
京公网安备11011402053654 
@muffin:链接已更新,分类图像功能...
AI网址导航模版图标库更换指南
作者大大,图标库邀请链接失效了,麻烦更新...
AI网址导航模版图标库更换指南
太好看了,快点更新!
友情链接
@戴墨镜的刘备:哈喽,路过点赞。
vue.js组件之手写签名板vue-signature-simple
太好看了,快点更新!太棒了!👍真的很赞!
vue.js组件之手写签名板vue-signature-simple
你写得非常清晰明了,让我很容易理解你的观...
Emlog插件RSS订阅器发布