0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tool Callingをさらに深掘りしてみた ~実装の流れ・OpenAI/Claudeの違い・エラーハンドリング~

0
Last updated at Posted at 2026-09-01

はじめに

以前、

「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段階に分けられるようです。

  1. ツール定義をリクエストに含める
  2. モデルが「このツールを呼びたい」と応答する
  3. アプリ側で実際にツール(関数)を実行する
  4. 実行結果をモデルに送り返す
  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.Schematypes.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で通知して」

という依頼なら、

  1. 天気APIを呼ぶ
  2. 結果(雨かどうか)を見て、雨ならSlack通知ツールを呼ぶ
  3. 通知が終わったら最終回答を返す

というように、複数回のツール呼び出しがひとつながりのループになります。

このとき気をつけないといけないのが、

ループが終わらなくなる可能性がある

という点です。

モデルが延々とツールを呼び続けてしまうケースを防ぐため、

  • 最大ループ回数(例: 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件・複数件のすべてのパターンを想定する必要がある
  • 実際にはマルチステップのループになることが多く、ループの上限も考える必要がある
  • ストリーミング時は引数まで少しずつ届くため、組み立てる処理が必要
  • ツール実行結果経由のプロンプトインジェクションなど、セキュリティ面の考慮も欠かせない

ということが分かりました。

まだ実際のプロダクションコードとしては改善の余地がありそうなので、今後は今回あまり触れられなかったサンドボックス実装や、複数ツールを組み合わせた実践的なエージェント構築についても、もう少し深掘りしていきたいと思います。

最後まで読んでいただき、ありがとうございました!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?