分析:言語モデルから機械可読な出力を得る方法と、「JSONで返す」が仕様とは言えない理由
モデルをパイプラインに組み込む製品は、いずれ信頼性の高い構造化出力を必要とする。失敗のパターンはよく知られており、解決策は地道なものだ。実践ガイド。
ひとことで言うと
言語モデルから信頼性の高い構造化データを返させるにはどうすればよいか?
利用できる場合は、プラットフォームの制約付きデコーディングやスキーマ強制モードを使う。これにより、不正な構文は「起こりにくくなる」のではなく、生成そのものが不可能になる。そのうえで、モデルに適したスキーマを設計する。構造はフラットにし、必須フィールドは少なく、列挙値は明示し、不確実性を表現する手段を設け、計算を必要とするフィールドは置かない。すべての応答をスキーマに照らして検証し、意味上の正しさ、つまり形が有効かどうかだけでなく値が正しいかどうかは、検証だけでは解決できない別の問題として扱う。
要点
- スキーマ制約付きデコーディングが解決するのは構文であり、意味ではない。形式上は完全に正しいJSONでも、内容がまったくの誤りということはあり得る。
- スキーマ設計はプロンプト設計である。すべてのフィールド名、列挙値、説明文が、モデルの読み取る指示となる。
- 「存在しない」または「不確か」と正当に回答できる手段をモデルに与えるべきだ。それがなければ、必須フィールドは値の捏造を促すことになる。
- 導出可能な合計、割合、日付をモデルに計算させてはならない。必要な入力値を抽出し、コードで計算する。
- 検証したうえで、検証エラーを添えて一度だけ再度問い合わせる。対象を絞った修正を1回行えば、残る不具合の大半を、再試行ループより低コストで解消できる。
言語モデルが人間ではなくプログラムのために書き始めた瞬間、まったく別種の要件が立ち上がる。文章はおおむね正しければ済む。だが total_amount という名のフィールドはそうはいかない。
ほとんどのチームは、決まった順序でこの事実を思い知る。プロンプトでJSONを要求すると、うまくいく。そのままリリースする。一週間後、モデルがオブジェクトをコードフェンスで囲んだり、前置きの一文を添えたり、末尾にカンマを残したりしたせいで、パーサーが例外を吐く。クリーンアップ関数を追加する。今度はうまくいく。ひと月後、本来 null であるべきフィールドに、もっともらしい捏造値が紛れ込む。しかし今度は何も落ちない。そちらのほうがはるかに厄介だ。
この二つの失敗は種類が異なる。両者を混同することこそが、問題の解決に時間がかかる理由である。
二つの問題、一つの呼び名
構文的な失敗とは、パースできない、あるいはスキーマに一致しない出力を指す。これは派手で、即座に露見し、完全に解決可能な問題だ。
意味的な失敗とは、パースは完璧に通るのに、内容が誤っている出力を指す。文書のどこにも存在しない日付が自信満々に書かれている。列挙型には収まるが内容とは合わないカテゴリー。明細の合計と一致しない総額。
制約付きデコーディング——主要なプラットフォームがこぞって提供するようになったスキーマ強制モードは、前者のカテゴリーをほぼ完全に排除する。各生成ステップで許容されるトークンを制限し、文法の外側にある出力そのものを生成不可能にする仕組みだ。これは、モデルがおおむね従うだけの「提案」に過ぎないプロンプトベースの要求とは、質的に異なる進歩である。その違いはテールで顕在化する。千回に一回だけ失敗していたリクエストこそが、誰かのポケベルを鳴らす当のリクエストだったからだ。
だが後者のカテゴリーには、何ら効果がない。そしてこの事実が心理的にどう受け止められるかには、看過できないリスクがある。スキーマ強制を導入したチームは、しばしば検証をやめてしまう。プラットフォームがスキーマを「保証」しているからだ。しかしその保証が及ぶのは形式だけである。残るすべての問い——この値は正しいか、フィールド同士は整合しているか、この日付はもっともらしい範囲に収まっているか——は、依然としてこちら側の責任である。
スキーマもプロンプトの一部である
これは最も有用な視点の転換でありながら、日常的に見落とされている。モデルはスキーマを読む。フィールド名、列挙値、説明文、そして並び順——そのすべてが指示であり、システムプロンプトと影響力を競い合っている。
つまり、スキーマ設計の選択は、出力品質に直接影響する。
フィールド名は、人に説明するときと同じ言葉で付ける。 flag2 よりも is_urgent のほうが良い結果を生む。フィールドに添える説明文——「顧客が48時間以内の締め切りを明言している場合のみtrue」——は、同じ一文をシステムプロンプトに書くよりもしばしば効果的である。判断のすぐ隣に置かれるからだ。
値の範囲が閉じているなら、自由記述の文字列ではなく列挙型を使う。 自由記述の category フィールドは、ほぼ同義語のロングテールを生み出し、後続のコードは永遠にそれを正規化し続ける羽目になる。列挙型であれば、その判断は生成時点で確定する。
フラットに保つ。 深くネストされたオブジェクトは、構造的な誤りが起きる確率と、一つの枝が壊れたときの被害範囲の両方を拡大する。対象領域が本当に階層的であるなら、深くネストした一回の応答よりも、二回の呼び出しに分けたほうがしばしば信頼性が高い。どの段階で失敗したかが見えるぶん、評価もしやすい。
推論が結論より先に来るようフィールドを並べる。 reasoning や evidence_quote フィールドを classification より前に置いたスキーマは、逆順のスキーマに比べて、測定可能なレベルで分類精度が上がる。モデルは左から右へ生成し、先に生成したフィールドが後のフィールドを条件づけるからだ。構造化された応答の中で、わずかな生成テキストがそのコストに見合う価値を持つ、数少ない場面である。
モデルに逃げ道を与える
捏造された値の原因として最も多いのは、値をでっち上げる以外に正当な選択肢を用意していないスキーマそのものである。
contract_end_date が必須かつnull不可の文字列であり、契約書に終了日の記載がない場合、モデルは何かを出力せざるを得ない。もっともらしい何かを生成するだろう。パイプラインの側は何も異議を唱えない。
解決策は、「存在しない」ことを表現可能にすることだ。null許容フィールド、明示的な not_stated という列挙値、あるいは値の有無を切り替える別個のブール値。「記載がないことは許容され、想定される回答である」という明確な指示と組み合わせれば、フィールドのハルシネーションの大半は取り除ける。しかもスキーマの一行を追加するだけで済む。
同じ論理は確信度にも当てはまる。high・medium・lowの三値をとる confidence 列挙型は、数値スコアよりも有用である。モデルは数値による自己評価の較正が不得手な一方、大まかな区分については比較的一貫性がある。lowの区分を人間に回せば、精度の問題をスループットの問題へと転換できる。こちらのほうがはるかに扱いやすい問題だ。
モデルに算術をさせない
subtotal、tax、total を含むスキーマは、一つが他から導出可能な三つの値を求めていることになる。モデルはその三つすべてを埋めるが、時折互いに食い違う。
入力値だけを抽出し、導出される値はコード側で計算する。同じ原則は、日付計算、パーセンテージ、件数、単位換算を伴うあらゆる値にも当てはまる。これはモデルの能力についての話ではない。決定的な計算のエラー率はゼロであり、生成による計算はそうではない、という単純な事実であり、前者が使える場面で後者を選ぶ理由はどこにもない。
検証と、一度だけの修復ターン
すべての応答を検証する。スキーマ強制エンドポイントからの出力であっても例外ではない。検証が失敗した場合、最も効果的な対処は、闇雲な再試行ではなく、具体的なエラー内容を含めた一度だけの修復リクエストである。「priority フィールドはlow・medium・highのいずれかでなければならないが、'urgent'が返された」と送り返せば、残存する失敗の大半は、追加の一ターンで解決する。
この回数は一回に限定する。敵対的な、あるいは本質的に曖昧な入力に対して無制限の再試行ループを回せば、目に見える失敗が目に見えないコストへとすり替わるだけでなく、まさに必要とされるシグナル——この入力の何かが、設計したスキーマに合っていないという事実——までもが握りつぶされてしまう。
再試行の先には、決定的なフォールバックを用意しておく。レビュー待ちとマークされたデフォルトレコード、ルールベースの処理経路、あるいは人間向けのキュー。唯一の失敗モードが「もう一度試す」であるパイプラインは、実質的に失敗モードを持たないのと同じであり、それは機能ではなく設計上の欠陥である。
構造化出力の評価
構造化出力は、評価という観点では異例なほど扱いやすい。チームはこの利点を積極的に活用すべきだ。
出力が機械可読であるため、ほとんどのチェックは厳密に実施できる。スキーマ妥当性、列挙値への適合、必須フィールドの有無、フィールド間の整合性、そしてラベル付きデータセットに対するフィールド単位の精度。追跡すべき指標はフィールド単位の精度であり、レコード全体の精度ではない。レコード全体を一つの単位として採点すると、どのフィールドが劣化しているかが見えなくなる。そして劣化するのはたいてい一つのフィールドであり、抽出全体ではない。
null率も追跡する。プロンプトやモデルの変更後にnullが急激に減少した場合、モデルがかつては値を保留していた箇所で捏造を始めたことを示すことが多い。そしてこれは、有効なレコードだけを見るチェックには決して映らない。
地味なまとめ
ここに気の利いたテクニックは何もない。制約付きデコーディングを有効にする。モデルがスキーマを読むという前提でスキーマを設計する。実際に読むのだから。「わからない」と言わせる余地を残す。算術はコード側にとどめる。それでも検証はする。修復は一度だけ、その後はフォールバックする。
そのどれもがコストの低い工夫であり、それらを積み重ねることこそが、モデルをシステムの一構成要素にできるか、それとも時折驚きを持ち込む厄介者のままにしてしまうかを分ける違いとなる。
よくある質問
- 制約付きデコーディングは、プロンプトでJSON形式を求めることと同じですか?
- いいえ。プロンプトによる要求は、モデルが通常は従う提案にすぎない。制約付きデコーディングでは、各段階で生成可能なトークンを制限し、文法から外れた出力を不可能にする。両者の違いは分布の裾野に表れ、本番環境での障害はまさにそこで発生する。
- 必須に指定したフィールドに、モデルが値を捏造するのはなぜですか?
- 空の値が許されていない必須フィールドは、何らかの値を出力せよという指示になるためだ。フィールドをnull許容にするか、「not_found(見つからない)」という列挙値を明示的に追加すれば、通常は値の捏造を防げる。
- 深くネストしたスキーマを使うべきですか?
- フラットな構造を優先すべきだ。ネストが深いほど、構造上の誤りが生じる可能性も、部分的な失敗によるコストも高まる。対象領域が本質的に階層構造を持つ場合は、深くネストした応答を1回で求めるのではなく、2回の呼び出しに分けることを検討したい。
- 再試行はどのように行うべきですか?
- 具体的な検証エラーを含めて修正を1回だけ試み、その後は決定論的な処理経路か人間による対応に切り替える。無制限の再試行は、誤った応答を高額な請求に変え、根本的な問題を覆い隠してしまう。
- APIがスキーマを保証している場合でも、検証は必要ですか?
- はい。保証されるのは構造であり、値ではない。列挙値への適合、フィールド間の整合性、値の範囲、参照整合性の確認は、いずれも引き続き利用者側の責任となる。
出典
- JSON Schema specification — JSON Schema
- Model Context Protocol — specification — Model Context Protocol
- OpenAPI Specification — OpenAPI Initiative