上周三我在用 Claude Code 跑一个自动化重构任务,模型切到 claude-opus-5.5 之后,工具调用的结果突然全变成空的了。stop_reason 明明是 tool_use,content 数组里也有 type: tool_use 的块,但 input 字段要么是 {},要么直接被客户端解析逻辑静默丢弃。折腾了大半天才搞清楚------不是 MCP 配置错了,也不是网络问题,是 input_schema 定义如果缺了 type 字段或者 required 不完整,模型会返回空的 input 对象而不是报错。修复方法很简单:把每个 tool 的 input_schema 补全为符合 Anthropic 要求的 JSON Schema 格式就行。下面是完整的排查过程和修复代码。
先看完整的调用链路
搞清楚问题出在哪一环之前,得先理解 Claude Code 工具调用到底是怎么跑的:
sequenceDiagram
participant U as 你的代码/Claude Code
participant A as Anthropic API (claude-opus-5.5)
participant T as 本地工具(bash/文件读写等)
U->>A: messages.create(tools=[...], messages=[...])
A->>U: stop_reason=tool_use, content=[{type:tool_use, input:{...}}]
U->>T: 解析 input,执行工具
T->>U: 返回执行结果
U->>A: messages.create(messages=[..., {role:user, tool_result}])
A->>U: 最终回复
关键在第二步:API 返回的 tool_use 块里,input 字段存放着模型希望传给工具的参数。如果这个 input 是空对象 {},你的工具执行逻辑拿不到参数,自然就"返回空"了。
为什么会出现这个问题
一句话:claude-opus-5.5 比之前的模型对 input_schema 的格式要求更严格了。
之前用 claude-opus-4.7 甚至 claude-sonnet-5 的时候,input_schema 写得糙一点也能跑。比如这样:
python
# 有问题的写法
tool_schema = {
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
注意到没?少了最外层的 "type": "object"。在旧模型上这居然能正常返回参数。但换到 claude-opus-5.5 之后,模型遇到不合规的 schema 会有两种表现,取决于问题的严重程度:
-
schema 格式不够严格 (如缺少
"type": "object"):不直接报 400,而是返回空的input: {}------它识别出你定义了工具,也决定调用这个工具,但因为 schema 不合规,生成的参数被内部校验拦截,最终给你一个空对象。 -
schema 存在明显错误 (如整个
input_schema字段都缺了type):API 直接返回 400,错误信息如下:Error: 400 Bad Request
{"type":"error","error":{"type":"invalid_request_error",
"message":"tools.0.input_schema: JSON Schema must have a 'type' field"}}
第一种情况特别阴间,因为它不报错。stop_reason 还是 tool_use,HTTP 状态码还是 200,你不仔细看 input 的内容根本发现不了。
我那天遇到的情况是:5 个工具里有 3 个的 schema 写法不够严格,其中 2 个静默返回空 input,1 个直接 400。排查起来挺烦人的,因为表现不一致。
方案一:补全 input_schema 为符合 Anthropic 要求的 JSON Schema 格式
根本解决方法。每个 tool 的 input_schema 必须包含三个东西:
- 最外层
"type": "object" properties里每个参数都有明确的typerequired数组列出所有必填参数
修复前(会触发空 input):
python
# 缺少 type 和 required 的写法
bad_schema = {
"properties": {"command": {"type": "string"}}
}
修复后:
python
# 补全后的正确写法
good_schema = {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]
}
就这么点区别。加上 "type": "object" 和 "required" 之后,claude-opus-5.5 立刻正常返回完整的 input 参数了。
如果你在 Claude Code 里用的是 settings.json 配置自定义工具,对应的修复片段长这样:
json
{
"tools": [{
"name": "bash",
"description": "Run a bash command",
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute"
}
},
"required": ["command"]
}
}]
}
每个工具都检查一遍,别偷懒。我当时就是因为觉得"之前能跑就不改了",结果在 opus-5.5 上翻车。
方案二:加一层 input 校验防御,避免静默失败
即使 schema 写对了,网络抖动、API 行为变更这些事谁也说不准。加一个防御性检查,遇到空 input 直接抛异常而不是静默继续:
python
for block in response.content:
if block.type == "tool_use":
if not block.input:
raise ValueError(f"空 input: {block.name}")
# 正常处理逻辑
这段加在你解析 tool_use 块的地方就行。比静默丢弃好一万倍------至少你能在日志里看到是哪个工具出了问题。
方案三:用聚合 API 网关统一管理,减少配置出错的概率
我这次踩坑还有一个背景:我同时在测好几个模型(claude-opus-5.5、claude-sonnet-5.5、gpt-5.5),每个模型的 base_url 和 key 都不一样,切来切去特别容易搞混。后来我把调用统一走了聚合网关,像 OpenRouter、ofox.io 这类平台,改一个 base_url 就能切模型,不用每次都改 key 和 endpoint。
比如通过这类聚合平台调 opus-5.5,代码就这样:
python
import anthropic
client = anthropic.Anthropic(
api_key="your-ofox-key",
base_url="https://api.ofox.io/v1" # 示例值,以平台实际文档为准
)
注意 :
base_url的具体格式取决于平台实现。若平台走 OpenAI 兼容接口,应改用openai库;若平台实现了完整 Anthropic 协议则可直接用anthropic库,以平台文档为准。ofox.io 为第三方平台,使用前请自行核实其服务条款与可用性。
模型名、tools 定义、schema 格式这些完全不变。好处是切模型的时候只改 model 参数,不用动 key 和 URL,出问题排查范围小很多。当然聚合网关只是解决了调用入口统一的问题,schema 写错该报错还是报错,这个锅谁也替不了你。
完整的 agentic loop 别忘了回传 tool_result
顺便提一个相关问题。有些人的"工具调用返回空"其实不是 input 空,而是忘了把工具执行结果回传给 API。Claude 的工具调用是一个往返协议:
graph TD
A[发送请求] --> B{stop_reason?}
B -->|end_turn| C[直接用 response]
B -->|tool_use| D[解析 input 执行工具]
D --> E[构造 tool_result 回传]
E --> A
如果你收到 stop_reason: tool_use 之后直接取 content 当最终结果用,那当然是"空"的------模型还没给你最终答案呢,它在等你把工具结果喂回去。
漏了 tool_result 回传的话,下一轮请求会直接报这个:
Error: 400 Bad Request
{"type":"error","error":{"type":"invalid_request_error",
"message":"tool_use block at index 1 has no corresponding tool_result block"}}
回传的代码:
python
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
然后把 tool_results 作为 user message 的 content 传入下一轮 messages.create。
常见问题 FAQ
Q: Claude Code 报 "model not found",模型名到底怎么写?
直连 Anthropic API 的话模型名写 claude-opus-5.5 就行,不需要厂商前缀。通过聚合平台的话,部分平台要求加 anthropic/ 前缀,写成 anthropic/claude-opus-5.5,具体以你使用的平台文档为准。
Q: 并行工具调用(parallel tool use)的多个 tool_use 块怎么处理?
claude-opus-5.5 支持一次返回多个 tool_use 块。你需要遍历 response.content,收集所有 type == "tool_use" 的块,分别执行,然后把所有 tool_result 打包成一个 user message 回传。不能只处理第一个就把剩下的扔了。
Q: 工具调用频繁报 529 Overloaded 怎么办?
实现指数退避重试。初始等待 1 秒,每次翻倍,最多重试 5 次(具体参数可根据实际情况调整)。opus-5.5 的推理开销比较大,高峰期 529 不少见。报错长这样:
Error: 529
{"type":"error","error":{"type":"overloaded_error",
"message":"Overloaded"}}
Q: 修完 schema 还是返回空 input,还有什么可能?
检查一下你的 tool_choice 设置。如果设成了 {"type": "none"},模型不会调用工具。设成 {"type": "auto"} 让模型自己决定,或者 {"type": "tool", "name": "你的工具名"} 强制调用特定工具。另外确认 max_tokens 给够了------token 预算太小的话,模型可能来不及生成完整的 input 就被截断。
Q: opus-5.5 的工具调用成本大概多少?
我的最终做法
这个问题的根因很简单------input_schema 格式不规范。但之所以排查了大半天,是因为 claude-opus-5.5 的静默丢弃行为太坑了:schema 格式不够严格时不报错不警告,200 OK 给你一个空对象;只有 schema 存在明显错误时才会直接返回 400。两种情况混在一起,表现不一致,排查起来格外费劲。
我现在的做法是三管齐下:schema 写严格、解析时加空值校验、调用入口走聚合网关方便切模型排查。搞定之后再也没遇到过这个问题。希望这篇能帮你少踩半天坑。