Tool CallingとStructured Outputs:破綻しない契約
24回の呼び出しで壊れたJSONはゼロ、有効な日付は2件。同じendpointを説明だけ改善し、schemaで直せないものを見る。
このページの内容
modelにフライト検索ツールを渡し、マドリードからベルリンへの便を探すよう頼んでみます。返ってくるのは次のようなものです。
<tool_call>
{"name": "search_flights",
"arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>JSONは有効です。ツール名も正しい。必須フィールドもすべてあります。それでもこの呼び出しは役に立ちません。空港コードが必要な場所に"Madrid"を、日付が必要な場所に"3rd October 2026"を受け付けるフライトAPIはありません。
このギャップ — 構文的には完璧で、意味的には使い物にならない — が、この章のテーマです。そして最初に確認すべきなのは、これはJSONの問題ではないということです。このツールで24回リクエストしたところ、modelは24件の有効なtool callを生成し、壊れたJSONはゼロでした。誰もがデバッグしがちな部分で失敗したことは一度もありません。
modelは何も実行しない
セクション「modelは何も実行しない」へのリンク仕組みに入る前に、もっとも多くの混乱を防ぐ一文があります。tool callはリクエストであって、アクションではありません。
modelは、search_flightsをこの引数で呼びたいという構造化メッセージを出します。そしてそこで止まります。あなたのコードがそのメッセージを受け取り、それを許可するかを判断し、必要なものを呼び出し、結果を別のメッセージとして返します。modelはデータベースに触れていません。HTTPリクエストも送っていません。認証情報も持っていません。
第30章のagentセキュリティに関するすべてはこの分担から導かれますし、第23章のagent設計についても同じです。modelは提案し、あなたのコードが処理する。そして、あらゆる保証が存在する場所はコードです。
語彙を取り払うと、ツールとは2つのものです。
schema。 関数を説明するJSON Schemaです。名前、何をするか、どの引数を取り、それぞれの型や制約が何かを記述します。これがpromptに入るものであり、modelが実際に見る唯一のものです。
endpoint。 それらの引数を受け取り、何かを返すあなたのコード内の関数です。modelはそれを見ません。どの言語で書かれているかも知りません。データベースクエリとハードコードされた文字列を見分けることもできません。
リクエストと一緒にschemaを送る
セクション「リクエストと一緒にschemaを送る」へのリンクツール定義はpromptに入り、modelが学習した形式にシリアライズされます。これは毎回の呼び出しでtokenを消費します。この章の後半で、その数字が出てきます。
modelはテキストの代わりにcallで答える
セクション「modelはテキストの代わりにcallで答える」へのリンク散文ではなく、レスポンスには構造化されたリクエストが含まれ、APIはそれを示すfinish reasonを返します。この理由が重要です。あなたのコードはこれによって、ユーザーに回答を表示するのではなくツールを実行すべきだと判断します。
あなたのコードが実行する — または拒否する
セクション「あなたのコードが実行する — または拒否する」へのリンクここにはmodelは関わりません。引数をschemaに照らして検証し、この呼び出し元に実行権限があるかを判断し、実行します。
結果をメッセージとして送り返す
セクション「結果をメッセージとして送り返す」へのリンク結果は会話内の別のターンになり、そのために予約されたroleで送られます。modelはそれを他のcontextと同じように読みます。
modelが答える、または別のツールを求める
セクション「modelが答える、または別のツールを求める」へのリンクこれが第23章のループであり、1つのリクエストが何度もの往復に変わりうる理由です。
これは創発ではありません。第11章で確認したように、tool callingは訓練された振る舞いです。1 post-trainingの間、modelはまさにこの形の会話を何千件も見ています。だから形式はmodelごとに違い、同程度のサイズのmodel間でも信頼性が大きく異なり、modelは見たことのないツールも呼び出せます。形は訓練で学び、具体的なツールはあなたのpromptから来るのです。
悪いschemaのコストを測る
セクション「悪いschemaのコストを測る」へのリンク多くの人が最初に書くツールはこうです。これ自体に間違いがあるわけではありません。ただ薄いのです。
{
name: "search_flights",
description: "Search for flights.",
parameters: {
type: "object",
properties: {
from: { type: "string", description: "Airport." },
to: { type: "string", description: "Airport." },
date: { type: "string", description: "The date." },
},
required: ["from", "to", "date"],
},
}24件のリクエスト。6組の都市ペアに、日付の表現4種類(「来月の3日」、「次の金曜日」、「12月15日」、「明日」)を掛け合わせました。結果を再現できるようgreedy decodingです。
| tool called | broken JSON | date in ISO | airports as IATA | everything correct | |
|---|---|---|---|---|---|
| 上のschema | 24/24 | 0 | 2/24 | 4/24 | 1/24 |
最後の3列を見る前に、最初の2列を見てください。modelは毎回正しいツールを呼び、毎回整形式のJSONを生成しています。失敗は完全に値にあります。そしてその値は使えません。"Madrid"の代わりにMAD、"3rd October 2026"の代わりに2026-10-03です。
ここは強調しておく価値があります。何かが壊れたときに、どこを見るべきかを決めるからです。反射的には、リトライ付きのJSONパーサーを足したり、有効なJSONを出すようmodelにもっと強く頼んだりしがちです。しかし、ここで起きたことにはどちらも対処していません。
変えるのはdescriptionだけ
セクション「変えるのはdescriptionだけ」へのリンク同じendpointです。背後のコードも同じ。modelもpromptもdecodingも同じ。変えるのはschema内のテキストだけです。
{
name: "search_flights",
description: "Search scheduled flights between two airports on a given day.",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",
pattern: "^[A-Z]{3}$",
},
to: { /* same */ },
date: {
type: "string",
description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",
format: "date",
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
},
},
required: ["from", "to", "date"],
},
}| date FORMAT | date VALUE | airport FORMAT | airport VALUE | |
|---|---|---|---|---|
| 薄いschema | 2/24 | 1/24 | 4/24 | 4/24 |
| description付きschema | 24/24 | 12/24 | 16/24 | 8/24 |
日付の形式は24件中2件から、24件中24件になります。コードに触れず、リトライロジックもなしに、テキストの変更だけで完璧になります。この章から運用上の習慣を1つ持ち帰るなら、これです。ツールが誤って呼ばれるとき、修正はほとんど常にdescriptionの中にあり、それはシステム内でもっとも安い修正です。
次に、より重要な半分である2列目を見てください。
schemaは形を制約する。知識は供給できない。
セクション「schemaは形を制約する。知識は供給できない。」へのリンク日付は24件中24件でISO形式です。しかし正しい日なのは24件中12件です。
つまり半分の呼び出しは、完全にフォーマットされた間違った日付を運んでいます。descriptionはmodelに生成すべき形を伝え、modelはそれを完璧に生成しました。しかし「次の金曜日」を2026-09-11に変えるには、今日の日付を知り、カレンダー計算をする必要があります。どれだけdescriptionを書いても、その情報は供給されません。空港も同じです。形式は4から16に増えましたが、値は4から8にしか増えませんでした。MADを書くには、マドリードの空港がMADだと知っている必要があるからです。
この区別こそが、この章を支える考え方です。
schemaは形式についての契約です。modelの出力をparseableで、型付きで、一貫したものにできます。しかしそれを真実にすることはできません。良いschemaを通り抜けて残る失敗モードはすべて、形式の失敗ではなく知識の失敗です。
この2つには別々の修正が必要で、混同すると何週間も無駄にします。形式の失敗はdescription、または後述するconstrained decodingで直します。知識の失敗は、知識をpromptに入れることで直します。system messageに現在日付を入れる。空港検索をmodelが先に呼ぶ2つ目のツールにする。集合が列挙できるほど小さいならschema内のenumにする。この3つに共通する点に注目してください。問題をmodelの記憶から入力へ移しているのです。これは第24章の全体テーマです。
Structured outputsと、「constrained decoding」とは実際に何か
セクション「Structured outputsと、「constrained decoding」とは実際に何か」へのリンクここまでのすべては、modelが正しい形を生成することを選ぶ前提にまだ依存しています。より強い保証があり、それは第17章の最大の見返りです。
生成の仕組みを思い出してください。各ステップでmodelは語彙内のすべてのtokenに対してlogitを出し、samplerが1つを選びます。Constrained decodingはその間に1ステップを挿入します。JSON Schemaから導かれた文法に基づいて、次に合法的に来られるtokenを計算し、それ以外すべてのlogitを負の無限大に設定し、残ったものからsamplerに選ばせます。
schemaが次に来るものは{でなければならないと言うなら、{ではないすべてのtokenの確率はゼロです。「ありそうにない」ではありません。ゼロです。無効なtokenはsamplingの前に分布から取り除かれているので、modelは無効なJSONを出せません。
これが「structured outputs」「JSON mode」「guided generation」の内部で起きていることです。そして、そこから2つの性質が説明できます。文法で表現できるもの — 型、必須フィールド、enum、ネスト — についての保証は完全です。丁寧に依頼しているのではなく、機械的に強制しているからです。そして、これは内容については何も言いません。文法は"date"を日付パターンに一致する文字列に強制できますが、それを正しい日に強制することはできません。前の節と同じ壁に、反対側から到達しただけです。
実務上の注意が2つあります。これは無料ではありません。maskは各ステップで計算する必要があり、複雑な文法は測定可能なlatencyを生みます。さらに、modelがしていることを変えます。好みのtokenから逸らされたmodelは、完璧な構造を出しながら内容の品質を落とすことがあります。だから、単純な形では「丁寧に頼んで検証する」が今でも妥当なデフォルトであり、形が複雑なときや利用側が厳格なときにconstrained decodingはそのコストに見合います。
副作用と、重要なたった1つの性質
セクション「副作用と、重要なたった1つの性質」へのリンク第14章では、タイムアウト後のリトライにより、1つの回答に対して2回の生成が課金される例を測りました。ツールでは同じ失敗がさらに悪化します。ツールは何かを実行できるからです。
あなたのコードがcharge_cardを呼び、タイムアウトし、リトライしたら、請求は2回です。modelはこの一連の出来事を何も知りません。見えているのは1つのツール結果だけです。修正はあらゆる分散システムと同じで、modelの問題ではありません。呼び出しにキーを持たせ、2回目の実行が1回目を認識して、作業を再度行う代わりにその結果を返すようにして、操作を冪等にします。
ここから導かれる設計ルールは、はっきり言う価値があります。ツールカタログでは読み取りと書き込みを分けてください。 読み取りは自由にリトライでき、並列実行でき、キャッシュできます。書き込みはそうはいきません。キー、権限チェック、そしてユーザーが事前に知りたいと思うものについては、リクエストとアクションの間に人間を挟む承認ステップを持つべきです。その承認ステップは単なる配慮ではありません。prompt injectionと現実の結果の間に立つ数少ないものの1つです。そして第30章で測るように、その中でもっとも弱いものです。
劣化するまで、ツールはいくつ載せられるのか
セクション「劣化するまで、ツールはいくつ載せられるのか」へのリンク多くのツールを読み込むとmodelの選択が悪くなる、という俗説があります。繰り返すより測る価値があります。そこで、同じ24件のリクエストを、フライトツールに増えていく他のツール群を加えて実行しました。その中には、あえて紛らわしくした3つ(列車時刻表、フェリー航路、バス路線)も含めています。
| tools loaded | prompt tokens | chose search_flights | date in ISO |
|---|---|---|---|
| 1 | 353 | 24/24 | 24/24 |
| 5 | 730 | 24/24 | 24/24 |
| 10 | 1,193 | 21/24 | 21/24 |
| 20 | 2,119 | 24/24 | 24/24 |
選択は劣化しませんでした。20個のツールがあり、そのうち3つがもっともらしく紛らわしい状態でも、5億parameterのmodelは24件中24件で正しいものを選びました。10個のところの落ち込みは、別のツール名を挙げた3件によるもので、20個に増やすと再現しません。
これは否定的な結果であり、そのように報告すべきです。このタスク、このツール群では、「ツールが多すぎる」ことは問題ではありませんでした。 単調に、そして6倍に増えたのはpromptです。353 tokenから2,119 tokenへ。会話内のすべてのリクエストで、ツールが使われるかどうかに関係なく、永続的に支払われます。
つまり、この俗説の正直な版は精度ではなく、コストとcontextについてです。20個のツールはすべてのメッセージに対する恒久的な税であり、第16章はすでに、40ターンにわたって恒久的なprefixが請求に何をするかを示しました。多くのツールが品質を傷つけると報告されるとき、その機構はたいてい、定義が重要なcontextを押し出したというものです。これは第18章の衣装を着た第24章の問題です。本当に互いにほぼ重複しているツールも実際の問題ですが、その修正はツールを減らすことではなく、より良いdescriptionとnamespaceです。システムごとにprefixを付け(crm.search_customer、billing.search_customer)、2つのチームから統合された2つのカタログが衝突しないようにし、modelが見分ける材料を持てるようにします。
3種類のツールと、次の部への扉を開くもの
セクション「3種類のツールと、次の部への扉を開くもの」へのリンクツールを、世界に対して何をするかで分類すると役に立ちます。engineeringがそれぞれで違うからです。
データツールは読み取ります。検索、取得、query。リトライ可能で、並列化でき、キャッシュできます。失敗は有用なものを返せないという形で起きます。そして主なリスクは、信頼できないテキストをcontextに持ち込むことです。これは第30章の攻撃面そのものです。
アクションツールは書き込みます。送信、作成、課金、削除。キーなしにはリトライできず、安全には並列化できません。そして承認フローが存在する理由です。
オーケストレーションツールは他のmodelを呼び出します。実装が別のagentであるツールです。そのagentは自分のprompt、自分のツール、自分のループを持ちます。そして呼び出し元のmodelから見ると、これは他の2種類とまったく同じに見えます。modelが見るのはschemaとendpointだけだからです。
3つ目は珍品ではありません。第25章に出てくるagent-as-a-toolの半分を支える仕組みです。もう一方の構成である引き継ぎは、会話を渡して二度と取り戻しません。そしてそれが機能するのは、この章のinterfaceが、まるごとのagentを背後に収められるほど十分に狭いからです。
次に進む場所
セクション「次に進む場所」へのリンクこれで、何かを求められるmodelと、その求め方をparseableにする契約が手に入りました。まだ持っていないのは、promptに収まるものを超えて、modelが何について求めるかです。
本番でもっとも一般的なツールは、圧倒的に、modelがtraining中に見たことのない大量のテキストに対する検索です。あなたのドキュメント、チケット、契約書です。それは解決済みの問題に聞こえます。embeddingし、nearest neighboursを探し、貼り付ける。しかし未解決の部分こそが、回答を信頼できるかどうかを決めます。embeddingする前にテキストをどう切るか。どのsimilarity thresholdなら十分低く、わかりませんを意味するのか。読者が確認できるよう、主張にcitationをどう紐づけるのか。
第19章はretrievalです。そして、間違った回答が珍事ではなく責任問題になり始める章です。
Sources and method
セクション「Sources and method」へのリンクこの章の測定値は、Qwen/Qwen2.5-0.5B-Instructを使い、greedy decodingで得たものです。6組の都市ペアと4種類の日付表現を掛け合わせた24件の生成リクエストに対し、ツール定義にはmodel自身のchat templateを使いました。結果は完全に再現します。そしてこれは小さなmodelです。format/valueの分離は、現在のmodelのbenchmarkではなく、仕組みの実演として読んでください。frontier modelは「次の金曜日」をはるかに高い頻度で正しく解決します。それでもschemaによってそうさせることはできません。一般化するのはその部分です。
上で使ったJSON Schemaの語彙(type、properties、required、pattern、format、enum)は、providerのドキュメントが示すJSON Schema draftで定義されています。有用なsubsetは小さく、provider間でほぼ同じです。実際に存在する差分 — どのkeywordが、単にmodelへ渡されるだけでなくconstrained decodingによって強制されるのか — は、推測するのではなくproviderのstructured-output guideで読む価値があります。
技術としてのconstrained decodingについては、guidance系のlibraryとoutlinesプロジェクトが、文法からlogit maskを作る構成を、第17章のsamplerに直接対応する形で文書化しています。そして往復そのものについて、もっとも明快な仕様はtutorialではなくprotocolです。第26章ではそれを1行ずつ読んでいきます。
参考文献
セクション「参考文献」へのリンク-
Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). post-trainingのレシピを標準にした論文です。tool callの形は、回答の形とまったく同じように、demonstrationからここで学習されます。 ↩