LLMの構造化出力とJSON Schemaを用いた堅牢なプロンプト設計

Tech

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

LLMの構造化出力とJSON Schemaを用いた堅牢なプロンプト設計

近年、大規模言語モデル(LLM)は多様なタスクで高い性能を発揮していますが、その出力はしばしば自由形式のテキストであり、ダウンストリームのシステムで利用するには構造化された形式への変換が必要です。この課題を解決し、LLMの出力を信頼性の高い形で利用するために、JSON Schemaを用いた構造化出力の設計と評価が重要となります。

1. はじめに:LLMにおける構造化出力の重要性とJSON Schema

LLMの出力を他のシステム(データベース、API、アプリケーションロジックなど)と連携させる際、曖昧な自由形式テキストでは処理の自動化や正確な解釈が困難です。構造化出力、特にJSON形式での出力は、このような連携を効率的かつ堅牢にします。 主要なLLMプロバイダーも構造化出力機能の強化を進めており、例えばOpenAIは2023年6月13日にFunction Calling機能を導入し、LLMがJSON Schemaに準拠した引数を持つ関数を呼び出すための構造化出力を生成可能にしました[1]。Google AIも2024年2月8日のGemini 1.5 Pro発表で、Function Callingおよび構造化出力の改善を強調しています[2]。また、Anthropicは2024年6月20日にClaude 3.5 Sonnetを発表し、ツール利用と構造化出力の能力が大幅に向上したと報告しています[3]。

JSON Schemaは、JSONデータの構造と制約を記述するための標準化された仕様であり[4]、LLMの出力フォーマットを厳密に定義し、期待されるデータ型、必須フィールド、値の範囲などを指定することで、モデルの出力品質を保証する強力なツールとなります。

2. ユースケース定義:顧客レビュー分析における感情とエンティティ抽出

、「顧客レビューテキストから、その感情、主要キーワード、重要度、および簡潔な要約を抽出する」というユースケースを想定します。これにより、マーケティング部門は製品改善や顧客対応の優先順位付けに役立てることができます。

3. 入出力契約(制約付き仕様化)

LLMとのやり取りにおいて、期待される入出力の形式、失敗時の挙動、禁止事項を明確に定義することが、堅牢なシステム構築の第一歩です。

3.1. 出力フォーマット(JSON Schema)

LLMは以下のJSON Schemaに厳密に準拠したJSONオブジェクトを出力するものとします。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "CustomerReviewAnalysis",
  "description": "顧客レビューから感情、キーワード、重要度を抽出するためのスキーマ",
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": ["positive", "negative", "neutral"],
      "description": "レビューの全体的な感情"
    },
    "keywords": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "レビューから抽出された主要キーワードのリスト",
      "minItems": 1,
      "maxItems": 5
    },
    "importance": {
      "type": "string",
      "enum": ["high", "medium", "low"],
      "description": "レビューの重要度(例: 緊急性、影響度)"
    },
    "summary": {
      "type": "string",
      "description": "レビューの簡潔な要約",
      "maxLength": 100
    }
  },
  "required": ["sentiment", "keywords", "importance", "summary"]
}

3.2. 失敗時の挙動

  1. 無効なJSON: LLMが出力したテキストがJSONとしてパースできない場合、システムはパースエラーを報告し、当該出力を破棄します。

  2. スキーマ不適合: JSONとして有効でも、上記のJSON Schemaに準拠しない(例: sentiment"positive", "negative", "neutral"以外の値、keywordsが6個以上など)場合、システムはバリデーションエラーを報告し、当該出力を破棄します。

  3. 幻覚(Hallucination): スキーマに準拠していても、レビュー内容と矛盾する感情や存在しないキーワードを生成した場合、これを幻覚として識別し、評価時に減点または破棄の対象とします。

  4. 脱線(Off-topic): 指示されたJSONオブジェクト以外に、余計な説明文や雑談を付加した場合、システムはこれを脱線とみなし、JSONパース前に除去を試みるか、エラーとして報告します。

3.3. 禁止事項

  1. 個人特定情報: レビュー内に含まれる個人を特定できる情報(氏名、住所、電話番号、メールアドレスなど)は、いかなる形式であっても抽出・出力してはなりません。

  2. 不適切な内容: ヘイトスピーチ、差別的表現、暴力的な内容など、公共の秩序や良俗に反する情報は、その抽出や要約を避ける必要があります。

4. プロンプト設計

LLMの性能を最大限に引き出し、構造化出力を安定させるためには、プロンプトの設計が極めて重要です。ここでは、3種類のプロンプト案を提示します。共通のシステム指示として、モデルがレビュー分析の専門家として振る舞い、JSON Schemaに厳密に従うことを明示します。

共通システム指示

You are an expert sentiment analysis AI. Your task is to extract sentiment, keywords, importance, and a summary from customer reviews, strictly adhering to the provided JSON Schema. Do not include any other text or explanation outside of the JSON.

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

最も基本的な形式で、モデルにタスクとJSON Schemaのみを与えます。

Your task is to analyze the following customer review and output a JSON object strictly conforming to the provided JSON Schema.
Do not include any other text, explanations, or formatting besides the JSON.

JSON Schema:
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "CustomerReviewAnalysis",
  "description": "顧客レビューから感情、キーワード、重要度を抽出するためのスキーマ",
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": ["positive", "negative", "neutral"],
      "description": "レビューの全体的な感情"
    },
    "keywords": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "レビューから抽出された主要キーワードのリスト",
      "minItems": 1,
      "maxItems": 5
    },
    "importance": {
      "type": "string",
      "enum": ["high", "medium", "low"],
      "description": "レビューの重要度(例: 緊急性、影響度)"
    },
    "summary": {
      "type": "string",
      "description": "レビューの簡潔な要約",
      "maxLength": 100
    }
  },
  "required": ["sentiment", "keywords", "importance", "summary"]
}

Customer Review:
この製品のバッテリー寿命は本当にひどいです。すぐに切れるので、外出先で使うには不便すぎます。改善を強く求めます。

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

ゼロショットに加えて、いくつかの入出力例を提供することで、モデルの理解と出力精度を向上させます。

Your task is to analyze the following customer review and output a JSON object strictly conforming to the provided JSON Schema.
Do not include any other text, explanations, or formatting besides the JSON.

JSON Schema:
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "CustomerReviewAnalysis",
  "description": "顧客レビューから感情、キーワード、重要度を抽出するためのスキーマ",
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": ["positive", "negative", "neutral"],
      "description": "レビューの全体的な感情"
    },
    "keywords": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "レビューから抽出された主要キーワードのリスト",
      "minItems": 1,
      "maxItems": 5
    },
    "importance": {
      "type": "string",
      "enum": ["high", "medium", "low"],
      "description": "レビューの重要度(例: 緊急性、影響度)"
    },
    "summary": {
      "type": "string",
      "description": "レビューの簡潔な要約",
      "maxLength": 100
    }
  },
  "required": ["sentiment", "keywords", "importance", "summary"]
}

Example 1 (Review):
このスマホのカメラは素晴らしい!特に夜景モードが最高で、写真がとても綺麗に撮れます。バッテリーも一日中持ちますし、非常に満足しています。
Example 1 (JSON Output):
{
  "sentiment": "positive",
  "keywords": ["カメラ", "夜景モード", "バッテリー"],
  "importance": "medium",
  "summary": "カメラ機能とバッテリー寿命に非常に満足している。"
}

Example 2 (Review):
カスタマーサポートの対応が悪すぎます。問い合わせても返事が遅く、問題が全く解決しませんでした。二度と利用しません。
Example 2 (JSON Output):
{
  "sentiment": "negative",
  "keywords": ["カスタマーサポート", "返事", "問題解決"],
  "importance": "high",
  "summary": "カスタマーサポートの対応が遅く、問題が解決しなかったことに不満。"
}

Customer Review:
この製品のバッテリー寿命は本当にひどいです。すぐに切れるので、外出先で使うには不便すぎます。改善を強く求めます。

4.3. Chain-of-Thought制約型プロンプト

モデルに思考プロセスを段階的に指示することで、より論理的かつ正確な出力を促します。最終的な出力はJSONのみに限定します。

Your task is to analyze the following customer review and output a JSON object strictly conforming to the provided JSON Schema.
First, analyze the overall sentiment (positive, negative, or neutral). Second, identify 1 to 5 key entities and phrases from the review as keywords. Third, determine the importance (high, medium, or low) based on the review's urgency or impact. Fourth, summarize the review concisely within 100 characters. Finally, output the results as a JSON object.
Do not include any other text, explanations, or formatting besides the JSON.

JSON Schema:
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "CustomerReviewAnalysis",
  "description": "顧客レビューから感情、キーワード、重要度を抽出するためのスキーマ",
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": ["positive", "negative", "neutral"],
      "description": "レビューの全体的な感情"
    },
    "keywords": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "レビューから抽出された主要キーワードのリスト",
      "minItems": 1,
      "maxItems": 5
    },
    "importance": {
      "type": "string",
      "enum": ["high", "medium", "low"],
      "description": "レビューの重要度(例: 緊急性、影響度)"
    },
    "summary": {
      "type": "string",
      "description": "レビューの簡潔な要約",
      "maxLength": 100
    }
  },
  "required": ["sentiment", "keywords", "importance", "summary"]
}

Customer Review:
この製品のバッテリー寿命は本当にひどいです。すぐに切れるので、外出先で使うには不便すぎます。改善を強く求めます。

5. 評価戦略

LLMの構造化出力の品質を評価するには、機械的なバリデーションと内容の正確性の両面からアプローチします。

5.1. 評価シナリオ

複数の種類のレビューを用いて、LLMの堅牢性を検証します。

  • 正例: 感情が明確で、キーワードが特定しやすい標準的なレビュー。

    • 例: 「このイヤホンの音質は素晴らしいです。バッテリーも長持ちします。満足!」
  • 難例:

    • 複合感情: 「機能は良いが、デザインが気に入らない」のように、複数の感情が混在するレビュー。

    • 皮肉: 「最高の顧客対応ですね(棒読み)」のような皮肉を含むレビュー。

    • 曖昧な表現: 「ちょっと微妙だった」といった抽象的な表現のレビュー。

    • キーワード不足: 具体的な製品や機能の言及が少ないレビュー。

  • コーナーケース:

    • 極端に短いレビュー: 「最悪。」

    • 極端に長いレビュー: 複雑な問題解決の経緯が詳細に書かれたレビュー。

    • 絵文字のみのレビュー: 「😡😡😡」

    • 多言語混在: 「This is coolだけど、値段が高い。」

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

LLMの出力は、JSONパース、JSON Schemaバリデーション、そして内容の正確性という3つのステップで評価します。

# Python pseudo-code for automated evaluation

import json
from jsonschema import validate, ValidationError

# 定義済みのJSON Schema (上記3.1のものを想定)

JSON_SCHEMA = {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "CustomerReviewAnalysis",
  "description": "顧客レビューから感情、キーワード、重要度を抽出するためのスキーマ",
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": ["positive", "negative", "neutral"],
      "description": "レビューの全体的な感情"
    },
    "keywords": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "レビューから抽出された主要キーワードのリスト",
      "minItems": 1,
      "maxItems": 5
    },
    "importance": {
      "type": "string",
      "enum": ["high", "medium", "low"],
      "description": "レビューの重要度(例: 緊急性、影響度)"
    },
    "summary": {
      "type": "string",
      "description": "レビューの簡潔な要約",
      "maxLength": 100
    }
  },
  "required": ["sentiment", "keywords", "importance", "summary"]
}

def evaluate_llm_output(llm_output_text: str, expected_json_content: dict) -> dict:
    """
    LLMの出力テキストを評価する。

    Args:
        llm_output_text (str): LLMから得られた生テキスト出力。
        expected_json_content (dict): 人間が期待する正しいJSON内容(スキーマ準拠済み)。

    Returns:
        dict: 評価スコアと検出されたエラーのリスト。
              スコアは0.0 (最低) から 1.0 (最高) の範囲。
    """
    score = 1.0 # 初期スコア
    errors = []

    # 1. JSONパースの検証

    try:
        parsed_output = json.loads(llm_output_text)
    except json.JSONDecodeError as e:
        errors.append(f"Invalid JSON format: {e}")
        return {"score": 0.0, "errors": errors} # JSONパース失敗は致命的

    # 2. JSON Schemaバリデーション

    try:
        validate(instance=parsed_output, schema=JSON_SCHEMA)
    except ValidationError as e:
        errors.append(f"Schema validation failed: {e.message}")
        score -= 0.5 # スキーマ不適合は大きな減点

        # スキーマ不適合の場合、内容評価に進むのは困難なためここで終了することも検討


        # 本例では部分点を与えるため続行

    except Exception as e:
        errors.append(f"Unexpected validation error: {e}")
        score -= 0.5

    # 3. 内容の正確性評価(スキーマ準拠を前提としたルーブリック)


    # 感情の正確性

    if parsed_output.get("sentiment") != expected_json_content.get("sentiment"):
        score -= 0.3
        errors.append(f"Sentiment mismatch: Expected '{expected_json_content.get('sentiment')}', Got '{parsed_output.get('sentiment')}'")

    # キーワードの網羅性と正確性(順序不問、部分一致考慮は別途実装)

    expected_keywords = set(expected_json_content.get("keywords", []))
    actual_keywords = set(parsed_output.get("keywords", []))

    missing_keywords = expected_keywords - actual_keywords
    if missing_keywords:
        score -= 0.15 * len(missing_keywords)
        errors.append(f"Missing expected keywords: {list(missing_keywords)}")

    extra_keywords = actual_keywords - expected_keywords
    if extra_keywords: # 幻覚の一種として減点
        score -= 0.1 * len(extra_keywords)
        errors.append(f"Extra keywords (potential hallucination): {list(extra_keywords)}")

    # 要約の簡潔性 (maxLengthはJSON Schemaでカバーされるが、念のため)

    actual_summary = parsed_output.get("summary", "")
    if len(actual_summary) > JSON_SCHEMA["properties"]["summary"]["maxLength"]:
        score -= 0.05
        errors.append(f"Summary exceeds max length ({JSON_SCHEMA['properties']['summary']['maxLength']} chars).")

    # 要約の内容の正確性は、Embedding類似度やNLIモデルを用いるとより高度な評価が可能

    # 最終スコアは0.0から1.0の範囲に調整

    return {"score": max(0.0, score), "errors": errors}

# --- 使用例 ---


# 2024年7月28日時点のデータとして記述

llm_response_1 = '{"sentiment": "negative", "keywords": ["バッテリー寿命", "不便", "外出先"], "importance": "high", "summary": "バッテリー寿命が短く、外出先での使用に不便を感じている。"}'
expected_1 = {"sentiment": "negative", "keywords": ["バッテリー寿命", "不便", "外出先"], "importance": "high", "summary": "バッテリー寿命が短く、外出先での使用に不便を感じている。"}
print(f"評価結果1: {evaluate_llm_output(llm_response_1, expected_1)}")

# 出力例: 評価結果1: {'score': 1.0, 'errors': []}

llm_response_2 = '{"sentiment": "negative", "keywords": ["バッテリー", "充電", "持たない"], "importance": "high", "summary": "バッテリーがすぐに切れてしまうので改善希望。"}'
expected_2 = {"sentiment": "negative", "keywords": ["バッテリー寿命", "不便", "外出先"], "importance": "high", "summary": "バッテリー寿命が短く、外出先での使用に不便を感じている。"}
print(f"評価結果2: {evaluate_llm_output(llm_response_2, expected_2)}")

# 出力例: 評価結果2: {'score': 0.65, 'errors': ["Missing expected keywords: ['外出先', '不便', 'バッテリー寿命']", "Extra keywords (potential hallucination): ['充電', '持たない']"]}

llm_response_3 = '{"sentiment": "super_bad", "keywords": ["battery"], "importance": "critical", "summary": "Poor battery life."}' # スキーマ違反
expected_3 = {"sentiment": "negative", "keywords": ["バッテリー寿命"], "importance": "high", "summary": "バッテリー寿命が短い。"}
print(f"評価結果3: {evaluate_llm_output(llm_response_3, expected_3)}")

# 出力例: 評価結果3: {'score': 0.0, 'errors': ["Schema validation failed: 'super_bad' is not one of ['positive', 'negative', 'neutral']", "Sentiment mismatch: Expected 'negative', Got 'super_bad'", "Missing expected keywords: ['バッテリー寿命']", "Extra keywords (potential hallucination): ['battery']"]}

この擬似コードは、JSONパースの成功、JSON Schemaの準拠、および主要な内容要素(感情、キーワード)の正確性に基づいてスコアを算出します。キーワードの評価において、instructorのようなライブラリ[5]はPydanticモデルを通じてより洗練されたスキーマ駆動のバリデーションとエラーハンドリングを提供します。

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

LLMの構造化出力における主な失敗モードとその抑制手法を以下に示します。

失敗モード 説明 抑制手法
幻覚 (Hallucination) 事実に反する情報、または入力テキストに存在しないキーワードや感情を生成する。 Few-shot学習: 正確な出力例を提示し、モデルの挙動を誘導。
Chain-of-Thought: 思考プロセスを明示させ、論理的な一貫性を促す。
System指示: 「入力テキストからのみ情報を抽出せよ」と明示する。
検証ステップ: 出力後、キーワードが入力テキストに実際に含まれているか、別のLLMや検索でファクトチェックする。
様式崩れ (Malformation) 無効なJSON形式、またはJSON Schemaに準拠しない出力(型エラー、enum外の値など)。 JSON Schema: 出力形式の厳密な定義。
System指示: 「JSONのみを出力し、スキーマに厳密に従え」と強く指示。
プロンプト内の例: 正しい形式のJSON出力例を示す。
検証ステップ: 出力後にJSONパースとJSON Schemaバリデーションを必ず実行。
リトライ戦略: バリデーション失敗時、モデルにエラーメッセージとともに再生成を指示する(例:「JSON Schemaに準拠していません。エラー: … 再度JSONのみを出力してください」)。
脱線 (Off-topic) 指示されたJSON以外の余計なテキスト(説明、コメント、雑談など)を付加する。 System指示: 「JSON以外のテキストは一切含めるな」と厳しく指示する。
プロンプトのフォーマット: 期待する出力の開始と終了を明確にする(例: \“json““)。
前処理/後処理: 出力されたテキストからJSON部分のみを抽出する正規表現やパーサーを実装する。
禁止事項違反 個人情報や不適切な内容を抽出、または生成してしまう。 System指示: 「個人情報を抽出するな」「不適切な内容を生成するな」と明確に指示。
データフィルター: 入力テキストから個人情報や不適切表現を事前にマスク・除去する。
ガードレール: LLMの出力結果を外部のモデレーションAPIやルールベースシステムでチェックし、禁止事項に抵触する場合はブロックまたは修正する。

7. 改良と再評価のループ

堅牢な構造化出力を実現するには、一度のプロンプト設計で終わらせず、継続的な評価と改良のループを回すことが不可欠です。このプロセスをMermaidのフローチャートで可視化します。

graph TD
    A["レビュー入力"] --> B("プロンプト適用");
    B --> C{LLM};
    C --> D("LLM出力 - JSON");
    D --> E["JSONパース & スキーマバリデーション"];
    E -- バリデーション成功 --> F["内容評価"];
    E -- バリデーション失敗 --> G{"エラー分析 & プロンプト改良"};
    F --> H{"評価スコア"};
    H -- 低スコア/不合格 --> G;
    H -- 高スコア/合格 --> I["システム統合"];
    G --> B;
    I --> J["運用監視"];
  • A[レビュー入力]: 未処理の顧客レビューがシステムに入力されます。

  • B(プロンプト適用): LLMに与えるプロンプトがレビューに適用されます。

  • C{LLM}: 大規模言語モデルがプロンプトとレビューを処理します。

  • D(LLM出力 - JSON): LLMはJSON形式の出力テキストを生成します。

  • E[JSONパース & スキーマバリデーション]: 出力が有効なJSONであり、定義されたJSON Schemaに準拠しているか検証します。

  • E -- バリデーション成功 --> F[内容評価]: スキーマに準拠している場合、出力された内容の正確性(感情、キーワード、要約など)を評価します。

  • E -- バリデーション失敗 --> G{エラー分析 & プロンプト改良}: スキーマに準拠しない場合、エラーの原因を分析し、プロンプトの改良を検討します。

  • F --> H{評価スコア}: 内容評価の結果に基づいて、モデルの出力品質スコアを算出します。

  • H -- 低スコア/不合格 --> G: 評価スコアが低い場合や要件を満たさない場合、エラー分析とプロンプト改良のステップに戻ります。

  • H -- 高スコア/合格 --> I[システム統合]: 評価スコアが高い場合や要件を満たした場合、LLMの出力を実際のシステムに統合します。

  • G --> B: プロンプトが改良されたら、再度LLMに適用し、評価プロセスを繰り返します。

  • I --> J[運用監視]: システム統合後も、実際の運用データに基づいて継続的に監視・評価を行います。

8. まとめ

LLMの構造化出力は、AIシステムを現実世界のアプリケーションに統合するための鍵となります。JSON Schemaを用いることで、LLMの出力を厳密に定義し、予測可能で信頼性の高いものにすることができます。本記事で示したように、入出力契約の明確化、多様なプロンプト設計、堅牢な評価戦略、そして継続的な改良ループを組み合わせることで、高品質な構造化出力を実現し、様々なビジネス要件に対応するAIアプリケーションを構築することが可能になります。


参照: [1] OpenAI. “GPT-3.5 Turbo and GPT-4 updates”. OpenAI Blog. 2023年6月13日公開. https://openai.com/blog/function-calling-and-other-api-updates [2] Google AI. “Gemini 1.5 Pro: A powerful, multimodal long-context model”. Google AI Blog. 2024年2月8日公開. https://blog.google/technology/ai/gemini-next-generation-model-google-ai/ [3] Anthropic. “Announcing Claude 3.5 Sonnet”. Anthropic Blog. 2024年6月20日公開. https://www.anthropic.com/news/claude-3-5-sonnet [4] JSON Schema Project. “What is JSON Schema?”. 継続更新. https://json-schema.org/ [5] JXNL. “Instructor GitHub Repository”. 継続更新. https://github.com/jxnl/instructor

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

コメント

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