動画で読む
まず結論 — outlines は「文法的に正しい出力」を生成時に保証する
結論から書きます。dottxt-ai/outlines は、LLM の出力を JSON・正規表現・型に「生成しながら」強制する構造化生成(structured generation)ライブラリです。後からパースして弾くのではなく、トークンを 1 個選ぶ瞬間に、文法に合わないトークンを候補から消す。だから出力は必ず指定した形に収まります。
ここで大事なのは、保証されるのは「構造」だけで「中身の正しさ」ではない、という一点です。JSON として壊れていないことは保証されても、その JSON に入っている値が正しいかは別問題です。私はこの区別を最初に取り違えて痛い目を見たので、仕組みと落とし穴の両方を運用者目線で書き残しておきます。
なぜ「後からパースしてリトライ」では足りないのか
従来のやり方が苦しいのは、モデルに整形を「お願い」しているだけで、外れたら投げ直すしかないからです。プロンプトで「JSON で返して」と頼み、返ってきた文字列を JSON.parse に通し、失敗したらもう一度呼ぶ。この構造が根本的に不安定です。
私も以前、抽出タスクで最大 3 回リトライするループを書いていました。ローカルの 8B クラスのモデルだと、体感で 1 割前後は前後に余計な説明文が付いたり、末尾の閉じ括弧が抜けたりして parse に失敗します。リトライが増えれば当然レイテンシもトークン課金も膨らむ。呼ぶたびにコストが乗るという話はLLM API のコスト暴走を「呼ばない」で防ぐ三段防御でも書きましたが、リトライ前提のパイプラインはその防御と真っ向からぶつかります。
もう一つ厄介なのは、リトライしても収束する保証がないことです。モデルが同じ癖で同じように壊すと、3 回投げて 3 回とも失敗する。制約付きデコーディングは、この「そもそも壊れた出力を生成させない」という方向で問題を解きます。
仕組み — 正規表現を FSM に変換し、次トークンをマスクする
outlines の核は、出力の仕様を有限状態機械(FSM、finite-state machine)に変換し、各ステップで「今の状態から許されるトークン」だけを残して残りの確率を潰す点にあります。JSON schema も正規表現も、内部ではこの FSM に落とされます。
流れはこうです。まず JSON schema や Pydantic モデルを正規表現へ、正規表現を FSM へコンパイルします。生成時はモデルが語彙全体のロジット(logit、各トークンの生スコア)を出しますが、outlines は現在の FSM 状態で許可されないトークンのロジットを負の無限大に落とす。結果、サンプリングは合法なトークンからしか選べません。文字列の状態遷移を 1 トークンずつ進めながら、閉じ括弧を出すべき状態では閉じ括弧系トークンしか通さない、という具合です。
このアプローチの理論的な背骨は、Brandon T. Willard と Rémi Louf の論文 Efficient Guided Generation for Large Language Models にあります。テキスト生成を FSM の状態遷移として定式化し直したのがこの仕事で、outlines はその実装です。
状態→許可トークン集合を事前計算するのが速さの肝
速度で効いてくるのは、FSM の各状態から「次に許されるトークン集合」への対応表を、生成前に一度だけ作っておく設計です。生成中は語彙を毎回スキャンせず、状態番号でこの表を引くだけで済みます。
素朴に実装すると、1 トークン出すたびに数万語の語彙すべてを正規表現に照らすことになり、これは重い。論文が示したのは、この照合を語彙とインデックスの事前計算に押し込めば、生成時の追加コストをトークンあたりほぼ定数に抑えられる、という点です。代わりに複雑な schema や文法では、この索引づくり自体に前払いのコストがかかります。私が長い grammar を渡したときは、初回の準備で数秒待たされました。一度作れば同じ schema の使い回しは速いので、起動時に暖めておくのが実運用の定石です。
実際に使う — 型 / JSON schema / 正規表現
使い方はモデルを包んで、欲しい出力の「型」を渡すだけです。2026 年時点の最新は v1 系(記事執筆時点で 1.3.2)で、API は outlines.from_transformers(...) でモデルを包み、呼び出し時に出力型を第 2 引数で渡す形に整理されています。
import outlines
from pydantic import BaseModel
from transformers import AutoModelForCausalLM, AutoTokenizer
model = outlines.from_transformers(
AutoModelForCausalLM.from_pretrained("microsoft/Phi-3-mini-4k-instruct"),
AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct"),
)
class Review(BaseModel):
rating: int
pros: list[str]
cons: list[str]
# 返り値は必ず Review に収まる
result = model("このレビューを分類して: ...", Review, max_new_tokens=200)
Pydantic モデルの代わりに正規表現を渡せば、電話番号や日付のようなパターンにも縛れます。Literal["yes", "no"] のような選択肢に絞る使い方もでき、分類タスクではこれが一番手堅い。バックエンドは Hugging Face の transformers に加え、llama.cpp・vLLM・Ollama といったローカル/サーバ実行系に対応します。ローカル LLM が実務で使える水準に届いた話はローカルLLMが「使える」に変わった2026年に書きましたが、そこで抽出や分類を安定させる裏方が、まさにこの手の制約付きデコーディングです。
運用の落とし穴 — 構造は保証されるが中身は保証されない
一番刺さる落とし穴を先に書きます。制約付きデコーディングは「JSON として壊れていない」ことは 100% 保証しますが、「値が事実として正しい」ことは 1% も保証しません。ここを混同すると、緑のテストに油断します。
私がやった失敗はこうです。数値フィールドを int で縛ったら、モデルが本来「不明」と答えるべき場面でも、何らかの数字を無理やり埋めてくるようになった。構造の穴を許さない設計が、逆に「分からない」という正直な出力を封じてしまったわけです。強制はモデルの逃げ場を奪う。だから Optional や「不明」を表す明示的な選択肢を型側に用意しておかないと、幻覚を構造できれいにラッピングするだけになります。「動いた」が安全の証明にならないのは、AI 生成コードを静的スキャンで検証する運用で書いた話と同じ構図で、構造の検証と中身の検証は別レイヤーとして両方要ります。
もう一つは、早すぎる強制が推論の質を落とすことがある点です。考える前から出力を JSON に閉じ込めると、モデルが途中で言語化して整理する余地を奪います。実務では、まず自由記述で考えさせ、その後で構造化ステップに渡す二段構えのほうが精度が出る場面が多い。トークン境界と正規表現の境界がずれる(1 トークンが複数文字にまたがる)ことに起因する細かな不整合もあり、ここはライブラリ側の tokenizer 対応に依存します。
運用者として、いつ outlines に乗せるか
私の現実的な線引きはこうです。ローカル/セルフホストのモデルで、出力を機械が確実に受け取る必要がある抽出・分類・関数引数の生成には積極的に乗せる。逆に、API 越しで各社の structured output が既に使えるなら、そちらを先に検討する。
判断の軸は「重みを手元で回しているか」と「壊れた出力の代償が高いか」の二つです。後段が壊れた JSON で落ちるパイプラインなら、生成時保証の価値は大きい。一方で人間が最終確認する下書き用途なら、多少の整形失敗は許容できるので、わざわざ FSM の準備コストを払う必要は薄い。導入するなら、型に「不明」の逃げ道を必ず用意し、構造の検証とは別に値の検証を後段に置く。この二点さえ守れば、outlines は「パースに祈る」パイプラインを「パースが必ず通る」パイプラインに変えてくれます。祈りをコードに置き換えられるのは、地味だが確かな前進です。
Tags
よくある質問
- outlines を使えば LLM の出力の中身も正しくなりますか?
- いいえ。保証されるのは JSON や正規表現に沿った「構造」だけで、値が事実として正しいかは別問題です。数値フィールドを強制すると本来「不明」とすべき場面でも数字を埋めることがあるため、型に逃げ道を用意し、値の検証は後段に分けて行ってください。
- OpenAI や Gemini の API でも outlines の FSM マスクは効きますか?
- API 越しのモデルでは手元でロジットをマスクできないため、outlines は各社ネイティブの structured output 機能に委譲します。トークン単位で FSM 制約を効かせられるのは、transformers・llama.cpp・vLLM・Ollama など重みを手元で回せるローカル/セルフホスト系です。
- 制約付きデコーディングは生成が遅くなりませんか?
- 生成時の追加コストはトークンあたりほぼ定数に抑えられます。FSM の各状態から許可トークン集合への索引を生成前に一度だけ作るためです。ただし複雑な JSON schema や文法では、この索引づくりに数秒の前払いコストがかかるので、起動時に暖めておくのが実務的です。
- 後からパースしてリトライする方式と何が違いますか?
- リトライ方式はモデルに整形を「お願い」しているだけで、外れたら投げ直すためレイテンシとコストが膨らみ、収束の保証もありません。制約付きデコーディングは壊れた出力をそもそも生成させないので、パースは必ず通ります。