中转站的工具调用为什么老失败:五个层面的原因
工具定义、schema、返回解析、多轮工具、流式工具,每一层都有各自的失败方式,附最小验证请求。
Agent 框架跑不起来,八成是工具调用这一环。而中转站是这一环里最容易加戏的地方:它可能改写工具定义、丢字段、把结构化返回拍平成自然语言。
一、先跑一个最小的工具调用
{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "北京现在天气怎么样?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"tool_choice": {"type": "function", "function": {"name": "get_weather"}}
}
tool_choice 强制指定,是为了排除"模型自己决定不调用"这个变量。如果这样都不返回 tool_calls,问题不在模型。
二、五层失败方式
第一层:工具定义被丢弃
有些站点只代理文本生成,tools 字段直接被忽略。表现是模型用自然语言回答"我无法查询天气",而不是返回工具调用。
验证:看响应里有没有 tool_calls 字段。没有就是被丢了。
第二层:schema 被简化
站点的转换层可能只保留 name 和 description,把 parameters 丢掉。模型不知道该传什么参数,就会瞎编。
验证:在参数里放一个必填的、不常见的字段(例如 city_code),看模型返回的参数里有没有它。没有就说明 schema 没传过去。
第三层:返回结构不标准
标准返回:
{
"choices": [{
"message": {
"role": "assistant",
"tool_calls": [{
"id": "call_abc",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"北京\"}"}
}]
},
"finish_reason": "tool_calls"
}]
}
注意 arguments 是字符串,不是对象。有些站点会直接给对象,你的 JSON.parse 就会炸。
验证:打印 typeof arguments。应该是 string。
第四层:多轮工具调用断了
完整流程是:模型返回 tool_calls → 你执行工具 → 把结果作为 role: tool 消息发回去 → 模型继续。
有些站点在第二轮不认 role: tool 消息,或者要求 tool_call_id 必须匹配但又不返回 id。表现是第二轮 400,或者模型无视工具结果。
验证:手动构造两轮请求,第二轮带一条 {"role":"tool","tool_call_id":"...","content":"..."}。
第五层:流式工具调用拼接错误
流式下 tool_calls 是分片到达的:第一个块有 id 和 name,后续块只有 arguments 的片段。客户端要按 index 拼接。
站点如果在这里改写,可能把片段顺序打乱或丢掉 index。
验证:流式请求,打印每个块的 tool_calls,看 index 是否连续、arguments 拼起来是否是合法 JSON。
三、Anthropic 和 Gemini 的差异
| OpenAI | Anthropic | Gemini | |
|---|---|---|---|
| 工具定义 | tools[].function | tools[](name/description/input_schema) | tools[].functionDeclarations |
| 调用返回 | tool_calls[].function.arguments(字符串) | content[].type == "tool_use",input 是对象 | parts[].functionCall,args 是对象 |
| 强制调用 | tool_choice | tool_choice: {"type":"tool"} | toolConfig.functionCallingConfig.mode = "ANY" |
| 结果回传 | role: tool | role: user,content 里放 tool_result block | role: user,parts 里放 functionResponse |
如果站点声称"兼容 Anthropic",但工具调用返回的是 OpenAI 的 tool_calls,你的 Anthropic SDK 会解析失败。
四、一个自测脚本的思路
- 发强制工具调用请求
- 断言
finish_reason是tool_calls(或对应值) - 断言
arguments是字符串且能解析 - 断言解析出的对象包含 schema 里的必填字段
- 第二轮带上工具结果,断言模型能继续
- 流式重复一遍,断言片段能拼成合法 JSON
六步都过,工具调用这一环才算通。
五、本站的覆盖情况
检测台的第六项检查做的是"强制函数调用必须返回标准结构",当前版本只覆盖 OpenAI 两种协议;Anthropic 和 Gemini 的工具检查记 SKIP,不假装通过。这个限制写在方法页里,是因为不同协议的工具体差异太大,用一个通用检查会给出误导性的结论。
六、如果站点不支持工具
有两个选择:
- 换站。如果工具调用是你的核心需求,不值得在兼容层上赌。
- 自己实现 ReAct。让模型输出 JSON 格式的动作,你解析后执行。这样不依赖站点的工具支持,但需要处理模型输出格式不稳定的问题。
第一种更省事,第二种更可控。取决于你的容错预算。