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. [1]
    STANDARDJSON Schema — Getting Started Step-By-Step

    JSON Schema

    JSONの型、必須項目、制約を宣言する標準スキーマの基礎

  2. [2]
    OFFICIALStructured model outputs

    OpenAI

    JSON Schemaに従うネイティブ構造化出力、refusal、SDKの型連携

  3. [3]
    OFFICIALJSON Schema — Objects

    JSON Schema

    required、properties、additionalPropertiesの挙動

  4. [4]
    OFFICIALModels — Pydantic Validation

    Pydantic

    型付きモデルによる入力検証とJSON Schema生成