DBC Tech Academy / AI / 上級

RAG の精度が出ない
原因の切り分け到達率と忠実度に分解して、直す場所を 1 つに絞る

答えが「もっともらしいのに違う」「聞き方を変えると変わる」「記載があるのに無いと言う」。こういうとき、多くの現場は生成モデルを大きなものに替えます。しかし答えが悪い原因の大半は、正しい根拠が LLM に届いていないことにあります。ここでは、答えの悪さを検索の到達率と生成の忠実度に分解し、どちらを直すべきかを数字で決める手順を扱います。

where is it losing
$ python diagnose.py --pairs eval/pairs.jsonl --k 6 count recall@6 faithful correct 識別子入りの質問 20 100% 95% 95% 言い換えた質問 20 55% 91% 50% 2 文書にまたがる質問 10 40% 100% 40% --------------------------------------------------------- 全体 50 68% 94% 64% bottleneck: retrieval (recall 68% caps correct at <= 68%) next: 言い換え・2 文書の到達率 -> section 06

正答率 64% の内訳は、到達率 68% × 忠実度 94% です。生成モデルを替えて忠実度を 100% にしても、正答率は 68% を超えられません。

The ceiling

正答率には、
天井があります。

RAG の回答は、検索が渡した根拠の範囲でしか正しくなれません。正しい根拠が届いていなければ、どんなモデルでも正しく答えられず、答えたように見えるならそれは推測です。

到達率が天井を決める

到達率とは、答えに必要なチャンク (chunk) が、LLM に渡した k 件の中に入っていた割合です。

到達率が 70% なら、正答率の天井は 70% です。残りの 30% では、LLM の手元に正解が存在しません。この天井の下で、届いた根拠を正しく使えたかを表すのが忠実度です。

正答率 ≒ 到達率 × 忠実度 到達率 = 必要なチャンクが上位 k 件に入っていた割合(検索の実力) 忠実度 = 正解のチャンクを手で渡したときに正しく答えた割合(生成の実力)

直す順番は、天井が低いほうからです。到達率が 85% を下回っていれば、生成には手を付けず検索を直します。到達率が高いのに正答率が低いときだけ、生成側の根拠の渡し方と指示を直します。

生成モデルの変更は、この両方を測った後の最後の選択肢です。モデルが動かすのは忠実度だけで、天井そのものは 1 ポイントも動きません。

到達率

検索が正しいチャンクを上位に置けた割合。ここが正答率の天井を決めます。上げる手は分割・配分・リランク・絞り込みで、モデルの変更では上がりません。

忠実度

届いた根拠を正しく使えた割合。根拠の並べ方・件数・指示で決まり、最後の手段としてモデルでも動きます。

測っていない状態

この 2 つを測らずにモデルを替えると、費用が上がって正答率は天井のまま張り付きます。まず測ることが、最初の打ち手です。

Where it breaks

7 段階のどこかで、
必ず落ちています。

質問から回答までを 7 段階に分けます。前半 3 つは取り込み時、後半 4 つは質問時に動きます。1 から 5 の失敗は到達率に、6 と 7 の失敗は忠実度に現れます。

INGEST — 取り込み時 QUERY — 質問時 01取り込み 表が崩れる旧版が残る 02分割 答えが割れる文脈が切れる 03埋め込み input_type 違いモデル混在 04検索 配分が偏る候補が切れる 05並べ替え 正解が末尾重複が占める 06組み立て 根拠が多すぎる指示がない 07生成 推測で補う記載なしと 01-05 の失敗は、到達率に現れる 正しいチャンクが、LLM の手元に届いていない状態 06-07 は、忠実度 届いた根拠を使えていない状態 段階 05(並べ替え)を到達率に数えるのは、k 件に入っていても LLM が末尾を読まなければ、届いていないのと同じだからです。 したがって到達率は「上位 k 件に入ったか」ではなく、「LLM が実際に使える位置に入ったか」で測ります。

Two numbers

2 つの数字が、
直す場所を決めます。

到達率は対の質問を検索に流して測り、忠実度は正解のチャンクを手で渡して生成させて測ります。検索を通さないので、生成だけの実力が出ます。 値を動かして、モデルの変更がどこに効くのかを確かめてみましょう。

-到達率(k 件中)
-忠実度
-正答率
-天井までの余地

k を上げると到達率は上がりますが、根拠が増えるぶん中ほどが読まれにくくなり、忠実度はわずかに下がります。生成モデルを替えると忠実度だけが動きます。

この図が再現しているのは、正答率が到達率を超えられないことと、モデルの変更が動かすのは忠実度だけだということです。到達率と忠実度の実際の値、k を上げたときの忠実度の下がり方は、文書と質問で変わります。自分の環境では、次の節のログと質問・正解の対から 2 つの数字を実測し、その値で判断してください。

判定表

到達率忠実度直す場所理由
85% 未満問わず検索天井が低いので、生成をいくら直しても正答率は上がりません
85% 以上85% 未満生成正解は届いています。使い方の問題です
85% 以上85% 以上並べ替え、または対正答率が積より低いなら並べ替え。積と一致するなら、対が現場の質問を代表していません

3 行目が起きたときの見分け方。到達率と忠実度の積が実測の正答率と一致していれば、測定は正しく、モデルは想定どおり動いています。それでも現場から苦情が出るなら、対の質問が現場の実際の質問と違うのです。次の節のログから feedback = 0 の質問を拾い、対を作り直してください。

Three experiments

悪い質問 1 件で、
原因を 1 段階に絞る。

答えが悪い質問を 1 件取り、次の順に試します。各実験は 1 分で終わり、結果で次が決まります。全体を測る前に、まず 1 件で当たりを付けるほうが速いことが多くあります。

この 3 つで、直す段階は 1 つに決まります。ここを飛ばして複数の打ち手を同時に入れると、効いたものと効かなかったものが区別できなくなり、次に同じ症状が出たときに何も学べません。

Instrumentation

測れるように、
ログを残す。

質問 1 件につき 1 行を、検索と生成の両方について残します。これがないと、切り分けのたびに現象を再現するところから始めることになります。

CREATE TABLE rag_queries ( query_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, asked_at timestamptz NOT NULL DEFAULT now(), question text NOT NULL, retrieved_ids bigint[] NOT NULL, retrieved_scores real[] NOT NULL, answer text NOT NULL, cited_ids bigint[] NOT NULL, retrieval_ms integer NOT NULL, generation_ms integer NOT NULL, input_tokens integer NOT NULL, feedback smallint ); COMMENT ON TABLE rag_queries IS 'RAG の質問ごとの記録。検索が返した chunk_id と回答が引用した chunk_id を残し、後から到達率と忠実度を出す'; COMMENT ON COLUMN rag_queries.retrieved_ids IS '検索が LLM に渡した chunk_id を順位順に。到達率の実測に使う'; COMMENT ON COLUMN rag_queries.cited_ids IS '回答が [n] で引用した chunk_id。渡したのに使われなかった根拠を見つける'; COMMENT ON COLUMN rag_queries.feedback IS '利用者の評価。1 = 役に立った、0 = 立たなかった、NULL = 未評価';

retrieved_ids と cited_ids を分けて持つのは、「渡したが使われなかった」を見つけるためです。正解が渡されているのに引用されていなければ、原因は段階 05 か 06 で、検索ではありません。この 1 列があるかどうかで、切り分けの速さが変わります。

対は、役に立たなかった質問から作る

SELECT query_id, question, retrieved_ids, answer FROM rag_queries WHERE feedback = 0 ORDER BY asked_at DESC LIMIT 50;

各行について、設計書のどのチャンクを読めば答えられるかを人が付け、質問・正解の chunk_id・質問の種類(識別子入り/言い換え/複数文書)を書き出します。

役に立たなかった質問こそ、正解を付ける価値があります。うまくいっている質問を集めても、改善の材料にはなりません。

ログから到達率を出す

SELECT count(*) FILTER ( WHERE q.retrieved_ids && p.answer_chunk_ids ) * 100.0 / count(*) AS recall_pct FROM rag_queries AS q INNER JOIN eval_pairs AS p ON p.question = q.question;

&& は配列が重なるかどうかを見る演算子です。対ができれば、過去のログにさかのぼって到達率が出せます。設定を変える前と後を、同じ質問で比べられます。

忠実度も、対があれば自動で測れる

実験 1 の「正解のチャンクを手で渡す」は、対に answer_chunk_ids があれば手作業ではなくなります。SELECT content FROM chunks WHERE chunk_id = ANY(:ids) で本文を引き、検索を通さずにそのまま根拠として生成に渡すだけです。

この 1 本を diagnose.py に入れておくと、到達率と忠実度が同じコマンドで出ます。ヒーローの表がその出力です。切り分けのたびに人が根拠を貼り付けている現場は、この自動化がないだけで測る回数が減り、判断が遅れます。

正しさの判定を、いつ LLM に任せるか

「正しく答えたか」の判定は、最初の 50 件は必ず人が行います。そのうえで、人の判定を例として渡した別の LLM に同じ 50 件を判定させ、人との一致率が 9 割を超えてから、以後の判定を任せます。

一致率を確かめずに任せると、判定そのものがずれていても気付けず、正しさの数字が改善の根拠として使えなくなります。一致率は四半期ごとに、新しい 20 件で測り直してください。

このテーブルには、利用者が入力した文と文書の内容が入ります。閲覧範囲のある文書を扱っているなら、rag_queries の閲覧権限も文書と同じ範囲にそろえてください。切り分けのために作ったログが、権限の抜け道になります。

Fixing retrieval

検索を直す。

実験 3 で決まった段階ごとに、打ち手は 1 つずつ試します。1 つ変えるたびに到達率を測り直してください。2 つ同時に変えると、どちらが効いたか分かりません。

分割を直す(段階 02)

  • 答えが 2 チャンクに割れている。見出しの境界で切り直します。表は分けません。
  • 見出しの経路がない。チャンクの本文の先頭に「文書名 > 見出し > 小見出し」を付けて埋め込み直します。埋め込みも全文検索も見るのは本文であって、別の列ではありません。
  • チャンクが小さすぎて文脈がない。隣接するチャンクを結合するか、検索は小さい単位で行い、LLM に渡すときは親の見出し全体を渡します。

分割し直すと chunk_id が変わります。対の正解を付け直してから到達率を測ってください。付け直しを忘れると、到達率が 0 に近い値になって別の原因を探し始めることになります。

質問を書き換える(段階 03〜04)

  • 複数クエリ。質問を 3 通りに言い換え、それぞれで検索して順位で合わせます。言い換えで落ちる質問に効きます。
  • 仮の回答で検索する。質問から「設計書にありそうな答えの文」を LLM に作らせ、その文を埋め込んで検索します。質問と文書の文体の差を埋めます。

どちらも検索の前に生成が 1 回増え、レイテンシが 1〜2 秒延びます。到達率の伸びと引き換えにできるかは、対で測ってから決めてください。

配分を直す(段階 04)

種類別の到達率を見れば、どちらへ寄せるかが決まります。

落ちている種類打ち手合わせて確かめる
識別子入り全文検索側の配分を上げる本文に識別子がそのままの表記で入っているか
言い換えベクトル側の配分を上げる埋め込みモデルが多言語に対応した版か
2 文書にまたがる質問を 2 回の検索に分ける両方の正解が k 件に入る必要がある。片方ずつ検索して合わせる
両方k を上げるhnsw.ef_search が k より小さくないか

リランクを入れる(段階 05)

正解が k 件に入っているのに末尾にいるなら、検索で 20〜30 件取り、リランクモデルで並べ直して上位 6 件を渡します。リランクは質問とチャンクを 1 対ずつ読んで関連度を出すので、埋め込みの近さより精度が高く、その分 1 回の呼び出しに数百ミリ秒かかります。

# 検索は広く、渡すのは狭く。間にリランクを挟む hits = search(question, top_k=25) # ベクトル+全文で広めに取る hits = rerank(question, hits)[:6] # 質問と 1 対ずつ読み直して並べ替える answer = generate(question, hits) # 渡す件数は 6 のまま

リランクのモデルは、Bedrock なら Cohere Rerank、手元なら日本語に対応した交差符号化のモデルを使います。どれを選んでも、組み込む位置と、渡す件数を 6 に保つことは変わりません。広く取って狭く渡す、という形が要点です。

-- 正解のチャンクが retrieved_ids の何番目にいたか(リランク前後で比べる) SELECT avg(array_position(q.retrieved_ids, p.answer_chunk_ids[1])) AS avg_rank FROM rag_queries AS q INNER JOIN eval_pairs AS p ON p.question = q.question WHERE q.retrieved_ids && p.answer_chunk_ids;

確かめ方は、到達率ではなく平均順位です。リランクは k 件に入っているものを並べ替えるだけなので、到達率の数字は変わらないことがあります。効いているかどうかは、正解の平均順位が下がったか(前に来たか)で見ます。

絞り込みを入れる(段階 04)

「バッチ設計書の中で」のように範囲が分かる質問は、文書の種別で WHERE を絞ってから検索します。候補が減るぶん、正解が上位に来ます。質問から範囲を取り出すのは正規表現で足りることが多く、足りなければ LLM に種別を 1 語で出させます。

SET hnsw.ef_search をセッションで上げると、その接続のすべての検索が遅くなります。検索の直前に SET LOCAL でトランザクション内に閉じてください。接続プールを使う構成では、閉じないと前のリクエストの値が次の検索に効き、到達率が測るたびに違うという現象になります。

Fixing generation

生成を直す。

実験 1 で「正解を手で渡しても答えない」と分かったときの打ち手です。ここでも 1 つずつ変え、そのたびに忠実度を測り直します。

根拠の並べ方と件数(段階 06)

  • 関連度の高い順に並べ、6 件を上限にします。中ほどの根拠は読まれにくく、増やすほど見落としが増えます。
  • 同じ内容の重複を除きます。重複が上位を占めると、それ以外の根拠が押し出されます。分割の重なりを大きく取っている環境で起きやすい形です。
  • 各根拠に [n] 文書名 > 見出し を付けます。LLM が「どの根拠を使ったか」を書ける形にしておくと、cited_ids が取れるようになり、次の切り分けが速くなります。

指示(段階 07)

  • 「根拠に書かれていないことは『記載がありません』と答える」を明示します。これがないと、根拠が薄いときに推測で埋めます。
  • 「各文に [n] を付ける」を明示します。出典を強制すると、根拠にないことを書きにくくなります。
  • 逆に、根拠があるのに「記載なし」と答えるなら、指示が強すぎます。「根拠の一部からでも答えられるなら答える」を足してください。

指示の変更は、対の質問には効いても現場の別の質問で副作用が出ます。「記載なし」を減らす指示は推測を増やし、増やす指示は答えられる質問を減らします。変更後 1 週間は、feedback = 0 の件数を変更前と比べてください。

モデルを替えるのは最後です。到達率が 85% 以上あり、根拠の並べ方と指示を直しても忠実度が 85% に届かないとき、初めてモデルを 1 段上げます。上げた後、同じ対で忠実度を測り、差を記録してください。差がなければ戻します。測らずに上げたままにすると、効果のない費用が毎月続きます。

Read the symptom

症状から、
原因を見抜く。

運用中の RAG でよく持ち込まれる 3 つの症状です。それぞれ、どの段階に原因があるのかを考えてみましょう。

Myth vs. reality

費用だけが増える、
3 つの判断。

どれも一見もっともらしく、実際には正答率を動かしません。

MISCONCEPTION 01

「生成モデルを大きくすれば答えが良くなる」

モデルが動かすのは忠実度だけで、到達率の天井は変わりません。到達率を測る前にモデルを替えると、料金が上がって正答率は天井に張り付いたままです。まず 2 つの数字を測り、天井が低いほうから直してください。

MISCONCEPTION 02

「k を増やせば到達率が上がるから、増やすほど良い」

k 件に入っても末尾は読まれず、根拠が増えるほど中ほどが見落とされます。到達率は上がっても正答率が下がる領域があります。k を上げるならリランクと組み合わせ、実際に渡す件数は 6 前後に保ってください。

MISCONCEPTION 03

「到達率は一度測れば十分」

文書が増え、利用者の質問の傾向が変わると、到達率は下がります。しかもエラーは出ず、答えの質だけが静かに落ちます。ログから四半期ごとに feedback = 0 の質問で対を足し、測り直してください。

Knowledge check

8問で、理解を
確かめよう。

答えを当てるだけでなく、なぜそう判断できるのかを説明できれば合格です。

YOUR PROGRESS 01 / 08

選択肢を1つ選んでください。

Where to look first

モデルを替える前に、
天井を測る。

対を用意する
2 つに分解する
3 つの実験で絞る
1 つずつ直す