はじめに
以前、
「Tool Callingとは?Function Callingとの違いを調べてみた」
という記事を書きました。
前回の記事では、
- Tool Callingとは何か
- Function Callingとの違い
- AIエージェントとの関係
について、初心者なりに調べてまとめました。
前回の最後で、
「今後はOpenAI APIやAIエージェントの実装を通して、Tool Callingの理解をさらに深めていきたい」
と書いたのですが、実際に案件でTool Callingに触れる機会があり、
- 実際どういう流れで処理が動いているのか
- OpenAIとClaude(Anthropic)で書き方が違うって聞いたけど何が違うの?
- エラーが起きたときってどう扱えばいいの?
という疑問が次々と出てきました。
そこで今回は、前回の記事からもう一歩踏み込んで、Tool Callingの「実装の中身」について調べてみました。
この記事はこんな方におすすめ
- Tool Callingの基本は何となく分かった方
- 実際にTool Callingを実装しようとしている方
- OpenAIとClaudeでツール呼び出しの書き方の違いを知りたい方
- 並列ツール呼び出し(parallel tool calls)が気になる方
- Tool Calling実装時のエラーハンドリングを知りたい方
まず前回のおさらい
前回の記事で、Tool Callingは
LLM
↓
Tool(関数・API・検索など)
↓
結果
↓
LLM
↓
回答
という流れで動く仕組みだと分かりました。
今回は、この「LLM → Tool → LLM」の間で、実際に何が起きているのかを見ていきます。
実際の処理の流れを分解してみる
調べてみると、Tool Callingの処理は、だいたい次の5段階に分けられるようです。
- ツール定義をリクエストに含める
- モデルが「このツールを呼びたい」と応答する
- アプリ側で実際にツール(関数)を実行する
- 実行結果をモデルに送り返す
- モデルが最終的な回答を生成する
図にすると、
ユーザーの質問
↓
①ツール定義とあわせてモデルへ送信
↓
②モデルが「tool_calls」で呼び出したいツールを返す
↓
③アプリ側で関数・API処理を実行
↓
④結果をモデルに返送
↓
⑤モデルが結果をもとに回答を生成
という流れです。
前回は「LLMがツールを呼ぶ」というイメージだけで終わっていましたが、実際には
「ツールを呼び出すのはモデルの意思表示だけで、実行するのはあくまでアプリ側」
という点が重要そうです。モデル自身がAPIを叩いているわけではなく、「この関数をこの引数で呼んでほしい」という指示を返しているだけ、という理解で合っていそうです。
ツール定義の書き方:OpenAIとClaudeで違う
ここが個人的に一番混乱したポイントでした。
同じ「Tool Calling」でも、OpenAIとAnthropic(Claude)ではツールの定義方法が違います。
| 項目 | OpenAI | Claude(Anthropic) |
|---|---|---|
| 引数スキーマのキー名 | parameters |
input_schema |
| 全体構造 |
{"type": "function", "function": {...}} というラッパーが必要 |
ラッパーなしでオブジェクトを直接定義 |
| 呼び出し結果の返し方 |
role: "tool" のメッセージ |
role: "user" メッセージ内に結果を格納 |
OpenAIは「function」というラッパーの中に定義を書くのに対し、Claudeはラッパーなしでシンプルに書ける、というイメージのようです。(参考: agentsindex.ai)
ちなみにGoogle(Gemini)は、また少し毛色が違い、types.Schema や types.Type といったプロトコルバッファ形式のスキーマを使うようです。
「Tool Calling」と一言でいっても、プロバイダごとに書き方がバラバラなんだな、というのが率直な感想でした。
並列ツール呼び出し(parallel tool calls)とは
調べていて新しく知ったのが「並列ツール呼び出し」という考え方です。
例えば、
「東京の天気と、大阪の天気を教えて」
という質問の場合、
天気API(東京)
+
天気API(大阪)
という2つのツール呼び出しが必要になります。
これを1回のレスポンスでまとめて返してくれるのが、並列ツール呼び出しです。
- OpenAIでは
parallel_tool_callsというオプションがあり、デフォルトで有効になっているようです。1回のレスポンスの中に複数のtool_callsが含まれる形です。 - Claudeも同様に、複数の
tool_useブロックを同時に返すことで並列実行に対応しています。 - Geminiも、
function_callパートを複数含めることで並列呼び出しに対応しているようです。
「1つずつ順番に呼ぶもの」だと思い込んでいたので、これは知らないと実装で詰まりそうなポイントだと感じました。
strictモード・構造化出力についても調べてみた
Tool Callingを調べていると、strictというパラメータもよく見かけました。
これは、
モデルが返すツール呼び出しの引数が、指定したJSON Schemaに厳密に従うようにする
というモードのようです。
strict: trueにしておくことで、
- 存在しないパラメータを勝手に追加される
- 必須パラメータが抜け落ちる
といった事故を防ぎやすくなる、というのが個人的な理解です。
エージェントのように「ツールの実行結果がそのままシステムの動作に直結する」場合、このあたりの厳密さは地味に重要なポイントだと感じました。
エラーハンドリングで気をつけたいこと
実装するにあたって、エラーハンドリングも避けて通れない部分でした。
調べて分かったポイントをまとめると、
- ツール呼び出しは「0個」「1個」「複数個」のどのパターンでも来る可能性があるため、決め打ちで実装しない
- 関数の実行自体が失敗した場合は、例外を投げっぱなしにせず、「失敗したこと」が分かる文字列などにして返す
- Claudeの場合、エラーは
role: "user"のメッセージの中にis_error: trueというフラグを付けて返す、という独自の仕組みがある
という点が印象的でした。
特に、
「ツールが呼ばれなかった場合」
を考慮していないと、実装時に思わぬバグにつながりそうだと感じました。
ツール数が多くなってきたときの注意点
もう一つ、実務的に気になったのが「ツールの数」についてです。
ツール定義もリクエストのトークンとしてカウントされるため、
- 最初から大量のツールを渡しすぎるとコンテキストを圧迫する
- 目安として、最初に渡すツール数は20個未満に抑えるのが推奨されている
という情報もありました。
ツールが増えてきた場合は、必要なツールだけを検索して都度読み込むような仕組み(ツールの遅延ロード)も検討されているようです。
「ツールをとにかくたくさん渡せばエージェントが賢くなる」わけではなさそうだ、というのは意外な発見でした。
マルチステップのツール呼び出しループについて
ここまでは「1回ツールを呼んで終わり」のイメージで説明してきましたが、実際のAIエージェントでは、
ツール呼び出し
↓
結果をモデルに返す
↓
モデルがまた別のツールを呼びたがる
↓
また結果を返す
↓
…
という形で、何度もツール呼び出しが連鎖することが普通にあるようです。
例えば、
「東京の天気を調べて、雨ならユーザーにSlackで通知して」
という依頼なら、
- 天気APIを呼ぶ
- 結果(雨かどうか)を見て、雨ならSlack通知ツールを呼ぶ
- 通知が終わったら最終回答を返す
というように、複数回のツール呼び出しがひとつながりのループになります。
このとき気をつけないといけないのが、
ループが終わらなくなる可能性がある
という点です。
モデルが延々とツールを呼び続けてしまうケースを防ぐため、
- 最大ループ回数(例: 5回まで)を決めておく
- 上限に達したら、強制的に「ここまでの情報で回答してください」と伝える
といった安全装置が必要になりそうです。前回・前々回で調べたLangGraphやOpenAI Agents SDKのようなフレームワークは、こうしたループ制御をあらかじめ仕組みとして持っている、というのが個人的な理解です。
ストリーミングでツール呼び出しを受け取る場合
チャット画面のように、回答を少しずつ表示したい場合はストリーミング(stream=True)を使いますが、これがツール呼び出しと組み合わさると少しやっかいでした。
ストリーミング時は、ツール呼び出しの情報が
- 最初のチャンクで「ツール呼び出しが1つある」というID・関数名の情報
- その後のチャンクで、引数のJSON文字列が少しずつ分割されて届く
という形で送られてくるようです。
複数のツールが同時に呼ばれる場合は、チャンクごとに付与されるindexという値を見て、
「このチャンクはどのツール呼び出しの続きなのか」
を判断し、同じindexの引数文字列を後ろにつなげていくことで、最終的に完全なJSONを組み立てる、という処理が必要になります。
イメージとしては、
チャンク1: index=0, id="call_1", name="get_weather"
チャンク2: index=0, arguments="{"cit"
チャンク3: index=0, arguments="y": "To"
チャンク4: index=0, arguments="kyo"}"
のような断片が届き、これをすべてつなげてはじめて
{"city": "Tokyo"}
という引数が完成する、というイメージです。
「引数もストリーミングで少しずつ届く」というのは、実装するまで知らなかったポイントでした。すべてのツール呼び出しの引数が揃うまで待ってから実行する、という設計が一般的なようです。
セキュリティ面で気をつけたいこと
Tool Callingを調べていて、地味に重要だと感じたのがセキュリティの話です。
ツールの実行結果(Web検索結果や外部ドキュメントなど)に、悪意のある指示文が紛れ込んでいた場合、
外部データ
↓
ツールの実行結果としてLLMに渡る
↓
LLMがその指示に従ってしまう
という「間接的なプロンプトインジェクション」が起こりうる、という点です。
対策として調べて出てきたのは、
- 最小権限の原則: 各ツールに必要最小限の権限しか渡さない(例えば「読み取り専用」にする、金額の上限を設けるなど)
- 人間による確認: 送金やデータ削除など、取り返しのつかない操作の前にはユーザーの承認を挟む
- サンドボックス化: ツールを実行する環境自体を隔離し、ネットワークやファイルシステムへのアクセスを制限する
- 信頼できるモデルと信頼できないコンテンツを分離する設計: 外部コンテンツを直接扱う部分と、重要な判断をする部分を分ける
といった多層的な対策でした。
「ツールを呼べるようになる」というのは便利な反面、「エージェントが変なことをしてしまうリスク」もセットで増える、というのは今回調べていて改めて実感した点でした。
過去に調べたAIエージェント関連の記事とのつながり
今回調べてみて、以前書いた
- 「LangGraphとは?AIエージェント開発で注目されるフレームワークを調べてみた」
- 「OpenAI Agents SDKとは?AIエージェント開発を効率化するフレームワークを調べてみた」
の内容ともつながっていることに気づきました。
どちらのフレームワークも、結局のところ中身では
Tool Calling
+
マルチステップのループ
+
状態管理
を組み合わせて動いています。今回Tool Calling単体を深掘りしたことで、これらのフレームワークが「何を肩代わりしてくれているのか」が、以前よりも具体的にイメージできるようになった気がします。
Pythonで簡単な流れを書いてみる
雰囲気をつかむため、OpenAIのツール呼び出しの流れを簡単なコードにしてみました(実際にはエラーハンドリングなどをもっと丁寧に書く必要がありますが、あくまで流れの確認用です)。
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "指定した都市の天気を取得する",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}
]
# ① ツール定義つきでモデルに問い合わせ
response = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": "東京の天気を教えて"}],
tools=tools,
)
message = response.choices[0].message
# ② モデルがツール呼び出しを要求してきた場合
if message.tool_calls:
for tool_call in message.tool_calls:
# ③ アプリ側で実際に関数を実行
result = get_weather(tool_call.function.arguments)
# ④ 実行結果をモデルに返す
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
# ⑤ 最終的な回答を取得
final_response = client.chat.completions.create(
model="gpt-4.1",
messages=messages,
)
こうして書いてみると、前回の記事の図で書いた
LLM → Tool → 結果 → LLM
という流れが、そのままコードの構造になっていることが分かり、個人的には理解がだいぶ進みました。
個人的に調べてみて思ったこと
前回は「Tool CallingとFunction Callingの違い」という言葉の整理だけで終わっていましたが、実際に実装まわりを調べてみると、
- 呼び出すのはモデル、実行するのはアプリ側
- プロバイダごとにスキーマの書き方が違う
- 並列呼び出し・strictモードなど、地味だが重要な機能がある
- エラーハンドリングを甘く見ると実装で詰まる
など、言葉の意味だけでは見えてこない部分が多いと感じました。
特に、「モデルは実行の指示を出すだけ」という役割分担は、AIエージェントの安全性を考えるうえでも重要な前提だと感じています。
まとめ
Tool Callingの実装まわりを調べてみて、
- 処理の流れは「定義→呼び出し要求→実行→結果返却→最終回答」の5段階
- OpenAIとClaudeではツール定義のスキーマの書き方が異なる
- 並列ツール呼び出しやstrictモードなど、実装上の細かい仕様がある
- エラーハンドリングは0件・1件・複数件のすべてのパターンを想定する必要がある
- 実際にはマルチステップのループになることが多く、ループの上限も考える必要がある
- ストリーミング時は引数まで少しずつ届くため、組み立てる処理が必要
- ツール実行結果経由のプロンプトインジェクションなど、セキュリティ面の考慮も欠かせない
ということが分かりました。
まだ実際のプロダクションコードとしては改善の余地がありそうなので、今後は今回あまり触れられなかったサンドボックス実装や、複数ツールを組み合わせた実践的なエージェント構築についても、もう少し深掘りしていきたいと思います。
最後まで読んでいただき、ありがとうございました!