RAG(検索拡張生成)は、質問に関係する文書を検索し、その内容をLLMに渡して回答させる仕組みです。答えが文書に書いてあるのに「記載がありません」と返る。あるいは関係ない内容で答える。この症状の原因は2つに分かれます。検索で該当箇所が取れていないか、取れているのにLLMが使っていないかです。まずこの2つを分けます。そのあと、取り込みから順に確認します。
最初の分岐:検索結果をそのまま見る
最初に、LLMに渡す直前の検索結果をログに出します。1つの質問について、どのチャンクが何件、どのスコアで返ったかを表示します。チャンクとは、検索しやすいように文書を分割した断片のことです。LangChainやLlamaIndexなどのフレームワークでも、検索部分(retriever)だけを呼び出せば確認できます。
答えを含むチャンクが検索結果になければ、問題は検索側にあります。段階1から順に見てください。チャンクがあるのに回答が間違っていれば、問題は生成側にあります。段階5に進んでください。この確認を飛ばしてプロンプトやモデルを変えても、原因が検索側なら何も改善しません。
| 症状 | 最初に見る段階 |
|---|---|
| 特定の文書だけ一切ヒットしない | 段階1 取り込み |
| 表の中の情報だけ取れない | 段階1・段階2 |
| 該当箇所の周辺は取れるが、肝心の一文がない | 段階2 チャンク分割 |
| 型番・社内用語・固有名詞で外れる | 段階3 埋め込みとtop-k |
| 結果が0件になる、または無関係な質問にも何か返る | 段階4 閾値 |
| 正しいチャンクが取れているのに誤答する | 段階5・段階6 |
段階1:取り込んだ本文が壊れていないか
ベクトルDB(文書を数値ベクトルとして保存し検索するデータベース)から、保存済みのチャンク本文を直接取り出して読みます。元のファイルではなく、取り込んだ後のテキストを見ることが重要です。
よくある原因は3つです。1つ目は文字化けです。Shift_JIS(WindowsではCP932)で保存されたCSVやテキストをUTF-8として読むと、日本語が意味のない文字列になります。そうなると検索に一切かかりません。2つ目はPDFの抽出失敗です。スキャンしたPDFは中身が画像なので、OCR(画像から文字を読み取る処理)をかけないと本文が空になります。テキストを含むPDFでも崩れは起きます。段組みの左右の列が混ざる、表のセルが一行につながる、改行のたびに単語が分断される、などです。3つ目は表記ゆれです。全角と半角の英数字が混ざっていると、キーワード検索で一致しません。Unicode正規化(NFKC)をかけると多くは揃います。
本文が壊れていたら、ローダーやOCRの設定を変えて取り込み直します。本文が正しければ段階2へ進みます。
段階2:チャンク分割で答えが途中で切れていないか
答えの箇所が、どのチャンクにどう入っているかを確認します。
チャンクが小さすぎると、条件と結論が別々のチャンクに分かれます。たとえば「対象は正社員」と「有給休暇の付与日数」が分かれると、どちらか片方しか検索されません。逆に大きすぎると、1つのチャンクに複数の話題が混ざります。すると埋め込みベクトル(文章の意味を数値の並びで表したもの)の意味がぼやけ、類似度が下がります。文字数だけで機械的に区切った場合は、表の見出し行とデータ行が分かれてしまうことがよくあります。
基本の対処は3つです。見出しや段落の区切りで分割すること。隣り合うチャンクに重なり(オーバーラップ)を持たせること。各チャンクの先頭に文書名や章見出しを付けることです。最適なサイズは文書の性質や埋め込みモデルによって変わり、決まった正解はありません。何パターンかで取り込み直し、後述する評価用の質問で比べます。分割が妥当なのに取れない場合は段階3へ進みます。
段階3:埋め込みモデルと検索件数(top-k)
まず、取り込み時と検索時で同じ埋め込みモデルを使っているかを確認します。途中でモデルを変えた場合、古いベクトルとは比較できません。全件を取り込み直す必要があります。また、モデルによっては決まった使い方が前提になっています。たとえばmultilingual-e5は、質問の先頭にquery: 、文書の先頭にpassage: を付ける前提です。使っているモデルの公式ドキュメントで、推奨される使い方を確認してください。
日本語への対応も見ます。英語中心に学習したモデルは、日本語での検索精度が下がる場合があります。多言語対応のモデルに変えて比べると判断しやすくなります。
型番、社内用語、人名で外れる場合は、埋め込み検索の性質が原因であることが多いです。埋め込みは意味の近さを測るので、「ABC-123」と「ABC-124」のような文字の違いを区別しにくい傾向があります。この場合は、BM25などのキーワード検索を組み合わせるハイブリッド検索が有効です。
top-k(上位何件を取ってくるか)が小さすぎないかも確認します。kを一時的に大きくして、答えのチャンクが何位に出てくるかを見てください。少し下の順位に出てくるなら、検索はほぼ正解に近づいています。その場合はkを増やすか、多めに取ってからリランカーで並べ替えます。リランカーとは、質問と各チャンクの関連度を細かく採点し直すモデルです。上位にまったく出てこない場合は、段階1・2を見直すか、モデルを変える必要があります。
段階4:類似度の閾値で切り捨てていないか
一定のスコアに満たない結果を捨てる閾値を設定していると、正しいチャンクがそこで落ちることがあります。結果が0件になるとき、または答えのチャンクのスコアが閾値をわずかに下回っているときが、このケースです。
スコアの分布は、モデルや距離の種類(コサイン類似度、内積など)によって大きく変わります。別のモデルの例や他の記事から持ってきた閾値は、そのままでは使えません。いったん閾値を外します。そして、文書に答えがある質問とない質問の両方でスコアを記録し、2つの間に線を引きます。反対に、閾値がまったくないと別の問題が起きます。無関係な質問にも何かしらのチャンクが必ず返り、LLMがそれをもとに関係ない回答を作ってしまいます。
段階5:プロンプトにどう渡しているか
ここからは、正しいチャンクが検索できている前提で進めます。LLMに送った最終的なプロンプトの全文をログに出します。
まず、チャンクが本当にプロンプトに入っているかを確認します。トークン上限に合わせて末尾が黙って切り捨てられていることがあります。テンプレートの変数名を間違えて、空の文字列が入っていることもあります。次に、各チャンクに出典(文書名や見出し)が付いているかを確認します。断片だけを並べると、LLMにはどれが何の文書なのか判断できません。
チャンクを並べる順番も結果に影響します。長い入力の中ほどにある情報は使われにくい、という傾向が研究で報告されています(「Lost in the Middle」として知られる現象)。関連度の高いチャンクを先頭に置く、渡す件数を絞る、といった方法で改善することがあります。
段階6:LLMが文書を無視する・幻覚を起こす
正しい情報がプロンプトに入っているのに誤答する場合は、指示かモデルに問題があります。
「記載がありません」と答えるなら、指示が厳しすぎるのかもしれません。完全に一致する記述がなければ答えない、と読める指示だと、言い換えた記述を拾えません。反対に、文書にない内容をもっともらしく答える(幻覚)なら、指示を明確にします。与えられた文書だけを根拠に答え、根拠がなければそう答えるように書きます。あわせて、根拠になった箇所を回答に引用させます。そうすれば、どのチャンクを使ったのかを後から確かめられます。
会話の流れに頼った質問も、見落としやすい原因です。「それの期限は?」のような質問は、そのまま検索すると外れます。会話履歴をもとに質問を書き換えてから検索する処理を入れてください。これは検索側の問題でもあるので、段階3に戻って確認します。
指示を直しても改善しない場合は、より高性能なモデルで同じプロンプトを試します。結果が良くなればモデルの能力の問題です。変わらなければ、プロンプトか検索側にまだ原因が残っています。
直らないときは評価用の質問セットで数値を取る
ここまで試しても原因が絞れないなら、1〜2問だけで試行錯誤している可能性があります。答えの場所がわかっている質問を数十問用意します。そして、2つの数値を分けて記録します。正解のチャンクが上位k件に入った割合と、回答が正しかった割合です。前者が低ければ検索側に問題があります。前者は高いのに後者が低ければ、生成側に問題があります。設定を1つ変えるたびにこの2つの数値を見ると、本当に改善したのか、たまたま当たっただけなのかを区別できます。Ragasのような評価ツールもありますが、最初は表計算ソフトで管理するだけでも十分です。次に設定を変えるときは、変更を1回に1つだけにして、この2つの数値を見比べてください。