Logo
Home|Blog|Speaker Deck
LanguageEnglish

ツール呼び出し機能がないモデルがなぜツールを呼び出せるのか?

2026-05-162026-05-17
Icon of AIAIIcon of LLMLLMIcon of ITIT
OGP ツール呼び出し機能がないモデルがなぜツールを呼び出せるのか?

ツール呼び出しはモデルの機能のように見えますが、モデルは結局のところ次のトークンを予測しているにすぎません。この「機能」は実際にはモデルの外側にある2つのもの、すなわち訓練された出力パターンそれを解析するランタイムから成り立っています。だからこそ、ネイティブのツール呼び出し機能を持たないLLMでもツールを呼び出せるのです。

ネイティブツール呼び出しがある場合#

  1. ツール定義をプロバイダーに渡すと、特殊なシステムプロンプトへと展開されます。
  2. LLMを呼び出します。
  3. モデルは訓練されたツール呼び出しトークン(例:<|python_tag|>{...}<|eom_id|>)を出力し、ランタイムがそれを解析して stop_reason: "tool_use" を返します。
  4. サーバー側でツールを実行します。
  5. 結果を tool result として送り返し、再度モデルを呼び出します。

#

Anthropic Messages API は、トップレベルの tools 配列としてツールを受け取ります。各ツールは名前、説明、そして JSON スキーマで書かれた input_schema を持ちます:

json

{
  "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 コンテンツブロックを返します。

ネイティブツール呼び出しがない場合#

  1. プロンプト内でツールを説明し、モデルに固定のパターン(例:<tool>...</tool>)を出力するように指示します。
  2. 閉じタグをストップシーケンスとして登録します。必要であれば、制約付きデコーディングによってスキーマを強制します。
  3. LLM を呼び出します。閉じタグが現れた時点でデコーディングは停止し、finish_reason は単に "stop" となります。
  4. テキスト内でパターンを検出し、解析してツールを実行します。
  5. 結果を次のプロンプトに含めて送り返し、再度モデルを呼び出します。

2つのフローはステップ2〜3でのみ異なり、その違いはどちらもモデルではなくランタイム側にあります。

#

ネイティブツール呼び出しを持たないモデルでもツールを呼び出せる、最小限のプロンプトを示します。ここで使う XML タグの規約はオープンソースエコシステムで広く採用されており、主要な推論サーバーもデフォルトで認識します。

システムプロンプト#

markdown

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 が吸収してしまうため、抽象化が抽象化のように感じられなくなりました。これこそが、「ネイティブ」と「非ネイティブ」の境界がこれほど曖昧になった理由です。

Profile IconIkuma Yamashita

Rust が好きです。仕事ではインフラエンジニア、趣味ではアプリケーションエンジニアです。イラストなどを嗜む。