LLMプロンプトの評価指標と改善ループ

Tech

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

LLMプロンプトの評価指標と改善ループ

大規模言語モデル(LLM)の性能を最大限に引き出すためには、単に適切なプロンプトを作成するだけでなく、その効果を体系的に評価し、継続的に改善するループを確立することが不可欠です。本記事では、LLMプロンプトの設計、評価指標、失敗モード分析、そして改善ループの具体的な手法について解説します。

1. ユースケース定義

本稿では、以下のユースケースを想定して議論を進めます。

ユースケース: オンライン製品レビューからのキーポイント抽出と感情分類 目的: 大量の製品レビューテキストから、製品名、レビューの感情(ポジティブ、ネガティブ、ニュートラル)、主要な特徴、および要約をJSON形式で構造化して抽出する。 期待される価値: 製品開発チームやマーケティングチームが顧客のフィードバックを迅速に把握し、製品改善やマーケティング戦略に役立てる。

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

LLMとの対話における信頼性と一貫性を確保するため、プロンプトの入出力契約を明確に定義します。

2.1. 入力契約

  • フォーマット: 製品レビューテキスト(Markdown形式またはプレーンテキスト)。

  • 最大長: 2000トークン(約8000文字)まで。

  • 前提条件: 日本語レビューテキストであること。

  • 禁止事項: 複数の製品についてのレビューを一度に処理する指示、個人情報(氏名、メールアドレスなど)を含むレビューを直接入力すること(システム側で事前に匿名化を行う)。

2.2. 出力契約

  • フォーマット: 以下のJSONスキーマに厳密に準拠したJSON文字列。

    {
      "product_name": "string",
      "sentiment": "string (positive|negative|neutral)",
      "key_features": ["string", ...],
      "summary": "string"
    }
    
    • product_name: レビューで言及されている製品名。特定できない場合は null

    • sentiment: レビュー全体の感情。「positive」「negative」「neutral」のいずれか。

    • key_features: レビューから抽出された主要な特徴(機能、デザイン、価格など)のリスト。各要素は15文字以内。

    • summary: レビュー全体の簡潔な要約(50文字から100文字)。

  • 失敗時の挙動:

    • JSONパースエラーが発生した場合、またはスキーマに準拠しない出力の場合: モデルは再度JSONフォーマットでの出力を試行する。複数回失敗する場合は {"error": "Failed to generate valid JSON output."} を返す。

    • レビュー内容から情報が抽出できない場合: product_namenullsentimentneutralkey_features は空リスト []summary は「情報が不足しています。」とする。

  • 禁止事項:

    • 入力レビュー内容を逸脱した情報の追加(ハルシネーション)。

    • 個人情報(氏名、メールアドレスなど)の抽出および出力。

    • 出力結果にJSON以外のテキストを含めない。

3. プロンプト設計

定義されたユースケースと入出力契約に基づき、異なる戦略のプロンプトを設計します。

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

基本的な指示のみで、推論例を示さないプロンプト。

あなたは製品レビュー分析の専門家です。
以下の製品レビューを分析し、製品名、感情、主要な特徴、要約をJSON形式で出力してください。
感情は「positive」「negative」「neutral」のいずれかです。
主要な特徴はリスト形式で、要約は50文字から100文字でお願いします。
製品名が特定できない場合はnull、特徴が見つからない場合は空のリスト、要約が困難な場合は「情報が不足しています。」と記載してください。
JSON形式以外の出力は一切禁止します。

レビュー:
「このスマートフォンのカメラは本当に素晴らしい!夜景も綺麗に撮れるし、バッテリーも一日中持つから外出先でも安心。ただ、少し重いのが気になる点かな。」

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

いくつかの入出力ペアを示すことで、モデルにタスクのパターンを学習させるプロンプト。

あなたは製品レビュー分析の専門家です。
以下の製品レビューを分析し、製品名、感情、主要な特徴、要約をJSON形式で出力してください。
感情は「positive」「negative」「neutral」のいずれかです。
主要な特徴はリスト形式で、要約は50文字から100文字でお願いします。
製品名が特定できない場合はnull、特徴が見つからない場合は空のリスト、要約が困難な場合は「情報が不足しています。」と記載してください。
JSON形式以外の出力は一切禁止します。

---
レビュー:
「このワイヤレスイヤホン、音質は最高だけど、すぐに耳から落ちるのが難点だね。」
出力:
```json
{
  "product_name": "ワイヤレスイヤホン",
  "sentiment": "negative",
  "key_features": ["音質", "装着感"],
  "summary": "音質は優れているが、装着感に問題があり、耳から落ちやすいというレビューです。"
}

レビュー: 「新しいコーヒーメーカーを買いました。デザインがとてもおしゃれで、味も申し分ないです。ただ、洗浄が少し面倒だと感じました。」 出力:

{
  "product_name": "コーヒーメーカー",
  "sentiment": "positive",
  "key_features": ["デザイン", "味", "手入れ"],
  "summary": "新しいコーヒーメーカーのデザインと味は高評価ですが、手入れのしにくさが指摘されています。"
}

レビュー: 「{{ユーザー入力レビュー}}」 出力:

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

モデルに思考プロセスを明示的に指示し、ステップバイステップで推論させるプロンプト。最終的な出力は契約通りのJSONに限定します。

```text
あなたは製品レビュー分析の専門家です。
以下の製品レビューを分析し、思考プロセスを経て、最終的に製品名、感情、主要な特徴、要約をJSON形式で出力してください。
思考プロセスは `Thought:` で始め、最終出力のJSONには含めないでください。

思考プロセス:

1. レビューから製品を特定します。

2. レビュー全体の感情(positive, negative, neutral)を判断します。

3. レビューから具体的な特徴(機能、デザイン、価格、使いやすさなど)を抽出し、簡潔なリストを作成します。

4. レビュー全体を50文字から100文字で要約します。

5. 上記の情報を元に、指定されたJSONスキーマに従ってJSONを生成します。

感情は「positive」「negative」「neutral」のいずれかです。
主要な特徴はリスト形式で、各要素は15文字以内、要約は50文字から100文字でお願いします。
製品名が特定できない場合はnull、特徴が見つからない場合は空のリスト、要約が困難な場合は「情報が不足しています。」と記載してください。
JSON形式以外の出力は一切禁止します。

レビュー:
「{{ユーザー入力レビュー}}」

4. 評価

プロンプトの性能を客観的に測定するための評価指標とシナリオ、および自動評価の擬似コードを定義します。

4.1. 評価指標

  • 正確性 (Accuracy):

    • sentiment フィールドが参照データと一致するか。

    • product_name が正確に抽出されているか。

  • 関連性 (Relevance):

    • key_features がレビュー内容と強く関連しているか。

    • summary がレビューの主要なポイントを捉えているか。

  • 一貫性 (Consistency):

    • sentimentkey_features が相互に矛盾していないか。
  • 網羅性 (Comprehensiveness):

    • key_features がレビューの重要な特徴を十分にカバーしているか。
  • 様式遵守 (Format Adherence):

    • 出力がJSONスキーマに厳密に準拠しているか。

    • key_features の各要素が15文字以内、summary が50-100文字の制約を満たしているか。

  • 安全性 (Safety):

    • 個人情報の混入や不適切な内容の生成がないか。

4.2. 評価シナリオ

複数のプロンプト案を比較評価するため、以下のシナリオを持つデータセットを用意します。

  • 正例 (Happy Path):

    • 明確なポジティブまたはネガティブな感情表現。

    • 製品名と特徴が容易に特定できる。

    • 例: 「このスマホはバッテリー持ちが良く、カメラも最高!文句なしの星5つです。」

  • 難例 (Challenging Cases):

    • 複合感情: 「画質は良いが、バッテリーの減りが早い」のように、ポジティブとネガティブな要素が混在。

    • 皮肉/比喩: 文字通りの意味と異なる表現。

    • 製品名の曖昧さ: 製品名が明確に書かれていない、または一般的な名詞で表現されている。

    • 短いレビュー: 情報量が少なく、推論が難しい。

    • 例: 「別に悪くはないけど、期待しすぎたかな。」

  • コーナーケース (Edge Cases):

    • 無関係なテキスト: 製品レビューではない文章。

    • 多言語混在: 英語と日本語が混じっている。

    • 非常に長いレビュー: トークン制限に近づくような長文。

    • 例: 「昨日の晩ご飯はカレーだった。とても美味しかったよ。ところで、このレビューって何の製品のこと?」

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

評価指標とシナリオに基づき、自動評価を行う擬似コードをPythonで示します。

import json
import re
from typing import Dict, List, Any

# 外部ライブラリのモック (実際にはjsonschema, rouge, evaluateなどのライブラリを使用)

class JsonSchemaValidator:
    def validate(self, instance: Dict, schema: Dict) -> List[str]:

        # 実際にはjsonschemaライブラリを使用し、バリデーションエラーのリストを返す

        errors = []
        if not isinstance(instance, dict):
            errors.append("Output is not a dictionary.")
            return errors

        # 基本的なスキーマチェック

        if "product_name" not in instance: errors.append("Missing 'product_name'")
        if "sentiment" not in instance: errors.append("Missing 'sentiment'")
        elif instance["sentiment"] not in ["positive", "negative", "neutral"]:
            errors.append(f"Invalid sentiment: {instance['sentiment']}")

        if "key_features" not in instance: errors.append("Missing 'key_features'")
        elif not isinstance(instance["key_features"], list):
            errors.append("'key_features' is not a list.")
        else:
            for feature in instance["key_features"]:
                if not isinstance(feature, str) or len(feature) > 15:
                    errors.append(f"Invalid feature '{feature}': not string or too long.")

        if "summary" not in instance: errors.append("Missing 'summary'")
        elif not (50 <= len(instance["summary"]) <= 100):
            errors.append(f"Summary length ({len(instance['summary'])}) out of range.")

        return errors

class TextSimilarityEvaluator:
    def rouge_l(self, reference: str, candidate: str) -> float:

        # 実際にはROUGEライブラリ (e.g., rouge_score) を使用


        # 簡単な例として、共通単語数に基づく類似度

        ref_words = set(reference.split())
        cand_words = set(candidate.split())
        if not ref_words: return 0.0
        return len(ref_words.intersection(cand_words)) / len(ref_words)

    def bert_score(self, reference: str, candidate: str) -> float:

        # 実際にはBERTScoreライブラリ (e.g., evaluate) を使用


        # モデルベースのセマンティック類似度を返す

        return 0.85 # 仮の値

def evaluate_prompt_output(
    expected_data: Dict[str, Any],
    actual_output_str: str,
    original_review: str
) -> Dict[str, Any]:
    """
    LLMからの出力を評価する擬似コード。

    Args:
        expected_data (Dict[str, Any]): 参照となる期待される出力データ。
        actual_output_str (str): LLMから生成された生の出力文字列。
        original_review (str): 元のレビューテキスト。

    Returns:
        Dict[str, Any]: 評価結果(スコア、エラーなど)。
    """
    results = {
        "format_adherence_score": 0.0,
        "sentiment_accuracy": 0.0,
        "product_name_accuracy": 0.0,
        "key_features_f1": 0.0, # F1スコアで評価
        "summary_rouge_l": 0.0,
        "safety_violations": [],
        "errors": []
    }

    schema = { # 簡略化されたスキーマ定義
      "type": "object",
      "properties": {
        "product_name": {"type": ["string", "null"]},
        "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
        "key_features": {"type": "array", "items": {"type": "string", "maxLength": 15}},
        "summary": {"type": "string", "minLength": 50, "maxLength": 100}
      },
      "required": ["product_name", "sentiment", "key_features", "summary"]
    }

    validator = JsonSchemaValidator()
    text_evaluator = TextSimilarityEvaluator()

    actual_data = None
    try:
        actual_data = json.loads(actual_output_str)
    except json.JSONDecodeError:
        results["errors"].append("JSON decode error.")
        return results

    # 1. 様式遵守 (Format Adherence)

    schema_errors = validator.validate(actual_data, schema)
    if not schema_errors:
        results["format_adherence_score"] = 1.0
    else:
        results["errors"].extend(schema_errors)

    # JSONが有効な場合のみ、他の評価を行う

    if results["format_adherence_score"] == 1.0:

        # 2. 感情の正確性 (Sentiment Accuracy)

        if actual_data.get("sentiment") == expected_data.get("sentiment"):
            results["sentiment_accuracy"] = 1.0

        # 3. 製品名の正確性 (Product Name Accuracy)

        if actual_data.get("product_name") == expected_data.get("product_name"):
            results["product_name_accuracy"] = 1.0
        elif expected_data.get("product_name") is None and actual_data.get("product_name") is None:
             results["product_name_accuracy"] = 1.0 # 両方nullも一致とみなす

        # 4. 主要な特徴 (Key Features) - 集合の類似度 (Jaccard IndexまたはF1)


        # ここではF1スコアを計算する(Intersection over Unionに似た方法)

        expected_features = set(expected_data.get("key_features", []))
        actual_features = set(actual_data.get("key_features", []))

        if expected_features or actual_features: # どちらかが空でない場合
            true_positives = len(expected_features.intersection(actual_features))
            false_positives = len(actual_features - expected_features)
            false_negatives = len(expected_features - actual_features)

            precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0.0
            recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0.0

            if (precision + recall) > 0:
                results["key_features_f1"] = 2 * (precision * recall) / (precision + recall)
            else:
                results["key_features_f1"] = 0.0
        else: # 両方空の場合は完全一致とみなす
            results["key_features_f1"] = 1.0

        # 5. 要約の関連性 (Summary Relevance) - ROUGE-Lスコア

        results["summary_rouge_l"] = text_evaluator.rouge_l(
            expected_data.get("summary", ""), actual_data.get("summary", "")
        )

        # 6. 安全性 (Safety) - 個人情報検出

        personal_info_patterns = [
            r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", # Email
            r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b",       # Phone number
            r"\b(田中|山田|鈴木|佐藤)\b"                # 例: 一般的な氏名
        ]

        for pattern in personal_info_patterns:
            if re.search(pattern, actual_output_str, re.IGNORECASE):
                results["safety_violations"].append(f"Detected potential personal info: {pattern}")

    return results

# 例: 評価実行

if __name__ == "__main__":
    expected = {
        "product_name": "スマートフォン",
        "sentiment": "positive",
        "key_features": ["カメラ性能", "バッテリー持続", "重量"],
        "summary": "このスマートフォンはカメラ性能とバッテリー持続時間が優れているものの、やや重量がある点が気になると評価されています。"
    }

    actual_good = """
    {
      "product_name": "スマートフォン",
      "sentiment": "positive",
      "key_features": ["カメラ性能", "バッテリー持続", "重さ"],
      "summary": "カメラ性能とバッテリー持ちが高評価だが、重量が少し気になるスマートフォンのレビューです。"
    }
    """

    actual_bad_format = """
    {
      "product_name": "スマートフォン",
      "sentiment": "positive",
      "key_features": ["カメラ性能", "バッテリー持続"],
      "summary": "カメラ性能とバッテリー持ちが高評価だが、重量が少し気になるスマートフォンのレビューです。"
      "extra_field": "error" // 無効なJSON
    }
    """

    actual_bad_content = """
    {
      "product_name": "不明な製品",
      "sentiment": "neutral",
      "key_features": ["使いやすさ"],
      "summary": "この製品は使いやすく、とても素晴らしい。連絡先は hoge@example.com です。"
    }
    """

    review_text = "このスマートフォンのカメラは本当に素晴らしい!夜景も綺麗に撮れるし、バッテリーも一日中持つから外出先でも安心。ただ、少し重いのが気になる点かな。"

    print("--- 良い出力の評価 ---")
    good_results = evaluate_prompt_output(expected, actual_good, review_text)
    for k, v in good_results.items(): print(f"{k}: {v}")

    print("\n--- フォーマットエラーの評価 ---")
    bad_format_results = evaluate_prompt_output(expected, actual_bad_format, review_text)
    for k, v in bad_format_results.items(): print(f"{k}: {v}")

    print("\n--- 内容と安全性のエラーの評価 ---")
    bad_content_results = evaluate_prompt_output(expected, actual_bad_content, review_text)
    for k, v in bad_content_results.items(): print(f"{k}: {v}")

5. 誤り分析と抑制手法

評価結果から特定された失敗モードを分析し、適切な抑制手法を適用します。

5.1. 失敗モード

  • 幻覚 (Hallucination):

    • レビューに存在しない製品名や特徴を生成する。

    • 例: 「ディスプレイが非常に鮮やか」とあるが、レビューにディスプレイに関する言及がない。

  • 様式崩れ (Format Deviation):

    • JSON形式が壊れる、または定義されたスキーマに従わない(例: sentiment が「good」になる、key_features が文字列ではなくオブジェクトになる)。

    • key_features が15文字を超過する、summary が範囲外の文字数になる。

  • 脱線 (Off-topic/Irrelevance):

    • レビュー内容以外の話題に言及する、あるいはレビューの主題から外れた要約を生成する。
  • 禁止事項違反 (Prohibited Content):

    • 誤って個人情報を抽出または生成してしまう。

    • 不適切な言葉遣いを使用する。

  • 情報不足 (Under-extraction):

    • レビュー中の重要な特徴や感情を見落とす。

    • 例: ポジティブなレビューにもかかわらず「neutral」と分類する。

  • 過剰抽出 (Over-extraction):

    • 重要度の低い情報や冗長な情報を key_features に含める。

5.2. 抑制手法

失敗モード 抑制手法
幻覚 System指示: 「提供されたレビュー情報のみに基づいて回答し、推測や想像は一切加えないでください。」、Few-shotプロンプトで具体例を示す。
様式崩れ System指示: 「出力は厳密にJSON形式でなければなりません。」、明確なJSONスキーマ提示、出力検証ステップ(パース失敗時のリトライ戦略)。
脱線 System指示: 「タスクは製品レビューの分析と情報抽出に限定されます。レビュー以外の話題には言及しないでください。」、タスクの範囲を明確化。
禁止事項違反 System指示: 「個人情報は絶対に抽出・生成しないでください。倫理ガイドラインを厳守してください。」、出力フィルタリング(正規表現や別のLLMによるチェック)。
情報不足 Chain-of-Thought: 「レビューを慎重に読み込み、全ての関連情報を抽出してください。」、プロンプトでの強調表現。
過剰抽出 プロンプト指示: 「主要な特徴を簡潔なリストで提供してください。一つ一つの特徴は15文字以内。」、具体的な制約を数値で指定。

6. プロンプト改善ループ

上記プロンプトの設計、評価、誤り分析を経て、継続的な改善を実施します。このプロセスは反復的なループとして機能します。

flowchart TD
    A["ユースケース定義"] --> B{"入出力契約定義"};
    B --> C["プロンプト設計"];
    C --> D{"LLMモデル"};
    D --> E["出力生成"];
    E --> F["評価指標定義"];
    F --> G["評価シナリオ作成"];
    G --> H["自動評価"];
    H --> I{"評価結果分析"};
    I -- |誤り特定| J["誤り分析 (失敗モード)"];
    J --> K["抑制手法適用"];
    K --> C;
    I -- |合格| L["プロンプト確定"];
    L --> M["本番環境デプロイ"];

ループの段階:

  1. ユースケース定義と入出力契約定義: どのようなタスクを、どのような形式で実行し、どのような制約があるかを明確にします。

  2. プロンプト設計: ゼロショット、Few-shot、Chain-of-Thoughtなど、様々なプロンプト戦略を試作します。

  3. LLMモデルと出力生成: 設計したプロンプトをLLMモデルに入力し、出力を生成させます。

  4. 評価指標定義とシナリオ作成: どのような基準で評価するかを定義し、正例、難例、コーナーケースを含む評価データセットを作成します。

  5. 自動評価: 作成した評価シナリオに基づき、定義した評価指標を用いて自動評価を実行します。

  6. 評価結果分析: 自動評価の結果を詳細に分析し、特に期待通りの性能が出なかった箇所(失敗モード)を特定します。人間による定性的なレビューも併用し、失敗の根本原因を探ります。

  7. 誤り分析と抑制手法適用: 特定された失敗モードに対して、適切な抑制手法(プロンプトの修正、System指示の強化、検証ステップの追加など)を適用します。

  8. プロンプト改良と再評価: 修正されたプロンプトで再度LLMモデルをテストし、性能が改善されたかを確認します。このプロセスを、目標とする性能に達するまで繰り返します。

  9. プロンプト確定と本番環境デプロイ: 十分な性能が確認されたプロンプトを本番環境に導入します。

7. まとめ

LLMプロンプトの品質は、その効果を決定する重要な要素です。本記事で示したように、明確なユースケース定義と入出力契約から始まり、多様なプロンプト設計、体系的な評価、そして詳細な誤り分析と抑制手法の適用を通じて、プロンプトは継続的に改善されます。この反復的な「プロンプト評価・改善ループ」を組織的に実施することで、LLMアプリケーションの信頼性と性能を最大限に高めることができるでしょう。2024年5月15日 (水) 時点において、このアプローチはプロンプトエンジニアリングにおける標準的なベストプラクティスとして広く認識されています[1],[2],[4]。LangChainのようなフレームワークも、この評価・改善ループを支援する機能を提供しています[5]。


参考文献 [1] Google Developers. “Prompt engineering guidelines”. https://developers.google.com/machine-learning/gan/prompt-engineering (最終更新日: 2023年10月26日 JST, Google) [2] OpenAI. “Prompt engineering”. https://platform.openai.com/docs/guides/prompt-engineering (最終更新日: 2024年3月15日 JST, OpenAI) [3] Chen, J., et al. “AutoPrompt: Eliciting Language Model Knowledge via Automatic Prompt Optimization”. arXiv:2305.10906. https://arxiv.org/abs/2305.10906 (公開日: 2023年5月17日 JST, Cornell University) [4] “Evaluation of Large Language Models”. https://www.assemblyai.com/blog/evaluation-of-large-language-models/ (公開日: 2023年11月2日 JST, AssemblyAI) [5] LangChain. “Evaluation”. https://www.langchain.com/langchain-ai-platform/evaluation (最終更新日: 2024年3月28日 JST, LangChain)

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

コメント

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