RAG 上級講座 #1 RAG が間違う場所をまず特定する

読了 5分

LLM アプリ開発 第8回で基本の RAG パイプラインを作り、第13回ではそれを使って社内文書 Q&A ボットまで完成させました。ところが実際に運用してみると、どこか物足りません。ある質問には正確なのに別の質問には見当外れの答えを返し、直そうとプロンプトをいじると今度は別の質問が悪くなります。このシリーズは、その「物足りない RAG」を体系的に引き上げる全7回です。

順序はこうです。失敗の診断とベースライン(第1回)、チャンキング戦略(第2回)、ハイブリッド検索(第3回)、クエリ変換とリランキング(第4回)、引用によるハルシネーション削減(第5回)、評価パイプライン(第6回)を経て、最後の第7回で第13回の Q&A ボットを段階的にアップグレードしながら数値の変化を確認します。

初回のテーマは改善のテクニックではなく診断です。どこが壊れているのか分からないまま直し始めると、良くなったのかどうかさえ分からないからです。

RAG の失敗は二つの場所で起きます #

RAG の答えがおかしいとき、原因は大きく二つに分かれます。

失敗のタイプ症状直す場所
検索の失敗正解が入ったチャンクを取ってこられなかったチャンキング、検索(第2〜4回)
生成の失敗正解のチャンクは取ってきたのに答えが間違っているプロンプト、引用(第5回)

この区別が重要なのは、処方箋が正反対だからです。検索の失敗なのにプロンプトを直すのは無駄骨ですし、生成の失敗なのに埋め込みモデルを替えるのも同じです。そして現場で出会う失敗の多数は検索の失敗です。第8回でも一行で触れましたが、見当外れのチャンクを取ってくれば、Claude がどれだけうまく答えても意味がありません。

最初の診断ツールは目です #

大げさな道具の前に、いちばん効果的な診断は、検索結果を直接見ることです。間違った答えが出た質問について、検索が取ってきたチャンクをそのまま出力してみます。

inspect_retrieval.py
def inspect(question: str, top_k: int = 5):
    chunks = search(question, top_k=top_k)   # 第8回の検索関数
    print(f"質問: {question}\n")
    for i, c in enumerate(chunks, 1):
        print(f"--- チャンク {i}(類似度 {c.score:.3f})---")
        print(c.text[:200])
        print()

inspect("返金手数料はいくらですか?")

チャンクの中に正解があるかないかだけ見れば、検索の失敗か生成の失敗かはその場で分かれます。正解がなければ検索側を、あるのに答えが間違っていれば生成側を掘ればよいわけです。間違った質問を10件ほどこうして眺めるだけでも、パターンが見え始めます。固有名詞の質問でだけ検索が外れるとか、表の中にあった内容だけが漏れるといった具合です。

ゴールデンセットを作る #

目で見る診断は速いものの、改善が本当に改善なのか確かめるには、同じ試験用紙で繰り返し測る必要があります。その試験用紙がゴールデンセット(golden set、正解をあらかじめ確定しておいた評価用データの束)です。RAG では質問、正解、そして正解が入っているチャンクの出どころを一緒に書いておきます。

golden_set.py
GOLDEN = [
    {
        "question": "返金手数料はいくらですか?",
        "answer_keywords": ["10%", "手数料"],      # 答えに必ず入っているべき内容
        "source_doc": "refund-policy.md",          # 正解チャンクがある文書
    },
    {
        "question": "年次有給休暇は入社1年目に何日ですか?",
        "answer_keywords": ["11日"],
        "source_doc": "hr-handbook.md",
    },
    # 実際のユーザー質問から選んだ 20〜30 件あれば始めるには十分
]

質問は頭で作るのではなく実際のユーザー質問ログから選ぶことが重要です。作った質問は文書の言い回しに似てしまい検索が簡単に成功しすぎますが、実際の質問は文書と違う語彙を使うので難しいのです。難しい試験用紙が良い試験用紙です。

ベースラインを測る #

ゴールデンセットがあれば、二つの数字を測れます。検索が正解の文書を取ってきたか(検索ヒット率)、答えに正解の内容が入っているか(回答正確度)です。

baseline.py
def measure(golden: list, top_k: int = 5) -> tuple:
    retrieval_hits, answer_hits = 0, 0
    for case in golden:
        chunks = search(case["question"], top_k=top_k)
        if any(c.source == case["source_doc"] for c in chunks):
            retrieval_hits += 1
        answer = rag_answer(case["question"])     # 第8回の RAG 応答関数
        if all(kw in answer for kw in case["answer_keywords"]):
            answer_hits += 1
    n = len(golden)
    return retrieval_hits / n, answer_hits / n

retrieval, answer = measure(GOLDEN)
print(f"検索ヒット率: {retrieval:.0%}  回答正確度: {answer:.0%}")

キーワードが含まれるかどうかで答えを採点するのは粗い方法ですが、ベースラインの用途には十分です。より精密な採点(LLM 判定者)は第6回で扱います。いま重要なのは、数字が二つ手に入ったという事実です。たとえば「検索ヒット率 70%、回答正確度 55%」というベースラインがあれば、今後のすべての改善をこの数字と比較して判断できます。

二つの数字の差も情報です。検索ヒット率は高いのに回答正確度が大きく低いなら生成側の問題が大きいという意味ですし、両方低いなら検索から直すべきだという意味です。

よくつまずく場所 #

  • 感覚で改善を判断する — いくつか質問を投げてみて「良くなった気がする」と結論づけると、見えないところで悪くなったものを見逃します。同じゴールデンセットで前後を比較します。
  • 簡単な質問でゴールデンセットを埋める — 文書の文章をほぼそのまま写した質問は常に通過してしまい、変化を検知できません。実際のログから、特に間違えた質問から選びます。
  • 検索と生成をひとかたまりで見る — 「RAG が間違えた」で止まってしまうと、どこを直すか決められません。必ず検索結果を開いて、失敗の地点を切り分けます。

まとめ #

今回は RAG 改善の出発点である診断と測定を扱いました。

  • 失敗は検索の失敗と生成の失敗に分かれ、処方箋が互いに異なります。検索結果を自分で開いて見るのが最初の診断です。
  • 実際のユーザー質問でゴールデンセットを作り、検索ヒット率と回答正確度という二つの数字でベースラインを測ります。
  • これ以降のすべての改善は、このベースラインとの比較で判断します。

ベースラインができたので、いよいよ直し始めます。検索の失敗のいちばんよくある根っこは、検索アルゴリズムではなくその前の段階にあります。次回の「RAG 上級講座 #2 検索品質を左右するチャンキング戦略」で、文書の分け方から手を入れます。

X