这是消息顺序错误,不是工具错误。Claude 要求 assistant 回合中的每个 tool_use 块,都必须由紧接着的下一条 user 消息中的 tool_result 块作答——ID 必须相同,中间不能有其他内容。你的循环漏掉了一个,通常是因为工具抛出了异常。
错误
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "messages.1: tool_use ids were found without tool_result blocks immediately after: toolu_01A... Each tool_use block must have a corresponding tool_result block in the next message."
}
}原因和解决方法一览
| 原因 | 解决方法 |
|---|---|
| 工具抛出了异常,因此没有追加任何内容 | 仍需发送 tool_result,并设置 is_error: true,附上错误文本。 |
| tool_result 出现在更靠后的消息中 | 它必须出现在紧接着的下一条消息中——中间不能有 assistant 或 user 回合。 |
| tool_use_id 不相符 | 原样回传 tool_use 块中的 ID;不要重新生成。 |
| 历史记录在一次往返的中途被截断 | 按完整的工具往返截断,绝不要在一次往返的两部分之间截断。 |
不变量,一次说清
assistant 回合中的每个 tool_use 块,都必须在紧接着的下一条 user 消息中有且仅有一个 tool_result 块,并携带相同的 tool_use_id。同一回合中有多个 tool_use 块时,下一条消息中也需要有多个 tool_result 块。两个回合之间不能有任何内容。
即使工具执行失败,也始终要回复
模型完全能够处理失败的工具;它无法处理缺失的工具。将错误作为 tool_result 返回可以保持对话有效,通常也能得到合理的恢复处理,而不是 400 错误。
results = []
for block in (b for b in resp.content if b.type == "tool_use"):
try:
out = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(out),
})
except Exception as e:
# A failed tool still owes the model an answer.
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Tool failed: {e}",
"is_error": True,
})
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": results})发送前验证最后两个回合
只需十几行断言,就能在调用处捕获问题,而不是等到网络返回 400 错误——遍历 assistant 回合中的 tool_use id,并确认下一个 user 回合已对它们全部作出回应。
def check_pairs(messages):
for i, m in enumerate(messages):
if m["role"] != "assistant" or not isinstance(m.get("content"), list):
continue
ids = {b.get("id") for b in m["content"]
if isinstance(b, dict) and b.get("type") == "tool_use"}
if not ids:
continue
nxt = messages[i + 1] if i + 1 < len(messages) else None
answered = {b.get("tool_use_id") for b in (nxt or {}).get("content", [])
if isinstance(b, dict) and b.get("type") == "tool_result"}
missing = ids - answered
assert not missing, f"message {i}: unanswered tool_use {missing}"在往返边界截断历史
按消息数量截断上下文窗口最终会在 tool_use 和 tool_result 之间截断。决定丢弃内容时,应将这两个块视为不可分割的整体。
如果你通过 Kunavo 调用
这是你的请求载荷问题,Kunavo 不会替你掩盖:400 属于不可重试列表,因此格式错误的工具往返只会失败一次,而不会再消耗一次上游往返的延迟来得到同样的错误;被拒绝的请求记为零成本。在 /v1/messages 上,你直接使用 Messages 协议,因此 tools、tool_use 和 tool_result 块会被转发,而不会被转换。此时返回的 400 类型为 invalid_request_error,与 Anthropic 的 API 一致,并附带上游的消息文本,随后是上游自己的 request id,且没有 request_id 字段;在 2026 年 9 月 24 日之前,类型显示为 api_error,因此如果日志包含更早的记录,请根据 HTTP 状态和消息文本进行分支判断。
常见问题
我可以直接丢弃 tool_use 回合,而不回答它吗?
可以,只要丢弃整个 assistant 回合。无效的做法是保留 tool_use,却省略其 tool_result。
OpenAI 兼容端点也有同样的规则吗?
需要进行同样的配对,只是写法不同——assistant 消息中使用 tool_calls,然后针对每个调用使用一条 role: "tool" 消息,并携带 tool_call_id。
被拒绝的请求会产生费用吗?
在 Kunavo 上不会。失败的请求记为零成本,且永远不会到达计费的上游调用。