RAGにおける情報検索と応答生成のためのプロンプト設計と評価戦略

Tech

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

RAGにおける情報検索と応答生成のためのプロンプト設計と評価戦略

1. ユースケース定義

RAG(Retrieval Augmented Generation)システムは、大規模言語モデル(LLM)が外部の情報源から関連情報を取得し、それに基づいて応答を生成する仕組みです。本稿では、企業内のドキュメント(例: 製品マニュアル、FAQ、社内規定)に基づくQAシステムをユースケースとして想定します。ユーザーの質問に対し、RAGが関連する社内ドキュメントを検索し、その情報を用いて正確かつ具体的な回答を生成することを目指します。

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

RAGシステムにおけるLLMとの入出力契約を以下に定義します。

2.1 入力フォーマット

LLMへの入力は、ユーザーの質問と検索システムが取得した関連コンテキスト(ドキュメントチャンク)で構成されます。

  • ユーザーの質問 (question): ユーザーが知りたい情報を含む自然言語のテキスト。

  • 検索コンテキスト (context): 検索システムが取得した、質問に関連する可能性のあるドキュメントのテキストチャンク(複数可能)。各チャンクにはソース情報(ドキュメント名、ページ番号、URLなど)を含む。

{
  "question": "〇〇製品の保証期間と修理プロセスについて教えてください。",
  "context": [
    {"source": "製品マニュアルV2.1, P.15", "text": "〇〇製品の標準保証期間は購入日から1年間です。修理を依頼する場合は、サポートセンターに連絡し、製品のシリアル番号と購入証明を提示してください。"},
    {"source": "FAQドキュメント, No.45", "text": "保証期間外の修理は有償となります。まずサポートセンターへご相談ください。"}
  ]
}

2.2 出力フォーマット

LLMからの出力は、ユーザーの質問に対する回答と、その回答の根拠となった参照元情報で構成されます。

  • 応答 (answer): ユーザーの質問に対する、自然言語での具体的かつ簡潔な回答。

  • 参照元 (sources): 応答の生成に使用された検索コンテキストの参照元リスト。

{
  "answer": "〇〇製品の標準保証期間は購入日から1年間です。修理を依頼する際は、製品のシリアル番号と購入証明を準備し、サポートセンターへご連絡ください。保証期間外の修理は有償となりますのでご注意ください。",
  "sources": ["製品マニュアルV2.1, P.15", "FAQドキュメント, No.45"]
}

2.3 失敗時の挙動

  • 関連情報が見つからない場合: {"answer": "関連する情報が見つかりませんでした。別のキーワードでお試しいただくか、サポート窓口にお問い合わせください。", "sources": []} のような定型メッセージを返す。

  • 生成エラー: モデルの内部エラーや出力フォーマット崩れが発生した場合、内部ログに記録し、ユーザーには {"answer": "システムエラーが発生しました。時間をおいて再度お試しください。", "sources": []} といったメッセージを返す。

2.4 禁止事項

  • 幻覚(Hallucination): 検索コンテキストに存在しない事実や情報を生成すること。

  • 情報の逸脱: 検索コンテキストの範囲を超えて、モデルが持つ一般知識や事前学習知識で回答を補完すること。

  • 機密情報の漏洩: 検索コンテキスト内の機密情報(もし含まれる場合)をユーザーに開示すること。

  • 不適切なコンテンツ生成: ヘイトスピーチ、差別的表現、暴力的な内容など、倫理的に問題のあるコンテンツの生成。

3. プロンプト設計

LLMにRAGの役割を適切に果たさせるためのプロンプトを設計します。異なるアプローチで3種類のプロンプト案を提示します。

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

基本的な指示のみで、モデルにコンテキストからの回答生成を求める最もシンプルな形式です。

あなたはユーザーの質問に対し、提供された情報に基づいてのみ回答するアシスタントです。
提供された情報に回答がない場合は、その旨を明確に述べてください。

---
質問: {{question}}

情報:
{{#each context}}

- {{this.source}}: {{this.text}}
{{/each}}
---

回答と参照元を以下のJSON形式で出力してください:
{
  "answer": "ここに回答を記述",
  "sources": ["参照元1", "参照元2"]
}

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

具体的な入出力例をいくつか提示することで、モデルに望ましい振る舞いを学習させます。

あなたはユーザーの質問に対し、提供された情報に基づいてのみ回答するアシスタントです。
提供された情報に回答がない場合は、その旨を明確に述べてください。
回答と参照元を以下のJSON形式で出力してください:
{
  "answer": "ここに回答を記述",
  "sources": ["参照元1", "参照元2"]
}

---
例1:
質問: 製品Aの返品ポリシーは?

情報:

- 返品規定V1.0, P.3: 製品Aは購入後30日以内であれば、未開封の場合に限り返品可能です。開封済みの場合は返品できません。

- FAQ, No.12: 返品の際は購入証明が必要です。

出力:
{
  "answer": "製品Aは購入後30日以内、かつ未開封の場合に限り返品可能です。開封済みの製品は返品できません。返品の際は購入証明をご準備ください。",
  "sources": ["返品規定V1.0, P.3", "FAQ, No.12"]
}

---
例2:
質問: 製品Bの最新ファームウェアバージョンは何ですか?

情報:

- 製品Bリリースノート202312: 製品Bの最新ファームウェアはバージョン1.5.0です。

- 製品BマニュアルV3.0, P.5: ファームウェアアップデートは製品設定メニューから行えます。

出力:
{
  "answer": "製品Bの最新ファームウェアバージョンは1.5.0です。ファームウェアアップデートは製品設定メニューから実行できます。",
  "sources": ["製品Bリリースノート202312", "製品BマニュアルV3.0, P.5"]
}

---
質問: {{question}}

情報:
{{#each context}}

- {{this.source}}: {{this.text}}
{{/each}}
---

出力:

3.3 Chain-of-Thought(CoT)制約型プロンプト

モデルに思考プロセスを明確にさせることで、正確性と信頼性を向上させます。特に、与えられたコンテキストから「重要な情報抽出」と「論理的結合」を促します。

あなたはユーザーの質問に対し、提供された情報に基づいてのみ回答するアシスタントです。
以下のステップに従って、詳細な思考プロセスを経てから回答を生成してください。

1. 質問に関連するコンテキスト内の重要な情報を抽出し、引用元を明記してください。

2. 抽出した情報のみを用いて、質問に対する回答を構成してください。コンテキストにない情報は一切追加しないでください。

3. 回答をJSON形式で出力してください。

---
質問: {{question}}

情報:
{{#each context}}

- {{this.source}}: {{this.text}}
{{/each}}
---

思考プロセス:

1. 関連情報抽出:

   - (コンテキストから重要な情報を箇条書きで抽出。各項目に引用元を明記。)

2. 回答構成:

   - (抽出した情報のみを用いて、質問への回答を組み立てる。)

出力:
{
  "answer": "ここに回答を記述",
  "sources": ["参照元1", "参照元2"]
}

4. 評価

プロンプトの有効性を評価するため、複数のシナリオと自動評価の擬似コードを提示します。

4.1 評価シナリオ

  1. 正例(Simple Positive): 質問がコンテキストに直接存在し、明確な回答が得られるケース。

    • 質問: 「〇〇製品の標準保証期間は?」

    • コンテキスト: {"source": "製品マニュアルV2.1, P.15", "text": "〇〇製品の標準保証期間は購入日から1年間です。"}

    • 期待される回答: 「〇〇製品の標準保証期間は購入日から1年間です。」

  2. 難例(Multi-hop Reasoning): 複数のコンテキストチャンクからの情報を結合して回答を導き出す必要があるケース。

    • 質問: 「サポートセンターへの連絡方法と保証期間について教えてください。」

    • コンテキスト:

      • {"source": "製品マニュアルV2.1, P.15", "text": "〇〇製品の標準保証期間は購入日から1年間です。修理を依頼する場合は、サポートセンターに連絡してください。"}

      • {"source": "連絡先リスト", "text": "サポートセンターの電話番号は0120-XXXX-YYYYです。ウェブサイトはsupport.example.comです。"}

    • 期待される回答: 「〇〇製品の標準保証期間は購入日から1年間です。サポートセンターへは電話番号0120-XXXX-YYYY、またはsupport.example.comを通じて連絡してください。」

  3. コーナーケース(No Answer/Out-of-Scope): 質問に関連する情報がコンテキストに存在しない、または不十分なケース。

    • 質問: 「〇〇製品の次期モデルの発売日はいつですか?」

    • コンテキスト: {"source": "製品マニュアルV2.1, P.15", "text": "〇〇製品の標準保証期間は購入日から1年間です。"}

    • 期待される回答: 「関連する情報が見つかりませんでした。別のキーワードでお試しいただくか、サポート窓口にお問い合わせください。」(または類似の定型メッセージ)

4.2 自動評価の擬似コード

応答の正確性、関連性、忠実性、フォーマット遵守を評価します。RAGAS [2]のようなフレームワークが提供するメトリクスを参考にします。

import json
import re

def evaluate_rag_response(question: str, context: list, generated_response: str, expected_answer: str, expected_sources: list) -> dict:
    """
    RAG応答を自動評価する擬似コード。
    Args:
        question: ユーザーの質問。
        context: 検索されたコンテキストのリスト。各要素は辞書{"source": ..., "text": ...}。
        generated_response: LLMによって生成されたJSON形式の応答文字列。
        expected_answer: 期待される回答テキスト。
        expected_sources: 期待される参照元リスト。
    Returns:
        評価結果を含む辞書。
    """
    evaluation_results = {
        "format_adherence": False,
        "answer_relevance_score": 0.0,
        "faithfulness_score": 0.0,
        "source_citation_accuracy": False,
        "hallucination_detected": False,
        "overall_score": 0.0
    }

    try:
        response_json = json.loads(generated_response)
        generated_answer = response_json.get("answer", "")
        generated_sources = response_json.get("sources", [])
        evaluation_results["format_adherence"] = True
    except json.JSONDecodeError:
        print("Error: Generated response is not a valid JSON.")
        return evaluation_results

    # 1. 応答の関連性 (Answer Relevance)


    # LLMベースの評価器や埋め込み類似度を用いるのが理想だが、ここでは簡易的にキーワードマッチや期待回答との類似度で判断。

    if expected_answer.lower() in generated_answer.lower() or \
       all(word in generated_answer.lower() for word in question.lower().split() if len(word) > 2):
        evaluation_results["answer_relevance_score"] = 0.8 # 高い関連性
    elif generated_answer == "関連する情報が見つかりませんでした。別のキーワードでお試しいただくか、サポート窓口にお問い合わせください。":
        if not context or "関連する情報が見つかりませんでした。" in expected_answer: # コンテキストが空か、期待回答が情報なしの場合
             evaluation_results["answer_relevance_score"] = 1.0
        else:
            evaluation_results["answer_relevance_score"] = 0.0 # 不適切に情報なしと回答
    else:
        evaluation_results["answer_relevance_score"] = 0.4 # 部分的な関連性

    # 2. 忠実性 (Faithfulness)


    # 生成された回答が提供されたコンテキストのみに基づいているか。


    # LLMを利用して生成回答内の各文がコンテキストに根拠があるかを判定するのが理想。

    context_text = " ".join([c["text"] for c in context])
    faithfulness_count = 0
    sentences = [s.strip() for s in re.split(r'[.!?。?!]', generated_answer) if s.strip()]
    total_sentences_in_answer = len(sentences)
    if total_sentences_in_answer == 0: total_sentences_in_answer = 1 # ZeroDivisionError回避

    for sentence in sentences:
        if len(sentence) > 5 and sentence in context_text: # 短すぎる文は無視
            faithfulness_count += 1

    if generated_answer == "関連する情報が見つかりませんでした。別のキーワードでお試しいただくか、サポート窓口にお問い合わせください。":
        if not context or "関連する情報が見つかりませんでした。" in expected_answer:
             evaluation_results["faithfulness_score"] = 1.0 # 正しく情報なしと判断
        else:
            evaluation_results["faithfulness_score"] = 0.0 # 誤って情報なしと判断
    else:
        evaluation_results["faithfulness_score"] = faithfulness_count / total_sentences_in_answer

    # 3. 参照元引用の正確性 (Source Citation Accuracy)


    # 生成された参照元が期待される参照元と一致するか。

    if sorted(generated_sources) == sorted(expected_sources):
        evaluation_results["source_citation_accuracy"] = True

    # 4. 幻覚の検出 (Hallucination Detection)


    # 忠実性スコアが低い場合、または期待される回答と大きく異なる場合、幻覚の可能性がある。

    if evaluation_results["faithfulness_score"] < 0.6 and generated_answer not in ["関連する情報が見つかりませんでした。別のキーワードでお試しいただくか、サポート窓口にお問い合わせください。"]:
        evaluation_results["hallucination_detected"] = True

    # 総合スコア (Overall Score)

    evaluation_results["overall_score"] = (
        (1.0 if evaluation_results["format_adherence"] else 0.0) * 0.1 +
        evaluation_results["answer_relevance_score"] * 0.3 +
        evaluation_results["faithfulness_score"] * 0.4 +
        (1.0 if evaluation_results["source_citation_accuracy"] else 0.0) * 0.2
    )

    return evaluation_results

# 計算量 (Big-O notation):


# json.loads: O(L), Lはgenerated_responseの長さ。


# 文字列操作 (in/lower/split): O(L)またはO(Q), Qはquestionの長さ。


# re.split: O(L)。


# faithfulnessループ: O(S * C), Sは回答の文数、Cはcontext_textの長さ。


# sorted(list): O(N log N), Nは参照元の数。


# 全体的な計算量: 最悪ケースで忠実性チェックが支配的となり、約 O(S * C)。


# メモリ使用量: O(L + Q + C + S + N) で入力とパースされたデータを格納。

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

RAGシステムにおいて発生しうる主な失敗モードとその抑制手法を以下に示します。

5.1 失敗モード

  1. 幻覚(Hallucination):

    • 現象: 検索コンテキストに存在しない情報をモデルが生成してしまう。

    • 原因: LLMが持つ事前学習知識が、提供されたコンテキストよりも優先されてしまう。

  2. 様式崩れ(Format Breakage):

    • 現象: JSON形式など、指定された出力フォーマットが守られない。

    • 原因: プロンプトの指示が不明確、モデルの性能不足、複雑すぎるフォーマット要求。

  3. 脱線(Off-topic/Irrelevant):

    • 現象: ユーザーの質問やコンテキストから逸脱した回答を生成する。

    • 原因: プロンプトで関連性への制約が弱い、検索されたコンテキストが質問と関連性が低い。

  4. 禁止事項の生成:

    • 現象: 差別的、暴力的、機密情報開示など、不適切なコンテンツを生成する。

    • 原因: 不適切なプロンプト、モデルの安全対策の限界、悪意のあるユーザー入力。

5.2 抑制手法

  1. 幻覚の抑制:

    • System指示: プロンプトの先頭で「提供された情報に基づいてのみ回答せよ。情報がない場合はその旨を明確に述べよ。」と強く指示する。

    • 検証ステップ: 応答生成後、ファクトチェッカー(別のLLMやルールベース)で、生成された各文が提供されたコンテキスト内に存在するかを検証する。存在しない文は削除または修正を促す。

    • 自信スコア: 各文の生成に対する自信スコアをモデルに算出させ、閾値以下の文は排除する (例: Self-RAGのIdeation Critics [1])。

  2. 様式崩れの抑制:

    • System指示: 「厳密にJSON形式で出力せよ。追加のテキストは一切不要。」と明確に指示し、具体的なJSONスキーマ例を提示する。

    • 検証ステップ: 生成された応答をJSONパーサーで解析し、失敗した場合は再試行を促す (リトライ戦略)。

    • リトライ戦略: JSON解析失敗時、エラーメッセージと元のプロンプトをモデルに再入力し、再度生成を試みる。

  3. 脱線の抑制:

    • System指示: 「提供された情報と質問に厳密に関連する内容のみを回答せよ。」と強調する。

    • プロンプト設計: Chain-of-Thoughtで「関連情報の抽出」ステップを設け、モデルに自ら関連性を評価させる。

    • 検索品質の向上: 検索システムがより高品質で関連性の高いコンテキストを返すようにチューニングする。

  4. 禁止事項の抑制:

    • System指示: 「いかなる場合も、差別的、暴力的、または不適切なコンテンツを生成してはならない。機密情報を開示してはならない。」と明示的に指示する。

    • 入力フィルタリング: ユーザーの質問に対して、事前に安全フィルタリング(例: Content Safety API)を適用する。

    • 出力フィルタリング: モデルの生成結果に対して、追加の安全フィルタリングを適用し、不適切な内容を検出・修正・ブロックする。

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

プロンプト設計と評価は反復的なプロセスです。

graph TD
    A["要求定義・ユースケース"] --> B("プロンプト設計");
    B --> C{"RAGプロンプト"};
    C --> D[LLM];
    D --> E["応答生成"];
    E --> F["評価モジュール"];
    F --> G{"評価結果"};
    G -- |問題発見(幻覚, 形式崩れ等)| H["誤り分析"];
    H --> I["改良点の特定"];
    I --> B;
    G -- |成功| J["デプロイ"];
  1. 要求定義・ユースケース: RAGシステムの目的、対象ドキュメント、ユーザーニーズを明確化します。

  2. プロンプト設計: 定義したユースケースと入出力契約に基づき、ゼロショット、少数例、CoT制約型などのプロンプトを設計します。Google AIのPrompt Guidance [3]も参考になります。

  3. LLM: 設計したプロンプトと検索コンテキストをLLMに入力します。

  4. 応答生成: LLMがユーザー質問に対する回答を生成します。

  5. 評価モジュール: 生成された応答を自動評価(セクション4.2)および人手評価(任意)で評価します。RAGAS [2]のような専門ツールも活用できます。

  6. 誤り分析: 評価結果に基づき、失敗モード(幻覚、様式崩れなど)を特定し、その根本原因を分析します。特に、Self-RAG [1]で示唆されるような「自己反省」のメカニズムは、モデル自身が誤りを特定し改善に繋げる強力なヒントとなります。

  7. 改良点の特定: プロンプトの修正、検索コンテキストの品質向上、モデルのファインチューニングなど、具体的な改良点を特定します。

  8. 再評価: 改良を適用した後、再度評価シナリオを用いてシステムの性能を測定します。このサイクルを繰り返すことで、プロンプトとシステムの全体的な性能を継続的に向上させます。

まとめ

RAGにおけるプロンプト設計と評価は、LLMが外部知識を効果的に活用し、正確で信頼性の高い応答を生成するための重要なプロセスです。入出力契約の厳密な定義、複数のプロンプト設計アプローチの試行、そして自動評価と誤り分析に基づく継続的な改良サイクルが成功の鍵となります。特に、幻覚や様式崩れといった失敗モードに対する明確な抑制戦略を講じることで、システムの堅牢性を高めることができます。これらの戦略は、Geminiのような高性能LLMの能力を最大限に引き出し、実用的なRAGアプリケーションを構築するための基盤となります。


参考文献: [1] Asai, A., Wu, Z., Raja, Y., Choi, J., Duskin, R., Ma, C., … & Hajishirzi, H. (2023). Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. arXiv preprint arXiv:2310.11511. (公開日: 2023年10月17日) [2] RAGAS Documentation. ragas.io. (最終確認日: 2024年5月18日) [3] Google AI for Developers. Prompt Guidance. ai.google.dev. (最終確認日: 2024年5月18日)

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

コメント

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