LabHub

ブログ

LLM構造化出力とConstrained Decoding実践ガイド:JSON Schemaから本番アプリケーションまで

한국어English日本語

LLM Structured Output Constrained Decoding

##入り

LLMを本番パイプラインに統合するときに最も頻繁に遭遇する問題は、出力形式の不確実性です。プロンプトに「JSONで応答してください」と指示するだけでは、スキーマの不一致、フィールドの欠落、誤ったタイプ、不完全なJSONなど、さまざまな障害が発生する。実際にプロンプ​​トだけでJSON出力を要求したとき、約5~15%の要求で解析失敗が発生するという報告がある。

この問題を根本的に解決する技術がConstrained Decoding(制約復号)である。プロンプトレベルの「要求」ではなく、トークン生成プロセス自体に文法的制約を付与して100%スキーマの遵守を保証する。

この記事では、Constrained Decodingのコア原理(FSM / CFG)から始まり、主要エンジン(Outlines、XGrammar、llguidance)を比較し、OpenAI / AnthropicのネイティブAPI、vLLM / TGIなどオープンソースサービングフレームワークでの適用法、Function Calling統合とプロダクション環境で。

構造化出力が必要な理由

自由形式出力の制限

LLMの自由形式テキスト出力をプログラム的に処理するには、正規表現またはカスタムパーサーを作成する必要があります。しかし、このアプローチは本質的に脆弱です。

構造化出力が解決するもの

構造化出力は以下を保証します。

これを達成する方法は大きく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の動作方法は次のとおりです。

  1. JSON Schemaを正規表現に変換する2.正規表現をDFA(Deterministic Finite Automaton)にコンパイルする
  2. 各復号ステップで現在のFSM状態で遷移可能なトークンのみを許可する
  3. 選択したトークンに従ってFSM状態を更新する

Outlinesライブラリはこのアプローチの代表的な実装です。利点は、実装が比較的単純で状態追跡が速いということですが、再帰構造(ネストされたJSONオブジェクト、可変長配列など)を自然に処理することが難しいという制限があります。

Context-Free Grammar(CFG)ベースのアプローチ

JSONの完全な文法(ネストされたオブジェクト、配列、再帰構造を含む)を正確に表現するには、CFGが必要です。 CFGベースのアプローチは次のように動作します。

  1. JSON SchemaをEBNF(Extended Backus-Naur Form)文法に変換する
  2. 各復号ステップでPDA(Pushdown Automaton)またはパーサの状態を追跡する
  3. 現在の文法状態で許可されているトークンのみを選択する
  4. スタックベースでネスト深さを管理する

XGrammarとllguidanceはCFGベースのアプローチを採用しています。再帰的構造を自然に処理することができますが、FSMコントラスト状態管理のオーバーヘッドが大きい。

###トークンマスキングの重要なメカニズム

どちらの方法も最終的にはlogit maskingを行います。デコード時に logit ベクトルで許可されないトークンの位置に-infを設定すると、softmax以降該当トークンの確率が0になる。

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 = {345, 678, 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

##主要なConstrained Decodingエンジンの比較

比較表

アイテムアウトラインXGrammarllguidanceネイティブAPI(OpenAI)
文法モデルFSM(正規表現+ JSONスキーマ)CFG(EBNF)CFG(EBNF)+バイトレベル内部実装(プライベート)
サポートフォーマットJSON、正規表現、CFGJSON、EBNF、正規表現JSON、EBNF、正規表現、SubstrsJSONスキーマ
フレームワーク統合vLLM、TGI、SGLangvLLM、SGLang、MLC-LLMマウサーGuidance、Azure AIOpenAI APIのみ
前処理時間通常(FSMインデックスビルド)高速(適応トークンマスク)高速(パーサーベース)なし(サーバー側)
ランタイムオーバーヘッドLow(〜3%遅延増加)非常に低い(〜1%遅延増加)Low(〜2%遅延増加)なし(透明)
バッチデコードサポートサポート高効率サポート(ビットマスクの再利用)サポート該当なし
入れ子構造の処理制限的(深い再帰時の性能低下)優秀(プッシュダウンオートマタ)優秀優秀
ライセンスApache 2.0Apache 2.0MIT商用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%保証します。

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を対応するツールで強制すると構造化された出力を得ることができる。

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はデフォルトのバックエンドに設定されています。

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コードをほとんど変更せずにオープンソースモデルに適用できます。

# 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を渡します。

# 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メソッドを使用すると、プロバイダに関係なく、統一されたインターフェイスで構造化された出力を取得できます。

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 結合パターン

エージェントがツールを呼び出しますが、最終応答は構造化形式で返すパターンです。

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にコンパイルする前処理時間が必要です。同じスキーマを繰り返し使用すると、コンパイル結果をキャッシュしてオーバーヘッドを減らすことができます。

##パフォーマンスの影響とトレードオフ

デコード速度の影響

Constrained Decodingには、トークンの作成時に追加の操作が必要です。実際のベンチマークを見ると次のようになる。

シナリオ制約なし(tok / s)アウトライン(tok / s)XGrammar(tok / s)オーバーヘッド
シンプルJSON(5フィールド)5205055151〜3%
複雑なJSON(20フィールド、ネスト)5204705004〜10%
正規表現(メール)5205105180.5〜2%
大規模な列挙型(100値)5204905102〜6%

ほとんどの場合、オーバーヘッドは無視できるレベルです。ただし、非常に複雑なスキーマ(深いネスト、大規模な列挙、長い正規表現)では、10%以上の性能低下が発生する可能性があります。

###品質影響

Constrained Decodingはモデルの自由度を制限するので、意味的な品質にわずかな影響を与える可能性があります。特に次の状況で注意が必要です。

##トラブルシューティング

よく発生する問題と解決策

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が設定されます。parsedNoneかどうか必ず確認しましょう。

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のすべての機能をサポートしていません。主な制約は次のとおりです。

-additionalPropertiesfalseに設定する必要があります

###スキーマのバージョン管理

本番環境でスキーマを変更するときは、下位互換性を維持する必要があります。新しいフィールドを追加する場合はOptionalで始まり​​、既存のフィールドを削除するときはdeprecated期間を置く必要があります。

コストに関する考慮事項

構造化出力はフリーテキストよりもトークン数が増加します。 JSONの構造的オーバーヘッド({}":,)のためだ。平均して15〜30%のトークンオーバーヘッドが発生します。コストに敏感な環境では、フィールド数を最小限に抑え、短いキー名を使用することが役立ちます。

モニタリング指標

本番環境で追跡する必要がある重要な指標は次のとおりです。

失敗事例と回復

###ケース1:スキーマの過渡複雑化によるタイムアウト

30個以上のフィールド、4段階のネスト、複雑な正規表現パターンを持つスキーマを使用したとき、OutlinesバックエンドでFSMインデックスビルドに60秒以上かかり、タイムアウトが発生した。

回復:スキーマを2つの単純なスキーマに分割し、2段階の呼び出しに変更しました。最初の呼び出しからコアフィールドを抽出し、2番目の呼び出しから詳細情報を抽出する方法です。

###ケース2:列挙値とモデル知識の競合

国コードをISO 3166-1 alpha-2に制限しましたが、モデルは「韓国」をKR代わりにKO(存在しないコード)にマッピングしようとする最も近い有効な値。KP(北朝鮮)を選択する問題が発生した。

回復:列挙値の説明をプロンプトに含め、頻繁に混同されるマッピングを明示的に例として提供した。

###ケース3:配列サイズの制限によるメモリ爆発maxItemsが設定されていない状態で「すべての可能な項目をリストしてください」というプロンプトを使用したとき、モデルは何百もの配列要素を生成し、max_tokensに達するまで続けました。

回復:すべての配列フィールドにmaxItemsを設定し、プロンプトでも「最大N個」を明示した。

チェックリスト

本番環境に構造化出力を適用する前に、次の点を確認してください。

##参考資料

  1. OpenAI Structured Outputs Guide
  2. [Anthropic Tool Use Documentation] (https://docs.anthropic.com/en/docs/build-with-claude/tool-use)
  3. Outlines - Structured Text Generation
  4. XGrammar - Flexible and Efficient Grammar-Guided Generation
  5. XGrammar: Flexible and Efficient Structured Generation Engine (arXiv 2501.10868)
  6. [vLLM Guided Decoding Documentation] (https://docs.vllm.ai/en/latest/features/structured_outputs.html)
  7. llguidance - Rust-based Parser Engine
  8. [LangChain Structured Output Guide] (https://python.langchain.com/docs/concepts/structured_outputs/)

コメント

まだコメントはありません。

ログインするとコメントできます