structured-tool-use-round-trip · ZH · 2026-10-19

工具调用往返测试:哪些后端经聚合站转一手后字段不丢

通过聚合站调用多个模型时,工具调用(function calling)的字段在转发环节可能被改写或丢失。本文给出一个可复现的往返测试方法:同一份 function schema 分别发给六个后端,对比 tool_calls 结构、arguments 字符串与 finish_reason 的返回差异,并说明如何定位问题出在客户端、网关聚合层还是上游模型。

为什么工具调用值得单独测一轮

工具调用是结构化输出里最容易在转发链路上被“磨掉细节”的部分。一次请求至少要经过客户端 SDK、聚合站网关、上游供应商三层,任何一层对字段做了解析再序列化,都可能让原本合规的响应变得不可用。

常见症状包括:toolcalls 数组变成单个对象、arguments 从 JSON 字符串变成已解析的对象、finishreason 从 tool_calls 退化成 stop,以及并行调用被合并成一个。这些差异不会报错,只会让下游的解析逻辑悄悄走错分支。

测试的基本思路:一次 schema,六次调用

核心原则是控制变量。同一份 function schema、同一段用户消息、同一组采样参数,只更换后端模型。

建议准备一个最小但覆盖典型难点的 schema:

  • 一个必填字符串参数(如 city)
  • 一个枚举参数(如 unit: celsius | fahrenheit)
  • 一个带默认值的可选整型参数
  • 一个嵌套对象或数组参数(这是最容易在转发中被压平的部分)

用户消息设计成必须触发工具调用,例如直接询问一个需要查询才能回答的事实性问题。不要用“请调用工具”这类提示,它会掩盖模型自身的决策差异。

需要逐字段核对的返回结构

拿到响应后,不要只看“有没有 tool_calls”,要按下面的清单逐项比对。

tool_calls 的容器结构

确认 tool_calls 是数组,且每个元素带 id、type、function 三个字段。重点看:

  • id 是否非空且在各后端间格式不同(有些是 call_ 前缀,有些是纯 UUID)
  • 单次请求返回多个并行调用时,数组长度是否与模型实际意图一致
  • type 是否始终为 function,而不是被改写成别的字面量

如果聚合层把多个调用合并成一个,或把数组降级为对象,客户端的多工具分发逻辑就会失效。

arguments 的形态

规范要求 arguments 是 JSON 字符串,而不是对象。这是往返测试里最高频的坑。

  • 类型必须是 string,即使内容看起来像对象
  • 字符串内部必须是合法 JSON,可用 JSON.parse 直接解析
  • 嵌套结构是否保留:数组是否还是数组,嵌套对象是否被展平成点号键
  • 特殊字符与中文是否被正确转义,而不是变成乱码或 \u 序列

如果网关对 arguments 做了“友好化”处理直接返回对象,那么所有按规范写的客户端都会解析失败。这类改写通常发生在聚合层而非模型侧。

finish_reason

工具调用场景下,finishreason 应为 toolcalls。对比时留意:

  • 是否被统一改写成 stop(常见于做响应归一化的网关)
  • 是否出现 length,说明参数 JSON 被截断,此时 arguments 往往不完整
  • 部分后端可能返回 function_call 这类旧字段名

finishreason 决定了客户端要不要进入工具执行分支,比 toolcalls 本身更早生效,因此必须单独断言。

定位问题出在哪一层

比对出现差异后,按下面的顺序缩小范围:

  1. 用同一个 key 直连上游官方端点(若条件允许),拿到基线响应
  2. 用聚合站的同一个 key 再发一次,比较两次的原始 JSON,而不是 SDK 解析后的对象
  3. 若官方直连就存在差异,属于模型侧行为,记录为后端特性
  4. 若只有经聚合站时字段变形,问题在网关的解析或序列化环节

关键操作是打印原始响应体。多数 SDK 会把响应解析成对象,这会掩盖“字符串被提前解析”这类问题。用 curl 或直接读 response.text 再手动解析,才能看到真实形态。

多后端对比时的预期差异

不同供应商对工具调用的实现成熟度不同,出现差异是正常的。把差异分成两类处理:

  • 合规性差异:字段类型、容器结构、finish_reason 取值,这些属于必须一致的部分,不一致就是缺陷
  • 行为性差异:是否倾向并行调用、参数填充的完整度、枚举值的选择偏好,这些属于模型特性,应记录并在业务侧做容错

把两类混在一起评估,会导致误判——把模型风格差异当成网关 bug,或反过来放过真正的字段丢失。

把它做成可重复的回归检查

一次手工测试的价值有限,建议固化成脚本:

  • 把 function schema、用户消息、断言规则写进配置文件
  • 对每个后端跑一次,输出原始响应快照与断言结果
  • 断言拆成硬性(类型、必填字段)和软性(参数语义)两组
  • 在上游模型版本更新或聚合站调整后重跑,对比快照差异

这样工具调用的兼容性问题会在影响线上业务之前暴露出来,而不是等到用户反馈“有时候工具不触发”时才开始排查。