流式工具调用在一把 key 上:各后端的 arguments 分片怎么拼回完整 JSON
聚合站用一把 Key 调多个模型时,流式工具调用的最大坑在于不同后端把 tool_call 的 arguments 切成 chunk 的方式不一致。本文梳理 Claude、GPT、DeepSeek、Qwen、GLM、Kimi 在流式分片上的常见差异,给出「按 index/id 聚合、收齐再解析」的统一策略,并说明在聚合网关层如何做保守兜底(缓冲区上限、半截 JSON 处理、严格模式降级),让工具调用在切换模型时不中断。
为什么流式工具调用容易出问题
非流式请求里,tool_calls[].function.arguments 是一个完整的 JSON 字符串,JSON.parse 一次就完事。流式请求里,这个字符串会被切成若干片(delta),逐个塞进 SSE chunk。问题在于:切片规则没有跨厂商统一标准。
同一段参数,有的后端按 token 边界切,有的按字符切,有的在开头补一个空片,有的把 id、name 只放在第一片。客户端如果假设「每个 chunk 都是合法 JSON」,就一定会在某个后端上崩。
在聚合站场景下这一点被放大:你手上只有一把 API Key,同一个 SDK、同一段解析代码要同时面对六种后端。所以解析层必须做得比「对接单一厂商」更保守。
六个后端的分片差异,归成三类
不必背每个厂商的细节,把差异归纳成三类就够写代码了。
1. 分片粒度与边界
- 有的后端按 token 切,片与片之间可能落在 JSON 字符串内部,甚至切在多字节 UTF-8 字符中间(取决于上游序列化方式)。
- 有的后端倾向于在较自然的边界切,单片的字符串更完整。
- 还有的会在第一个分片里给出
arguments: ""或很小的前缀,之后再逐片追加。
结论:不要对单片长度或内容做任何假设。字符串拼接是唯一安全的操作。
2. 元信息出现的位置
tool_call 的 index、id、function.name 出现位置不一致:
- 通常第一个分片带
index和id,name也在第一片。 - 后续分片只带
index和arguments的增量,id/name缺省。 - 个别后端会在每个分片重复
index,但id只在首片出现。
结论:首片负责建立「这是哪个工具调用」的身份,后续分片只做追加。聚合时要按 index 归并,而不是按到达顺序新建对象。
3. 结束信号的位置
- 有的后端在最后一个参数分片之后,再发一个
finishreason: "toolcalls"的 chunk。 - 有的把
finish_reason放在同一个 chunk 里。 - 有的在参数流结束后先发一个空 delta,再发结束标记。
结论:不要用「收到 finish_reason」当作参数已完整的充分条件,要同时跟踪「该 index 是否已收到结束」。
统一策略:按 index 聚合,收齐再解析
核心思路很简单:流式只做搬运和累积,解析放到最后一次性做。
数据结构
维护一个按 index 索引的 map:
calls: Map<index, {
id: string | null,
name: string | null,
argsBuffer: string, // 纯字符串累加
done: boolean
}>
每收到一个 chunk:
- 遍历
delta.tool_calls(可能为空数组,也可能不存在)。 - 对每一项取
index;若 map 中不存在则创建。 id、name只在当前值为 null 时写入(首片优先,后续不覆盖)。arguments无条件argsBuffer += fragment。不要 trim,不要尝试中途 parse。- 若该 chunk 的
finishreason为toolcalls,把对应 index 标记done(或在流结束时统一标记)。
收尾与解析
流结束后:
- 按
index升序遍历。 - 对每个
argsBuffer做一次JSON.parse。 - 空字符串按
{}处理(无参工具很常见)。 - 解析成功则组装成标准的
tool_calls数组回给业务层。
这一段的关键纪律是:在流进行中绝不 parse。半截 JSON 必然抛错,而错误处理一旦触发重试,流已经消费掉的部分就丢了。
聚合站上的严格模式兜底
上面的策略在单厂商下够用。经过聚合网关时,还有几件事要考虑。
缓冲区上限
参数理论上可以很长(比如让模型生成一个大 JSON)。给 argsBuffer 设一个上限,超过就截断并报错,而不是无限吃内存。这个上限按你的业务能接受的参数规模定,不要照抄别人的数字。
半截 JSON 的处理
如果流在参数没收齐就断了(网络中断、上游超时),argsBuffer 是半截 JSON。此时:
- 不要尝试用正则「修补」补全引号或括号,那会产出错误但看起来合法的参数。
- 直接把该次工具调用标记为失败,让上层决定是重试整个请求还是降级。
严格模式与非严格模式
建议在客户端分两档:
- 严格模式:必须拿齐
id、name、完整可解析的argsBuffer,否则视为失败。适合会真正执行副作用的工具(写文件、发请求)。 - 宽松模式:
argsBuffer解析失败时,把原始字符串交给上层人工/二次模型处理,或直接丢弃该工具调用、继续对话。适合只读型、可重试的场景。
聚合站无法改变上游的分片行为,但可以在网关侧做统一归一:把各后端的 delta 转成同一种聚合形态再下发。这样客户端只需实现一套收集逻辑,切模型时不用改代码。这也是「一把 Key 调多个模型」在工程上真正省事的地方。
切换模型时的注意点
- 不同后端对「无参数工具调用」的表达不同:有的给
"",有的给"{}",有的直接不发 arguments 分片。统一按{}兜底。 - 并行工具调用(一次返回多个 tool_call)依赖
index区分;如果某个后端不保证index连续,仍按出现的index建 map,不要用数组下标硬对。 name缺失时不要猜,视为该调用无效。
小结
流式工具调用的解析难点不在 JSON 本身,而在分片契约的不统一。把它当成一个纯粹的字符串累积问题:按 index 建桶、首片定身份、中途只追加、结束再解析、失败不修补。再加上缓冲区上限和严格/宽松两档兜底,同一套代码就能在聚合站的一把 Key 上稳定跑通多个后端。