LLMに「JSONで返してください」と書くと、デモでは動きます。本番ではキーの欠落、説明文、enumの表記揺れ、途中切れ、拒否応答が混ざります。JSONとして正しくても、存在しない商品IDや矛盾した日付は起こります。
構造化出力は、生成を信頼せず、スキーマ・検証・再試行・失敗処理を組み合わせる設計です。用途別のパターンと実装の骨格をまとめます。
まず「正しい」を3層に分ける
構造化出力の失敗を一括して「JSONエラー」と呼ぶと、対策がぼやけます。正しさを3層に分けます。
| 層 | 例 | 検証する場所 |
|---|---|---|
| 構文 | JSONとしてparseできる | JSON parser |
| スキーマ | 必須キー、型、enum、範囲が合う | JSON Schema / Pydantic / Zod |
| 意味・業務 | 商品IDが存在する、合計金額が一致する | アプリケーションとDB |
JSON Schemaは、JSON文書の型と制約を宣言する標準です[1]。ただし、スキーマに適合した出力でも、内容が事実か、業務上実行してよいかまでは保証しません。LLMの出力は、型が付いた後も外部入力として扱います。
6つの出力パターンを使い分ける
プロンプトだけで形式を指示する
「次のキーを持つJSONだけを返す」と書く方法です。モデルやAPIを選ばず試作には速い一方、構文とスキーマの保証がありません。失敗を人が直せる作業か、後段に厳密なvalidatorがある場面に限ります。
JSON modeで構文だけを保証する
一部のAPIには、出力を有効なJSONへ制約するモードがあります。コードフェンスや前置き文を避けるには有効ですが、必須キーやenumの保証とは別です。アプリ側のスキーマ検証は残し、parse成功率と業務成功率を同じにしないようにします。
ネイティブなスキーマ制約を使う
APIがJSON Schema準拠の構造化出力を提供するなら、基本の第一候補です。OpenAIのStructured Outputsは、指定したJSON Schemaに出力を適合させ、拒否応答もプログラムから判別できる設計です[2]。SDKからPydanticやZodの型を渡せるAPIもあります。
提供者ごとに対応するSchemaの範囲や拒否・途中終了の表現は異なります。差はアダプター層で吸収し、ドメインモデルをAPI固有型へ直結させません。
Tool / Function callingで「行動」と結びつける
出力が検索、メール送信、注文登録などの操作につながるなら、ツール呼び出しとして定義します。ツール名と引数スキーマを固定できます。
重要なのは、引数がスキーマに合うことと、実行を許可してよいことは別だという点です。権限、対象ID、金額上限、冪等性キー、ユーザー確認はツール実行側で検証します。
validatorのエラーを使って再試行する
ネイティブ制約がない、または意味検証に落ちた場合は、validatorの短いエラーだけを返して再生成します。元の長い出力を丸ごと会話へ戻すと、コストが膨らみ、入力中の命令を再注入する面も増えます。
再試行は上限を決め、同じ失敗を繰り返したら止めます。推奨する分類は「構文エラー」「スキーマ違反」「業務違反」「拒否」「途中終了」「API障害」です。種類ごとに、再試行、モデル切り替え、人手確認を分けます。
抽出と正規化を2段に分ける
長い文書から巨大な業務オブジェクトを一度に作らず、原文に近い引用付き候補の抽出と、マスタ照合・単位変換を分けます。
請求書なら、記載された品名・数量・金額の抽出と、社内商品ID・通貨・税区分への正規化を分けます。根拠が残り、レビューしやすくなります。
スキーマは狭く、明示的にする
LLM向けスキーマは、アプリの全データモデルではなく、タスクに必要な最小形にします。
- 自由記述で済ませず、候補が閉じている値は
enumにする - 数値には最小・最大、文字列には長さ制約を置く
- 必須と任意を分け、欠落と
nullの意味を決める additionalProperties: false相当で未知キーを拒否する- 説明文には、形式よりフィールドの意味と判断基準を書く
schema_versionを持たせ、変更時の互換性を管理する
JSON Schemaではpropertiesに書いただけでは必須にならず、requiredで指定します。また既定では未定義の追加プロパティが許されるため、キーの打ち間違いを捕捉したい場合はadditionalPropertiesを制御します[3]。
Pydanticで検証とリトライを分離する
次は、問い合わせ分類を例にしたベンダー非依存の骨格です。call_llmは文字列を返す関数として注入し、生成APIと検証を分けています。Pydanticは型検証に加え、同じモデルからJSON Schemaも生成できます[4]。
from collections.abc import Callable
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, ValidationError
class Ticket(BaseModel):
model_config = ConfigDict(extra="forbid")
schema_version: Literal["1"]
category: Literal["billing", "bug", "question"]
priority: Literal["low", "normal", "high"]
confidence: float = Field(ge=0.0, le=1.0)
summary: str = Field(min_length=1, max_length=120)
def generate_ticket(
call_llm: Callable[[str], str],
request: str,
max_attempts: int = 3,
) -> Ticket:
feedback = ""
for _ in range(max_attempts):
raw = call_llm(request + feedback)
try:
return Ticket.model_validate_json(raw)
except ValidationError as error:
feedback = "\n次の検証エラーだけを修正してください:\n" + error.json()
raise RuntimeError("structured_output_exhausted")
このコードはPydantic 2系で、無効なenumの次に正しいJSONを返す偽のcall_llmを使い、2回目に成功することを確認済みです。実サービスでは拒否と途中終了を通常テキストから分離し、エラーの長さも制限します。
業務検証は別関数で行います。契約IDが必要、商品IDがDBに存在するといった規則を型検証と分けると、失敗理由と再試行方針を変えられます。
リトライできない失敗を決める
何でも再試行すると、同じ誤りにコストと待ち時間を使い続けます。
| 失敗 | 基本方針 |
|---|---|
| JSON構文・型・enum違反 | エラーを短く返し、少数回だけ再試行 |
| 必要情報が入力にない | unknownや保留状態を返す。推測させない |
| 安全上の拒否 | 通常JSONとしてparseせず、拒否フローへ |
| 出力上限による途中終了 | 入力分割、項目削減、上限見直し |
| DB不整合・権限不足 | モデルに再試行させず、アプリ側で処理 |
| API障害・タイムアウト | バックオフ、冪等性、キューへの退避 |
失敗時のフォールバックも仕様です。空オブジェクトで処理を続けるのではなく、人手確認キュー、前回の確定値、機能の一時停止など、用途に合う安全な停止先を決めます。医療LLMの安全設計で扱ったDraft-only原則は、構造化出力でも有効です。
本番で観測する指標
構造化出力を改善するには、成功・失敗を層別に記録します。
- 初回のparse成功率と最終成功率
- スキーマ違反率、業務違反率、拒否率、途中終了率
- 成功までの試行回数、レイテンシ、トークン量、費用
- フィールド別の欠落・修正頻度
- スキーマ版、モデル版、プロンプト版ごとの比較
- 人手で上書きされた割合と修正内容
入力や生の出力は全文保存せず、必要なメタデータと匿名化したエラー分類を残します。
出力品質の評価セットは、正常系だけでなく、情報不足、矛盾、長文、複数候補、入力内の命令文も含めます。RAGを組み合わせる場合は、構造化出力の成功率と検索の正しさを分けて測ります。RAGの精度改善ガイドの検索ミスと生成ミスの切り分けが、そのまま使えます。
まとめ
- 構造化出力の正しさは、JSON構文・スキーマ・業務意味の3層に分ける
- 対応APIではネイティブなスキーマ制約を優先し、アプリ側の意味検証は残す
- 行動につながる出力はTool / Function callingにし、実行権限を別に検証する
- スキーマは小さく厳密にし、未知キー、範囲、enum、バージョンを明示する
- リトライはエラー分類、回数上限、安全なフォールバックとセットで設計する
- 成功率だけでなく、違反種別、再試行回数、人手修正を継続的に観測する
最初の改善として、現在の実装から「JSONを取り出す処理」と「業務上実行してよいかを判断する処理」を別関数にしてください。構造化出力の信頼境界が、そこから見えるようになります。
参考文献・一次情報
- [1]
- [2]
- [3]
- [4]