レッスン2: ツール呼び出しの完全なラウンドトリップ
学習目標:
- 1回のツール呼び出しラウンドトリップにおいて、リクエストとレスポンスがそれぞれ持つキーフィールドの名前を言える
- tool_use / tool_result コードの一部が正しくマッチしているかどうかを判断できる
- 同じ並列バッチ内の呼び出し間のデータ依存関係を見抜き、呼び出しを2ラウンドに分割すべきタイミングを知る
- 「モデルがツールを呼び出す」というフレーズそのものが不正確である理由を説明できる
前提: レッスン1を読み、エージェントにツールが必要な理由を知っている | 前: レッスン1 << | 次: レッスン3 >>
3つのJSONの塊から始める
サポートボットを構築しています。ユーザーが「注文ORD-2026-8842がどこにあるか確認してもらえますか?」と尋ねます。あなたのコードはそのメッセージをツール定義と一緒にモデルに送信します:
新しいtoolsフィールドに注目してください。これはメッセージではありません; モデルに手元にどんなツールがあるか、それぞれがどのように見えるか、そしてそれぞれが必要とするパラメータは何かを伝えるマニフェストです。1 このマニフェストはリクエストごとに送信する必要があります — モデルはそれを「記憶」しないので、あなたのコードは毎回それを含めなければなりません。
モデルはマニフェストを読み、注文ステータスを直接答える代わりに、このようなものを返します:
ここで2つの新しいことが現れます: stop_reasonが"tool_use"になり、content配列にtype: "tool_use"を持つ新しいブロックがあります。モデルは注文情報を調べていません — 注文システムがどこにあるかさえ知りません。ただ「これらのパラメータでget_order_statusを呼び出してください、そして結果を教えてください」と言っているだけです。
ここからあなたのコードが引き継ぎ、実際に注文システムにクエリを投げ、結果を取得し、その結果を次のリクエストにパックして送り返します:
何が追加されたかに注目してください: 前のラウンドのモデルの完全な応答がmessagesに逐語的に戻され、その後に新しいuserメッセージが続きます。そのメッセージはユーザーが入力したテキストを保持していません — モデルがちょうど渡してきたidと正確に一致するtool_use_idを持つtype: "tool_result"ブロックを保持しています。
このリクエストを見て初めて、モデルは最終的に「ご注文は上海トランジットセンターを出発中で、8月27日に配達予定です」のようなことを言います。3つのJSONの塊、3つのロールの切り替え: モデルがリクエストを出し、あなたのコードがそれを実行し、結果がフィードバックされます。これが1回のツール呼び出しラウンドトリップのすべてです。
stop_reasonはシグナルであり、実行記録ではない
初心者が最も頻繁に間違えることがこれです: stop_reason: "tool_use"はツールがすでに呼び出されたことを意味すると思い込むことです。そうではありません。これはモデルがこのメッセージを終えたときに停止した理由に過ぎず、"end_turn"(話し終わった)や"max_tokens"(スペースが足りなくなった)と同じ種類のフィールドで、ただ値が異なるだけです。2
モデルは決してデータベースに触れたり、HTTPリクエストを発行したり、シェルコマンドを自力で実行したりしません。できることは構造化されたリクエストを発行することだけです; 残りの作業はあなたのコードまたはAnthropicのサーバーに委ねられます。3 だからこそツールは「クライアントツール」(ホストアプリケーションが実行する)と「サーバーツール」(Anthropicがあなたの代わりに実行する)に分かれています — 違いは誰がこのステップを実行するかだけで、モデルが自分でそれを実行できるかどうかではありません。2
tool_useブロックの3つのフィールド、どれもオプションではない
そのtool_useブロックを見返してください。必須フィールドは3つだけです:4
id: この呼び出しの一意の識別子で、toolu_01XYZ...の形式です。1つの仕事しかありません — 後で結果を送り返すときに照合することです。
name: モデルが選んだツールで、toolsマニフェスト内のツールの1つのnameと正確に一致しなければなりません。
input: この呼び出しのパラメータを保持するオブジェクトで、input_schemaで定義したルールを満たすように形成されています。
これら3つのフィールドをまとめると、モデルが表現できるすべてが揃います:「このidでnameツールを呼び出したい、そしてこれがinputです。」「3回リトライ」のようなロジックを付け加えることはありません — それはホストコードで自分で書きます。モデルがパラメータミスを減らすようにツールインターフェースを設計する方法はレッスン3の領域です; このレッスンはこれら3つのフィールドがどのようにパックされ、読み返されるかだけを気にします。
tool_resultはtool_use_idで照合する
モデルからの1つの応答には複数のtool_useブロックが含まれることがあります。ユーザーが「注文ORD-2026-8842がどこにあるか確認して、それとORD-2026-9001が発送されたかどうかも確認してください」と尋ねたとします。モデルは同じcontent配列に2つのtool_useブロックを入れ、stop_reasonは依然として"tool_use"です。
あなたのコードは両方の注文を調べ、それから同じuserメッセージで、両方の結果をcontent配列に一緒に入れ、各tool_resultが自分のtool_use_idで呼び出しを主張します:
ショートカットを取って最初のtool_use_idだけで1ラウンド送信すると、モデルは会話を続けることを拒否します。なぜなら「前のラウンドにはtool_resultを得なかったtool_useブロックがあった」からです — 両方のブロックは次のuserメッセージで一緒に主張される必要があります; それらを2つのリクエストに分割してバッチで送り返すことはできません。5 tool_resultブロックにはオプションのis_errorフィールドもあります: ツールが失敗したときにtrueに設定すると、モデルはこの呼び出しが問題に遭遇したことを知ります。4
同じバッチ内の呼び出しは互いの結果を見ることができない
バッチ返却ルールが確立されたので、より深い罠があります: 同じバッチ内のtool_useブロック間のデータ依存関係です。
シナリオを切り替えます。送金エージェントが2つのツールで設定されています: read_balance(account_id)は残高を読み、withdraw(account_id, amount)はお金を引き出します。ユーザーが「A001から$100を引き出して、十分あるなら」と言います。1つのレスポンスで、モデルは2つのtool_useブロックを返します: read_balance({"account_id": "A001"})とwithdraw({"account_id": "A001", "amount": 100})。
withdrawのamountを見てください: 100、ユーザーの文の数字から直接コピーされ、残高が十分かどうかとは無関係です。これはモデルが怠惰だからではありません; 選択肢がないのです。このレスポンスを生成する瞬間、read_balanceはまだ「それがやろうとしていること」に過ぎません — その戻り値はまだ存在していないので、withdrawはそれを読むことができません。1つのバッチのtool_useブロック内では、どの呼び出しもそのバッチ内の他の呼び出しの結果を見ることができません。なぜならそれらの結果はその時点でまだ実行されておらず送り返されていないからです。
だからあなた自身が守らなければならない線がこれです: 書き込み操作のパラメータが、理論上は同じバッチ内の読み取り操作の戻り値と等しくあるべきなら、その2つの呼び出しは同じレスポンスに現れるべきではありません。 本当に安全なアプローチは、それらを2ラウンドに分割することです: まずread_balanceだけを実行し、実際の残高をtool_resultとして送り返し、モデルが「残高は60しかない」と見たら、withdrawを呼び出すかどうか、いくらで呼び出すかを決めさせます。
実際に機能する3つの戦術:
- ツールの説明に前提条件を書く。
withdrawのdescriptionに一行追加します:「read_balanceから返された最新の残高を見た後にのみ呼び出してください。」ツールの説明はモデルが読めるプロンプトの一部そのものなので、モデルが依存関係を自力で把握することを期待するよりもはるかに信頼できます。6
- disable_parallel_tool_useで並列性をオフにする。 リクエストの
tool_choiceに{"type": "auto", "disable_parallel_tool_use": true}を設定すると、モデルは1レスポンスにつき最大1つのツールを呼び出します。5 動作をまず1回ずつに絞り、ステップ間の依存関係を頭の中で明確にし、その後でそれを緩めることを検討します。
- 実行レイヤーでバックストップする。
withdrawを実行するコードに最新の残高を再チェックさせ、条件が満たされていない場合は実行を拒否し、理由をtool_resultエラー情報に書いてモデルに見せます。成功したふりをするのではなく。たとえモデルが今回また2つの呼び出しをバンドルしても、このチェックがリスクを捕捉します。
図として描く
上記のラウンドトリップを描くとこのようになります:
この図で最も頻繁に間違えられるステップは「append」矢印です: tool_resultだけを単独で送信し、そのラウンドのモデルの完全なtool_use応答をmessagesに戻すのを忘れることです。すると、モデルは自分が出したリクエストの記録がコンテキストにないまま、どこからともなく現れたツール結果を受け取ることになります — 論理の飛躍やあからさまなエラーが起こりやすくなります。正しい動きは、毎ラウンドのレスポンスを逐語的に履歴に保存することです; messagesは常に長くなるだけで、決してトリミングされません。3
1つのタスクが複数回のラウンドトリップを必要とすることがある
上記の例は1回のツール呼び出しの後に終わりました。実際のシナリオでは、モデルは完了する前に何度も往復することがよくあります。デプロイボットを想像してください。ユーザーが「サービスを再起動して、ログにエラーがないか教えて」と言います:
- モデルは最初のラウンドで
tool_useを返し、restart_serviceを呼び出します; あなたはそれを実行し、結果を送り返します
- モデルは2回目のラウンドで再び
tool_useを返し、read_logsを呼び出してエラーをチェックします; あなたはそれを実行し、ログを送り返します
- 3回目のラウンドで、モデルは最終的に
stop_reason: "end_turn"を返し、テキストで要約します
ホスト側のコードロジックは本質的にループです: stop_reasonがまだ"tool_use"である限り、ツールを実行し続け、結果をパックし直し、別のラウンドを送信します; "end_turn"になったら、最終テキストをユーザーに渡します。3
このループには反復回数の固定上限がありません — 1つのユーザーリクエストに対して、モデルは1回だけツールを呼び出すかもしれませんし、十分に集めるまで5回か6回呼び出すかもしれません。レッスン3では、ツールインターフェース設計がラウンドトリップ回数をどのように削減できるかをカバーします; このレッスンでは、ただ覚えておいてください: 複数回のラウンドトリップは例外ではなく標準です。
ホストを交換すると、フィールド名は変わるが、構造は変わらない
OpenAI互換APIを使用している場合、同じメカニズムが異なるラッパーで提供されます: 呼び出しリクエストはchoices[0].message.tool_calls配列に現れ、終了シグナルはstop_reasonではなくfinish_reasonと呼ばれ、その値は"tool_use"ではなく"tool_calls"です。7 OpenAIの公式ドキュメントはこのプロセスを次のように説明しています:
"Tool calling is a multi-step conversation between your application and a model via the OpenAI API. When the model calls a function, you must execute it and return the result"
(ツール呼び出しは、OpenAI APIを介したアプリケーションとモデル間の複数ステップの会話です。モデルが関数を呼び出すとき、あなたはそれを実行して結果を返さなければなりません) — モデルが呼び出しリクエストを発行し、アプリケーションがそれを実行して結果を送り返す、まさにClaudeと同じです。8
フィールド名はAPIによって変わりますが、骨格 — 「モデルはリクエストを送るだけ、ホストが実行を処理する、結果は識別子を運んで戻ってくる、そして複数ラウンドをループすることがある」 — は普遍的です。
まとめ
- モデルは何も直接実行しません。
stop_reason: "tool_use"と1つ以上のtool_useブロックを発行するだけです; 実行はホストアプリケーションに留まります
tool_useブロックには3つの必須フィールドしかありません: id(照合用)、name(選ばれたツール)、input(パラメータ)
- 結果は
tool_resultブロックとして戻り、tool_use_idは対応するtool_useブロックのidと正確に一致しなければなりません
- 1つのレスポンスには複数の
tool_useブロックが含まれることがあります; 照合するtool_resultブロックは複数のリクエストに分割するのではなく、同じuserメッセージにパックされなければなりません
- 同じバッチ内の
tool_useブロックは互いの実行結果を見ることができません: 書き込み操作のパラメータが同じバッチ内の読み取り操作の戻り値に依存する場合は、それらを2ラウンドに分割するか、disable_parallel_tool_useで1回ずつに強制します
- 1つのタスクが複数回のラウンドトリップを必要とすることがあります: ホスト側の実装は本質的にループです —
stop_reasonがまだ"tool_use"である間は実行と送り返しを続け、"end_turn"になって初めて完了します
>> レッスン3: 5つの一般的なツールタイプ: 読む、書く、実行する、検索する、呼び出す