LLM 出力の制御・抽出手法を 4 層で整理する — Constrained Decoding からプロンプト設計まで
はじめに
LLM の出力をアプリケーションに組み込むとき、「自由形式のテキスト」のままでは扱いにくい場面が多くあります。JSON で返してほしい、特定のフォーマットに従ってほしい、余計な前置きなしで答えだけ返してほしい — こうした要求に対して、現在さまざまな手法が存在します。
しかし、これらの手法は「どのレイヤーで出力を制御しているか」によって信頼性が大きく異なります。プロンプトで「JSON で返して」と書くだけの方法と、デコーディング時にトークン生成を文法レベルで制約する方法では、保証の強さがまったく違います。
この記事では、LLM 出力の制御・抽出手法を 4 つの層に分類し、それぞれの仕組み・コード例・信頼性の違いを整理します。

4 層モデルの全体像
出力制御の手法を、介入するタイミングが早い順(=保証が強い順)に 4 層に分類します。
| 層 | 介入タイミング | 信頼性 | 代表的な手法 |
|---|---|---|---|
| Layer 1: デコーディング層 | トークン生成時 | 最高 | Constrained Decoding、Logit Bias、Stop Sequence、Response Prefill |
| Layer 2: API 層 | API リクエスト/レスポンス | 高 | JSON Mode、JSON Schema 強制、Tool Use / Function Calling |
| Layer 3: 後処理層 | 生成完了後 | 中 | 正規表現、Pydantic バリデーション、出力パーサー、AST パース |
| Layer 4: プロンプト層 | プロンプト設計時 | 低 | Few-shot Examples、XML タグ、Chain-of-Thought + 最終回答フォーマット |
下の層ほど手軽ですが、モデルが指示を無視するリスクがあります。上の層ほど堅牢ですが、柔軟性やプロバイダー依存度が高くなります。
実務では複数の層を組み合わせるのが一般的です。たとえば「Tool Use(Layer 2)で構造化出力を得つつ、Pydantic(Layer 3)でバリデーションし、失敗時はリトライする」というパターンはよく見られます。
Layer 1: デコーディング層 — トークン生成時の制約
最も強力な制御手法です。モデルがトークンを 1 つ生成するたびに、次に生成可能なトークンを制限します。「構文的に不正な出力がそもそも生成されない」ため、信頼性が最も高くなります。
Constrained Decoding(制約付きデコーディング)
形式文法(CFG や正規表現)でトークン生成を制約する手法です。各ステップで文法的に許容されるトークンだけにマスクをかけ、不正なトークンの生成確率をゼロにします。
仕組み:
- 出力の期待する構造を形式文法(JSON Schema、正規文法など)で定義
- 各デコーディングステップで、現在の生成状態から「次に許容されるトークン集合」を計算
- 許容されないトークンの logit を
-infに設定(マスク) - 残ったトークンの中からサンプリング

llama.cpp の GBNF 文法の例:
# JSON オブジェクトの文法定義(GBNF形式)
root ::= "{" ws "\"name\"" ws ":" ws string "," ws "\"age\"" ws ":" ws number "}"
string ::= "\"" [a-zA-Z ]+ "\""
number ::= [0-9]+
ws ::= [ \t\n]*
この文法を指定すると、モデルは {"name": "John", "age": 30} のような出力しか生成できなくなります。{"name": 123} や {name: John} といった不正な出力は原理的に発生しません。
Python(Outlines ライブラリ)の例:
import outlines
model = outlines.models.transformers("mistralai/Mistral-7B-v0.1")
# JSON Schema で出力構造を定義
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"]
}
generator = outlines.generate.json(model, schema)
result = generator("Extract: John is 30, lives in Tokyo")
# 必ずスキーマ準拠の dict が返る
特徴:
- スキーマ準拠を 100% 保証
- ローカルモデル向け(Outlines、llama.cpp、vLLM など)
- API プロバイダーも内部的にこの仕組みを使っている(OpenAI の
strict: trueなど) - 複雑な文法では推論速度が低下する場合がある
Logit Bias
特定のトークンの生成確率を直接操作する手法です。Constrained Decoding ほど体系的ではありませんが、API 経由で簡易的にトークン生成を制御できます。
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "1から10の数字を1つ選んで"}],
logit_bias={
"16": 100, # "7" のトークンIDを大幅にブースト
}
)
用途:
- 特定のトークン(例: 特殊文字、不要な言語)を抑制する
- 分類タスクで選択肢を制限する
- ただしトークン ID はモデル/トークナイザーに依存するため、可搬性が低い
Stop Sequence(停止シーケンス)
指定した文字列が生成された時点で出力を打ち切る手法です。出力の「終端」を制御します。
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "東京の天気を一言で"}],
stop=["\n", "。"] # 改行または句点で停止
)
# 1文だけの簡潔な回答が得られる
用途:
- 冗長な出力の防止
- 区切り文字ベースの構造化出力で「1 ブロック分だけ」取得
- コード生成時に
\n\nで関数単位の出力に限定
Response Prefill(応答プリフィル)
アシスタントの応答の冒頭を事前に埋めておく手法です。Claude がこの機能をサポートしています。
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content": "東京の天気情報を JSON で返して"},
{"role": "assistant", "content": "{"} # JSON の開始を強制
]
)
# モデルは "{" から続きを生成するため、JSON で始まる出力が得られる
Response Prefill はデコーディング自体を制約するわけではありませんが、出力の初期状態を固定することで、モデルが特定のフォーマットに沿いやすくなります。
Layer 2: API 層 — プロバイダー提供の構造化出力機能
各 API プロバイダーが提供する構造化出力機能です。内部的には Constrained Decoding を使っているケースが多く、信頼性は高いですが、プロバイダーごとに API の設計が異なります。
JSON Mode
「出力が有効な JSON であること」を保証するモードです。スキーマの準拠までは保証しません。
# OpenAI
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "東京の天気情報をJSON で"}],
response_format={"type": "json_object"}
)
# 有効な JSON は保証されるが、キーや型は保証されない
JSON Schema 強制
JSON Mode に加えて、出力が指定したスキーマに準拠することを保証します。
# OpenAI — strict: true で Constrained Decoding 有効
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Extract: John is 30, lives in Tokyo"}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"],
"additionalProperties": False
}
}
}
)
# Gemini — responseSchema で構造を定義
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Extract: John is 30, lives in Tokyo",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema={
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"]
}
)
)
Tool Use / Function Calling
モデルに「ツール(関数)を呼び出す」形式で構造化データを出力させる手法です。本来はエージェント向けの機能ですが、スキーマ準拠の構造化出力を得る手段としても広く使われています。
# Claude — Tool Use でスキーマ準拠の出力を強制
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": "Extract: John is 30, lives in Tokyo"}],
tools=[{
"name": "extract_person",
"description": "Extract person information",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"]
}
}],
tool_choice={"type": "tool", "name": "extract_person"}
)
tool_block = next(b for b in response.content if b.type == "tool_use")
result = tool_block.input # {"name": "John", "age": 30, "city": "Tokyo"}
API 層の比較
| 手法 | スキーマ準拠保証 | プロバイダー |
|---|---|---|
| JSON Mode | JSON 構文のみ | OpenAI, Gemini, Claude |
| JSON Schema 強制 | 100%(Constrained Decoding) | OpenAI (strict: true), Gemini (responseSchema) |
| Tool Use | 高い信頼性 | OpenAI, Gemini, Claude |
各プロバイダーの API 差異(Required の扱い、Nullable の方言差など)について詳しくは、別記事「OpenAI・Gemini・Claude の構造化出力を徹底比較」を参照してください。
Layer 3: 後処理層 — 生成後のパース・バリデーション
モデルの出力が生成された後に、パースやバリデーションを行う手法です。Layer 1・2 が使えない環境や、追加の安全策として使われます。
正規表現による抽出
最もシンプルな後処理です。自由形式のテキストから必要な情報をパターンマッチで抽出します。
import re
import json
raw_output = """
考えてみましょう。ジョンは30歳で東京に住んでいます。
{"name": "John", "age": 30, "city": "Tokyo"}
以上が抽出結果です。
"""
# コードブロック内の JSON を抽出
match = re.search(r"```json\s*(.*?)\s*```", raw_output, re.DOTALL)
if match:
data = json.loads(match.group(1))
注意点:
- モデルが期待するフォーマットで出力しない場合、抽出に失敗する
- 複雑な構造の抽出には向かない
- 「だいたい動く」レベルであり、本番環境では追加のバリデーションが必要
Pydantic によるバリデーション(Instructor パターン)
Instructor ライブラリは、Pydantic モデルを定義するだけで LLM の出力をバリデーション付きで取得できます。バリデーション失敗時の自動リトライも組み込まれています。
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field
client = instructor.from_openai(OpenAI())
class Person(BaseModel):
name: str
age: int = Field(ge=0, le=150)
city: str
person = client.chat.completions.create(
model="gpt-4o",
response_model=Person,
messages=[{"role": "user", "content": "Extract: John is 30, lives in Tokyo"}]
)
# person.name == "John", person.age == 30, person.city == "Tokyo"
# 型やバリデーションルールに違反すると自動リトライ
Instructor の仕組み:
- Pydantic モデルから JSON Schema を自動生成
- API の構造化出力機能(Tool Use や
response_format)でスキーマを送信 - レスポンスを Pydantic モデルにパース
- バリデーション失敗時、エラーメッセージとともにリトライ

つまり Instructor は Layer 2(API 層)と Layer 3(後処理層)を橋渡しするライブラリです。
出力パーサー(LangChain スタイル)
LangChain の OutputParser は、プロンプトにフォーマット指示を挿入し、出力をパースする仕組みを提供します。
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
city: str
parser = PydanticOutputParser(pydantic_object=Person)
# parser.get_format_instructions() がプロンプトに挿入するフォーマット指示を生成
# parser.parse(output) が出力をパース
AST / コードパース
コード生成タスクでは、出力を AST(抽象構文木)にパースして構文的な正当性を検証できます。
import ast
generated_code = """
def greet(name):
return f"Hello, {name}!"
"""
try:
tree = ast.parse(generated_code)
# 構文的に有効な Python コード
except SyntaxError as e:
# 構文エラー → リトライや修正を試みる
print(f"Syntax error: {e}")
Layer 4: プロンプト層 — ソフトな制約
最も手軽ですが、最も信頼性が低い層です。モデルへの「お願い」であり、従う保証はありません。ただし、他の層と組み合わせることで効果を発揮します。
Few-shot Examples(少数例示)
期待するフォーマットの具体例を示すことで、モデルの出力を誘導します。
以下の形式で回答してください:
入力: Alice is 25, lives in Osaka
出力: {"name": "Alice", "age": 25, "city": "Osaka"}
入力: Bob is 40, lives in Kyoto
出力: {"name": "Bob", "age": 40, "city": "Kyoto"}
入力: Carol is 35, lives in Nagoya
出力:
Few-shot は「どんなフォーマットか」をモデルに理解させる最も直感的な方法です。Layer 2 の構造化出力と併用すると、モデルがスキーマの意図をより正確に解釈しやすくなります。
XML / デリミタタグ
出力にセクションの境界を明示させることで、後処理での抽出を容易にします。
以下のXMLタグで回答を構造化してください:
<analysis>分析内容をここに</analysis>
<answer>最終回答をここに</answer>
<confidence>high/medium/low</confidence>
import re
output = """
<analysis>入力テキストから人物情報を抽出します。名前はJohn、年齢は30、都市はTokyoです。</analysis>
<answer>{"name": "John", "age": 30, "city": "Tokyo"}</answer>
<confidence>high</confidence>
"""
answer = re.search(r"<answer>(.*?)</answer>", output, re.DOTALL).group(1)
Claude は XML タグとの相性が特に良く、ドキュメントでも推奨されています。
Chain-of-Thought + 最終回答フォーマット
「思考過程」と「最終回答」を分離するパターンです。モデルに自由に推論させつつ、最終出力だけを構造化します。
ステップバイステップで考えてから、最後に以下の形式で回答してください:
FINAL_ANSWER: <答え>
質問: 128 × 47 は?
output = """
まず 128 × 47 を分解します。
128 × 40 = 5120
128 × 7 = 896
5120 + 896 = 6016
FINAL_ANSWER: 6016
"""
answer = output.split("FINAL_ANSWER:")[-1].strip() # "6016"
各層の比較まとめ
| 信頼性 | 柔軟性 | 実装コスト | プロバイダー依存 | |
|---|---|---|---|---|
| Layer 1: デコーディング | 最高(100% 保証) | 低(定義した構造のみ) | 高 | ローカルモデル or プロバイダー内部 |
| Layer 2: API | 高(ほぼ 100%) | 中 | 低 | 高(プロバイダーの API に依存) |
| Layer 3: 後処理 | 中(失敗時リトライ) | 高 | 中 | 低(どのモデルでも使える) |
| Layer 4: プロンプト | 低(ベストエフォート) | 最高 | 最低 | なし |
実務での組み合わせパターン
単独で使うよりも、複数の層を組み合わせて多層防御するのが実務のベストプラクティスです。
パターン 1: API 層 + 後処理層(最も一般的)
import instructor
from openai import OpenAI
from pydantic import BaseModel
client = instructor.from_openai(OpenAI())
class Person(BaseModel):
name: str
age: int
city: str
# Layer 2: API の構造化出力
# Layer 3: Pydantic バリデーション + 自動リトライ
person = client.chat.completions.create(
model="gpt-4o",
response_model=Person,
max_retries=3,
messages=[{"role": "user", "content": "Extract: John is 30, lives in Tokyo"}]
)
パターン 2: プロンプト層 + API 層 + 後処理層
import anthropic
import json
from pydantic import BaseModel, ValidationError
client = anthropic.Anthropic()
class Person(BaseModel):
name: str
age: int
city: str
# Layer 4: Few-shot でフォーマットの意図を伝える
# Layer 2: Tool Use でスキーマ準拠を強制
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system="人物情報を抽出するタスクです。必ず全フィールドを埋めてください。",
messages=[{"role": "user", "content": "Extract: John is 30, lives in Tokyo"}],
tools=[{
"name": "extract_person",
"description": "Extract person info. Example: {\"name\": \"Alice\", \"age\": 25, \"city\": \"Osaka\"}",
"input_schema": Person.model_json_schema()
}],
tool_choice={"type": "tool", "name": "extract_person"}
)
# Layer 3: Pydantic で追加バリデーション
tool_block = next(b for b in response.content if b.type == "tool_use")
try:
person = Person.model_validate(tool_block.input)
except ValidationError as e:
# バリデーション失敗時のフォールバック処理
print(f"Validation failed: {e}")
まとめ
- LLM 出力の制御手法は 4 層に分類できる: デコーディング → API → 後処理 → プロンプト
- 上の層ほど信頼性が高いが、柔軟性やポータビリティは下がる
- 実務では複数層の組み合わせが基本 — API の構造化出力 + Pydantic バリデーション + リトライが最も一般的なパターン
- プロンプト層だけに頼るのは本番環境では避けるべき — 「JSON で返して」というプロンプトだけでは不十分
- プロバイダーごとの API 差異(Required/Optional の扱い、Nullable の方言)については 構造化出力の比較記事 も参照






