Claude 工具调用报错:schema、配对、ID 三件事

工具调用报错基本出在三处

工具定义里的 schema 不合法

tools 数组里每个工具都要带一份 JSON Schema 描述入参。字段名写错、类型写成字符串、漏了 properties,服务端在校验阶段就会拒绝整条请求。这类错误基本都是 400,响应体里会点名是第几个工具的第几个字段出了问题。

tool_use 和 tool_result 没有配对

模型返回工具调用请求后,你必须把执行结果按规定的格式回传。回传的位置、角色、条数都有硬要求。少回一条、回错角色、把结果放在 assistant 消息里,都会得到格式错误。

tool_use_id 对不上

每次工具调用请求都带一个 id,回传结果时要原样带上 id 做映射。模型并行请求了两个工具,你回传的顺序可以变,但两个 id 必须都在。少一个,或者把上次会话的 id 拿来复用,服务端会明确报找不到。

报错原文对照

走 OpenAI 兼容协议时的报错

{"error":{"message":"Invalid parameter: 'messages[3].tool_call_id' is required","type":"invalid_request_error"}}
{"error":{"message":"messages with role 'tool' must be a response to a preceding message with 'tool_calls'","type":"invalid_request_error"}}

第一条说明回传结果时忘了带 id,第二条说明你把 role: "tool" 的消息放在了一条没有工具调用的消息后面,顺序错了。

走 Anthropic 原生协议时的报错

{"type":"error","error":{"type":"invalid_request_error","message":"messages.1.content.0.type: Expected `tool_use` but got `text`"}}
{"type":"error","error":{"type":"invalid_request_error","message":"unexpected `tool_use_id` found in `tool_result` blocks: toolu_01abc. Each `tool_result` block must have a corresponding `tool_use` block in the previous message."}}

第二句是最常见的一条:回传的 tool_use_id 在上一轮里找不到对应的调用请求,通常是对话历史被截断了,或者客户端把旧的工具结果重新发了一遍。

输出被截断导致参数残缺

模型在生成工具参数时被 max_tokens 砍断,你会拿到一段不完整的 JSON,解析时报 Unterminated string in JSON at position 512 这类错误。这不是语法问题,而是输出长度不够。看到解析 JSON 失败,先去看结束原因是不是长度限制。

报错关键词真实原因改哪里
tool_call_id is required回传结果没带 id补上对应调用的 id
must be a response to a preceding message消息顺序错结果紧跟调用请求之后
unexpected tool_use_idid 在历史里找不到检查历史是否被截断
Expected tool_use but got text角色或内容类型错结果放对角色和位置
Unterminated string in JSON输出被长度截断提高 max_tokens
invalid schema工具定义不合法改成标准 JSON Schema

两套协议的写法差异

工具定义的结构不一样

OpenAI 兼容协议把工具包在 typefunction 两层里,参数 schema 放在 parameters;Anthropic 原生协议是扁平的 namedescriptioninput_schema。把一边的结构贴到另一边,就是 schema 校验失败。

项目OpenAI 兼容协议Anthropic 原生协议
工具声明tools[].function.parameterstools[].input_schema
模型请求调用assistant.tool_calls[]content[] 里的 tool_use
请求里的 id 字段tool_calls[].idtool_use.id
结果回传角色role: "tool"role: "user"
结果里的 id 字段tool_call_idtool_result.tool_use_id
结束原因finish_reason: "tool_calls"stop_reason: "tool_use"

结果回传的位置差异最容易踩

OpenAI 兼容协议是「一条结果一条消息」,每条 role: "tool" 的消息对应一次调用;Anthropic 原生协议是把所有结果作为 tool_result 内容块,统一放进一条 role: "user" 的消息里。并行调用两个工具时,后者是一条 user 消息带两个块,不是两条消息。写错了就会被判定为角色不交替。

结束原因的字段名不同

判断「模型是要调工具还是说完了」靠的是结束原因。OpenAI 兼容协议看 finish_reason,值为 tool_calls 时表示要执行工具;Anthropic 原生协议看 stop_reason,值为 tool_use 时同理。这两个字段名不一样,很多跨协议改写的代码就是在这里断的。

工具 schema 写错的四种典型

漏了必填字段声明

参数 schema 必须是标准 JSON Schema:有 type: "object",有 properties,必填的参数要列进 required。少一样都可能被判为不合法。下面这份是能直接用的一小份:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "查询指定城市的当前天气,参数为城市名",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "description": "城市名,例如 杭州"},
        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
      },
      "required": ["city"],
      "additionalProperties": false
    }
  }
}

description 写得太短

描述是给模型看的说明书。写「查询天气」它可能不知道该传什么参数;写清参数含义和取值范围,模型才能填对。参数错了的报错往往不是 schema 校验失败,而是工具真的执行失败,排查方向完全不同。

enum 和实际取值不一致

声明了枚举值,模型就一定只在这些值里挑。但执行端如果对大小写敏感、或者代码里写的是另一套取值,就会在真正调用时失败。声明和执行必须用同一套值。

用了不支持的字段

additionalProperties$ref、复杂的嵌套 oneOf 在部分实现里支持有限。遇到 schema 报错时,先把结构砍到最朴素的形态跑通,再一层层加回来。

参数可以是可选的,但类型不能含糊

可选参数用 required 之外的方式表达,类型声明要明确写出来。把可选参数的 type 省略,部分实现会当成任意类型放行,模型可能传一个对象进来,你的执行端就崩了。写清楚类型是几秒钟的事。

配对规则:一条调用一条回复

并行调用要在同一条回复里答复

模型一次请求三个工具,就要在同一条回复里给出三个结果。只回一个,剩下的会一直被算作「未完成」,部分实现会直接报配对失败。

不能只回一半的东西

工具执行失败也是结果,必须回传。把失败信息写进结果里,模型会据此调整策略。直接不回,整轮对话就卡在这里。

顺序不能颠倒

结果必须紧跟在发起调用的那条消息后面。中间插一条普通用户消息,角色交替规则就被破坏,报错通常是「消息顺序不合法」。

一套能跑通的多轮循环

OpenAI 兼容协议写法

import json, requests

URL = "https://xyuapi.top/v1/chat/completions"
HEAD = {"Authorization": "Bearer " + key, "Content-Type": "application/json"}

def run_tool(name, args):
    if name == "get_weather":
        return {"city": args.get("city"), "temp": 26, "sky": "多云"}
    return {"error": "unknown tool"}

messages = [{"role": "user", "content": "杭州天气怎么样"}]

for step in range(5):                       # 迭代上限必须设
    r = requests.post(URL, headers=HEAD, timeout=(10, 300), json={
        "model": "claude-sonnet-4-5-thinking",
        "max_tokens": 1024,
        "messages": messages,
        "tools": TOOLS,
    }).json()
    msg = r["choices"][0]["message"]
    messages.append(msg)
    if r["choices"][0]["finish_reason"] != "tool_calls":
        print(msg["content"])
        break
    for call in msg["tool_calls"]:          # 每个调用都要回一条
        result = run_tool(call["function"]["name"],
                          json.loads(call["function"]["arguments"]))
        messages.append({
            "role": "tool",
            "tool_call_id": call["id"],     # id 原样带回
            "content": json.dumps(result, ensure_ascii=False),
        })

Anthropic 原生协议写法

resp = client.messages.create(
    model="claude-sonnet-4-5-thinking",
    max_tokens=2048,
    tools=TOOLS_NATIVE,
    messages=messages,
)

if resp.stop_reason == "tool_use":
    blocks = [b for b in resp.content if b.type == "tool_use"]
    results = []
    for b in blocks:
        results.append({
            "type": "tool_result",
            "tool_use_id": b.id,            # 一一对应
            "content": json.dumps(run_tool(b.name, b.input), ensure_ascii=False),
        })
    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": results})   # 全部结果放一条

循环必须有终止条件

for step in range(5) 里的上限不是可选项。模型偶尔会反复调用同一个工具,没有上限就是一个停不下来的循环,每一次都产生一次计费请求。

历史太长时不要随手截断

多轮工具调用会迅速拉长对话历史。截断时如果把发起调用的那条消息删了,只留下结果消息,下一轮立刻报 must be a response to a preceding message。裁剪的正确单位是「一整组调用加它对应的全部结果」,要么整组留,要么整组删,不能拆开。

参数为空和空转怎么处理

参数解析失败要回传错误

json.loads 抛异常时不要直接崩掉,把错误信息当成工具结果回传,模型通常会自己修正参数再试一次。

结果是空值也要回传

工具返回空数组或者 null 都是合法结果。跳过不回,就会出现配对数量不足的报错。

别把工具结果拼成自然语言

结果的 content 字段用结构化文本,不要写「查询成功,结果是 26 度」这种话术,模型需要的字段和值。

参数缺失时给默认值而不是报错

模型偶尔会漏掉非必填参数。执行端给出合理默认值,比直接抛异常更稳,也更符合「工具是给模型用的」这个前提。抛异常会让整轮对话停住,而默认值最多让结果略有偏差。

工具真的执行失败怎么办

把异常转成结构化结果

工具内部报错时,不要让程序崩掉,把错误包装成正常结果回传:

{"ok": false, "error": "city_not_found", "hint": "请使用城市全称"}

模型收到这份结果,通常会换一个参数再试,或者直接告诉用户查不到。抛栈退出,整轮对话就断在这里,前面付出的几次请求全部作废。

超时和权限错误也要回传

工具调用外部接口超时,同样是一条结果,内容是超时说明。不回传,模型会一直等,你以为它在思考,其实它已经停了。结果里带上「可以重试」这类提示,能明显提高它自我修正的概率。

别让工具结果超过必要长度

工具结果会进入上下文,一次返回几千行原始数据,会挤占后续对话的空间,还会拉长每次请求的处理时间。只回模型用得上的字段,是控制成本的基本做法。

description 写得好不好,决定模型调不调它。

写描述时把三件事讲清:这个工具干什么、每个参数填什么、什么情况下该调用它。第三点经常被忽略,但它才是模型判断「该不该调用」的主要依据。描述里补一句「用户询问天气、气温、下雨时使用」,模型误调用的次数会明显下降。

循环终止还有一条收敛判断。

除了次数上限,还可以加一条:如果模型连续两次请求完全一样的工具和同样的参数,就视为卡住,直接结束循环并把已有结果交给用户。这条判断能挡住大多数空转,代价只有几行代码。

工具结果的编码要统一。

返回中文时统一用 UTF-8,也不要把结果二次转义成带反斜杠的字符串。转义错了的话,模型收到的是 \u 开头的码点串,读不懂内容,只能重复调用同一个工具。发现模型反复调同一个工具时,先去检查工具返回的那段文本长什么样。

排查顺序清单

顺序检查项通过标准
1打印结束原因确认是工具调用还是已结束
2校验工具 schema结构完整,类型正确
3核对调用与结果条数一次调用对应一次结果
4核对 id每个结果都带对应调用的 id
5核对位置结果紧跟调用之后,角色正确
6检查输出长度生成工具参数时空间充足

六步顺序不要跳。从结束原因看起,能立刻分清是「模型没想调工具」还是「调了但你回传错了」,这两类问题的改法完全相反。

接入地址和计费

一个 Key 调全系列模型

小鱼API 是 AI API 接入平台,走 OpenAI 兼容协议,主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1。支持 Chatbox、Cherry Studio、NextChat、Cline 等客户端,也支持带工具调用的多轮流程。按次计费,一次请求一个固定价,输入长短不改价,所以给工具塞大段上下文不会额外加钱。

模型单次价格
gemini-2.5-pro0.031 元/次
deepseek-v3.2-thinking0.049 元/次
claude-sonnet-4-5-thinking0.09 元/次
claude-opus-4-5-thinking0.12 元/次

工具调用流程一轮下来往往要发好几次请求,按次计费下这个次数是可控的、可预期的,迭代上限设成 5,成本上限就是 5 次请求,不会失控。

相关阅读

🚀 想要立即使用?来小鱼API体验全系列AI模型

查看全部产品

支持Gemini / Claude / GPT / Grok / DeepSeek · 国内直连 · 支付宝/微信