這是訊息順序錯誤,不是工具錯誤。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 回合已回覆全部 ID。
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 不會。失敗請求的成本記錄為零,也不會抵達任何計費的上游呼叫。