LLMのJSON出力制御と評価

Tech

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

LLMのJSON出力制御と評価

大規模言語モデル(LLM)からのJSON形式での出力制御は、API連携、データ抽出、自動化ワークフローなど、多岐にわたるアプリケーションで不可欠です。本記事では、LLMに正確で構造化されたJSONを出力させるためのプロンプト設計、評価手法、そして一般的な失敗モードとその抑制戦略について解説します。

1. ユースケース定義

LLMのJSON出力制御は、以下のようなシナリオで特に重要となります。

  • API呼び出し引数の生成: ユーザーの自然言語によるリクエストを解析し、外部APIが期待するJSON形式の引数を生成する。

  • 構造化データ抽出: 非構造化テキスト(例: 記事、レビュー)から特定のエンティティや情報を抽出し、JSONオブジェクトとして整形する。

  • レポート自動生成: 複数のデータポイントを結合し、特定のスキーマに準拠したサマリーレポートをJSON形式で出力する。

  • 設定ファイル生成: アプリケーションの構成情報をユーザーの指示に基づきJSON形式で出力する。

2. 入出力契約の定義

LLMのJSON出力において、期待される挙動と制約を明確にする「入出力契約」を事前に定義することが重要です。

フォーマット

  • 必須: 常に出力は有効なJSON形式であること。

  • スキーマ準拠: 事前に定義されたJSON Schemaに厳密に準拠すること。余分なプロパティを含めないこと。

  • データ型: 各プロパティの値は、スキーマで指定されたデータ型(文字列、数値、真偽値、配列など)と一致すること。

失敗時の挙動

  • 無効なJSON: モデルが無効なJSONを生成した場合、アプリケーション側でバリデーションエラーを発生させ、リトライ戦略を適用する。

  • スキーマ逸脱: JSONは有効だがスキーマに準拠しない場合、特定のエラーコードを返したり、ログを記録したりする。

  • 情報不足: 入力テキストから必要な情報を抽出できない場合、必須プロパティにnullまたは特定のデフォルト値を設定するか、ユーザーに情報不足を通知する特別なJSON構造を返す。

禁止事項

  • JSON以外の余計なテキスト(例: 説明文、謝辞)をJSONの前後に含めない。

  • 指定されたJSON Schemaに含まれないプロパティを勝手に追加しない。

  • 出力は常に単一のJSONオブジェクト(またはJSON配列)であること。

3. 制約付き仕様化

JSON Schemaは、期待されるJSON出力の構造、データ型、必須プロパティ、許容値などを厳密に定義するための強力なツールです。主要なLLMプラットフォームでは、Function Calling (OpenAI) [1] や Tool Calling (Google Gemini) [2] のメカニズムを通じて、このJSON Schemaを直接モデルに渡し、構造化出力を誘導できます。

例: ユーザーの問い合わせからタスク情報を抽出するJSON Schema

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "TaskInformation",
  "description": "ユーザーの問い合わせからタスクのタイトル、優先度、期限を抽出するスキーマ。",
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "description": "タスクのタイトル。50文字以内。"
    },
    "priority": {
      "type": "string",
      "description": "タスクの優先度。'高', '中', '低'のいずれか。",
      "enum": ["高", "中", "低"]
    },
    "due_date": {
      "type": "string",
      "format": "date",
      "description": "タスクの期限。YYYY-MM-DD形式。"
    },
    "assigned_to": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "タスクを割り当てるユーザー名(複数可)。"
    }
  },
  "required": ["title", "priority"]
}

4. プロンプト設計

LLMにJSON形式で出力させるためのプロンプトには、いくつかの戦略があります。

4.1. ゼロショットプロンプト

モデルに特別な例を与えず、指示のみでJSONを生成させる最もシンプルな方法です。Systemプロンプトで出力形式を厳しく指定することが重要です。

System:
あなたは与えられたテキストからタスク情報を抽出し、JSON形式で出力するアシスタントです。
JSONは以下のスキーマに厳密に従ってください。スキーマにないプロパティは追加しないでください。
```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "TaskInformation",
  "description": "ユーザーの問い合わせからタスクのタイトル、優先度、期限を抽出するスキーマ。",
  "type": "object",
  "properties": {
    "title": { "type": "string", "description": "タスクのタイトル。50文字以内。" },
    "priority": { "type": "string", "description": "タスクの優先度。'高', '中', '低'のいずれか。", "enum": ["高", "中", "低"] },
    "due_date": { "type": "string", "format": "date", "description": "タスクの期限。YYYY-MM-DD形式。" },
    "assigned_to": { "type": "array", "items": { "type": "string" }, "description": "タスクを割り当てるユーザー名(複数可)。" }
  },
  "required": ["title", "priority"]
}

User: 来週金曜までに「月次レポート作成」を完了してください。優先度は高めです。

Assistant:

{
  "title": "月次レポート作成",
  "priority": "高",
  "due_date": "2024-08-02",
  "assigned_to": []
}
### 4.2. 少数例プロンプト (Few-shot Prompting)

具体的な入出力例をいくつか示すことで、モデルが期待されるJSON構造と抽出ルールを学習しやすくなります。複雑な抽出や、曖昧な入力に対する挙動を定義するのに有効です。

```text
System:
あなたは与えられたテキストからタスク情報を抽出し、JSON形式で出力するアシスタントです。
以下の例を参考に、正確なJSONを生成してください。スキーマにないプロパティは追加しないでください。

---
Text: 「緊急!システム障害調査を本日中に。担当は田中さん。」
Output:
```json
{
  "title": "システム障害調査",
  "priority": "高",
  "due_date": "2024-07-26",
  "assigned_to": ["田中"]
}

Text: 「提案書ドラフト作成をお願いします。期限は来月末、優先度は中です。」 Output:

{
  "title": "提案書ドラフト作成",
  "priority": "中",
  "due_date": "2024-08-31",
  "assigned_to": []
}

Text: 「新規プロジェクトの企画書作成。優先度は低くていいです。」 Output:

{
  "title": "新規プロジェクト企画書作成",
  "priority": "低",
  "due_date": null,
  "assigned_to": []
}

User: 「請求書処理を進めてください。来週の火曜日までには。」

Assistant:

{
  "title": "請求書処理",
  "priority": "中",
  "due_date": "2024-07-30",
  "assigned_to": []
}
### 4.3. Chain-of-Thought制約型プロンプト

直接JSONを生成させるのではなく、まず思考プロセスを言語化させ、その後でその思考に基づいたJSONを生成させる手法です。複雑な推論や、複数のステップを要する抽出に適しています。

```text
System:
あなたは与えられたテキストからタスク情報を抽出し、JSON形式で出力するアシスタントです。
まず、タスクの各要素(タイトル、優先度、期限、担当者)を識別し、その思考プロセスをステップバイステップで記述してください。
その後、最終的な結論としてJSON形式で出力してください。

User:
「ウェブサイトのデザイン修正、急ぎでお願いします。できれば明後日までに、担当は佐藤さんと鈴木さん。」

Assistant:
思考プロセス:

1.  **タイトル**: 「ウェブサイトのデザイン修正」と識別。

2.  **優先度**: 「急ぎでお願いします」から「高」と判断。

3.  **期限**: 「明後日までに」をJST基準の2024年7月28日と判断。

4.  **担当者**: 「佐藤さんと鈴木さん」を抽出。

結論:
```json
{
  "title": "ウェブサイトのデザイン修正",
  "priority": "高",
  "due_date": "2024-07-28",
  "assigned_to": ["佐藤", "鈴木"]
}
## 5. 評価

LLMのJSON出力の品質を評価するには、複数の側面から検証が必要です。

### 5.1. 評価シナリオ

*   **正例 (Happy Path)**: 明確な指示と十分な情報を含む入力。モデルが意図通りにJSONを生成できるかを確認。

*   **難例 (Edge Cases)**:

    *   **情報不足**: 必要な情報が入力テキストにない場合、nullやデフォルト値が適切に設定されるか。

    *   **曖昧な指示**: 複数の解釈が可能な指示に対して、どの程度妥当なJSONを生成できるか。

    *   **競合する情報**: 矛盾する情報が含まれる場合、どのように解決または報告されるか。

*   **コーナーケース (Adversarial Cases)**:

    *   **特殊文字**: JSON値にカンマ、引用符、改行などの特殊文字が含まれる場合の処理。

    *   **ネストの深さ**: 複雑なJSON Schemaに対する対応。

    *   **空文字列/空配列**: 特定のプロパティが空になる場合の適切な表現。

### 5.2. 自動評価の擬似コード

LLMのJSON出力評価は、手動レビューだけでなく、自動化されたテストが不可欠です。Pythonの`json`モジュールとPydanticライブラリ \[3] を組み合わせることで、堅牢な評価システムを構築できます。

```python
import json
from datetime import date
from typing import List, Optional
from pydantic import BaseModel, ValidationError, Field

# 評価に使用するPydanticモデル (JSON Schemaから生成可能)

class TaskInformation(BaseModel):
    title: str = Field(max_length=50, description="タスクのタイトル")
    priority: str = Field(..., pattern="^(高|中|低)$", description="タスクの優先度")
    due_date: Optional2026/01/16 = Field(None, description="タスクの期限 (YYYY-MM-DD)")
    assigned_to: List[str] = Field([], description="タスクを割り当てるユーザー名")

    # カスタムバリデーション (オプション)


    # @model_validator(mode='after')


    # def check_due_date_priority_consistency(self) -> 'TaskInformation':


    #     if self.priority == '高' and self.due_date is None:


    #         raise ValueError("高優先度のタスクは期限が必要です。")


    #     return self

def evaluate_json_output(llm_output_str: str, expected_data: dict) -> dict:
    """
    LLMのJSON出力を評価する関数。

    Args:
        llm_output_str: LLMから出力されたJSON文字列。
        expected_data: 期待される正しい出力データ(意味的正確性の評価用)。

    Returns:
        評価結果を格納した辞書。
    """
    results = {
        "is_valid_json": False,
        "schema_conforms": False,
        "semantic_accuracy": False,
        "error_details": None
    }

    # 1. JSON構文の検証

    try:
        parsed_json = json.loads(llm_output_str)
        results["is_valid_json"] = True
    except json.JSONDecodeError as e:
        results["error_details"] = f"JSONDecodeError: {e}"
        return results

    # 2. スキーマ準拠の検証 (Pydanticモデルを使用)

    try:
        validated_task = TaskInformation(**parsed_json)
        results["schema_conforms"] = True
    except ValidationError as e:
        results["error_details"] = f"ValidationError: {e}"
        return results
    except Exception as e:
        results["error_details"] = f"Unexpected Schema Validation Error: {e}"
        return results

    # 3. 意味的正確性の評価


    # 実際の実装では、ここでより複雑なロジック(例: キーごとの値比較、許容範囲のチェック、NLPベースの類似度評価)が必要。


    # ここでは単純な部分一致で評価の概念を示す。

    semantic_matches = 0
    total_fields = 0
    for key, expected_value in expected_data.items():
        if key in parsed_json:
            total_fields += 1

            # Pydanticはdateオブジェクトに変換するので、比較もdateオブジェクトで行う

            actual_value = getattr(validated_task, key)
            if actual_value == expected_value:
                semantic_matches += 1

    if total_fields > 0 and semantic_matches == total_fields:
        results["semantic_accuracy"] = True
    else:
        results["error_details"] = f"Semantic mismatch. Expected: {expected_data}, Got: {parsed_json}"

    return results

# 使用例 (JST 2024年7月26日)

if __name__ == "__main__":
    test_cases = [
        {
            "name": "正例",
            "llm_output": '{"title": "月次レポート作成", "priority": "高", "due_date": "2024-08-02", "assigned_to": []}',
            "expected": {"title": "月次レポート作成", "priority": "高", "due_date": date(2024, 8, 2), "assigned_to": []}
        },
        {
            "name": "無効なJSON",
            "llm_output": '{"title": "テスト", "priority": "中",',
            "expected": {}
        },
        {
            "name": "スキーマ不適合 (priorityの値が不正)",
            "llm_output": '{"title": "緊急タスク", "priority": "最高", "due_date": "2024-07-27", "assigned_to": ["山田"]}',
            "expected": {}
        },
        {
            "name": "意味的に不正確",
            "llm_output": '{"title": "ウェブサイトのデザイン修正", "priority": "高", "due_date": "2024-07-27", "assigned_to": ["佐藤", "鈴木"]}',
            "expected": {"title": "ウェブサイトのデザイン修正", "priority": "高", "due_date": date(2024, 7, 28), "assigned_to": ["佐藤", "鈴木"]} # 期限が1日ずれている
        }
    ]

    for case in test_cases:
        print(f"\n--- 評価ケース: {case['name']} ---")
        result = evaluate_json_output(case["llm_output"], case["expected"])
        print(json.dumps(result, indent=2, ensure_ascii=False, default=str)) # dateオブジェクトをstrに変換

6. 失敗モードと抑制手法

LLMがJSON出力に失敗する主なモードと、それらを抑制するための戦略を以下に示します。

6.1. 失敗モード

  • 幻覚 (Hallucination): 入力テキストに存在しない情報をJSONプロパティの値として生成する。

  • 様式崩れ (Malformed JSON): JSONの構文規則に違反する出力を生成する(例: カンマの欠落、引用符の閉じ忘れ)。

  • スキーマ逸脱 (Schema Deviation): 有効なJSONではあるが、指定されたJSON Schemaの型、必須プロパティ、enumなどの制約を満たさない。

  • 脱線 (Off-topic Output): JSONの前後に追加の説明文や余計なテキストを生成してしまう。

  • 禁止事項違反: 指定された以外のプロパティを追加したり、本来生成すべきでない値を生成したりする。

6.2. 抑制手法

  • System指示の厳格化: Systemプロンプトで、出力形式、スキーマ準拠、余計なテキストの禁止を極めて明確かつ繰り返し指示する。

    • 例:「いかなる場合もJSON以外のテキストは出力しないでください。」「提供されたJSONスキーマに厳密に準拠し、スキーマ外のフィールドは含めないでください。
  • Few-shot例の活用: 失敗モードに対応する具体的な例(例: 「情報がない場合はnullを設定する」)を含める。

  • Function Calling/Tool Callingの利用: LLMが特定の関数呼び出しを生成するように促すことで、モデルの出力形式を強力に制約できます。これにより、JSON Schemaに準拠した引数のみを生成させることが可能になります [1, 2]。

  • 出力後の検証ステップ:

    • JSON構文検証: 生成された文字列が有効なJSONであるかjson.loads()などでチェックする。

    • スキーマバリデーション: Pydantic [3] やjsonschemaライブラリなどを用いて、定義されたJSON Schemaに準拠しているか検証する。PythonライブラリのOutlines [4] やMarvin.aiなども、Pydanticモデルとの統合を通じて、モデルの出力段階からスキーマ準拠を強制する機能を提供しています。

  • リトライ戦略:

    • バリデーションエラーが発生した場合、エラーメッセージをLLMにフィードバックし、再生成を促す。

    • 複数回のリトライ後も失敗する場合は、デフォルト値の適用、人間による介入、またはエラー通知に切り替える。

  • モデル選択と微調整 (Fine-tuning): 構造化出力に特化したモデルや、特定のスキーマに合わせて微調整されたモデルは、より高い精度でJSONを生成できます。

7. 改良と再評価

評価結果で特定された失敗モードや低いスコアは、プロンプトやシステム全体の改良点を示します。

  1. プロンプトの修正: 曖昧な指示の明確化、制約の強化、Few-shot例の追加・修正など。

  2. JSON Schemaの調整: スキーマ自体が複雑すぎる、あるいは不十分な場合に、よりシンプルにするか、必要な制約を追加する。

  3. モデルの再選択: より高性能なモデルや、構造化出力に強いモデルへの切り替えを検討する。

  4. ファインチューニング: 大量の高品質な入出力ペアがある場合、特定のタスクに特化してモデルをファインチューニングすることで、JSON出力の精度を大幅に向上させることが可能。

  5. ツールの導入: OutlinesやGuidanceのような出力制御ライブラリを導入し、正規表現やCFGベースの文法制約を適用する。

改良後は、同じ評価シナリオと自動評価スクリプトを用いて、必ず再評価を実施し、改善効果を定量的に測定します。このループを繰り返すことで、JSON出力の信頼性と堅牢性を高めます。

8. まとめ

LLMからのJSON出力の制御と評価は、AIアプリケーションの信頼性と実用性を高める上で極めて重要です。明確な入出力契約、厳密なJSON Schemaによる制約、そしてゼロショット、少数例、Chain-of-Thoughtなどのプロンプト設計を組み合わせることで、精度の高いJSON生成が期待できます。さらに、Pydanticなどを用いた自動評価と、失敗モードに対する具体的な抑制手法を適用し、継続的な改良サイクルを回すことが、堅牢なシステム構築の鍵となります。2024年7月26日現在、Function Callingや専用ライブラリの進化により、この分野は日々進化しています。

graph TD
    A["要件定義"] --> |JSONスキーマ化| B{"プロンプト設計"};
    B --> |プロンプト提示| C["LLMによるJSON生成"];
    C --> |JSON出力| D["出力検証・評価"];
    D -- 成功の場合 --> E["成果物利用"];
    D -- 失敗の場合 --> F["誤り分析"];
    F --> |改善案| G["プロンプト改良"];
    G --> |再設計| B;
ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

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