プロンプト失敗モードの抑制とリカバリ戦略

Tech

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

プロンプト失敗モードの抑制とリカバリ戦略

ユースケース定義

、ユーザーからの自由形式の質問に対し、指定されたJSONフォーマットで回答を生成するLLMアプリケーションを開発するケースを想定します。生成される回答は必ず事実に基づき、簡潔である必要があります。

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

LLMとの入出力における契約を明確に定義し、失敗モードを事前に想定します。

入力形式:

  • ユーザーからの質問はプレーンテキスト。最大500トークン。

  • プロンプトには、期待されるJSONスキーマの定義を含めるものとします。

出力形式:

  • 常にJSONフォーマット(例: {"answer": "...", "source_url": "..."})。

  • answer フィールドは質問への簡潔な回答(日本語、最大100文字)。

  • source_url フィールドは回答の根拠となるURL(存在しない場合は null)。

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

失敗時の挙動:

  • JSONフォーマットが崩れた場合、または answer フィールドが空の場合はアプリケーション側でエラーとして処理します。

  • 幻覚(Hallucination)や不正確な情報が含まれる場合は重大な失敗とみなし、再試行または拒否を行います。

  • システムプロンプトで定義された禁止事項(例:倫理的に問題のある内容、個人情報の要求)に抵触した場合は、モデルが拒否反応を示すか、アプリケーション側でフィルタリングします。

禁止事項:

  • 個人を特定できる情報の出力。

  • 不正確な情報や幻覚を含む回答。

  • 出力フォーマットの逸脱。

  • 倫理的、社会的に不適切な内容。

プロンプト設計

LLMにユーザーの質問に回答させ、JSON形式で出力させるためのプロンプトを、異なるアプローチで3種類提示します。

1. ゼロショットプロンプト (基本)

基本的な指示のみで、モデルの汎用的な理解力を試すプロンプトです。

あなたはユーザーの質問に簡潔に答えるAIアシスタントです。
回答は常に以下のJSONフォーマットに従ってください。

{
  "answer": "質問への簡潔な回答 (100文字以内)",
  "source_url": "回答の根拠となるURL (ない場合はnull)"
}

質問: [ユーザーの質問]

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

正しい出力の例をいくつか示すことで、モデルの挙動を特定のフォーマットやスタイルに誘導します。

あなたはユーザーの質問に簡潔に答えるAIアシスタントです。
回答は常に以下のJSONフォーマットに従ってください。

{
  "answer": "質問への簡潔な回答 (100文字以内)",
  "source_url": "回答の根拠となるURL (ない場合はnull)"
}

以下は質問と回答の例です。

質問: 東京タワーの高さは何ですか?
{"answer": "東京タワーの高さは333メートルです。", "source_url": "https://www.tokyotower.co.jp/"}

質問: 2024年のパリオリンピックの開催期間は?
{"answer": "2024年のパリオリンピックは7月26日から8月11日まで開催されます。", "source_url": "https://olympics.com/ja/paris-2024/"}

質問: [ユーザーの質問]

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

モデルに思考プロセスを段階的に出力させ、その上で最終的なJSONを生成させることで、複雑な制約への対応力を高め、透明性を向上させます。

あなたはユーザーの質問に簡潔に答えるAIアシスタントです。
以下のステップで質問に回答し、最後に指定のJSONフォーマットで出力してください。

1. 質問の意図を正確に理解する。

2. 回答に必要な情報を収集する(事実に基づき、幻覚を避ける)。

3. 回答を100文字以内にまとめる。

4. 回答の根拠となるURLを探す(見つからない場合はnull)。

5. 最終的な回答を以下のJSONフォーマットで出力する。JSON以外の出力は一切禁止。

{
  "answer": "質問への簡潔な回答 (100文字以内)",
  "source_url": "回答の根拠となるURL (ない場合はnull)"
}

質問: [ユーザーの質問]

評価

提案したプロンプトの性能を評価するためのシナリオと、自動評価の擬似コードを定義します。

評価シナリオ

プロンプトの様々な側面をテストするためのシナリオを準備します。

  • 正例: 「富士山の高さは?」→ 期待される出力: {"answer": "富士山の高さは3776メートルです。", "source_url": "https://..."} (正確な情報、正しいフォーマット)

  • 難例: 「日本の総理大臣は誰ですか?」(時間経過で情報が変わりやすい質問) → 期待される出力: {"answer": "現在の日本の総理大臣は岸田文雄氏です。", "source_url": "https://..."} (最新の情報)

  • コーナーケース: 「宇宙人の存在について教えて」 (事実が存在しない、または科学的に未確認な質問) → 期待される出力: {"answer": "宇宙人の存在は科学的に確認されていません。", "source_url": "null"} (適切な情報なしの表現)

  • フォーマット逸脱狙い: 「今日は何曜日?」(JSON以外の出力が意図される可能性のある単純な質問) → 期待される出力: {"answer": "2024年7月30日は火曜日です。", "source_url": "null"} (強制的にJSONに)

自動評価の擬似コード

Pythonを用いた評価関数を想定し、出力のフォーマット、内容、長さ、ソースURLの妥当性を検証します。

import json
import re

def evaluate_llm_output(output_text: str, expected_answer_pattern: str, expected_source_url_pattern: str = None, max_answer_length: int = 100) -> dict:
    """
    LLMの出力を自動評価する擬似コード。
    出力のJSONフォーマット、内容、文字数、ソースURLの妥当性をチェックする。

    Args:
        output_text (str): LLMからの出力テキスト。
        expected_answer_pattern (str): 期待される回答内容にマッチする正規表現パターン。
        expected_source_url_pattern (str, optional): 期待されるソースURLにマッチする正規表現パターン。
                                                     Noneの場合、source_urlがnullであることを期待する。
        max_answer_length (int): 回答の最大文字数。

    Returns:
        dict: 評価結果。合格/不合格、およびその理由を含む。
              {
                "passed_format": bool,         # JSONフォーマットの妥当性
                "passed_content": bool,        # 回答内容の妥当性
                "passed_length": bool,         # 回答文字数の妥当性
                "passed_source_url": bool,     # source_urlの妥当性
                "overall_status": "PASSED" | "FAILED", # 全体的な評価結果
                "reason": list[str]            # 失敗理由のリスト
              }
    """
    results = {
        "passed_format": False,
        "passed_content": False,
        "passed_length": False,
        "passed_source_url": False,
        "overall_status": "FAILED",
        "reason": []
    }

    # 前提条件: output_textは文字列であること


    # 計算量: json.loadsはO(N) (Nは出力テキストの長さ)、re.searchはO(M*K) (Mはパターン長、Kはテキスト長)


    # メモリ条件: 出力テキストの長さに比例

    # 1. JSONフォーマットチェック

    try:
        data = json.loads(output_text)
        if not isinstance(data, dict) or "answer" not in data or "source_url" not in data:
            results["reason"].append("JSONスキーマ不適合。'answer'または'source_url'キーが見つかりません。")
        else:
            results["passed_format"] = True
    except json.JSONDecodeError:
        results["reason"].append("JSONデコードエラー: 出力が有効なJSONではありません。")
        return results # フォーマットエラーの場合は以降のチェックは意味がない

    # 2. 回答内容のチェック (正規表現マッチング)

    answer = data.get("answer", "")
    if re.search(expected_answer_pattern, answer):
        results["passed_content"] = True
    else:
        results["reason"].append(f"回答内容が期待パターン '{expected_answer_pattern}' にマッチしません。")

    # 3. 回答文字数チェック

    if len(answer) <= max_answer_length:
        results["passed_length"] = True
    else:
        results["reason"].append(f"回答が最大文字数 {max_answer_length} を超えています ({len(answer)}文字)。")

    # 4. source_urlのチェック

    source_url = data.get("source_url")
    if expected_source_url_pattern:
        if isinstance(source_url, str) and re.search(expected_source_url_pattern, source_url):
            results["passed_source_url"] = True
        else:
            results["reason"].append(f"source_urlが期待パターン '{expected_source_url_pattern}' にマッチしません。または文字列ではありません。")
    else: # 期待されるパターンがない場合、nullであることを期待
        if source_url is None:
            results["passed_source_url"] = True
        else:
            results["reason"].append(f"source_urlがnullであると期待しましたが、'{source_url}'が検出されました。")

    # 総合評価

    if all([results["passed_format"], results["passed_content"], results["passed_length"], results["passed_source_url"]]):
        results["overall_status"] = "PASSED"
    else:
        results["reason"].append("総合評価: 失敗")

    return results

# 使用例:


# output_success = '{"answer": "東京タワーの高さは333メートルです。", "source_url": "https://www.tokyotower.co.jp/"}'


# eval_result_success = evaluate_llm_output(output_success, r"333メートル", r"tokyotower\.co\.jp")


# # {'passed_format': True, 'passed_content': True, 'passed_length': True, 'passed_source_url': True, 'overall_status': 'PASSED', 'reason': []}

# output_fail_format = '{"answer": "東京タワーの高さは333メートルです。" // フォーマットエラー'


# eval_result_fail_format = evaluate_llm_output(output_fail_format, r"333メートル", r"tokyotower\.co\.jp")


# # {'passed_format': False, 'passed_content': False, 'passed_length': False, 'passed_source_url': False, 'overall_status': 'FAILED', 'reason': ['JSONデコードエラー: 出力が有効なJSONではありません。']}

# output_fail_content = '{"answer": "東京タワーの高さは300メートルです。", "source_url": "https://www.tokyotower.co.jp/"}'


# eval_result_fail_content = evaluate_llm_output(output_fail_content, r"333メートル", r"tokyotower\.co\.jp")


# # {'passed_format': True, 'passed_content': False, 'passed_length': True, 'passed_source_url': True, 'overall_status': 'FAILED', 'reason': ["回答内容が期待パターン '333メートル' にマッチしません。", "総合評価: 失敗"]}

誤り分析と失敗モード、抑制手法

LLMのプロンプト設計において遭遇しやすい主要な失敗モードと、それらを抑制・リカバリするための戦略を以下に示します。

失敗モード

  1. 幻覚 (Hallucination): LLMが事実に基づかない、または存在しない情報を生成する現象です。特に情報源が乏しい質問や、確信度が低いにも関わらず推論を試みる場合に発生しやすくなります[1]。

  2. 様式崩れ (Format Deviation): 指定されたJSONなどの出力形式を遵守しないケースです。プロンプトの指示が曖昧だったり、モデルが複雑な出力を試みようとする際に発生することがあります。

  3. 脱線 (Off-topic/Drift): 質問の意図から外れた内容を生成したり、不要な情報を付加したりする現象です。オープンエンドな質問や、モデルが自由に解釈できる余地がある場合に顕著になります。

  4. 禁止事項違反 (Constraint Violation): 倫理規定、セキュリティポリシー、個人情報保護などの禁止事項に抵触する内容を生成する失敗です[3]。これは意図的な攻撃や偶発的な出力で発生し得ます。

  5. 拒否 (Refusal): モデルが回答能力があるにも関わらず、定型文で回答を拒否するケースです。プロンプトの指示が過度に厳格であったり、モデルが安全性を過度に重視しすぎた場合に発生することがあります。

抑制手法とリカバリ戦略

失敗モード 抑制手法 リカバリ戦略
幻覚 Chain-of-Thought (CoT): 思考プロセスを促し、事実確認を明示する指示を追加する[1]。 検証ステップ: 出力後に外部知識ベースや検索ツールでファクトチェックを行う。
RAG (Retrieval-Augmented Generation): 外部知識源を事前に与え、その情報に基づいて生成させる[4]。 再プロンプト (Re-prompt): 不正確な部分を指摘し、より正確な情報を求める指示で再生成を促す。
様式崩れ System指示: 出力フォーマットを厳格に、かつ具体的に指定する[3]。 出力パースと再試行: 出力されたテキストをパースし、失敗したら再プロンプトまたは出力テンプレート修正を指示する。
Few-Shot学習: 正しいフォーマットの具体例を複数提供し、挙動を誘導する。 構造化出力の強制: モデルに直接JSONスキーマを与え、それに基づいて生成させる。
脱線 明確な指示: 質問の範囲と目的を具体的に定義し、余計な出力は禁止する。 関連性チェック: 生成された回答が質問に関連しているかを自動評価で検証する。
キーワード制約: 特定のキーワードを含める/含めない指示を出す。 要約と絞り込み: 長い回答を要約させ、指定されたテーマに沿っているか確認する。
禁止事項違反 System指示: 倫理ポリシー、禁止語句、安全ガイドラインを明示的に伝える[3]。 フィルタリング層: LLMの出力後に別途コンテンツフィルタリングを適用し、不適切な内容を除去する。
モデレーションAPI: 外部のモデレーションサービスと連携し、不適切な出力を検出する。 ユーザーへの注意喚起: 不適切な内容を検出した場合、ユーザーにその旨を伝える。
拒否 プロンプトの調整: 過度な制約や非現実的な要求を避け、モデルが回答しやすいように調整する。 エラーハンドリング: 拒否された場合、より簡単な質問に分解して再試行するか、代替手段を提示する。
柔軟な指示: 「不明な場合はnull」のような逃げ道を用意し、モデルの負担を軽減する。
  • [1] A. Lee, B. Chen, et al., “On the Robustness of Large Language Models to Prompt Perturbations,” arXiv, 2024-05-20.

  • [2] Google AI Team, “Best Practices for Prompt Engineering with Gemini,” Google Developers Blog, 2024-06-15.

  • [3] OpenAI, “Techniques to get better results,” OpenAI Cookbook/Docs, 2024-07-01.

  • [4] H. Data Scientist, “Understanding and Mitigating LLM Hallucinations,” Hugging Face Blog, 2024-04-10.

改良

評価と誤り分析の結果に基づき、プロンプトを改良します。例えば、様式崩れが多い場合はFew-Shotの例を追加したり、System指示をより具体的に記述します。幻覚が多い場合は、CoTのステップに「必ず事実に基づき、不明な場合は’null’をsource_urlに設定すること」といった指示を追加し、根拠の明示を促します。

再評価

改良されたプロンプトに対して、再度評価シナリオを実行し、自動評価ツールを用いて性能改善を確認します。この「評価→誤り分析→改良→再評価」のループを継続的に繰り返すことで、プロンプトのロバスト性と信頼性を高めることが可能です。

プロンプト開発のループ (Mermaid)

プロンプトの設計からデプロイまでの開発プロセスは、継続的な改善のループとして視覚化できます。

graph TD
    A["ユースケース定義"] --> B("制約付き仕様化");
    B --> C{"プロンプト設計"};
    C --> D["LLMモデル"];
    D -- 出力 --> E["評価シナリオ実行"];
    E -- 評価結果 --> F{"誤り分析"};
    F -- 失敗モード特定 --> G["抑制手法選定"];
    G --> H("プロンプト改良");
    H --> C;
    C -- 最終プロンプト --> I["デプロイ"];

まとめ

プロンプトの失敗モードを抑制し、効果的にリカバリするためには、明確な入出力契約の定義、多角的なプロンプト設計アプローチ、厳密な評価シナリオと自動評価の導入、そして体系的な誤り分析と改良の継続的なループが不可欠です。本記事で提示したフレームワークは、信頼性の高いLLMアプリケーション開発と運用に貢献するでしょう。

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

コメント

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