ステータス:ドラフト(2026-10-27)、実測値。 測定は8GBクラスのノートPC用GPU(Ada世代)1枚、vLLM 0.31.0、Qwen3-Reranker-0.6B(リランキング用にファインチューニングされた Qwen3 0.6B 因果言語モデル)を使用して行いました。公式ドキュメントは0.32.0(2026-10-18リリース)を基準に2026-10-27時点のものを確認し、0.31.0のドキュメントと異なる箇所は別途記載しています。 2026-10-27追加。 会話用の Qwen3-0.6B、Qwen3-0.6B-GPTQ-Int8、Qwen3-4B-AWQ と vLLM 0.32.0 を用いて再測定した結果をセクション3-1に追加しました。0.31.0で測定した前述の数値はそのまま残しています。 表記ルール:実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表記します。
目次
- 1. ツール呼び出しは決まった形式のテキストである
- 2. 4種類の tool_choice
- 3. 実際に測定してみる
- 3-1. 2026-10-27再測定:ツールを理解するモデルと0.32.0
- 4. 運用への示唆
- 5. 確認できなかったこと
- 参考
1. ツール呼び出しは決まった形式のテキストである
エージェントが天気APIを呼び出す場面を想像してみましょう。モデルの内部に「関数呼び出し」という特別な出力装置が存在するわけではありません。モデルはこれまで通りトークンを1つずつ生成しているのであり、単に学習時に「ツールを呼び出すときはこのような形式で書く」ことを学習したに過ぎません。Qwen系はHermes形式を使用します。
<tool_call>
{"name": "get_weather", "arguments": {"city": "Seoul", "unit": "c"}}
</tool_call>
サーバーが行う処理は次の3つです。
- ツールの一覧をプロンプトに挿入します。 リクエストの
toolsを、モデルのチャットテンプレートがシステムプロンプト内にテキストとして書き込みます。 - モデルが生成したテキストを選択します。 生成されたテキストから上記の形式を見つけ出し、OpenAI形式の
message.tool_callsに変換するのが ツールパーサーです。モデルによって形式が異なるため、パーサーもhermes,llama3_json,mistral,qwen3_xmlのように複数存在します。 - 必要に応じて形式を強制します。
tool_choiceがrequiredであるか特定の関数名である場合、第4回の構造化出力によってトークンレベルで呼び出しの形式が強制されます。
有効化するオプションは次の通りです。 --enable-auto-tool-choice がないと tool_choice="auto" リクエストを受け付けません。
vllm serve <모델> --enable-auto-tool-choice --tool-call-parser hermes
2. 4種類の tool_choice
| tool_choice | 呼び出しの義務 | 形式を保証するもの |
|---|---|---|
auto |
なし、モデルが決定 | パーサーのみ。モデルが形式を間違えた場合、テキストとして残るか欠落する |
required |
1つ以上が必須 | 構造化出力が形式を強制 |
名前を指定({"type":"function","function":{"name":...}}) |
その関数のみ | 構造化出力が形式を強制 |
none |
呼び出さない | ツール定義はデフォルトでプロンプトに残る(--exclude-tools-when-tool-choice-none で除外) |
公式ドキュメントには、名前を指定した呼び出しについて「パース可能な呼び出しは保証するが、質の高い呼び出しは保証しない」と記載されています。この一文が、本記事の実測結果をそのまま要約しています。
形式と引数は別々に制限される
0.21.0以降、xgrammar の 構造タグ(structural tag) によってツール呼び出しを強制する機能が導入され、現在のドキュメントでは強制の仕組みが2段階に分けられています。
- エンベロープ(envelope):
<tool_call>のような目印や関数名。requiredと名前指定では常に強制されます。 - 引数スキーマ: そのツールに
"strict": trueを付与したか、サーバーを--tool-strict-level parameterで起動した場合のみ強制されます。そうでない場合、引数は「JSONのように見えるもの」程度にしか制限されません。
サーバー側のデフォルト値 --tool-strict-level は auto(デフォルト、リクエストに従う)、 function(すべてのリクエストでエンベロープを強制)、 parameter(すべてのツールを strict のように扱う)の3つです。ほとんどのエージェントフレームワークは strict を付与しないため、オペレーターがデフォルトを引き上げられるように作られています。環境変数 VLLM_ENFORCE_STRICT_TOOL_CALLING=false は構造タグを丸ごと無効化します。
3. 実際に測定してみる
環境と方法
- GPU:8GBクラスのノートPC用GPU(Ada世代)1枚、ドライバー595.99、CUDA 13.0ビルド
- vLLM 0.31.0公式コンテナ、PyTorch 2.13.0、xgrammar 0.2.7、llguidance 1.7.6
- モデル:Qwen3-Reranker-0.6B(bf16)。リランキング用にファインチューニングされたモデルであるため、会話能力はほとんどありません。チャット形式で質問しても大抵「No.」と返答します。「上手にツールを使うモデル」ではなく、 「ツールを使うことのできない小さなモデル」をサーバーがどこまでサポートできるのかを検証するテストとしてお読みください。
- サーバー:
--enable-auto-tool-choice --tool-call-parser hermes、その他はデフォルト値 - リクエスト:「What is the weather in Seoul right now?」、ツール1つ
get_weather(city: string, unit: "c"|"f")、両方のフィールドが必須、temperature 0.7,max_tokens 200、シード0〜4で5回。同じリクエストを strict なし、およびstrict: trueとadditionalProperties: falseを付与して送信しました。 - 「スキーマに準拠」とは、クライアントが
argumentsをJSONとして読み込み、cityが文字列であり、unitが c または f であるかを確認した結果です。
結果
| tool_choice | strict | tool_calls が出力された回数 | スキーマに準拠した引数 | 応答時間(中央値) |
|---|---|---|---|---|
| auto | なし | 0/5 | 0/5 | 24.5ms |
| auto | あり | 0/5 | 0/5 | 24.6ms |
| required | なし | 4/5 | 0/5 | 216.6ms |
| 名前を指定 | なし | 4/5 | 0/5 | 216.2ms |
| required | あり | 5/5 | 5/5 | 263.1ms |
| 名前を指定 | あり | 5/5 | 5/5 | 261.7ms |
生データの応答の一部をそのまま転記します。
auto:5回すべて本文が「No.」であり、ツールを呼び出しませんでした。autoの場合、モデルが呼び出さないと判断すればそれで終了です。required、strict なし:1回目のリクエストは<tool_call>の中に"location": "1801, 401, 181, 182, …"のようにスキーマに存在しないフィールドや数字の羅列を200トークンの最後まで書き続け、途中で途切れたためパーサーが tool_calls に変換できませんでした(1,180ms)。残りの4回は{}が3回、{"city": "Hawaii"}が1回でした。関数名は合っていましたが、必須フィールドのunitは1度も含まれていませんでした。required、strict あり:{"city": "Baltic", "unit": "c"},{"city": "Berlin", "unit": "c"},{"city": "Hawaii", "unit": "f"},{"city": "none", "unit": "f"},{"city": "New York", "unit": "f"}.
同じシードにおいて required と名前指定の出力は文字単位で完全に一致していました。ツールが1つだけの場合、両方式とも同じ文法を生成するためです。
サーバー側もこれをカウントしています。テスト終了後、 /metrics の vllm:tool_call_parser_invocations_total は、非ストリーミングのチャットリクエストにおいて outcome="tool_call" 18, outcome="no_tool_call" 12 でした。クライアント側で計測した値(tool_calls が出力された応答18個、出力されなかった応答12個)と一致しています。
読み方
- strict のない required は「呼び出しが存在する」ことだけを保証しました。 関数名までは合致していましたが、引数が空のオブジェクトであったり必須フィールドが欠落していました。0.31.0および0.32.0のどちらのドキュメントでも「strict のないツールは required や名前指定でも緩い引数の文法しか受け付けない」と記載されており、実測結果はその記述と一致していました。
- strict を有効にするとスキーマは満たされるものの、意味合いは正しくなりませんでした。 すべての引数がJSONとして読み込まれ、必須フィールドや列挙値は遵守されましたが、質問にある Seoul は一度も出現せず、
"none"のような値も出力されました。文法は「許可されたトークン」を制限するだけであり、残されたトークンの中からどれを選ぶかは依然としてモデルが決定します。 - 時間の差分は大部分がトークン数の違いによるものです。 strict 側が50msほど長くなりましたが、
{}の代わりに2つのフィールドを両方記述したことでトークンが増えた分が混入しているため、文法チェック自体のコストとして捉えてはなりません。文法コスト単体の測定値は第4回に掲載されています。
3-1. 2026-10-27再測定:ツールを理解するモデルと0.32.0
これまでのテストでは、ツールを理解しないモデルを使用して「サーバーがサポートできる範囲」を確認しました。今回は、ツール呼び出しを学習済みのモデルを使用して、内容まで検証します。
- 環境は同じで、vLLMのみ0.32.0公式コンテナに変更しました。モデルは会話用の Qwen3-0.6B(bf16)、同モデルのGPTQ 8ビット版、および Qwen3-4B のAWQ 4ビット版です。思考プロセスは
chat_template_kwargs: {"enable_thinking": false}で無効化しました。 - リクエスト:「What is the weather in {都市} right now? Answer in celsius.」 都市は Seoul、Paris、Tokyo、Cairo、Lima、Oslo の6箇所。「正解」は引数の city にその都市が含まれており、unit が c である場合です。
| モデル | tool_choice | strict | 呼び出しが出力された回数 | 正しい都市・単位 | 応答時間の中央値 |
|---|---|---|---|---|---|
| Qwen3-0.6B | auto | なし | 6/6 | 6/6 | 162.1ms |
| Qwen3-0.6B | required | なし | 6/6 | 6/6 | 178.0ms |
| Qwen3-0.6B | 名前を指定 | なし | 6/6 | 6/6 | 178.1ms |
| Qwen3-0.6B | auto・required・名前指定 | あり | 18/18 | 18/18 | 184.1〜185.4ms |
| Qwen3-0.6B GPTQ 8ビット | 6つのケースすべて | なし・あり | 36/36 | 36/36 | 115.7〜140.6ms |
| Qwen3-4B AWQ 4ビット | 6つのケースすべて | なし・あり | 36/36 | 36/36 | 338.7〜362.4ms |
- ツールを理解するモデルは auto でも毎回呼び出しを行い、strict がなくても引数が正確でした。 リランキングモデルでは strict を有効にしてようやくスキーマに準拠し、それでも都市を間違えていた結果とは対照的です。
- 0.6B において strict を有効にすると、required・名前指定では約7ms、auto では22ms遅くなりました。auto は strict がないと文法を全く使用しない状態から有効化されるため、差分がより大きくなります。生成された引数のテキストは strict の有無に関わらず同一であったため、この差分は文法マスクのコストとして解釈できます。
- 同じ0.32.0コンテナでリランキングモデルを使い、前述のテストをそのまま再実行した結果は、0.31.0と出力文字に至るまで完全に一致していました(strict のない required のスキーマ準拠率 0/5、strict あり 5/5、都市は Baltic・Berlin・Hawaii・none・New York)。
このテストはツールが1つ、短い質問が1つというシンプルなものです。複数のツールがある場合や引数が複雑な場合の精度については、未測定のままです。
4. 運用への示唆
- エージェントサーバーの場合、デフォルト値を引き上げます。 クライアント側が strict を指定してくることを期待するよりも、
--tool-strict-level parameterを指定して起動すれば、すべてのモールの引数がスキーマに準拠します。一方で、スキーマが深い場合や大きなツールが多い場合は文法を生成するコスト(第4回の初回リクエストの遅延)が発生するため、初回リクエストの遅延も併せて確認します。 - 引数は常に信頼できない入力です。 スキーマに準拠した引数であっても、
"none"のような出鱈目な値を格納しました。ツールを実行する側で値の範囲や権限を再度検証する必要があり、ファイルパス、SQL、シェルコマンドのような危険な引数は許可リスト(ホワイトリスト)でのみ受け取るようにしなければなりません。プロンプトに入ってきた外部のテキスト(ウェブページ、ドキュメント)がモデルをそそのかしてツールを呼び出させるプロンプトインジェクションも、同じ理由で防げないものとして設計します。 - autoが静かに失敗することを指標として捉えます。
autoでモデルが形式を間違えた場合、エラーではなく普通の本文に戻ります。上記のvllm:tool_call_parser_invocations_totalでno_tool_callの割合をダッシュボードに配置しておくと、モデル、パーサー、チャットテンプレートのいずれかが変更されたときにすぐに分かります。ただし、この指標は「ツールが必要だったのに呼び出さなかった」と「ツールが必要なかった」を区別できないため、アプリケーション側の期待値と合わせて確認します。 - パーサーはモデルとペアです。 Qwen2.5・Qwen3は
hermes、Qwen3-Coderはqwen3_xml、Llama 3.1はllama3_jsonのように、モデルのドキュメントで定められたパーサーを使用します。0.32.0にはチェックポイントのresponse_templateを読み取ってモデル別のパーサーなしで解釈するhfパーサーが追加されましたが、このパーサーは strict、required、名前指定を受け付けないとドキュメントに記載されています(未実測)。
5. 確認できていない事項
- ツール呼び出しを学習したモデルの精度は、3-1節で単一のツール・短い質問でのみ測定しました。ツールが複数ある場合や、対話が長長しい場合は未実測です。
- 複数のツールを一度に呼び出すパラレルコール、ストリーミング時の tool_calls の断片、
--tool-strict-levelサーバーオプションは未実測です。 - 0.32.0の
hfパーサーとparallel_tool_calls=falseの強制は未実測です。
参考
- vLLM ドキュメント、Tool Calling(0.32.0, 0.31.0): https://docs.vllm.ai/en/latest/features/tool_calling.html
- vLLM 0.21.0 リリースノート(構造タグによる strict なツール呼び出し): https://github.com/vllm-project/vllm/releases/tag/v0.21.0
- Qwen ドキュメント、Function Calling と vLLM: https://qwen.readthedocs.io/en/latest/framework/function_call.html
- 本シリーズ第4弾 構造化出力
- 元データ: 本シリーズのフォルダーにある README の「元データ」セクション