マガジン

Claude APIのツール呼び出しの流れ:tool_useから結果の返却までのループ設計

AIを使いこなす

Claudeで執筆し、GPTで自動校閲・修正しています。

机上の紙に箱と矢印で描かれた、依頼・実行・結果返却の往復を表す手描き図。
記事の内容をもとに制作したAI生成イメージです。実際の製品画面や検証結果の写真ではありません。

結論から言うと、Claude APIのツール呼び出しは「モデルが tool_use で依頼する → 自分のコードが実行する → tool_result で返す」を、stop_reason が tool_use である間くり返す往復です。モデル自身は何も実行しません。ループを止める条件は公式に示されている stop_reason の分岐で決まりますが、回数の上限は自分で設けるものです。この記事では、往復の手順図と、回数上限つきループの擬似コードを示します。 対象は、自前で関数(クライアントツール)を定義して呼ばせる場合です。確認した資料は、Claude Platform Docsの Tool use with Claude、How tool use works、Handle tool calls、Stop reasonsです(2026-10-27に閲覧。ページの更新日は確認できていません)。仕様は変わり得るので、実装前に最新の記述を見てください。

この記事の目次
  1. 往復の手順図
  2. 結果を返すときの守るべき形式
  3. ループを止める条件を `stop_reason` で決める
  4. 回数上限つきループの擬似コード
  5. 外部由来の内容は信頼しない
  6. 実装前の確認リスト

往復の手順図

次の図は、公式の説明を私が整理した説明用のものです。

text
[あなたのアプリ]                         [Claude API]
 1. messages + tools を送信  ───────────▶
                              ◀───────────  2. stop_reason: "tool_use"
                                               content に tool_use ブロック
                                               (id / name / input)
 3. name で関数を選び、input で実行
 4. 直前の assistant 応答をそのまま履歴に追加
 5. user メッセージに tool_result を入れて送信
    (tool_use_id = 手順2の id)  ───────────▶
                              ◀───────────  6. stop_reason を確認
                                               ├ "tool_use" → 手順3へ戻る
                                               └ それ以外   → ループ終了

ポイントは3つあります。

  • tool_use ブロックには id、name、input が入っています。返す tool_result の tool_use_id には、この id を使います。
  • 履歴には、assistantの応答(content 全体)を残したうえで、次のuserメッセージとして結果を送ります。APIには tool や function といった専用のroleはなく、user と assistant のメッセージの中にブロックとして入ります。
  • 並列呼び出しで tool_use が複数あった場合は、すべてに対する tool_result を同じuserメッセージにまとめます。

結果を返すときの守るべき形式

公式が明記している制約は次のとおりです。

  • tool_result は、対応する tool_use を含むassistantメッセージの直後のuserメッセージに入れます。間に別のメッセージを挟めません。
  • そのuserメッセージでは、tool_result ブロックを先頭に置き、テキストを足すなら後ろです。テキストが前にあると400エラーになると説明されています。
  • 実行に失敗したときは、エラー内容を content に入れ、is_error: true を付けて返します。「failed」だけでなく、何が起きたか・次に何を試すべきかまで書くよう推奨されています。
  • 引数不足などで呼び出しが不正だった場合も、is_error つきの tool_result で返せます。公式には、Claudeが2〜3回修正して再試行してから謝罪する、とあります。

ループを止める条件を `stop_reason` で決める

公式の基本形は「stop_reason が tool_use の間だけ続ける」です。それ以外の end_turn、max_tokens、stop_sequence、refusal では抜けます。実装では、次のように振り分けると迷いません。

  • tool_use、意味:ツール実行の依頼、ループでの扱い:実行して tool_result を返し、続行
  • end_turn、意味:通常の完了、ループでの扱い:最終回答として返して終了
  • max_tokens、意味:出力上限に到達、ループでの扱い:max_tokens を上げるか、応答を継続する。未完成の tool_use がある場合は、そのブロックを実行せず、履歴を変えずに再試行する
  • pause_turn、意味:サーバーツールのループが反復上限に達した、ループでの扱い:assistantの応答を履歴に入れて再送し、続きを実行
  • refusal、意味:応答の拒否、ループでの扱い:終了し、stop_details を見て対処
  • model_context_window_exceeded、意味:コンテキストの上限に到達、ループでの扱い:切れたものとして扱う

pause_turn は、Anthropic側で動くサーバーツール(web_searchなど)を使うときに出ます。自前の関数だけなら通常は気にしなくて構いませんが、混在させるなら分岐に入れておきます。また、サーバーツールとクライアントツールが同じ並列呼び出しに含まれると、tool_use のまま返ることがあります。その場合は、自分が実行したクライアントツールの tool_result だけを、テキストを添えずに送り、tools 配列も変えません。

回数上限つきループの擬似コード

公式の基本形には、回数上限は含まれていません。上限は、モデルが同じ呼び出しをくり返したり、結果が毎回「見つかりません」だったりしたときに、費用が膨らむのを防ぐための編集上の提案です。以下は説明用の擬似コードで、実行も検証もしていません。call_claude、TOOLS は自分の実装に置き換えてください。

python
MAX_TURNS = 8          # 往復の上限(用途に合わせて決める)
MAX_RESULT_CHARS = 4000

def run_agent(user_text):
    messages = [{"role": "user", "content": user_text}]
    max_tokens, retried = 1024, False
    seen = set()  # 同一呼び出しの検出用

    for turn in range(MAX_TURNS):
        resp = call_claude(messages, max_tokens)   # tools付きで送る

        if resp.stop_reason == "max_tokens":
            # 切れた応答は履歴に追加せず、必要なら上限を上げて再試行
            if not retried:
                max_tokens, retried = max_tokens * 4, True
                continue                            # 履歴は変えず再試行
            return stop("truncated", resp)

        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason == "end_turn":
            return final_text(resp)
        if resp.stop_reason == "pause_turn":
            # サーバーツールの続き。クライアントのtool_useが未処理でないこと
            continue                                # assistant応答を履歴に含めて再送
        if resp.stop_reason != "tool_use":
            return stop(resp.stop_reason, resp)     # refusal など

        results = []
        for b in resp.content:
            if b.type != "tool_use":
                continue
            key = (b.name, json_dumps(b.input, sort_keys=True))
            if b.name not in TOOLS:
                out, err = "Unknown tool: " + b.name, True
            elif key in seen:
                out, err = "Same call already made. Use another approach.", True
            else:
                seen.add(key)
                try:
                    out, err = str(TOOLS[b.name](**b.input)), False
                except Exception as e:
                    out, err = f"{type(e).__name__}: {e}", True
            results.append({"type": "tool_result", "tool_use_id": b.id,
                            "content": out[:MAX_RESULT_CHARS], "is_error": err})

        messages.append({"role": "user", "content": results})  # resultのみ

    return stop("max_turns_reached", messages)  # 上限到達: 人かアプリ側に返す

設計上の要点は次のとおりです。

  • tool_use ブロックごとに必ず tool_result を作ります。 未知のツール名や実行拒否でも、is_error: true で返します。公式にも、実行しない呼び出しにも is_error つきの結果を返すとあります。
  • max_tokens の再試行は履歴を変えません。 切れた tool_use は不完全なので、assistantとして追加する前に分岐しています。
  • tool_result の本文は長さを切ります。 tool_tokens は入力トークンとして課金に含まれます。長いログをそのまま返さない方が安全です。
  • 上限到達時は、未回答の tool_use が履歴の最後に残ります。 そのまま会話を再開するには、対応する tool_result が必要です。再開しない前提なら、失敗として記録して終えるのが単純です。

外部由来の内容は信頼しない

Webページ、メール、アップロード、外部APIなどから取った内容を返すと、そこに紛れた指示でモデルが誘導される(間接プロンプトインジェクション)可能性があると、公式は警告しています。信頼できない内容は、system や通常のuserテキストではなく、tool_result の中に留めるよう案内されています。加えて、メール送信やファイル削除のような副作用のあるツールは、ループの外で人の承認を挟む設計にしておくと、回数上限だけでは防げない事故を抑えられます。これは編集上の提案です。

実装前の確認リスト

  1. 履歴に、assistantの content 全体を欠落なく入れているか。
  2. 並列の tool_use すべてに、同じ tool_use_id の tool_result を返しているか。
  3. tool_result がuserメッセージの先頭にあり、テキストが後ろか。
  4. end_turn 以外の stop_reason(特に max_tokens、refusal)に行き先があるか。
  5. 往復の上限と、上限到達時の挙動を決めたか。

SDKの「tool runner」を使えば、このループ・結果の整形・再試行をSDKが自動で処理すると公式にあります。細かい制御(上限、承認、ログ)が不要なら、自前実装より先に検討してください。

出典・参考資料