LLM出力のJSON Schema構造化と検証

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

LLM出力のJSON Schema構造化と検証

ユースケース定義

LLMから非構造化テキスト(例:顧客からのフィードバック、会議の議事録、製品説明)をインプットとして受け取り、これを特定のビジネスロジックで利用可能な構造化データ(JSON形式)に変換する必要がある場合。例えば、顧客フィードバックを感情分析システムに渡す前に、カテゴリ、評価点、意見テキストといったスキーマに沿った形式で抽出する。これにより、後続のシステムでのパース処理が不要になり、データの信頼性が向上する。

入出力契約

入力

  • フォーマット: プレーンテキスト。要求された情報を含む自然言語のテキスト。JSON Schema定義(文字列)を含む場合がある。

  • 失敗時の挙動: スキーマ定義が不正な場合や、入力テキストが極端に長くLLMのコンテキストウィンドウを超える場合は、エラーを返すか、処理を中断する。

  • 禁止事項: 悪意のあるプロンプト注入、個人情報など機微な情報の意図的な漏洩指示。

出力

  • フォーマット: 指定されたJSON Schemaに厳密に準拠したJSON文字列のみ。JSON文字列以外の余計なテキストは含めない。

  • 失敗時の挙動: LLMが出力したJSONが不正な場合(JSONパースエラー、スキーマ不適合)、外部の検証ステップで検出され、リトライまたはエラーとして報告される。LLM自体は、JSONを生成できない場合、空のJSONオブジェクト {} または null を返すよう指示することも可能。

  • 禁止事項: 不完全なJSON、JSONではない自由形式テキスト、スキーマで定義されていない追加プロパティ(additionalProperties: falseが指定されている場合)。

制約付き仕様化

  1. 単一JSONオブジェクト: 出力は常に単一のJSONオブジェクトであること。

  2. スキーマ厳守: 提供されたJSON Schemaに厳密に準拠すること。

    • すべての必須フィールド ("required") を含めること。

    • データ型 ("type") はスキーマ定義と一致させること。

    • 文字列の列挙型 ("enum") は定義された値のみを使用すること。

    • 追加プロパティの禁止 ("additionalProperties": false) が指定されている場合は、スキーマで定義されていないプロパティを含めないこと。

  3. データマッピング: 入力テキストから関連情報のみを抽出し、スキーマの各フィールドに適切にマッピングすること。情報が見つからない場合は、フィールドを省略するか、null(スキーマで許容されている場合)を設定すること。

  4. 説明文禁止: JSON出力の前後や内部に、説明や会話テキストを一切含めないこと。

プロンプト設計

1. ゼロショットプロンプト(Zero-shot Prompt)

あなたはJSONデータ抽出の専門家です。以下の[入力テキスト]から情報を抽出し、[JSONスキーマ]に厳密に準拠したJSONオブジェクトを生成してください。余計なテキストや説明は一切含めず、純粋なJSONのみを出力してください。情報が見つからない場合は、可能な限りnullを使用してください。

[JSONスキーマ]
```json
{
  "type": "object",
  "properties": {
    "product_name": { "type": "string", "description": "製品名" },
    "category": { "type": "string", "enum": ["Electronics", "Books", "Clothes", "Food"], "description": "製品カテゴリ" },
    "price": { "type": "number", "description": "価格" },
    "available": { "type": "boolean", "description": "在庫の有無" }
  },
  "required": ["product_name", "category"],
  "additionalProperties": false
}
```

[入力テキスト]
最新のスマートフォン「Xperia 10 V」が登場。薄型軽量で持ちやすく、写真や動画を長時間楽しめる大容量バッテリーが特徴です。カテゴリはElectronicsで、価格は69,300円。現在、予約受付中です。

2. 少数例プロンプト(Few-shot Prompt)

あなたはJSONデータ抽出の専門家です。以下の例を参考に、[入力テキスト]から情報を抽出し、[JSONスキーマ]に厳密に準拠したJSONオブジェクトを生成してください。余計なテキストや説明は一切含めず、純粋なJSONのみを出力してください。情報が見つからない場合は、可能な限りnullを使用してください。

---
例1:
入力テキスト: 新刊「AIと未来」が発売されました。カテゴリはBooksで、価格は2,500円です。在庫あり。
出力JSON:
```json
{
  "product_name": "AIと未来",
  "category": "Books",
  "price": 2500,
  "available": true
}
```
---
例2:
入力テキスト: オーガニックトマト。新鮮で美味しい。カテゴリはFoodです。価格は450円。
出力JSON:
```json
{
  "product_name": "オーガニックトマト",
  "category": "Food",
  "price": 450,
  "available": null
}
```
---

[JSONスキーマ]
```json
{
  "type": "object",
  "properties": {
    "product_name": { "type": "string", "description": "製品名" },
    "category": { "type": "string", "enum": ["Electronics", "Books", "Clothes", "Food"], "description": "製品カテゴリ" },
    "price": { "type": "number", "description": "価格" },
    "available": { "type": "boolean", "description": "在庫の有無" }
  },
  "required": ["product_name", "category"],
  "additionalProperties": false
}
```

[入力テキスト]
限定版Tシャツ「サマーコレクション」。非常に人気が高く、カテゴリはClothesです。価格は3,980円ですが、現在在庫はありません。

3. Chain-of-Thought制約型プロンプト(Chain-of-Thought with Constraints Prompt)

あなたはJSONデータ抽出の専門家です。以下の手順に従って、[入力テキスト]から情報を抽出し、[JSONスキーマ]に厳密に準拠したJSONオブジェクトを生成してください。

1.  **情報分析**: [入力テキスト]を注意深く読み、以下のスキーマの各フィールドに対応する情報を特定してください。

    *   `product_name`: 製品の名前。

    *   `category`: 製品のカテゴリ。指定された`enum`値から選択。

    *   `price`: 製品の価格。数値。

    *   `available`: 在庫の有無。真偽値。

2.  **マッピング**: 特定した情報をスキーマのフィールドに正確にマッピングしてください。情報が見つからない場合、`required`フィールドであれば生成せず、`null`を許可するフィールドであれば`null`を設定してください。

3.  **JSON生成**: 上記の分析とマッピングに基づき、[JSONスキーマ]に厳密に準拠するJSONオブジェクトのみを出力してください。余計なテキストや説明は含めないでください。`additionalProperties: false`が適用されていることを意識してください。

[JSONスキーマ]
```json
{
  "type": "object",
  "properties": {
    "product_name": { "type": "string", "description": "製品名" },
    "category": { "type": "string", "enum": ["Electronics", "Books", "Clothes", "Food"], "description": "製品カテゴリ" },
    "price": { "type": "number", "description": "価格" },
    "available": { "type": "boolean", "description": "在庫の有無" }
  },
  "required": ["product_name", "category"],
  "additionalProperties": false
}
```

[入力テキスト]
今週の目玉商品、新しいオーブンレンジ「HeatMaster 5000」。非常に高性能で、Electronicsカテゴリに属します。価格は不明ですが、現在多くの店舗で販売されています。

評価

評価シナリオ

  • 正例: 「最新のスマートフォン「Xperia 10 V」が登場。薄型軽量で持ちやすく、写真や動画を長時間楽しめる大容量バッテリーが特徴です。カテゴリはElectronicsで、価格は69,300円。現在、予約受付中です。」

    • 期待される出力: { "product_name": "Xperia 10 V", "category": "Electronics", "price": 69300, "available": true }
  • 難例: 「この商品は素晴らしかった。値段はちょっと高かったけど。詳細な情報は見つからない。」(製品名やカテゴリが不明瞭)

    • 期待される出力: { "product_name": null, "category": null, "price": null, "available": null } (nullが許容される場合)またはエラー(requiredフィールドがないため)。プロンプトにより挙動が変わる。requiredフィールドを厳守するため、実際にはスキーマ違反となる可能性があるため、期待値を調整する必要がある。ここではrequiredフィールドがproduct_namecategoryのため、これらを抽出できないと無効な出力となる。
  • コーナーケース: 「特に提供できる情報はありません。」

    • 期待される出力: { "product_name": null, "category": null, "price": null, "available": null } (もしrequiredを緩めるなら) または、空オブジェクト {} もしくはエラー。現在のスキーマではproduct_namecategoryが必須なので、空の出力はエラーとなる。

自動評価の擬似コード

import json
from jsonschema import validate, ValidationError

# 事前に定義されたJSON Schema

PRODUCT_SCHEMA = {
  "type": "object",
  "properties": {
    "product_name": { "type": "string", "description": "製品名" },
    "category": { "type": "string", "enum": ["Electronics", "Books", "Clothes", "Food"], "description": "製品カテゴリ" },
    "price": { "type": "number", "description": "価格" },
    "available": { "type": "boolean", "description": "在庫の有無" }
  },
  "required": ["product_name", "category"],
  "additionalProperties": false
}

def evaluate_llm_output(llm_output_str: str, expected_json: dict = None) -> dict:
    score = 0
    feedback = []

    # 1. JSONパースの検証

    try:
        parsed_json = json.loads(llm_output_str)
        feedback.append("JSON形式: OK")
        score += 10
    except json.JSONDecodeError as e:
        feedback.append(f"JSONパースエラー: {e}")
        score -= 10
        return {"score": score, "feedback": feedback, "status": "PARSE_ERROR"}

    # 2. JSON Schemaの検証

    try:
        validate(instance=parsed_json, schema=PRODUCT_SCHEMA)
        feedback.append("JSON Schema準拠: OK")
        score += 20
    except ValidationError as e:
        feedback.append(f"JSON Schemaエラー: {e.message} (Path: {e.path})")
        score -= 15
        return {"score": score, "feedback": feedback, "status": "SCHEMA_ERROR"}

    # 3. 値の正確性の検証 (期待値が提供された場合)

    if expected_json:

        # 必須フィールドの存在確認 (Schema検証でカバーされるが、念のため)

        missing_required = [f for f in PRODUCT_SCHEMA.get("required", []) if f not in parsed_json]
        if missing_required:
            feedback.append(f"必須フィールドが欠落: {', '.join(missing_required)}")
            score -= 5

        # 各フィールドの値の比較

        correct_values = 0
        total_fields = 0
        for key, expected_value in expected_json.items():
            total_fields += 1
            if key in parsed_json:
                if parsed_json[key] == expected_value:
                    correct_values += 1
                else:
                    feedback.append(f"値の不一致: フィールド '{key}', 期待値 '{expected_value}', 実際 '{parsed_json[key]}'")
                    score -= 5 # 部分的な減点
            else:
                feedback.append(f"フィールド欠落: '{key}'")
                score -= 3 # 部分的な減点

        if total_fields > 0:
            accuracy_percentage = (correct_values / total_fields) * 100
            feedback.append(f"値の正確性: {accuracy_percentage:.2f}% ({correct_values}/{total_fields}フィールドが一致)")
            score += (accuracy_percentage / 100) * 10 # 正確性に応じて加点
    else:
        feedback.append("期待JSONが指定されていないため、値の正確性は評価しません。")

    return {"score": score, "feedback": feedback, "status": "PASS" if score >= 25 else "FAIL"}

# 採点ルーブリック:


# - JSONパース成功: +10


# - JSON Schema準拠: +20


# - 期待値とのフィールド一致: 1フィールドあたり+10 (最大でスキーマフィールド数*10)


# - JSONパース失敗: -10


# - JSON Schema不適合: -15


# - 期待値とのフィールド不一致: -5


# - 期待フィールドの欠落: -3

誤り分析と抑制手法

失敗モード

  • 幻覚(Hallucination): 入力テキストに存在しない情報やスキーマで許可されていない値をLLMが生成する。

  • 様式崩れ(Format Deviation): LLMが有効なJSONを出力しない(例: 閉じ括弧の欠落、末尾に余計なテキスト)。

  • 脱線(Off-topic / Deviation): スキーマで"additionalProperties": falseが指定されているにも関わらず、LLMが未定義のプロパティを追加する。あるいは、スキーマの型制約やenum制約を無視する。

  • 禁止事項違反: プロンプトで明確に「説明文を含めない」と指示しているにも関わらず、LLMがJSONの前に解説文などを挿入する。

抑制手法

  • System指示の強化:

    • プロンプトの冒頭で「あなたはJSON生成に特化したAIです。出力は常にJSON形式のみであり、その他のテキスト(説明、コメント、コードブロックマークダウンなど)は一切含めません。」と強く指示する。

    • スキーマ定義に"additionalProperties": falseを明示的に含めることで、LLMに追加プロパティを生成させないよう制約をかける。

  • 検証ステップ(外部バリデーション):

    • LLMの出力後、即座に外部のJSONパーサーとJSON Schemaバリデーター(例: Pythonのjsonモジュールとjsonschemaライブラリ)を使用して出力を検証する。

    • 検証に失敗した場合、エラーメッセージをログに記録し、後続処理を中断またはリトライ戦略に移行する。

  • リトライ戦略:

    • 外部検証で失敗した場合、元のプロンプトに加えて、発生したエラーメッセージと修正指示(例: 「JSONパースエラーが発生しました。不正なJSONを修正してください。」、「product_nameフィールドが欠落しています。入力テキストから抽出して補完してください。」)を付加してLLMに再プロンプトする。

    • 複数回のリトライ後も失敗する場合は、人間の介入を促すか、エラーとして処理を終了する。

  • Few-shot Examplesの活用: 複雑なマッピングや特定の制約(例: enum値の選択)がある場合、正しい出力形式の具体例を複数提示することで、LLMの学習を促し、エラー率を低減させる。

  • Chain-of-Thought: LLMに思考プロセスを段階的に踏ませることで、最終的な出力の精度とスキーマ準拠を向上させる。特に、情報抽出→マッピング→JSON生成というステップを踏ませることで、幻覚や脱線を抑制できる。

改良

評価と誤り分析の結果に基づき、プロンプトを反復的に改良する。例えば、特定のフィールドで頻繁に型エラーが発生する場合、そのフィールドの抽出に関する指示を強化したり、Few-shotにそのケースを含めたりする。また、幻覚が多い場合は、additionalProperties: falseの強調や、「入力テキストにない情報は生成しない」という指示を追加する。

再評価

改良されたプロンプトに対して、再度評価シナリオを用いて自動評価を実施する。スコアの向上、失敗モードの減少を確認し、目標とする品質基準に達するまでこのループを繰り返す。

まとめ

LLMから構造化されたJSON出力を得るには、明確な入出力契約、厳格なJSON Schema、そしてそれを遵守させるためのプロンプト設計と外部検証の組み合わせが不可欠です。ゼロショット、少数例、Chain-of-Thoughtといったプロンプト手法を適切に選択し、自動評価と誤り分析を通じて継続的にプロンプトを改良していくことで、信頼性の高いLLMベースのデータ抽出システムを構築できます。


graph TD
    A["プロンプト設計"]|プロンプト作成/更新| --> B("LLMモデル")
    B --> C{"LLM出力"}
    C --> D["外部JSONスキーマ検証"]|JSONパース & スキーマチェック|
    D -- 成功 --> E["構造化データ利用"]|ビジネスロジックへの組み込み|
    D -- 失敗 --> F["誤り分析"]|エラータイプ分類、発生原因特定|
    F --> G["改良"]|プロンプト修正、システム指示調整、Few-shot追加など|
    G --> A
    D -- 失敗 --> H["リトライ戦略"]|エラーメッセージを付加し再プロンプト|
    H --> B
ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました