返回指南
故障排查·2026年8月28日·阅读约 6 分钟

Claude API 400“tool_use ids were found without tool_result blocks”——消息顺序规则

这是消息顺序错误,不是工具错误。Claude 要求 assistant 回合中的每个 tool_use 块,都必须由紧接着的下一条 user 消息中的 tool_result 块作答——ID 必须相同,中间不能有其他内容。你的循环漏掉了一个,通常是因为工具抛出了异常。

最后审核于 。

这是消息顺序错误,不是工具错误。Claude 要求 assistant 回合中的每个 tool_use 块,都必须由紧接着的下一条 user 消息中的 tool_result 块作答——ID 必须相同,中间不能有其他内容。你的循环漏掉了一个,通常是因为工具抛出了异常。

错误

响应(HTTP 400)
{
  "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 错误。

tool_loop.py
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 回合已对它们全部作出回应。

validate.py
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 上不会。失败的请求记为零成本,且永远不会到达计费的上游调用。

相关指南

更多错误语义请参阅 错误参考;通过 注册 和 身份验证指南 获取密钥只需一分钟。