プロンプトで祈るのはもうやめる
Outlines / Guidance で LLM の出力を 100% 制御する技術
はじめに:開発現場における「お祈り」の限界
LLMをシステムに組み込む際、私たちはいつまでプロンプトで「お祈り」を続けるのでしょうか。
- 「以下のJSONフォーマットで出力してください」
- 「余計な前置きや挨拶は書かないでください」
- 「必ず数値は半角で書いてください」
どれだけ丁寧にプロンプトエンジニアリングを施しても、LLMは確率的モデルです。
temperature=0 にしたところで、構造的な整合性が 100% 保証されるわけではありません。
よくある事故例:
- JSONの閉じカッコが抜ける
- 数値型を要求したのに文字列型が返ってくる
-
{"error": "..."}ではなくI'm sorry, I can't...と喋り出す
これらを防ぐためにリトライ処理(Re-prompting)を実装するのは、レイテンシとコストの無駄です。
本記事では、自然言語による指示(Prompt Engineering)ではなく、
論理的な制約(Constrained Decoding) によって LLM の出力を 100% 制御 する技術、
特に Outlines に焦点を当てて解説します。
なぜ「100%」と言えるのか?(仕組みの解説)
従来の「プロンプトで頑張る」アプローチと、
Outlines などの Structured Generation(構造化生成) の違いは、
生成プロセスそのものに介入しているかどうか
にあります。
通常の生成プロセス
LLMは Vocabulary(語彙)に含まれる全トークンに対して確率(Logits)を計算し、サンプリングします。
「JSONを出せ」と言われても、
HereSureI’m sorry
といったトークンの出現確率は ゼロにはなりません。
Constrained Decoding(制約付きデコーディング)
Outlines は、
- 正規表現(Regex)
- 文脈自由文法(CFG)
- JSON Schema / Pydantic
を 有限オートマトン(FSM: Finite State Machine) に変換します。
生成の各ステップで:
- 現在の FSM の状態を確認
- 遷移可能なトークン 以外
- Logits を
-inf(無限小)にマスク
つまり、
構造的に不正なトークンは、物理的に選択できない
状態になります。
これが「100% 制御できる」と言える数学的な根拠です。
技術検証:Outlines を使ってみる
実際に Outlines を使って、制御された生成を試してみます。
検証環境
- Library:
outlines - Model:
Qwen/Qwen-2.5-7B-Instruct - Backend: vLLM
- GPU: (必要に応じて記載)
import outlines
# モデルのロード(vLLMバックエンドを使用すると高速)
model = outlines.models.transformers(
"Qwen/Qwen-2.5-7B-Instruct"
)
Case 1: 正規表現(Regex)による制御
IPアドレスのような厳密なフォーマットが必要なケース。
# IPアドレスの正規表現
regex = r"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}" \
r"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
generator = outlines.generate.regex(model, regex)
prompt = "私のローカルサーバーのIPアドレスは、"
result = generator(prompt)
print(result)
検証結果
192.168.1.10
余計な前置きや文章は一切生成されません。
通常の LLM だと:
Sure, a typical local IP address is 192.168...
のようなテキストが混ざりがちですが、
Regex 制約下では 数字とドット以外のトークンは生成不能 になります。
Case 2: Pydantic(JSON Schema)による完全スキーマ制御
実務で最も使うパターン。
Agent のツール引数生成などで必須です。
from pydantic import BaseModel, Field
from enum import Enum
class WeaponType(str, Enum):
SWORD = "sword"
BOW = "bow"
MAGIC = "magic"
class Character(BaseModel):
name: str = Field(..., description="キャラクターの名前")
age: int = Field(..., description="年齢")
weapon: WeaponType = Field(..., description="使用する武器")
power: int = Field(..., description="戦闘力 (0-100)")
generator = outlines.generate.json(model, Character)
prompt = "中世ファンタジー風の最強の戦士を作成してください。"
result = generator(prompt)
print(result)
print(type(result))
検証結果
name='アリス' age=24 weapon=<WeaponType.SWORD: 'sword'> power=99
<class '__main__.Character'>
注目ポイント
-
型安全性
age,powerは必ずint -
Enum制約
axeなど未定義値は絶対に出ない -
パース不要
Pydantic オブジェクトとしてそのまま扱える
Case 3: 速度とトークン効率
「制約をかけると遅くなるのでは?」と思われがちですが、
実際には 高速化するケースが多い です。
理由:
- 思考過程
- 前置き
- 謝罪文
といった 不要トークンを生成しない ため。
例(参考)
-
通常生成
- 平均 1.5 秒
- リトライ込み:3.0 秒
-
Outlines
- 平均 0.8 秒
まとめ:プロンプトエンジニアから「モデル制御エンジニア」へ
LLMの出力は、もはや「お祈り」するものではありません。
設計するものです。
-
確実性
正規表現・スキーマによる 100% フォーマット保証 -
効率性
リトライ・例外処理の削減 -
安全性
Enum や型による予期せぬ値の排除
RAG や自律型 Agent において、
不安定な出力は致命的なバグになります。
Outlines、Guidance、OpenAI Structured Outputs の背後にある
Logit Masking / Constrained Decoding を理解し、
モデルの手綱を握れるエンジニアになることが、
これからの NLP / LLM エンジニアには求められるでしょう。