ガイド一覧へ戻る
トラブルシューティング·2026年8月28日·読了6分

Claude API 400「tool_use ids were found without tool_result blocks」— 順序のルール

これはツールエラーではなく、メッセージ順序のエラーです。Claude では、assistant ターン内のすべての tool_use ブロックに対し、直後の user ターンで tool_result ブロックを返す必要があります。ID は同じで、その間に何も入れてはいけません。通常はツールが例外を投げたため、ループが 1 つ落としています。

最終確認日:。

これはツールエラーではなく、メッセージ順序のエラーです。Claude では、assistant ターン内のすべての tool_use ブロックに対し、直後の user ターンで tool_result ブロックを返す必要があります。ID は同じで、その間に何も入れてはいけません。通常はツールが例外を投げたため、ループが 1 つ落としています。

エラー

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."
  }
}

原因と対処法の概要

原因対処法
ツールが例外を投げたため、何も追加されなかったis_error: true とエラーテキストを含む tool_result を必ず送信します。
tool_result が後続のメッセージに入った直後のメッセージでなければなりません — その間に assistant または user のターンを入れてはいけません。
tool_use_id が一致しないtool_use ブロックの正確な ID をそのまま返します。新しく生成してはいけません。
履歴を往復の途中で切り詰めたツールの往復全体の境界で切り詰め、1 回の往復を構成する 2 つの部分の間では決して切り詰めないでください。

不変条件を一度で説明すると

assistant ターン内のすべての tool_use ブロックには、直後の user メッセージに、同じ tool_use_id を持つ tool_result ブロックが正確に 1 つ必要です。1 つのターンに複数の tool_use ブロックがある場合、その次の 1 メッセージ内に複数の tool_result ブロックが必要です。2 つのターンの間には何も入れてはいけません。

ツールが失敗しても必ず返信する

モデルは失敗したツールを適切に処理できますが、欠落したツールには対応できません。エラーを 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})

送信前に最後の 2 つのターンを検証する

十数行のアサーションで、ネットワークから 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 の間で切れてしまいます。削除対象を決めるときは、このペアを分割できない 1 単位として扱います。

Kunavo経由で呼び出している場合

これはペイロード側の問題であり、Kunavo が取り繕うことはありません。400 は再試行不可リストに含まれるため、不正なツールの往復は、同じエラーに至るだけの 2 回目の上流ラウンドトリップの遅延を費やすことなく、1 回で失敗します。拒否されたリクエストのコストは 0 として記録されます。/v1/messages では Messages プロトコルを直接使用するため、tools、tool_use、tool_result ブロックは変換されずに転送されます。そこでは Anthropic API と同様に、400 は型が invalid_request_error のエラーとして返り、上流のメッセージテキストに続いて上流自身の request id が返され、request_id フィールドはありません。2026 年 9 月 24 日までは型が api_error だったため、ログがそれ以前まで遡る場合は HTTP ステータスとメッセージテキストで分岐してください。

よくある質問

応答せずに tool_use ターンを削除してもよいですか?

assistant ターン全体を削除するなら可能です。無効なのは、tool_use を残して tool_result を省略することです。

OpenAI 互換エンドポイントにも同じルールがありますか?

同じペアリングが必要ですが、表記が異なります。assistant メッセージの tool_calls の後に、各呼び出しにつき 1 つの role: "tool" メッセージを置き、tool_call_id を付けます。

拒否されたリクエストに料金はかかりますか?

Kunavo ではかかりません。失敗したリクエストはコスト 0 で記録され、課金対象の上流呼び出しには到達しません。

関連ガイド

エラーの詳しい意味はエラーリファレンスをご覧ください。キーは新規登録と認証ガイドから1分で取得できます。