JSON Schemaを使った出力制御

資料

LLM出力制御におけるJSON Schemaの活用と評価戦略

LLMの出力構造化は、後続システム連携に不可欠である。本稿ではJSON Schemaを用いた厳密な出力制御と評価手法を詳述する。

ユースケース定義

顧客からの問い合わせ内容を分類し、必要な情報を構造化されたJSONとして抽出する。具体的には、製品名、問い合わせ種別、緊急度、連絡先メールアドレス、要約を抽出する。この情報は、顧客サポートシステムへの自動連携に利用される。

入出力契約

  • 入力: 自然言語テキスト(顧客の問い合わせ文)。
  • 出力: 定義されたJSON Schemaに厳密に準拠したJSONオブジェクト。
  • 失敗時の挙動: LLMがJSON Schemaに違反する出力を生成した場合、アプリケーション側はエラー内容を示す以下のJSONオブジェクトを返す。

    {
      "error": "JSON Schema validation failed.",
      "details": [
        {"path": "/product_name", "message": "'product_name' is a required property"},
        {"path": "/severity", "message": "'high' is not one of ['低', '中', '高', '緊急']"}
      ]
    }
    
  • 禁止事項:

    • JSON Schemaで定義されていないプロパティの追加。
    • データ型の不一致。
    • 必須プロパティの欠落。
    • JSONフォーマット以外の余計なテキストの出力。

制約付き仕様化

LLMが出力すべきJSONのスキーマを以下のように定義する。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Customer Inquiry",
  "description": "顧客からの問い合わせ情報を構造化",
  "type": "object",
  "properties": {
    "product_name": {
      "type": "string",
      "description": "問い合わせ対象の製品名",
      "maxLength": 50
    },
    "issue_type": {
      "type": "string",
      "enum": ["障害", "操作方法", "機能要望", "その他"],
      "description": "問い合わせ種別"
    },
    "severity": {
      "type": "string",
      "enum": ["低", "中", "高", "緊急"],
      "description": "問い合わせの緊急度"
    },
    "contact_email": {
      "type": ["string", "null"],
      "format": "email",
      "description": "連絡先メールアドレス。不明な場合はnull"
    },
    "summary": {
      "type": "string",
      "description": "問い合わせの要約",
      "minLength": 10,
      "maxLength": 200
    }
  },
  "required": ["product_name", "issue_type", "severity", "summary"],
  "additionalProperties": false
}

プロンプト設計

ゼロショットプロンプト

あなたはユーザーからの問い合わせをJSON形式で構造化する専門家です。
以下のJSON Schemaに厳密に準拠して、問い合わせ内容から情報を抽出してください。
Schemaで定義されていないプロパティは追加しないでください。
問い合わせ内容に該当する情報がない場合は、Schemaの定義に従って適切な値を設定してください(例: typeが["string", "null"]のプロパティはnull)。

--- JSON Schema ---
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Customer Inquiry",
  "description": "顧客からの問い合わせ情報を構造化",
  "type": "object",
  "properties": {
    "product_name": {
      "type": "string",
      "description": "問い合わせ対象の製品名",
      "maxLength": 50
    },
    "issue_type": {
      "type": "string",
      "enum": ["障害", "操作方法", "機能要望", "その他"],
      "description": "問い合わせ種別"
    },
    "severity": {
      "type": "string",
      "enum": ["低", "中", "高", "緊急"],
      "description": "問い合わせの緊急度"
    },
    "contact_email": {
      "type": ["string", "null"],
      "format": "email",
      "description": "連絡先メールアドレス。不明な場合はnull"
    },
    "summary": {
      "type": "string",
      "description": "問い合わせの要約",
      "minLength": 10,
      "maxLength": 200
    }
  },
  "required": ["product_name", "issue_type", "severity", "summary"],
  "additionalProperties": false
}
--- 問い合わせ内容 ---
「新製品XYZの購入を検討しています。機能についていくつか質問があり、操作方法を教えてほしいです。急ぎではないですが、できるだけ早く回答が欲しいです。担当者様からの連絡先は info@example.com です。」

少数例プロンプト

あなたはユーザーからの問い合わせをJSON形式で構造化する専門家です。
以下のJSON Schemaに厳密に準拠して、問い合わせ内容から情報を抽出してください。
Schemaで定義されていないプロパティは追加しないでください。
問い合わせ内容に該当する情報がない場合は、Schemaの定義に従って適切な値を設定してください(例: typeが["string", "null"]のプロパティはnull)。

--- JSON Schema ---
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Customer Inquiry",
  "description": "顧客からの問い合わせ情報を構造化",
  "type": "object",
  "properties": {
    "product_name": {
      "type": "string",
      "description": "問い合わせ対象の製品名",
      "maxLength": 50
    },
    "issue_type": {
      "type": "string",
      "enum": ["障害", "操作方法", "機能要望", "その他"],
      "description": "問い合わせ種別"
    },
    "severity": {
      "type": "string",
      "enum": ["低", "中", "高", "緊急"],
      "description": "問い合わせの緊急度"
    },
    "contact_email": {
      "type": ["string", "null"],
      "format": "email",
      "description": "連絡先メールアドレス。不明な場合はnull"
    },
    "summary": {
      "type": "string",
      "description": "問い合わせの要約",
      "minLength": 10,
      "maxLength": 200
    }
  },
  "required": ["product_name", "issue_type", "severity", "summary"],
  "additionalProperties": false
}
--- 例 ---
問い合わせ: 「私のPCでソフトウェアABCが起動しません。緊急度「高」です。連絡先はsupport@abc.com。」
出力:
```json
{
  "product_name": "ソフトウェアABC",
  "issue_type": "障害",
  "severity": "高",
  "contact_email": "support@abc.com",
  "summary": "PCでソフトウェアABCが起動しない"
}

問い合わせ: 「製品DEFの機能改善について提案があります。連絡先は不要です。」 出力:

{
  "product_name": "製品DEF",
  "issue_type": "機能要望",
  "severity": "低",
  "contact_email": null,
  "summary": "製品DEFの機能改善に関する提案"
}

— 問い合わせ内容 — 「新製品XYZの購入を検討しています。機能についていくつか質問があり、操作方法を教えてほしいです。急ぎではないですが、できるだけ早く回答が欲しいです。担当者様からの連絡先は info@example.com です。」

### Chain-of-Thought制約型プロンプト

```text
あなたはユーザーからの問い合わせをJSON形式で構造化する専門家です。
以下のJSON Schemaに厳密に準拠して、問い合わせ内容から情報を抽出してください。
Schemaで定義されていないプロパティは追加しないでください。
問い合わせ内容に該当する情報がない場合は、Schemaの定義に従って適切な値を設定してください(例: typeが["string", "null"]のプロパティはnull)。

回答は以下の手順で生成してください。
1. 問い合わせ内容から製品名、問い合わせ種別、緊急度、連絡先メールアドレス、要約をそれぞれ特定します。
2. 特定した情報をJSON Schemaの各プロパティにマッピングします。enumやformat制約に注意し、厳密に適合させます。
3. 最後に、生成したJSONオブジェクトを出力します。

--- JSON Schema ---
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Customer Inquiry",
  "description": "顧客からの問い合わせ情報を構造化",
  "type": "object",
  "properties": {
    "product_name": {
      "type": "string",
      "description": "問い合わせ対象の製品名",
      "maxLength": 50
    },
    "issue_type": {
      "type": "string",
      "enum": ["障害", "操作方法", "機能要望", "その他"],
      "description": "問い合わせ種別"
    },
    "severity": {
      "type": "string",
      "enum": ["低", "中", "高", "緊急"],
      "description": "問い合わせの緊急度"
    },
    "contact_email": {
      "type": ["string", "null"],
      "format": "email",
      "description": "連絡先メールアドレス。不明な場合はnull"
    },
    "summary": {
      "type": "string",
      "description": "問い合わせの要約",
      "minLength": 10,
      "maxLength": 200
    }
  },
  "required": ["product_name", "issue_type", "severity", "summary"],
  "additionalProperties": false
}
--- 問い合わせ内容 ---
「新製品XYZの購入を検討しています。機能についていくつか質問があり、操作方法を教えてほしいです。急ぎではないですが、できるだけ早く回答が欲しいです。担当者様からの連絡先は info@example.com です。」

評価シナリオと自動評価

評価シナリオ

  • 正例: 「製品PQRの利用方法について教えてください。緊急度は中。メールはuser@example.com。」
    • 期待される出力: product_name: "製品PQR", issue_type: "操作方法", severity: "中", contact_email: "user@example.com", summary: "製品PQRの利用方法について"
  • 難例: 「何も記載されていません。」
    • 期待される出力: 必須情報不足によるSchema違反(例: product_name, issue_typeが欠落)。LLMが可能な限り情報を抽出し、不足分はSchema違反とすることが望ましい。
  • コーナーケース: 「製品GHIの問い合わせ。これはバグですか?緊急度は”非常に高い”です。メールは invalid-email。」
    • 期待される出力: severityがenum違反、contact_emailがformat違反となることを期待。

自動評価の擬似コード

import json
from jsonschema import validate, ValidationError, FormatChecker

# 定義されたJSON Schema
INQUIRY_SCHEMA = {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Customer Inquiry",
  "description": "顧客からの問い合わせ情報を構造化",
  "type": "object",
  "properties": {
    "product_name": { "type": "string", "description": "問い合わせ対象の製品名", "maxLength": 50 },
    "issue_type": { "type": "string", "enum": ["障害", "操作方法", "機能要望", "その他"], "description": "問い合わせ種別" },
    "severity": { "type": "string", "enum": ["低", "中", "高", "緊急"], "description": "問い合わせの緊急度" },
    "contact_email": { "type": ["string", "null"], "format": "email", "description": "連絡先メールアドレス。不明な場合はnull" },
    "summary": { "type": "string", "description": "問い合わせの要約", "minLength": 10, "maxLength": 200 }
  },
  "required": ["product_name", "issue_type", "severity", "summary"],
  "additionalProperties": False
}

def evaluate_llm_output(llm_output_str: str, expected_data: dict) -> dict:
    score = 0
    validation_errors = []
    semantic_errors = []
    generated_json = {}

    try:
        # 1. JSONパースの試行
        generated_json = json.loads(llm_output_str)
        score += 20 # JSONとして有効

        # 2. JSON Schemaバリデーション
        validate(instance=generated_json, schema=INQUIRY_SCHEMA, format_checker=FormatChecker())
        score += 50 # Schemaに準拠

        # 3. 意味的正確性の評価 (expected_dataとの比較)
        # 各プロパティについて、期待値との一致度を評価
        correct_fields = 0
        total_fields = len(INQUIRY_SCHEMA['required']) + len([k for k in expected_data if k not in INQUIRY_SCHEMA['required']])

        for key, expected_value in expected_data.items():
            if key in generated_json:
                if generated_json[key] == expected_value:
                    correct_fields += 1
                else:
                    semantic_errors.append(f"Field '{key}' mismatch: Expected '{expected_value}', got '{generated_json[key]}'")
            else: # 期待されるフィールドが生成JSONにない場合
                semantic_errors.append(f"Expected field '{key}' is missing in generated output.")

        # 生成されたJSONに余計なプロパティがないかも確認 (additionalProperties: falseでSchemaバリデーションがカバー)
        # ここでは意味的なチェックに集中。Schemaバリデーションで大部分はカバーされる。

        if total_fields > 0:
            score += int(30 * (correct_fields / total_fields)) # 意味的正確性の点数を加算
        elif not expected_data and not generated_json: # 期待データも生成データも空の場合
            score += 30

    except json.JSONDecodeError as e:
        validation_errors.append(f"Invalid JSON format: {e}")
        score = 0 # JSON無効の場合は0点
    except ValidationError as e:
        validation_errors.append(f"JSON Schema validation error: {e.message} at path {list(e.path)}")
        score = max(score, 20) # JSONとして有効だがSchema違反の場合は最低20点

    return {
        "score": min(100, score), # スコアは最大100
        "validation_errors": validation_errors,
        "semantic_errors": semantic_errors,
        "is_valid_json": not bool(validation_errors),
        "is_schema_compliant": not any("JSON Schema validation error" in err for err in validation_errors)
    }

# 採点ルーブリック:
# - 0点: JSONとして無効
# - 20点: JSONとして有効だが、Schemaに重大な違反(例: requiredプロパティ欠落、追加プロパティ)
# - 50点: JSONとして有効かつSchemaに準拠しているが、意味的に不正確または一部欠落
# - 70点: JSONとして有効かつSchemaに準拠、意味的にも大部分が正確
# - 100点: JSONとして有効かつSchemaに完全準拠、意味的にも完全に正確

プロンプト→モデル→評価→改良のループ

graph TD
    A["プロンプト設計"] --> B{LLM};
    B --> C["出力JSON"];
    C --> D["JSON Schema バリデーション"];
    D --> E{"自動評価"};
    E -- 失敗/低スコア --> F["誤り分析"];
    F --> A;
    E -- 成功/高スコア --> G["デプロイ/完了"];

失敗モードと抑制手法

1. 失敗モード: 幻覚(Hallucination)

  • 内容: 問い合わせ内容に存在しない製品名や問い合わせ種別を生成する。
  • 抑制手法:
    • System指示: 「与えられた情報のみを抽出し、推測や創造的な内容は追加しないでください。」と明示的に指示する。
    • Few-shot例: 正しい情報の抽出と、情報がない場合の適切な null や「その他」といったfallbackの利用例を示す。
    • Chain-of-Thought: 情報抽出の根拠をステップバイステップで考えさせ、その後にJSONを生成させることで、無根拠な生成を抑制する。

2. 失敗モード: 様式崩れ(Malformed Output)

  • 内容: 無効なJSONフォーマットを生成する、またはJSON Schemaのデータ型、enum、formatなどの制約に違反する。
  • 抑制手法:
    • System指示: 「出力は厳密にJSON形式である必要があります。JSON Schemaの全ての制約を遵守してください。」を強調する。
    • Chain-of-Thought: 「最終的に生成するJSONが出力スキーマに準拠しているか確認せよ」といった自己検証ステップを指示する。
    • リトライ戦略: アプリケーション側でJSON Schemaバリデーションが失敗した場合、LLMにエラーメッセージと共に再生成を要求する。

3. 失敗モード: 脱線(Off-topic/Verbose Output)

  • 内容: JSONオブジェクトの前後に余計な説明文や挨拶、Markdown以外のテキストを付加する。
  • 抑制手法:
    • System指示: 「出力は純粋なJSONオブジェクトのみとし、他のテキストは一切含めないでください。」と厳しく指示する。
    • JSON Schema: additionalProperties: false により、スキーマに定義されていないトップレベルのプロパティを禁止する。

4. 失敗モード: 禁止事項(Prohibited Properties)

  • 内容: JSON Schemaで定義されていないプロパティをJSONに追加する。
  • 抑制手法:
    • JSON Schema: additionalProperties: false をルートオブジェクトに設定することで、スキーマに明示的に定義されていないプロパティの追加を技術的に禁止する。
    • System指示: 「JSON Schemaに定義されているプロパティのみを使用してください。追加のプロパティは許可されません。」と明示する。

まとめ

JSON Schemaを用いたLLM出力制御は、構造化されたデータの一貫性と信頼性を確保する上で極めて有効である。入出力契約の明確化、厳密なSchema定義、複数のプロンプト戦略、そして自動評価と継続的な改良サイクルを組み合わせることで、ロバストなLLMアプリケーションの構築が可能となる。失敗モードを特定し、それに対応する抑制手法を適用することが、性能向上への鍵となる。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

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