工具参数回来是一个 JSON 字符串而不是对象:在一把 key 上解析并校验六个后端的 tool call
多模型聚合 API 中,不同后端(Claude、GPT、DeepSeek、Qwen、GLM、Kimi)对工具调用参数的返回格式不一致:有的返回 JSON 字符串,有的直接返回对象。本文给出一段防御性解析与 schema 校验的写法,用一把 key 兼容全部后端,避免因格式差异导致的解析错误。
问题现象
用同一把 API key 调用多个模型时,工具调用(tool call / function call)的 arguments 字段有时是 JSON 字符串,有时已经是解析好的对象。如果不做兼容,代码会在其中一种后端上抛错。
典型错误包括:
- 对字符串直接取属性,得到
undefined - 对对象再调用
JSON.parse,抛出SyntaxError - 参数中出现未转义字符,导致 JSON 解析失败
- 后端返回空字符串或
null作为无参数调用的占位
为什么会有差异
工具调用的参数在协议层通常定义为字符串,便于流式传输和增量拼接。但不同厂商的 SDK、网关或聚合层可能在返回前做了预处理:有的原样透传字符串,有的直接反序列化为对象。聚合站不统一改写,是为了保留各家原始语义,因此兼容责任落在调用方。
防御性解析
核心思路:先判断类型,再决定是否解析;解析失败时保留原始文本用于排查。
function normalizeToolArguments(raw) {
// 无参数调用
if (raw == null) return {};
// 已经是对象
if (typeof raw === 'object' && !Array.isArray(raw)) {
return raw;
}
// 字符串:可能是 JSON,也可能是空串
if (typeof raw === 'string') {
const text = raw.trim();
if (text === '') return {};
try {
const parsed = JSON.parse(text);
// 解析结果必须是对象
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
}
throw new Error('arguments 不是 JSON 对象');
} catch (err) {
// 保留原文,便于定位是哪个后端返回了什么
err.rawArguments = raw;
throw err;
}
}
throw new Error(`不支持的 arguments 类型: ${typeof raw}`);
}
注意点:
- 空字符串要当作“无参数”,而不是解析失败
- 数组不是合法参数,应视为错误
- 解析失败时不要静默返回
{},否则会把参数错误伪装成“没有传参”
Schema 校验
解析成对象只是第一步。不同后端对同一工具的字段命名、必填性、类型可能理解不同,需要独立的校验层。可以用轻量校验库,也可以手写。
function validateAgainstSchema(obj, schema) {
const errors = [];
for (const [key, rule] of Object.entries(schema)) {
const value = obj[key];
if (value === undefined) {
if (rule.required) errors.push(`缺少必填字段: ${key}`);
continue;
}
if (rule.type && typeof value !== rule.type) {
errors.push(`字段 ${key} 类型应为 ${rule.type},实际为 ${typeof value}`);
}
}
// 拒绝未声明的字段,避免后端多传导致下游误用
for (const key of Object.keys(obj)) {
if (!(key in schema)) errors.push(`未知字段: ${key}`);
}
return errors;
}
工具定义示例:
const weatherSchema = {
city: { type: 'string', required: true },
unit: { type: 'string', required: false },
};
调用流程:
const args = normalizeToolArguments(toolCall.function.arguments);
const errors = validateAgainstSchema(args, weatherSchema);
if (errors.length) {
// 把 errors 回传给模型,让其修正参数
return { role: 'tool', content: JSON.stringify({ errors }) };
}
把错误回传给模型
工具参数校验失败时,推荐把结构化错误作为工具结果返回,而不是直接抛异常终止对话。模型通常能根据错误信息重新生成合法参数。这比在应用层猜测意图更可靠。
{
"role": "tool",
"tool_call_id": toolCall.id,
"content": JSON.stringify({ ok: false, errors })
}
流式场景的额外处理
流式返回时,arguments 是分片拼接的。不要在分片阶段解析,应在收到该工具调用的结束标记后,对完整字符串统一走 normalizeToolArguments。如果某个后端在流式结束后仍返回对象,同一个函数也能处理。
一把 key 的收益
用聚合站的一把 key 调多个后端时,把格式差异收敛到一个 normalizeToolArguments 加一层 schema 校验,可以让上层业务代码不感知后端差异。切换模型只需改模型名,工具调用链路不变。用户按官方价 ×1.3 扣费,贡献 key 按官方价 ×1.1(优质 ×1.2)返 USDC,成本结构透明。
小结
- 先判类型再解析,空串视为无参数
- 解析失败保留原文,不静默兜底
- 解析成功后独立做 schema 校验
- 校验错误回传给模型,让其自我修正
- 流式场景拼接完整后再解析
这样一套写法即可在一把 key 上兼容六个后端的工具调用返回格式。