LLMをアプリケーションに組み込むとき、多くのエンジニアが最初にぶつかる壁は「出力が毎回ちょっと違う」という現実です。本記事では、LLMの出力をJSONやPydanticスキーマで制御する構造化出力(Structured Output)について、実装パターンとベストプラクティスを解説します。結論から言えば、2026年時点ではOpenAI・Anthropicなど主要ベンダーがスキーマ強制APIを提供しており、プロンプトで「JSONで返してください」とお願いする時代は終わりつつあります。
なぜ構造化出力が必要か
LLMは自由文を生成するモデルですが、実運用システムに組み込むとなると話は別です。下流の処理では「顧客名」「金額」「日付」といったフィールドが決まった型で返ってくることを期待しており、文章中に埋め込まれた値を正規表現で切り出すのは持続可能な設計とは言えません。
構造化出力の必要性は、おおよそ次の3点に集約されます。第一に後続処理との接続です。データベース挿入、API呼び出し、帳票生成といった処理はスキーマ駆動で動いており、LLMの出力もスキーマに従っている必要があります。第二に品質のテスト可能性です。自由文の品質評価はLLM-as-a-Judgeなどの別コストを伴いますが、スキーマに対する検証は単体テストで容易にできます。第三にエラー検知です。「フィールドが欠けている」「型が違う」といった異常を早期に検出できれば、誤った結果をそのまま業務に流すリスクを減らせます。LLMを導入した企業が最初の数か月で撤退する理由の多くは、精度ではなく「出力がばらつく運用コスト」にあるのが実情です。
構造化出力の3つのアプローチ
構造化出力を実現するアプローチは、大きく3つに分類できます。それぞれ信頼性と柔軟性のトレードオフが異なるため、ユースケースに合わせて使い分けることが重要です。
| アプローチ | 信頼性 | 柔軟性 | 実装難易度 | 対応モデル |
|---|---|---|---|---|
| プロンプト指示のみ | 低(80〜90%) | 高 | 易 | すべて |
| JSON Mode | 中(95〜98%) | 中 | 中 | GPT-4o系・Gemini系 |
| Structured Output API | 高(100%に近い) | 低(スキーマ固定) | 中 | GPT-4o・Claude 3.5以降・Gemini 1.5以降 |
最も強力なのはStructured Output APIで、ベンダー側のデコーダが文法制約(CFG)やマスク付きサンプリングを使って、スキーマ外のトークンを生成させない仕組みを提供しています。以下はOpenAI Structured Output APIをPydanticで呼び出す例です。
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List
client = OpenAI()
class Invoice(BaseModel):
vendor_name: str = Field(description="請求元の会社名")
invoice_number: str
total_amount: int = Field(description="税込金額(円)")
due_date: str = Field(description="YYYY-MM-DD形式")
line_items: List[str]
resp = client.beta.chat.completions.parse(
model="gpt-4o-2024-11-20",
messages=[
{"role": "system", "content": "請求書から構造化情報を抽出してください。"},
{"role": "user", "content": invoice_text},
],
response_format=Invoice,
)
invoice = resp.choices[0].message.parsed
print(invoice.total_amount)
スキーマ設計のベストプラクティス
スキーマそのものの設計品質が、構造化出力の成功率を大きく左右します。経験則として、次の4点を守るとモデルの迷いが減り、精度が安定します。
- フィールド名はビジネス意味を反映させる――「field1」ではなく「customer_email」のように意味が明確な名前にします
- Field descriptionで制約を書く――「YYYY-MM-DD形式」「小数点2桁まで」など、自然言語で制約を添えるとモデルの準拠性が上がります
- Optionalとデフォルト値を適切に使う――不確実な項目はOptionalにしておくと、無理に埋めようとする幻覚を抑えられます
- 列挙型(Enum)を積極的に使う――カテゴリ系のフィールドはEnumで定義し、モデルの創造的な出力を抑制します
from enum import Enum
from pydantic import BaseModel, Field
from typing import Optional
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class SupportTicket(BaseModel):
title: str = Field(description="問い合わせの要約(50字以内)")
priority: Priority
category: str = Field(description="billing / technical / account のいずれか")
estimated_hours: Optional[float] = Field(default=None, description="対応工数の推定値")
tags: list[str] = Field(default_factory=list, max_length=5)
エラーハンドリングとリトライ戦略
Structured Output APIは100%に近い信頼性を持ちますが、完璧ではありません。特にスキーマ制約は満たしていても意味的に誤った値が入ることがあります。例えば「due_date」に過去の日付が入ったり、「total_amount」がゼロになったりといったケースです。
このため、スキーマ検証に加えて業務ルール検証を行い、失敗時にはエラー内容をプロンプトに含めてリトライする設計がおすすめです。リトライ時にエラー情報を伝えると、2回目の成功率が一気に上がるのが実務の知見です。
from datetime import date
from pydantic import ValidationError
def extract_invoice_with_retry(text: str, max_retries: int = 3) -> Invoice:
error_feedback = ""
for attempt in range(max_retries):
try:
resp = client.beta.chat.completions.parse(
model="gpt-4o-2024-11-20",
messages=[
{"role": "system", "content": "請求書から抽出。" + error_feedback},
{"role": "user", "content": text},
],
response_format=Invoice,
)
invoice = resp.choices[0].message.parsed
if invoice.total_amount <= 0:
raise ValueError("total_amountは正の値である必要があります")
if date.fromisoformat(invoice.due_date) < date.today():
raise ValueError("due_dateが過去日付です")
return invoice
except (ValidationError, ValueError) as e:
error_feedback = f"\n前回のエラー: {e}。修正して再出力してください。"
raise RuntimeError("構造化出力の抽出に失敗しました")
ユースケース別の実装例
構造化出力の典型的なユースケースは、データ抽出・分類・要約の3つに大別されます。それぞれスキーマ設計のポイントが微妙に異なりますので、下表にまとめます。
| ユースケース | 出力形式 | 主要フィールド | バリデーション |
|---|---|---|---|
| 請求書・契約書からのデータ抽出 | ネストしたオブジェクト | 金額・日付・当事者・明細 | 型・範囲・業務ルール |
| 問い合わせ分類・ルーティング | フラットなオブジェクト | カテゴリ・優先度・タグ | Enum準拠・タグ数上限 |
| 長文記事の要約 | 配列 + メタデータ | 要点・エンティティ・センチメント | 文字数上限・重複排除 |
| 会議議事録からのアクション抽出 | 配列オブジェクト | 担当者・期限・依存関係 | 担当者存在確認・期限妥当性 |
特にデータ抽出系では、PDFやスキャン画像からのOCR結果を入力とすることが多く、入力自体がノイズまみれです。この場合は「confidence」フィールドをスキーマに含めて、モデルに抽出の自信度を0.0〜1.0で返させ、閾値未満は人手レビューに回す、といったハイブリッド運用が効果的です。また、会議議事録のように同じ情報が複数の表現で繰り返される入力では、重複排除のロジックをスキーマ側ではなくポスト処理で実装すると、LLMの負担を下げられます。
まとめ
構造化出力は、LLMを実用システムに組み込む際の基盤技術です。2026年時点ではStructured Output APIが成熟しており、プロンプトでお願いする時代から、スキーマで強制する時代へと移行しました。スキーマ設計、バリデーション、リトライの3点セットを押さえれば、LLMの「毎回ちょっと違う」問題の大半は解決します。関連記事としてプロンプトエンジニアリング入門、Function Callingの活用、LLM自動分類・ラベリング、LLMとはもあわせてご参照ください。
よくある質問(FAQ)
Q1. LLMの構造化出力とは何ですか?
A. LLMの出力をJSONやXMLなど、決められた形式で生成させる技術です。出力をプログラムで自動処理する場合に不可欠で、Pydanticスキーマの指定やStructured Output APIで実現できます。構造化出力を使うことで、後続のデータベース挿入やAPI呼び出しとの接続が容易になります。
Q2. JSON ModeとStructured Output APIの違いは?
A. JSON ModeはJSON形式での出力を保証しますが、スキーマ(フィールド名、型)までは保証しません。Structured Output APIはスキーマレベルで出力形式を強制するため、より信頼性が高く、下流処理のエラーが大幅に減ります。新規設計ではStructured Output APIを優先的に検討することをおすすめします。
Q3. 構造化出力が失敗した場合の対策は?
A. Pydanticなどでバリデーションを行い、失敗時にリトライする仕組みを実装します。リトライ時にエラー内容をプロンプトに含めることで、2回目の精度が向上します。通常2〜3回のリトライで成功率99%以上になり、それでも失敗するケースは人手レビューに回すハイブリッド運用が現実的です。