返回指南
疑難排解·2026年8月28日·閱讀約 6 分鐘

Claude API 400「找到沒有 tool_result 區塊的 tool_use ID」— 順序規則

這是訊息順序錯誤,不是工具錯誤。Claude 要求 assistant 回合中的每個 tool_use 區塊,都必須由緊接著的下一個 user 回合中的 tool_result 區塊回覆 — ID 必須相同,中間不能插入任何內容。您的迴圈遺漏了一個,通常是因為工具拋出了錯誤。

最後審核於 。

這是訊息順序錯誤,不是工具錯誤。Claude 要求 assistant 回合中的每個 tool_use 區塊,都必須由緊接著的下一個 user 回合中的 tool_result 區塊回覆 — ID 必須相同,中間不能插入任何內容。您的迴圈遺漏了一個,通常是因為工具拋出了錯誤。

錯誤

response (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 回合已回覆全部 ID。

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 呼叫

這是您的 payload,Kunavo 不會替您掩蓋:400 屬於不可重試清單,因此格式錯誤的工具往返會失敗一次,不會再耗費第二次上游往返的延遲來得到相同錯誤;遭拒請求的成本記錄為零。在 /v1/messages 上,您直接使用 Messages 協定,因此 tools、tool_use 與 tool_result 區塊會原樣轉送,而不會被轉換。此時 400 會像 Anthropic API 一樣,以型別為 invalid_request_error 的形式回傳,並附上上游訊息文字、上游自己的 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 不會。失敗請求的成本記錄為零,也不會抵達任何計費的上游呼叫。

相關指南

更多錯誤語意請參閱 錯誤參考;透過 註冊 和 身分驗證指南 取得金鑰只需一分鐘。