ツール呼び出し機能がないモデルがなぜツールを呼び出せるのか?
ツール呼び出しはモデルの機能のように見えますが、モデルは結局のところ次のトークンを予測しているにすぎません。この「機能」は実際にはモデルの外側にある2つのもの、すなわち訓練された出力パターンとそれを解析するランタイムから成り立っています。だからこそ、ネイティブのツール呼び出し機能を持たないLLMでもツールを呼び出せるのです。
ネイティブツール呼び出しがある場合
- ツール定義をプロバイダーに渡すと、特殊なシステムプロンプトへと展開されます。
- LLMを呼び出します。
- モデルは訓練されたツール呼び出しトークン(例:
<|python_tag|>{...}<|eom_id|>)を出力し、ランタイムがそれを解析してstop_reason: "tool_use"を返します。 - サーバー側でツールを実行します。
- 結果を tool result として送り返し、再度モデルを呼び出します。
tool_use → tool_result のラウンドトリップは発生しません。例
Anthropic Messages API は、トップレベルの tools 配列としてツールを受け取ります。各ツールは名前、説明、そして JSON スキーマで書かれた input_schema を持ちます:
{ "model": "claude-sonnet-4-20250514", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get current weather for a city. Use this whenever the user asks about weather, temperature, or conditions in a specific location.", "input_schema": { "type": "object", "properties": { "city": { "type": "string" }, "unit": { "type": "string", "enum": ["c", "f"], "default": "c" } }, "required": ["city"] } } ], "messages": [ { "role": "user", "content": "What's the weather in Tokyo?" } ] }
モデルがツールを使うと判断すると、レスポンスは stop_reason: "tool_use" で終わり、構造化された tool_use コンテンツブロックを含みます。アプリケーション側はツールを実行し、次のターンで対応する tool_result コンテンツブロックを返します。
ネイティブツール呼び出しがない場合
- プロンプト内でツールを説明し、モデルに固定のパターン(例:
<tool>...</tool>)を出力するように指示します。 - 閉じタグをストップシーケンスとして登録します。必要であれば、制約付きデコーディングによってスキーマを強制します。
- LLM を呼び出します。閉じタグが現れた時点でデコーディングは停止し、
finish_reasonは単に"stop"となります。 - テキスト内でパターンを検出し、解析してツールを実行します。
- 結果を次のプロンプトに含めて送り返し、再度モデルを呼び出します。
2つのフローはステップ2〜3でのみ異なり、その違いはどちらもモデルではなくランタイム側にあります。
stop パラメータを使えば、その検出はプロバイダーのランタイム内部に移ります。これはネイティブツール呼び出しと同じメカニズムであり、特殊トークンの代わりにカスタム文字列を使っているだけです。いずれにせよ、境界はモデルの外側に存在します。例
ネイティブツール呼び出しを持たないモデルでもツールを呼び出せる、最小限のプロンプトを示します。ここで使う XML タグの規約はオープンソースエコシステムで広く採用されており、主要な推論サーバーもデフォルトで認識します。
システムプロンプト
You are an assistant that can answer directly OR call tools. <tools> [ { "name": "get_weather", "description": "Get current weather for a city. Use this whenever the user asks about weather or conditions in a specific location.", "parameters": { "city": { "type": "string" } } } ] </tools> ## Rules - If a tool is needed, output ONLY: <tool_call>{"name": "...", "arguments": {...}}</tool_call>, then stop. - Otherwise, answer the user in plain text. - After a <tool_result>...</tool_result> message, use the result to answer in plain text. ## Examples User: How is the weather in Tokyo today? Assistant: <tool_call>{"name": "get_weather", "arguments": {"city": "Tokyo"}}</tool_call> User: What's the weather like in Paris? Assistant: <tool_call>{"name": "get_weather", "arguments": {"city": "Paris"}}</tool_call> User: What is weather? Assistant: Weather is the state of the atmosphere at a particular place and time — temperature, humidity, wind, precipitation, and so on.
実際に何が違うのか
両方のフローは同じ結果を生み出します。本当の違いは、誰が境界を所有し、誰がフォーマットを保証するかにあります:
- ネイティブ — ランタイムが特殊なツール呼び出しトークンを検出し、ファインチューニングがフォーマットを保証します。
- 非ネイティブ — アプリケーションがストップシーケンスを検出し、プロンプト(および任意で制約付きデコーディング)がフォーマットを保証します。
非ネイティブのパスに制約付きデコーディングを加えれば、両者は信頼性の面で収束します。残るのは、ただ境界がどこにあるかだけです。
余計なものをすべて剥ぎ取ると、ツール呼び出しはひとつのループに過ぎません――プロンプト → パターン → 停止 → 実行です。自分で配線するか、プロバイダーのランタイムに任せるかは実装の詳細でしかありません。AI エージェントフレームワークの SDK を使うことが当たり前になり、この抽象化を SDK が吸収してしまうため、抽象化が抽象化のように感じられなくなりました。これこそが、「ネイティブ」と「非ネイティブ」の境界がこれほど曖昧になった理由です。