Function Callingとは?LLMがJSONで外部ツールを呼ぶ仕組み
関数スキーマを渡す → モデルが引数を選ぶ → 実行結果を返す、の流れ
先に結論。Function Callingは、モデルが関数を「実行」する機能ではない。「この関数をこの引数で呼んでほしい」というJSONを返すだけで、実行するのは自分のコードだ。この記事を読めば、その往復を自分で組めるし、構造化出力・MCPとの違いも見分けられる。
1往復の流れ
リクエストにツール一覧(名前・説明・JSON Schema)を一緒に送る。モデルがツールが要ると判断すると、テキストの代わりに呼び出しを返す。Claudeはstop_reasonがtool_useで、tool_useブロックが来る。OpenAIは名前とJSON「文字列」の引数が入ったfunction callが来る。
自分のコードがそれを実際に実行し、結果に呼び出しのidを付けて送り返す(Claudeならtool_resultのtool_use_id)。モデルは結果を読んで答えるか、また呼ぶ。
{"type": "tool_use", "id": "toolu_01...", "name": "get_weather", "input": {"location": "Tokyo"}}
うちのコードで見たこと
このリポジトリの社内チャット機能も、この往復を回している(Azure OpenAI)。読んでみて、2か所が目に付いた。
ループの上限。 呼び出し → 結果 → また呼び出し、が延々と続きうるので、5回で打ち切っている(MAX_TOOL_ITERATIONS = 5)。これがないと、モデルが同じツールを呼び続けたときに料金だけがかさむ。
壊れた引数。 うちのコードは引数JSONのパースに失敗すると、ログだけ残して空のハッシュ{}に置き換え、そのまま実行する。rails runnerで途中で切れたJSON({"name": "nega)を入れてみると、本当に{}が返ってきた。検索ツールなら条件なしの検索が走る。
最初は「エラーなら止まるだろう」と思っていたが、止まらなかった。直すなら、パース失敗もツールの結果としてモデルに返すほうがいい。知らないツール名はすでにそうしていて、{"error": "Unknown function: ..."}を返す。いまはツール定義にstrict: trueを付けてスキーマを強制することもできる(Claude・OpenAIとも)。
構造化出力・MCPと何が違うか
Function Calling モデルが「これを呼んで」と出す → 自分のコードが実行 → 結果を戻す
構造化出力 答えそのものをスキーマ通りのJSONで受け取る → 実行するものはない
MCP ツールをどこからどう取ってくるかの規格 → モデルには結局ツール定義として渡る
分類や抽出のように結果がJSONならいいだけなら、構造化出力のほうが単純だ。外のシステムを読んだり変えたりするならFunction Calling。同じツールを複数のアプリやクライアントで使いたくなったら、そこでMCPで包む。MCPについてはMCPの記事にある。
気をつけること
モデルは書き込み系のツールも、もっともらしく呼ぶ。削除・決済・メールのようなツールは、人の確認を挟むか、最初から渡さない。DBを見せるときに書き込みをDB権限で止めた話はこの記事に書いた。
社内システムにAIをつなぎたいけれど、どのツールをどこまで開けるか決めかねているならnegabaroに相談してもらって構わない。ツール一覧と権限の整理から一緒にできる。
キーコンセプト
リクエストにツール一覧(名前・説明・JSON Schema)を一緒に送る
モデルがテキストの代わりに呼び出しを返す(Claude: stop_reason tool_use + tool_useブロック)
自分のコードが引数を確かめて実際に実行する — パース失敗を黙って{}にしない
結果に呼び出しidを付けて送り返す(Claude: tool_resultのtool_use_id)
モデルが答えるか、また呼ぶ — ループには上限を置く
メリット
- ✓ リアルタイムデータ・計算・社内システムをツールで補う
- ✓ 引数がJSONなのでコードで受けやすい
- ✓ 実行権限が自分のコードにあるので、止める場所がはっきりしている
デメリット
- ✗ モデルが間違ったツール・引数を選ぶことがある
- ✗ 書き込み系ツールは権限・人の確認を別に設計する必要がある
- ✗ 往復が増えるほどトークン・遅延が増える