構造化出力と制約デコーディング — JSONを壊さない仕組み
「JSONで返して」とお願いする代わりに、文法的に許されないトークンを出す前に確率ゼロへ落とす — 制約デコーディングの仕組みを、logitマスクから語彙のインデックス化、function callingの裏側、そして「構文は守れても中身は守れない」限界まで1から解説する。
Efficient Guided Generation for Large Language Models
一次資料 — この記事の根拠
この解説の公開 2026-08-27
Efficient Guided Generation for Large Language ModelsarXiv:2307.09702論文ページ·PDFXGrammar: Flexible and Efficient Structured Generation Engine for Large Language ModelsarXiv:2411.15100論文ページ·PDF
「JSONで返して」とお願いしても壊れる理由
プロンプトに「必ずJSONで答えてください」と大文字で三回書いても、数百回に数回は壊れたものが返ってきます。閉じ括弧がひとつ足りない。末尾に余計なカンマが残る。Markdownのコードフェンスで丁寧に包まれてしまう。本文の前に「はい、承知しました。」という挨拶がついてくる。
原因は、生成が左から右へ1トークンずつ進み、後戻りできないことにあります。人間なら書き終えてから括弧の対応を数え直せますが、モデルにその工程はありません。「いま開いた { に対応する } を最後に出す」という規律は、学習データの中に正しいJSONが大量にあったおかげで身についた確率的な癖であって、機構的な保証ではないのです。
そして「だいたい当たる」は、本番でいちばん厄介な性質です。1000件のバッチのうち3件だけがパースに失敗し、その3件のためにリトライとアラートと当直の対応が必要になります。
比喩: 一筆書きで迷路を抜ける
生成を、鉛筆を紙から離さずに迷路を抜ける一筆書きだと考えてください。線を引いてしまった場所には戻れません。行き止まりに入ったら、そこで詰みです。
対策は原理的に二つしかありません。ひとつは、抜けたあとで正しいかを確認し、間違っていたら最初から引き直すこと。これがバリデーションとリトライで、実装は簡単ですが、成功は運任せで、失敗するたびに全部やり直しになります。
もうひとつは、行き止まりへ向かう道を、鉛筆が入る前に壁で塞いでしまうこと。これが制約デコーディング(constrained decoding、あるいはguided generation)です。壁があれば、迷路を抜けられないルートはそもそも選べません。
直感: 出せない手を、出す前に消す
モデルは各ステップで、語彙に含まれる全トークン(10万語規模)に「スコア」を付けます。この生のスコアがlogitです。ふだんはこれをsoftmaxで確率に直し、そこから1つ引いて次のトークンにします。
制約デコーディングがやることは一行で言えます。softmaxにかける前に、いま文法が許さないトークンのスコアを負の無限大に沈める。それだけです。確率がちょうど0になったトークンは、どんな温度設定でも、どんな乱数でも選ばれません。
こうして問題の質が変わります。「壊れたJSONが出る確率をどう下げるか」ではなく、「壊れたJSONは出せない」になる。この差は、運用してみるとかなり大きいものです。
仕組み: logitマスク
文法から作られるのは、各ステップごとのマスクです。語彙と同じ長さの、0と1が並んだ札だと思ってください。
はトークン に対するモデルの生スコア、 は「文法がいま を許すなら1、許さないなら0」の札、 が補正後のスコアです。 なので許されたトークンのスコアはそのまま、 なので禁止されたトークンは負の無限大まで落ちます。足し算ひとつで「そのまま」と「消滅」を切り替えているわけです。
消えた分は、残りに配り直される
補正後のスコアは、あとは通常どおりsoftmaxに通します。
は温度(大きいほど分布が平らになる調整つまみ)、 は が選ばれる確率です。 なので禁止トークンの確率はちょうど0になり、分母から消えた分は残った候補に自動的に配り直されます。もともと確率が2%しかなかった候補でも、他が全部禁止されているなら100%になります。
この「softmaxの前に を足す」という道具は、Transformerの因果マスクとまったく同じものです(Attention機構を1から理解するで扱った、未来を覗かせないための処理)。使う場所が違うだけで、発想は共通しています。
実装上の注意がひとつ。本当に -inf を代入すると、バグや文法の書き間違いで全トークンが禁止された瞬間にsoftmaxの分母が0になり、確率が nan になって静かに壊れます。多くの実装が -1e9 のような有限の大きな負数を使うのはこのためで、代わりに「許されたトークンが1つも無い」状態を明示的に検出してエラーにする必要があります。
トークンと文法は、目盛りがズレている
ここからが実装の本番です。文法はJSONの文字({、"、,)で定義されていますが、モデルが出すのはトークンであり、トークナイザが作るBPEトークンは文字の切れ目と一致しません。
実際の語彙には {" がひとつのトークンとして入っていたり、": や ", のような区切りの塊が入っていたりします。つまり「次に許される文字は " である」という文法側の情報を、「次に許されるトークンの集合」へ翻訳する工程が必要になります。
コメント
コメントにはログインが必要です