- Authors
- Name
- 構造化出力が必要な理由
- Constrained Decodingの原則:FSMとCFG
- 예시: JSON 객체 시작 직후, 키 문자열만 허용
- 현재 FSM 상태에서 허용되는 토큰: 쌍따옴표(")로 시작하는 키
- softmax 적용 후 허용된 토큰만 양의 확률을 가짐

##入り
LLMを本番パイプラインに統合するときに最も頻繁に遭遇する問題は、出力形式の不確実性です。プロンプトに「JSONで応答してください」と指示するだけでは、スキーマの不一致、フィールドの欠落、誤ったタイプ、不完全なJSONなど、さまざまな障害が発生する。実際にプロンプトだけでJSON出力を要求したとき、約5~15%の要求で解析失敗が発生するという報告がある。
この問題を根本的に解決する技術がConstrained Decoding(制約復号)である。プロンプトレベルの「要求」ではなく、トークン生成プロセス自体に文法的制約を付与して100%スキーマの遵守を保証する。
この記事では、Constrained Decodingのコア原理(FSM / CFG)から始まり、主要エンジン(Outlines、XGrammar、llguidance)を比較し、OpenAI / AnthropicのネイティブAPI、vLLM / TGIなどオープンソースサービングフレームワークでの適用法、Function Calling統合とプロダクション環境で。
構造化出力が必要な理由
自由形式出力の制限
LLMの自由形式テキスト出力をプログラム的に処理するには、正規表現またはカスタムパーサーを作成する必要があります。しかし、このアプローチは本質的に脆弱です。
- 解析失敗:JSONが途中で切り捨てられ、カンマが欠落したり、引用符が欠落している
- スキーマの不一致:フィールド名
nameではない名前として返されるか、ratingこの文字列で来る - 幻覚フィールド: 要求されていないフィールドが追加されるか、必須フィールドが欠落している
- フォーマットの混合: JSONと自然言語の説明が混ざって出る
構造化出力が解決するもの
構造化出力は以下を保証します。
- 型式保証: 出力が必ず有効なJSON/XML/YAMLである
- スキーマ準拠: 指定したJSON Schemaに100%適合
- タイプの安全性:数値フィールドに文字列が入らない
- 再試行不要: 解析失敗による再試行コストが除去される
これを達成する方法は大きく2つに分けられます。 APIネイティブ方式(OpenAI Structured Outputs, Anthropic Tool Use)とConstrained Decodingエンジン(Outlines, XGrammar, llguidance)である。
Constrained Decodingの原則:FSMとCFG
###トークン生成プロセスに制約を与える
LLMは、ステップごとに語彙全体の確率分布を生成します。 Constrained Decodingは、この確率分布で文法的に許可されていないトークンの確率を0(または-inf)にマスキングし、有効なトークンのみを選択します。
たとえば、JSONオブジェクトの先頭では{トークンのみを許可し、キー名の後に:トークンだけを許可する式だ。
###FSM(Finite State Machine)ベースのアプローチ
正規表現や単純な文法はFSMで表現できる。 FSMベースのConstrained Decodingの動作方法は次のとおりです。
- JSON Schemaを正規表現に変換する2.正規表現をDFA(Deterministic Finite Automaton)にコンパイルする
- 各復号ステップで現在のFSM状態で遷移可能なトークンのみを許可する
- 選択したトークンに従ってFSM状態を更新する
Outlinesライブラリはこのアプローチの代表的な実装です。利点は、実装が比較的単純で状態追跡が速いということですが、再帰構造(ネストされたJSONオブジェクト、可変長配列など)を自然に処理することが難しいという制限があります。
Context-Free Grammar(CFG)ベースのアプローチ
JSONの完全な文法(ネストされたオブジェクト、配列、再帰構造を含む)を正確に表現するには、CFGが必要です。 CFGベースのアプローチは次のように動作します。
- JSON SchemaをEBNF(Extended Backus-Naur Form)文法に変換する
- 各復号ステップでPDA(Pushdown Automaton)またはパーサの状態を追跡する
- 現在の文法状態で許可されているトークンのみを選択する
- スタックベースでネスト深さを管理する
XGrammarとllguidanceはCFGベースのアプローチを採用しています。再帰的構造を自然に処理することができますが、FSMコントラスト状態管理のオーバーヘッドが大きい。
###トークンマスキングの重要なメカニズム
どちらの方法も最終的にはlogit maskingを行います。デコード時に logit ベクトルで許可されないトークンの位置に-infを設定すると、softmax以降該当トークンの確率が0になる。```python import torch import numpy as np
def apply_grammar_mask(logits: torch.Tensor, allowed_token_ids: set, vocab_size: int) -> torch.Tensor: """ Constrained Decoding의 핵심: 문법적으로 허용된 토큰만 남기고 나머지 토큰의 logit을 -inf로 마스킹한다. """ mask = torch.full((vocab_size,), float('-inf'), dtype=logits.dtype, device=logits.device) allowed_ids = torch.tensor(list(allowed_token_ids), dtype=torch.long, device=logits.device) mask[allowed_ids] = 0.0 masked_logits = logits + mask return masked_logits
예시: JSON 객체 시작 직후, 키 문자열만 허용
vocab_size = 32000 logits = torch.randn(vocab_size) # 모델이 출력한 raw logits
현재 FSM 상태에서 허용되는 토큰: 쌍따옴표(")로 시작하는 키
allowed_tokens = 1234 # tokenizer에서 '"name', '"age' 등에 해당하는 ID masked = apply_grammar_mask(logits, allowed_tokens, vocab_size)
softmax 적용 후 허용된 토큰만 양의 확률을 가짐
probs = torch.softmax(masked, dim=-1) assert probs[0].item() == 0.0 # 허용되지 않은 토큰의 확률은 0
### 比較表
|アイテム|アウトライン| XGrammar | llguidance |ネイティブAPI(OpenAI)|
| -------------------- | ------------------------------- | ------------------------------- | ------------------------------- | --------------------- |
| **文法モデル** | FSM(正規表現+ JSONスキーマ)| CFG(EBNF)| CFG(EBNF)+バイトレベル|内部実装(プライベート)|
| **サポートフォーマット** | JSON、正規表現、CFG | JSON、EBNF、正規表現| JSON、EBNF、正規表現、Substrs | JSONスキーマ|
| **フレームワーク統合** | vLLM、TGI、SGLang | vLLM、SGLang、MLC-LLM |マウサーGuidance、Azure AI | OpenAI APIのみ|
| **前処理時間** |通常(FSMインデックスビルド)|高速(適応トークンマスク)|高速(パーサーベース)|なし(サーバー側)|
| **ランタイムオーバーヘッド** | Low(〜3%遅延増加)|非常に低い(〜1%遅延増加)| Low(〜2%遅延増加)|なし(透明)|
| **バッチデコードサポート** |サポート|高効率サポート(ビットマスクの再利用)|サポート|該当なし|
| **入れ子構造の処理** |制限的(深い再帰時の性能低下)|優秀(プッシュダウンオートマタ)|優秀優秀
| **ライセンス** | Apache 2.0 | Apache 2.0 | MIT |商用API |
| **成熟度** |高(2023〜)|中(2024〜)|中(2024〜)|高い|
### Outlines
OutlinesはConstrained Decoding分野の先駆的なライブラリです。正規表現をFSMに変換し、JSON Schemaを正規表現に変換する2段階のパイプラインを使用します。 vLLMでは、基本的なガイド付きデコードバックエンドとして採用されており、2025年以降、XGrammarにデフォルトのバックエンドが移行する傾向にありますが、依然として広く使用されています。
### XGrammarXGrammarはMLC-AIチームが開発したCFGベースエンジンで、2024年末に公開された。コアイノベーションは**適応トークンマスクキャッシュ**です。文法状態を「コンテキストに依存しない」部分と「コンテキストに依存する」部分に分離することによって、前者は事前計算されたビットマスクを再利用し、後者のみリアルタイムに計算する。これにより、バッチ復号において特に高い効率が達成される。
### llguidance
llguidanceは、MicrosoftのGuidanceプロジェクトから派生したRustベースのパーサーエンジンです。バイトレベルで動作し、トルクナイザーに依存せず、部分文字列マッチング(Substrs)などのユニークな制約タイプをサポートします。 Azure AI Inferenceで使用されています。
## OpenAI/Anthropic API ネイティブ方式
### OpenAI Structured Outputs
OpenAIは2024年8月からStructured Outputsを正式サポートする。内部的にConstrained Decodingを適用して、JSON Schema準拠を100%保証します。```python
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
client = OpenAI()
# Pydantic 모델로 출력 스키마 정의
class Severity(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class SecurityFinding(BaseModel):
vulnerability_type: str = Field(description="취약점 유형 (예: SQL Injection, XSS)")
severity: Severity = Field(description="심각도")
affected_component: str = Field(description="영향받는 컴포넌트")
description: str = Field(description="취약점 상세 설명")
remediation: str = Field(description="권장 조치")
cvss_score: Optional[float] = Field(default=None, ge=0.0, le=10.0, description="CVSS 점수")
class SecurityReport(BaseModel):
findings: List[SecurityFinding] = Field(description="발견된 취약점 목록")
overall_risk: Severity = Field(description="전체 위험 수준")
summary: str = Field(description="보안 분석 요약")
scan_timestamp: str = Field(description="스캔 시각 (ISO 8601)")
# Structured Outputs로 호출 - 스키마 100% 보장
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "당신은 보안 분석 전문가입니다. 주어진 코드를 분석하여 보안 취약점을 보고하세요."
},
{
"role": "user",
"content": """다음 Flask 코드의 보안 취약점을 분석해주세요:
@app.route('/search')
def search():
query = request.args.get('q')
result = db.execute(f"SELECT * FROM users WHERE name = '{query}'")
return render_template_string(f"<h1>Results for {query}</h1>")
"""
}
],
response_format=SecurityReport
)
report = response.choices[0].message.parsed
print(f"전체 위험 수준: {report.overall_risk.value}")
for finding in report.findings:
print(f" [{finding.severity.value}] {finding.vulnerability_type}: {finding.description}")
```### Anthropic: Tool Use ベースの構造化出力
Anthropicは、別々のStructured Outputs APIの代わりにTool Use(Function Calling)メカニズムを利用します。ツールを1つだけ定義し、`tool_choice`を対応するツールで強制すると構造化された出力を得ることができる。```python
import anthropic
import json
client = anthropic.Anthropic()
# Tool Use를 활용한 구조화된 출력
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
tools=[
{
"name": "analyze_code_security",
"description": "코드의 보안 취약점을 구조화된 형식으로 분석합니다",
"input_schema": {
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"vulnerability_type": {"type": "string"},
"severity": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
},
"affected_component": {"type": "string"},
"description": {"type": "string"},
"remediation": {"type": "string"}
},
"required": ["vulnerability_type", "severity", "description", "remediation"]
}
},
"overall_risk": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
},
"summary": {"type": "string"}
},
"required": ["findings", "overall_risk", "summary"]
}
}
],
tool_choice={"type": "tool", "name": "analyze_code_security"},
messages=[
{
"role": "user",
"content": "다음 코드의 보안 취약점을 분석해주세요:\n\n@app.route('/login', methods=['POST'])\ndef login():\n username = request.form['username']\n password = request.form['password']\n query = f\"SELECT * FROM users WHERE username='{username}' AND password='{password}'\"\n user = db.execute(query).fetchone()\n if user:\n session['user'] = username\n return redirect('/dashboard')"
}
]
)
# Tool Use 결과에서 구조화된 데이터 추출
tool_use_block = next(block for block in response.content if block.type == "tool_use")
report = tool_use_block.input
print(f"전체 위험: {report['overall_risk']}")
print(f"발견 항목: {len(report['findings'])}건")
```##オープンソースモデルの適用:vLLMとTGI
### vLLMのガイド付きデコード
vLLMはv0.5.0からguided decodingをサポートし、バックエンドとしてOutlinesとXGrammarを選択することができる。 v0.7.0以降、XGrammarはデフォルトのバックエンドに設定されています。```python
from vllm import LLM, SamplingParams
from pydantic import BaseModel, Field
from typing import List, Optional
import json
# Pydantic 모델로 출력 스키마 정의
class ExtractedEntity(BaseModel):
name: str = Field(description="엔티티 이름")
entity_type: str = Field(description="엔티티 유형: PERSON, ORG, LOCATION, DATE, PRODUCT")
confidence: float = Field(ge=0.0, le=1.0, description="신뢰도")
context: Optional[str] = Field(default=None, description="원문에서의 관련 문맥")
class EntityExtractionResult(BaseModel):
entities: List[ExtractedEntity] = Field(description="추출된 엔티티 목록")
source_language: str = Field(description="원문 언어")
# vLLM 엔진 초기화
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
max_model_len=4096,
gpu_memory_utilization=0.85,
guided_decoding_backend="xgrammar", # 또는 "outlines"
)
# JSON Schema를 가이드로 사용하는 SamplingParams
sampling_params = SamplingParams(
temperature=0.1,
top_p=0.95,
max_tokens=1024,
guided_decoding={
"json_object": EntityExtractionResult.model_json_schema()
}
)
# 배치 추론
texts = [
"삼성전자 이재용 회장이 2026년 3월 서울에서 AI 반도체 전략을 발표했다.",
"Apple CEO Tim Cook announced the new M5 chip at WWDC 2026 in Cupertino.",
"소프트뱅크 손정의 회장이 도쿄에서 ARM 기반 AI 인프라 투자를 발표했다.",
]
prompts = [
f"다음 텍스트에서 엔티티(사람, 조직, 장소, 날짜, 제품)를 추출하세요:\n\n{text}"
for text in texts
]
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
result = json.loads(output.outputs[0].text)
parsed = EntityExtractionResult(**result)
print(f"언어: {parsed.source_language}")
for entity in parsed.entities:
print(f" [{entity.entity_type}] {entity.name} (신뢰도: {entity.confidence})")
print("---")
```### vLLM OpenAI互換サーバーで使用
vLLMのOpenAI互換サーバーを使用すると、既存のOpenAI SDKコードをほとんど変更せずにオープンソースモデルに適用できます。```bash
# vLLM 서버 실행 (guided decoding 활성화)
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--guided-decoding-backend xgrammar \
--max-model-len 4096 \
--port 8000
from openai import OpenAI
# vLLM 서버에 연결
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
# extra_body로 guided decoding 전달
response = client.chat.completions.create(
model="meta-llama/Llama-3.1-8B-Instruct",
messages=[
{"role": "system", "content": "텍스트에서 엔티티를 추출하세요."},
{"role": "user", "content": "Google의 Sundar Pichai CEO가 Mountain View에서 Gemini 3.0을 공개했다."}
],
extra_body={
"guided_json": {
"type": "object",
"properties": {
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"type": {"type": "string", "enum": ["PERSON", "ORG", "LOCATION", "PRODUCT"]},
"confidence": {"type": "number"}
},
"required": ["name", "type", "confidence"]
}
}
},
"required": ["entities"]
}
}
)
import json
result = json.loads(response.choices[0].message.content)
print(json.dumps(result, indent=2, ensure_ascii=False))
```### TGI(Text Generation Inference)での適用
Hugging Face TGIもgrammarベースのguided generationをサポートしている。`grammar`パラメータとしてJSON Schemaを渡します。```bash
# TGI 서버 실행
docker run --gpus all -p 8080:80 \
ghcr.io/huggingface/text-generation-inference:latest \
--model-id meta-llama/Llama-3.1-8B-Instruct \
--max-input-tokens 2048 \
--max-total-tokens 4096
import requests
import json
# TGI의 grammar 파라미터로 JSON Schema 전달
response = requests.post(
"http://localhost:8080/generate",
json={
"inputs": "서울의 유명 관광지 3곳을 추천해주세요.",
"parameters": {
"max_new_tokens": 512,
"temperature": 0.7,
"grammar": {
"type": "json",
"value": {
"type": "object",
"properties": {
"places": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"category": {"type": "string"},
"description": {"type": "string"}
},
"required": ["name", "category", "description"]
},
"minItems": 3,
"maxItems": 3
}
},
"required": ["places"]
}
}
}
}
)
result = json.loads(response.json()["generated_text"])
for place in result["places"]:
print(f"{place['name']} ({place['category']}): {place['description']}")
```## Function Callingの統合
### 構造化出力と Function Calling の関係
Function Callingは本質的に構造化された出力の特別な形です。これは、LLMが「どの関数をどの引数として呼び出すか」を構造化JSONに出力するためです。両方の機能を組み合わせることで、強力なエージェントシステムを構築できます。
### LangChain with_structured_output
LangChainは`with_structured_output`メソッドを使用すると、プロバイダに関係なく、統一されたインターフェイスで構造化された出力を取得できます。```python
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from pydantic import BaseModel, Field
from typing import List, Literal
# 출력 스키마 정의
class DatabaseQuery(BaseModel):
"""자연어를 SQL 쿼리로 변환한 결과"""
sql: str = Field(description="생성된 SQL 쿼리")
tables_used: List[str] = Field(description="사용된 테이블 목록")
query_type: Literal["SELECT", "INSERT", "UPDATE", "DELETE"] = Field(description="쿼리 타입")
explanation: str = Field(description="쿼리 설명")
estimated_complexity: Literal["low", "medium", "high"] = Field(description="쿼리 복잡도")
# OpenAI 모델에 구조화된 출력 적용
openai_llm = ChatOpenAI(model="gpt-4o", temperature=0)
openai_structured = openai_llm.with_structured_output(DatabaseQuery)
# Anthropic 모델에도 동일하게 적용
anthropic_llm = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
anthropic_structured = anthropic_llm.with_structured_output(DatabaseQuery)
# 동일한 인터페이스로 호출
question = "지난 30일 동안 가장 매출이 높은 상위 10개 제품과 해당 카테고리를 보여줘"
result_openai = openai_structured.invoke(question)
result_anthropic = anthropic_structured.invoke(question)
print(f"[OpenAI] SQL: {result_openai.sql}")
print(f"[OpenAI] 테이블: {result_openai.tables_used}")
print(f"[OpenAI] 복잡도: {result_openai.estimated_complexity}")
print()
print(f"[Anthropic] SQL: {result_anthropic.sql}")
print(f"[Anthropic] 테이블: {result_anthropic.tables_used}")
print(f"[Anthropic] 복잡도: {result_anthropic.estimated_complexity}")
```### Function Calling + Structured Output 結合パターン
エージェントがツールを呼び出しますが、最終応答は構造化形式で返すパターンです。```python
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional
import json
client = OpenAI()
# 1단계: Function Calling으로 정보 수집
tools = [
{
"type": "function",
"function": {
"name": "search_database",
"description": "데이터베이스에서 제품 정보를 검색합니다",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "검색 쿼리"},
"category": {"type": "string", "enum": ["electronics", "clothing", "food"]},
"limit": {"type": "integer", "default": 10}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "get_price_history",
"description": "제품의 가격 이력을 조회합니다",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"days": {"type": "integer", "default": 30}
},
"required": ["product_id"]
}
}
}
]
# 2단계: 최종 응답은 Structured Output으로 반환
class ProductAnalysis(BaseModel):
product_name: str
current_price: float
price_trend: str # "rising", "falling", "stable"
recommendation: str
confidence: float = Field(ge=0.0, le=1.0)
class AnalysisReport(BaseModel):
analyses: List[ProductAnalysis]
market_summary: str
generated_at: str
# 멀티턴 대화로 Function Calling 후 Structured Output
messages = [
{"role": "system", "content": "제품 시장 분석 전문가입니다. 도구를 사용하여 정보를 수집하고 분석 보고서를 작성하세요."},
{"role": "user", "content": "최신 노트북 시장 동향을 분석해주세요."}
]
# Function Calling 단계
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
# 도구 호출 결과를 처리한 후 최종 Structured Output 요청
# (도구 실행 및 결과 피드백 로직 생략)
# 최종 분석 보고서를 Structured Output으로 생성
final_response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=messages, # 전체 대화 이력 포함
response_format=AnalysisReport
)
report = final_response.choices[0].message.parsed
print(f"시장 요약: {report.market_summary}")
for analysis in report.analyses:
print(f" {analysis.product_name}: {analysis.price_trend} (신뢰도: {analysis.confidence})")
```## 本番適用戦略
###階層フォールバック戦略
本番環境では、単一の方法に依存せずに階層フォールバックを構成する必要があります。```
1차: Constrained Decoding (100% 스키마 보장)
↓ 실패 시
2차: JSON Mode + Pydantic 검증
↓ 실패 시
3차: 자유 텍스트 + LLM 기반 재파싱
↓ 실패 시
4차: 기본값 반환 + 알림
```###スキーマ設計原則
Constrained Decodingの効果を最大化するには、スキーマ設計に注意が必要です。
1. **フィールド数の最小化**: フィールドが多いほどFSM/CFG状態空間が大きくなり、デコード速度が遅くなる。コアフィールドのみを含めましょう。
2. **ネスト深さ制限**: 3段階以上のネストはパフォーマンスに影響を与える可能性があります。可能であれば平坦化しよう。
3. **enum 積極的活用**: フリーテキストフィールドよりも enum がはるかに効率的です。許容値を事前定義すると、トークンマスクが狭くなり、品質が向上します。
4. **Optional フィールド 注意**: Optional フィールドが多い場合、モデルは`null`を過度に選択できる。必須フィールドを優先しましょう。
5. **description活用**: JSON Schemaの`description`フィールドはモデルにヒントを提供します。明確な説明を書きましょう。
### キャッシング戦略
Constrained Decodingエンジンは、スキーマをFSM / CFGにコンパイルする前処理時間が必要です。同じスキーマを繰り返し使用すると、コンパイル結果をキャッシュしてオーバーヘッドを減らすことができます。
- **Outlines**:`RegexGuide`インデックスを事前にビルドして再利用
- **XGrammar**:`GrammarCompiler`でコンパイルされた文法をキャッシュする
- **vLLM**: サーバーレベルで guided decoding キャッシュを管理
##パフォーマンスの影響とトレードオフ
### デコード速度の影響
Constrained Decodingには、トークンの作成時に追加の操作が必要です。実際のベンチマークを見ると次のようになる。
|シナリオ|制約なし(tok / s)|アウトライン(tok / s)| XGrammar(tok / s)|オーバーヘッド|
| ------------------------ | ----------------- | ---------------- | ---------------- | -------- |
|シンプルJSON(5フィールド)| 520 | 505 | 515 | 1〜3%|
|複雑なJSON(20フィールド、ネスト)| 520 | 470 | 500 | 4〜10%|
|正規表現(メール)| 520 | 510 | 518 | 0.5〜2%|
|大規模な列挙型(100値)| 520 | 490 | 510 | 2〜6%|
ほとんどの場合、オーバーヘッドは無視できるレベルです。ただし、非常に複雑なスキーマ(深いネスト、大規模な列挙、長い正規表現)では、10%以上の性能低下が発生する可能性があります。
###品質影響
Constrained Decodingはモデルの自由度を制限するので、意味的な品質にわずかな影響を与える可能性があります。特に次の状況で注意が必要です。
- **非常に制限的な列挙型**:モデルが表現したい意味に最も近い列挙値がないと、奇妙な値が選択されることがある
- **過度のフィールド数**:フィールドが30個以上の場合、後部フィールドの品質が低下する傾向がある
- **短いmax_tokens**:構造化出力はフリーテキストよりも多くのトークンを使用するため、十分な余裕を持たなければなりません。
##トラブルシューティング
### よく発生する問題と解決策
**1。 vLLMでguided decoding時に最初の要求が遅い場合**
FSM / CFGインデックスの構築に時間がかかります。サーバーの起動時にウォームアップ要求を送信するか、XGrammarバックエンドに切り替えると前処理時間が短縮されます。
**2。入れ子になった配列で無限ループが発生**`maxItems`を設定しないと、モデルは配列要素を生成し続けることができます。 JSON Schemaから`maxItems`を必ず指定しよう。
**3。 Unicode 文字が壊れた場合**
一部のConstrained Decodingエンジンは、マルチバイトのUnicodeトークン処理に脆弱です。 llguidanceはバイトレベルの動作でこの問題を回避します。 Outlinesを使用する場合は、正規表現にUnicodeの範囲を明示的に含めましょう。
**4。モデルがスキーマフィールドに空の文字列を埋める場合**`minLength: 1`を設定するか、プロンプトで各フィールドの具体的な指示を提供します。 Constrained Decodingはフォーマットのみを保証しない意味的な品質は保証しません。
**5。 OpenAI Structured Outputs から refusal が返される場合**
モデルが安全方針によって回答を拒否した場合`response.choices[0].message.refusal`が設定されます。`parsed`秋`None`かどうか必ず確認しましょう。```python
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=messages,
response_format=MySchema
)
if response.choices[0].message.refusal:
print(f"응답 거부: {response.choices[0].message.refusal}")
elif response.choices[0].message.parsed:
result = response.choices[0].message.parsed
# 정상 처리
```##操作時の注意事項
### JSON Schemaの制約事項
OpenAI Structured OutputsはJSON Schemaのすべての機能をサポートしていません。主な制約は次のとおりです。
-`additionalProperties`を`false`に設定する必要があります
- すべてのプロパティ`required`に含めるべきです(オプションはタイプです`null`を union として追加)
-`$ref`はサポートされていますが、再帰の深さに制限があります
-`patternProperties`、`if/then/else`はサポートされていません
- ネスト深さ最大5段階
###スキーマのバージョン管理
本番環境でスキーマを変更するときは、下位互換性を維持する必要があります。新しいフィールドを追加する場合はOptionalで始まり、既存のフィールドを削除するときはdeprecated期間を置く必要があります。
### コストに関する考慮事項
構造化出力はフリーテキストよりもトークン数が増加します。 JSONの構造的オーバーヘッド(`{`、`}`、`"`、`:`、`,`)のためだ。平均して15〜30%のトークンオーバーヘッドが発生します。コストに敏感な環境では、フィールド数を最小限に抑え、短いキー名を使用することが役立ちます。
### モニタリング指標
本番環境で追跡する必要がある重要な指標は次のとおりです。
- **解析成功率**:構造化出力のJSON解析成功率(目標:99.9%以上)
- **スキーマ検証通過率**:Pydantic検証を通過する割合
- **フォールバック発動率**:フォールバック戦略が動作する頻度(高い場合はスキーマやプロンプトの改善が必要)
- **トークン効率**:有効情報トークン/総出力トークン比
- **遅延時間分布**:P50、P95、P99遅延時間追跡
## 失敗事例と回復
###ケース1:スキーマの過渡複雑化によるタイムアウト
30個以上のフィールド、4段階のネスト、複雑な正規表現パターンを持つスキーマを使用したとき、OutlinesバックエンドでFSMインデックスビルドに60秒以上かかり、タイムアウトが発生した。
**回復**:スキーマを2つの単純なスキーマに分割し、2段階の呼び出しに変更しました。最初の呼び出しからコアフィールドを抽出し、2番目の呼び出しから詳細情報を抽出する方法です。
###ケース2:列挙値とモデル知識の競合
国コードをISO 3166-1 alpha-2に制限しましたが、モデルは「韓国」を`KR`代わりに`KO`(存在しないコード)にマッピングしようとする最も近い有効な値。`KP`(北朝鮮)を選択する問題が発生した。
**回復**:列挙値の説明をプロンプトに含め、頻繁に混同されるマッピングを明示的に例として提供した。
###ケース3:配列サイズの制限によるメモリ爆発`maxItems`が設定されていない状態で「すべての可能な項目をリストしてください」というプロンプトを使用したとき、モデルは何百もの配列要素を生成し、max_tokensに達するまで続けました。
**回復**:すべての配列フィールドに`maxItems`を設定し、プロンプトでも「最大N個」を明示した。
## チェックリスト
本番環境に構造化出力を適用する前に、次の点を確認してください。
- JSON Schemaへ`maxItems`、`maxLength`などサイズ制限が設定されているか
- すべての必須フィールド`required`に含まれているか
- 列挙値は、モデルが理解できる意味的に明確な値です。
- 重なり深さが3段階以内か(可能であれば2段階)
- フォールバック戦略が実装されているか
- 解析失敗時再試行ロジックがあるか(最大3回)
-`max_tokens`が予想出力サイズの1.5倍以上に設定されているか
- 応答拒否(refusal)処理が実装されているか
- スキーマ変更に対する下位互換性ポリシーがあるか
- 監視ダッシュボードに解析成功率、フォールバック率、遅延時間が含まれているか
- 負荷テストでguided decodingのオーバーヘッドが許容範囲内か
- Unicode/多言語テキストで正常に動作しているか検証したか
##参考資料
1. [OpenAI Structured Outputs Guide](https://platform.openai.com/docs/guides/structured-outputs)
2. [Anthropic Tool Use Documentation] (https://docs.anthropic.com/en/docs/build-with-claude/tool-use)
3. [Outlines - Structured Text Generation](https://github.com/dottxt-ai/outlines)
4. [XGrammar - Flexible and Efficient Grammar-Guided Generation](https://github.com/mlc-ai/xgrammar)
5. [XGrammar: Flexible and Efficient Structured Generation Engine (arXiv 2501.10868)](https://arxiv.org/abs/2501.10868)
6. [vLLM Guided Decoding Documentation] (https://docs.vllm.ai/en/latest/features/structured_outputs.html)
7. [llguidance - Rust-based Parser Engine](https://github.com/microsoft/llguidance)
8. [LangChain Structured Output Guide] (https://python.langchain.com/docs/concepts/structured_outputs/)