TT Lab
はじめる
学ぶ 学習パス コース

LLMエンジニアリング

RAG — 三つの部品のどこが壊れたかを先に分ける

TT Labで続きを見る

一言でいうと

RAGは、リトリーバー、リランカー、ジェネレーターの3つの部品の組み合わせで、「回答の品質が悪い」という報告を受けたとき、最初にすることは、どの部品で失敗したかを切り分けることです。そのためには、回答ごとに、何が検索され、何がモデルに入ったかが記録されている必要があります。

なぜ必要なのか

社内規定のチャットボットが「返金申請の期限は14日です」と答えたのに、規定は先月7日に変わっていました。担当者は、まずプロンプトに「最新の規定を優先しなさい」という文を入れます。それでも答えはそのままです。あとで見ると、新しい規定の文書はインデックスに入ってもおらず、モデルは受け取った文書のとおりに忠実に答えていたのでした。直す場所は、最初からインデックスでした。RAGの改善で最もよくある時間の無駄が、このような形です。

それでもRAGを使う理由ははっきりしています。モデルは学習時点の知識しか持っておらず、社内文書や最新情報は知りません。ファインチューニングで知識を入れる方法もありますが、高価で遅く、更新が難しいです。RAGは、質問が来たら関連する文書を先に探してプロンプトに入れ、その文書を根拠に答えさせます。知識が変わったら、文書だけを差し替えればよいのです。ただし、上の事例のように、「文書だけを差し替えればよい」が実際に守られているかは、別に確認する必要があります。

どう動くのか

パイプラインは、3つの部品に分かれます。

リトリーバー(Retriever)。クエリに似たチャンクを広く取ってきます。密検索(埋め込み)と疎検索(BM25のようなキーワード方式)があり、2つを合わせたハイブリッドが実務の標準です。違う種類の失敗をするからです。埋め込みは言い換えに強いですが、製品コードのような完全一致に弱く、キーワードはその逆です。2つの結果を合わせるときは、スコアの尺度が互いに違ってそのまま足せないので、順位を使う方法が広く使われています。逆順位融合(RRF)は、各リストで文書が受け取った順位でスコアを付けて足します。

RRF(d) = sum over lists of  1 / (k + rank(d))      k is commonly 60

kは、上位の数個が結果を独占しないように抑える定数です。Elasticsearchもデフォルトで60を使います。

リランカー(Reranker)。リトリーバーが広く取ってきた候補を、精密に並べ直します。クエリと文書を一緒にモデルに入れてスコアを付ける方式なので正確ですが遅いため、上位数十件にだけ適用します。

ジェネレーター(Generator)。取ってきたコンテキストを根拠に、答えを作ります。

品質の問題を診断するときは、検索と生成を必ず分けて見ます。正解の文書がそもそも検索されなかったなら、ジェネレーターをどれだけ手直ししても無駄で、検索の指標を見る必要があります。正解の文書がコンテキストにあるのに間違った答えが出たなら、そのときがジェネレーターの問題で、忠実性を見る必要があります。

指標 何を測るか
Recall@K 上位K個の中に正解の文書が入ったか
MRR 最初の正解が何位に出たか(順位の逆数の平均)
Precision@K 上位K個のうち、関連する文書の割合
Faithfulness 回答がコンテキストに基づいているか
Answer Relevancy 回答が質問に合っているか

小さな例で計算してみると、2つの指標の違いが見えます。3つのクエリの正解が、それぞれ1位、3位、そして上位3個の外で出たとします。Recall@3は、3つのうち2つが入ったので0.667です。MRRは、順位の逆数1、1/3、0を平均した0.444です。Recallは「入ったか」だけを見るので1位と3位を区別せず、MRRはその差を減点として与えます。コンテキストに上位の数個だけを入れるシステムなら、両方を見る必要があります。

現場で何が問題になるのか

症状と壊れた部品を対にしておくと、調査が速くなります。

古いインデックス。文書は変わったのに、インデックスは古いものです。症状は「直したという内容が反映されない」です。文書を削除したのに、チャンクがインデックスに残って、検索され続ける場合も同じです。インデックスの更新は、元の変更と一体の手順である必要があり、チャンクごとに元の更新時刻を付けておかないと、確認できません。

権限の漏えい。ユーザーが見られない文書が検索されて、回答に混ざります。生成のあとで隠すのでは、遅すぎます。すでにモデルがその内容を読んで要約したからです。権限フィルターは、必ず検索の段階でかけます。

バージョンの衝突。古い規定と新しい規定が一緒に検索されると、モデルはどちらかを選ぶか、混ぜます。症状は、同じ質問に答えがぶれることです。検索の前に、廃止された文書を除くか、メタデータの施行日で最新版だけを残します。

境界で切れた答え。正解の文が2つのチャンクにまたがって切れて、どちらも上位に上がれません。チャンキング戦略の問題で、次の理論で扱います。

コンテキストにあるのに間違った答え。このときが、初めてジェネレーターの問題です。関係のないチャンクが多すぎたり、正解のチャンクが長いコンテキストの真ん中に埋もれていたりする場合がよくあります。リランキングで個数を減らし、最も関連するチャンクを前に置きます。

もっともらしいでっち上げ。コーパスの外の質問にも、最も似た文書を根拠に答えを作ります。検索スコアがしきい値未満なら、答えずに、そう伝えるほうがはるかによいです。しきい値は評価セットで決めます。高すぎると、答えられる質問まで断り、低すぎると、ハルシネーションが増えます。そして、回答に出典を付けることは、機能ではなく安全装置です。ユーザーが元の文書を確認できれば、間違った答えの被害が減り、どのチャンクが誤って検索されるかを、運用中に観察できます。

どう確認するのか

すべての回答について、少なくとも次を1行で残します。クエリ、検索されたチャンクidとスコア、実際にモデルに入れたチャンクid、回答、回答が引用した出典。この記録がないと、報告が入ったときに、どの部品の問題かを切り分ける方法がありません。

失敗事例を集めたら、3つの欄に分類します。

gold chunk not in top-K        -> retriever (index, chunking, query)
in top-K but not in context    -> reranker or context budget
in context but answer wrong    -> generator (prompt, ordering, noise)

評価セットは、大げさである必要はありません。実際のユーザーの質問数十個に、正解の文書を付けておくだけでも、Recall@KとMRRを計算でき、チャンキングや重みを変えるたびに、同じ数値を測り直して、よくなったか悪くなったかを判断できます。この数値なしにプロンプトや設定を変えるのは、感覚に頼ることです。

続けて読むこと

すぐあとの理論で、検索の単位を決めるチャンキングを扱います。そのあとのラボで、文書をチャンクに分け、TF-IDFでインデックスして、8個のクエリを検索します。正解の文書のリストと比べて、Recall@3とMRRを計算し、コンテキストを組み立て、回答がコンテキストに基づいているかを語彙の重なりで判定し、コーパスの外の質問には答えないしきい値のルールを実装します。