構造化データ(JSON)を安定して出力させるLLMプロンプト&スキーマ定義テクニック
大規模言語モデル(LLM)を単なる対話型インターフェースとしてではなく、既存の業務システム、マイクロサービス、データパイプラインへ統合するケースが増加しています。その際、バックエンド処理とシームレスに連携させるための絶対条件となるのが、「LLMからの出力を厳密なJSONなどの構造化データとして受け取ること」です。
しかし、自然言語による指示(プロンプト)のみでJSONを出力させようとすると、「Markdownのコードブロック記法(```json)が混入する」「前後に『以下が結果です』といった挨拶文が挿入される」「カンマが欠落してJSONパースに失敗する(JSONDecodeError)」といった問題が一定の確率で発生し、パイプラインの停止を引き起こす要因となります。
本記事では、構造化データ出力における技術的アプローチの進化を整理した上で、最新の「制約付きサンプリング(Structured Outputs)」の仕組みと、PydanticやJSON Schemaを活用して堅牢なAPI連携を実現するための実装テクニックを解説します。
1. 構造化出力アプローチの進化とそれぞれの特性
LLMからJSONを取得する手法は、モデルおよびAPIの進化に伴い、大きく以下の3世代に分類されます。
| 手法 | 仕組み | 利点 | 課題・リスク |
|---|---|---|---|
| 第1世代:プロンプト指示 | 「JSON形式で出力せよ」と文中で指示 | すべてのモデルで即座に試行可能 | パースエラーやキー名の揺らぎ、説明文の混入が頻発 |
| 第2世代:JSON Mode | APIパラメータでJSON出力を明示指定 | シンタックス(構文)としての妥当性は保証 | スキーマ(指定したキーやデータ型)への準拠は保証されない |
| 第3世代:Structured Outputs | 文脈自由文法(CFG)による制約付きデコード | スキーマに100%合致することが保証される | 事前のスキーマ定義が必須、未定義フィールドの出力不可 |
プロンプト指示とJSON Modeの限界
プロンプトのみでフォーマットを制御しようとする場合、どれほど厳密に指示を書いても、確率的な生成ゆえに構文エラーを完全には排除できません。
その後の進化として登場した「JSON Mode」は、出力が文法的に正しいJSONであることを強制する仕組みですが、「必須キーが欠落する」「数値型が期待されるフィールドに文字列が入る」といったセマンティックなスキーマ違反を防ぎきることは困難でした。
2. 制約付きサンプリング(Structured Outputs)の内部メカニズム
現在、主要なモデル(OpenAI、Gemini、Claude、ならびにvLLMなどのローカル推論基盤)において標準化が進んでいるのが**Structured Outputs(制約付きサンプリング / Constrained Decoding)**です。
仕組み:ロジットマスキングによる文法強制
通常のLLM推論では、全語彙(数万〜数十万トークン)の中から確率分布に基づいて次のトークンを選択します。
Structured Outputsでは、開発者が定義したJSON Schemaを事前に構文解析し、**「現在の生成位置において、文法およびスキーマ上、次に来ることが許可されているトークン」以外をすべてマスク(確率を-infに設定)**してサンプリングを行います。
【生成途中の状態】: {"user_id":
│
▼
【許容トークン判定】: 数値型(整数)のみ許可
├─ 許可トークン: "1", "2", "3" ... ──> 確率を保持
└─ 禁止トークン: '"' (クォート), "true", "abc" ──> 確率をゼロにマスク
この処理をトークン生成ごとにリアルタイムで適用することにより、モデルがスキーマから逸脱した出力を生成すること自体が物理的に不可能となり、構文エラーやキーの不一致が完全に排除されます。
3. Pydantic / JSON Schema を用いた実践的なスキーマ定義
Python環境においてStructured Outputsを実装する場合、型バリデーションライブラリであるPydanticと連携させる構成がデファクトスタンダードとなっています。
基本的な実装パターン(Pydanticモデル定義)
モデル定義時には、各フィールドに明確な型アノテーションを付与し、さらにField(description=...)を活用してLLMに対する文脈情報を補完することが推奨されます。
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
class RiskLevel(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class ContractClauseAnalysis(BaseModel):
clause_title: str = Field(
description="契約書の条項名(例: 秘密保持義務、損害賠償上限)"
)
risk_level: RiskLevel = Field(
description="リスク評価レベル。低、中、高のいずれか"
)
risk_summary: str = Field(
description="リスク要因の具体的な要約説明(100文字以内)"
)
recommended_action: Optional[str] = Field(
default=None,
description="修正案または交渉方針の提案(特になければnull)"
)
class ContractAnalysisResponse(BaseModel):
contract_id: str = Field(description="分析対象の契約書識別子")
clauses: List[ContractClauseAnalysis] = Field(description="抽出された条項一覧")
スキーマ設計における3つのポイント
Field(description=...)の充実: フィールド名は単なる識別子にとどまらず、LLMに対する局所的なプロンプトとしても機能します。抽出条件や制約を簡潔に記述することで、抽出精度が向上します。- Enum(列挙型)による選択肢の固定: ステータスや分類ラベルなどの出力値が決まっている場合は、文字列型ではなくEnumとして定義することで、表記揺れや無効な文字列の生成を完全に防ぐことができます。
- Optional(Null許容)の明示: 抽出対象テキストに該当情報が存在しない可能性がある項目は、
Optional[T]として定義し、情報が欠如している場合に空文字やハルシネーションを起こさせず安全にnullを出力させることが重要です。
4. Tool Calling / Function Calling との使い分け
構造化出力を得る手段としては、Structured Outputsのほかに**Tool Calling(Function Calling)**の機能を利用するアプローチも広く用いられています。
| 用途・特性 | Structured Outputs(Response Format) | Tool Calling(Function Calling) |
|---|---|---|
| 主な目的 | 最終的な応答データそのものをJSON化する | 外部APIや関数の実行引数としてJSONを取得する |
| ワークフロー | 単一の入力に対する抽出・変換タスク | 自律型エージェント、マルチステップ処理 |
| ツール選択 | スキーマが1つに固定されている | 複数あるツールからどれを呼ぶかをLLMが自律判断 |
単に「契約書から指定フォーマットでデータを抽出したい」「ユーザーレビューを感情分析してDBへ保存したい」といった定型的なデータ処理パイプラインでは、オーバーヘッドの少ない**Structured Outputs(レスポンスフォーマット指定)**を選択するのが自然です。
一方で、「ユーザーの入力に応じて社内DBを検索するか、外部検索ツールを呼ぶかを切り替えたい」といった分岐が存在する場合は、Tool Callingを活用することが適しています。
5. 本番運用のための防御的エラーハンドリング
Structured Outputsの導入により構文パースエラー(Syntax Error)は劇的に減少しますが、システム統合においては依然として以下の点に対する考慮が必要です。
- タイムアウトとトークン制限: 巨大なネスト構造や大量の配列データを一度に生成させようとすると、最大出力トークン数を超過して途中で出力が切断されるリスクがあります。大量データはバッチ分割して並行処理することが望ましいとされています。
- 後方互換性とスキーマ移行: アプリケーション側のPydanticモデル定義を更新する際、以前のプロンプトログやキャッシュデータとの整合性を維持できるよう、適切なバージョン管理体制を整えることが推奨されます。
まとめ:確実な構造化出力がLLMのシステム統合を加速させる
LLMの出力をプログラムで確実に扱える構造化データへ落とし込む技術は、PoCから実運用サービスへと昇格させるための分水嶺となります。
場当たり的なプロンプトの微調整に依存するのではなく、スキーマ駆動(Schema-Driven)の制約付きサンプリング手法を適切に採用することで、パースエラーによるシステム障害を根本から排除し、堅牢で予測可能な生成AIシステムを構築することが可能になります。