DIVX テックブログ

catch-img

RAG精度改善、 「ハイブリッドは害」は私の実装ミスだった —— 評価に救われた話

こんにちは、DIVX エンジニアの遠矢です。

最先端のAI技術を開発にどう活かすかを日々研究している中で、RAG(検索拡張生成)を触っていてぶつかった壁があります。

それは、「精度を上げる手法は無数にあるのに、どれが本当に効くのかを、どうやって見極めるか」という課題です。

RAGの精度改善というと、チャンク分割の工夫、ハイブリッド検索、リランキング、HyDE……とにかく手法を積み重ねるイメージがあります。ネットの記事も「これを足せば良くなる」という構成が多く、私も正直、そう思っていました。

でも、本当にそうなのでしょうか?「良くなった気がする」で手法を足していって、もし実は効いていなかったら?

そんな時、私は「まず評価(精度を測る物差し)を先に作る」というアプローチにたどり着きました。今日は、RAGを一から作り、精度改善の手法を一つずつ「数字で確かめながら」検証した過程を共有します。

結論を先にお伝えすると、話は二段構えになります。

  1. 評価を先に作ったおかげで、「ハイブリッド検索を足したらむしろ精度が下がった」という異変に、数字で気づけました。 ここまでは「評価って大事だな」という、よくある話です。
  2. ところが後日冷静に見直すと、精度が下がった真犯人は"ハイブリッド検索という手法"ではなく、"私が組んだキーワード検索の実装が弱かった"ことでした。 キーワード側をちゃんとした BM25 に直したら、下がっていた精度はきれいに元へ戻り、「ハイブリッドは害」という私の結論そのものが消えてなくなりました。

この記事のいちばん大事な学びは、実はこの2番目にあります。評価は「手法の良し悪し」を教えてくれますが、その手前で「自分の実装が公平か」を疑わないと、評価の数字を根拠に間違った結論を出しかねない——それを、自分の失敗として身をもって知りました。

この記事を読んで分かること

  • RAGの精度を「数字で測る(評価する)」具体的な進め方
  • 評価があったからこそ気づけた「ハイブリッド検索で精度が下がった」という異変
  • そして、その原因が"手法"ではなく"自分の実装(pg_trgm を使っていた)"だったと、BM25 に直して確かめるまで

背景

私たちの開発では、お客様の課題解決に向けて、スピーディーかつ高品質なアウトプットを出すことが強く求められます。RAGのような仕組みを提案する場面でも、「なんとなく動く」ではなく「本当に正しく答えられているか」を説明できる状態にしておきたい、という思いがありました。

今回作ったのは、社内文書を根拠に質問へ答える、最小構成のRAGです。すべてローカル・無料(Gemini APIの無料枠のみ)で動きます。

質問 → ベクトル化(Gemini) → pgvector で意味が近い文書を検索
     → 根拠+質問をまとめてプロンプト化 → Gemini が根拠付きで回答
  • フロント:React + TypeScript + Vite
  • API:FastAPI
  • ベクトルDB:pgvector(PostgreSQLの拡張)
  • 埋め込み/生成:Gemini

ここで、多くの記事は「じゃあ精度を上げよう」と改善手法の実装に進みます。ですが私は、あえて先に評価の仕組みを作ることにしました。理由はシンプルで、測れないと「良くなった」と言えないからです。

検索の精度は Recall@k(上位k件に正解が入った割合)や MRR(正解の順位の良さ)で測れます。質問と「正解」を紐づけたデータセット(ゴールデンデータ)を自作し、これを計算するスクリプトを用意しました。この物差しがあると、たとえば「知識ベースから文書を1つ消す」だけで Recall@1 が 1.000 → 0.833 に下がるのが数字で見えます。変更の良し悪しを、勘ではなく数字で判定できる——これが以降すべての土台になりました。

試したこと:手法を「実データ」で測る

改善手法として、代表的な2つを実装しました。

ハイブリッド検索は、意味のベクトル検索に、単語一致のキーワード検索を足して、RRF(Reciprocal Rank Fusion)という方式で融合します。固有名詞や数値のように「意味が薄いが、厳密な一致が効く」ケースに強いはず、という狙いです。

リランキングは、検索で集めた候補を、クロスエンコーダ(質問と文書を「一緒に」読んで関連度を採点する専用の小さなモデル。生成用のAPIを使わないので無料で動きます)で精査し直し、本当に関連が高い順に並べ直します。

最初は自作のきれいなMarkdownで測ったのですが、ベクトル検索だけで満点でした。「データがきれいすぎるだけでは?」と思い、実データ(PDF)でやり直すことにしました。

ここで最初の洗礼を受けます。架空の社内規程集をPDFにして pypdf でテキスト抽出したところ、表が完全に崩壊したのです。

(抽出結果・抜粋)
役職
日当(円/日)
一般社員
3000
...

表のセルが1行ずつバラバラになり、「どの役職の、どの金額か」の対応関係が消えます。さらに文の途中で改行が入ったり、謎の空行が混ざったり。RAGの難しさの多くは、検索や生成よりも「汚いデータからきれいなテキストを取り出す」手前の工程にあることを、身をもって知りました。

この規程集は、「申請」「承認」「上限」「精算」「円」といった似た用語が全条文に散らばっているため、検索にとっても意地悪なデータです。ここで4つの方式(ベクトルのみ/キーワードのみ/ハイブリッド/リランキング)を比べました。

【第1幕】評価が「異変」を教えてくれた

最初の測定結果がこれです(8問・検索1位で正解を取れた割合)。

指標

vector

keyword

hybrid

rerank

正解@1

1.000

0.875

0.875

1.000

▲ 実際の実行結果(第1幕・キーワード=pg_trgm)。ハイブリッドが 0.875 に下がり、下部の「都市部」の質問で keyword / hybrid が一般ルール(10000円)を選んでいるのが見える。

  • ベクトル検索は実データでも満点。
  • リランキングも満点(ただしベクトルに追いついただけ)。
  • そしてキーワード検索も、ハイブリッド検索も 0.875。つまり、ベクトル単体よりハイブリッドのほうが精度が低い。手法を足したのに下がったのです。

「手法を足せば良くなる」という素朴な期待が、数字ではっきり裏切られました。評価を先に作っていなければ、この"下がった"に気づかず「ハイブリッドを入れました」と胸を張っていたかもしれません。 ここまでが、当初この記事に書こうとしていた話でした。

なぜハイブリッド検索は「下がった」のか

いちばん興味深かったのが、ハイブリッド検索が唯一間違えた質問「都市部に出張したときの宿泊費の上限は?」(正解:都市部・13000円)です。各方式が検索1位に返したものを並べます。

○ vector : 都市部への出張については、1泊あたり13000円を上限とする ← 正解
× keyword: 国内出張の宿泊費は、1泊あたり10000円を上限として精算する ← 一般ルール
× hybrid : (keywordと同じ 10000円 の方を採用してしまった)
○ rerank : 都市部への出張については、1泊あたり13000円を上限とする ← 正解

キーワード検索が「宿泊費」「上限」という表面的な語の一致に釣られ、質問の肝である「都市部」を無視して一般ルール(10000円)を選び、ハイブリッド検索はその弱いキーワード検索の誤りを「引き継いで」、正しかったベクトル検索の結果を押しのけてしまったのです。

ここで私は、いったんこう結論づけました——「強い部品に弱い部品を素朴に混ぜると、弱いほうに足を引っ張られる。素朴なハイブリッドはむしろ害だ」と。

【第2幕】"弱い部品"の正体は、私の実装だった

記事にまとめる前に、ふと引っかかったことがあります。そもそも、この「キーワード検索」は本当にフェアな実装だったのか?

見直すと、私のキーワード検索は PostgreSQL の pg_trgm(トライグラム類似)を使っていました。これは文字を3文字ずつに区切って、その重なりの多さで近さを測る方式です。手軽ですが、決定的な弱点があります。「その語がどれだけ珍しく、答えを特定する決め手になるか」をまったく見ていないのです。

「13000」という数値は、この質問の正解を一意に決める最重要のキーワードです。ところが pg_trgm にとっては「130」「300」「000」という3文字の断片にすぎず、他の条文に散らばる数字と区別がつきません。だから「都市部・13000」を拾えなかった。

一方、キーワード検索の"本来の定番"は BM25 です。検索エンジン(Elasticsearchなど)の標準スコアで、効き方の肝は IDF=「コーパス全体で珍しい語ほど、一致したときに強く加点する」 点にあります。「13000」のようなレアな数値が一致すれば、BM25 は強く反応して正解の条文を引き当てられるはず。つまり私は、キーワード検索の代表選手(BM25)ではなく、控えめな代役(pg_trgm)で「ハイブリッドは弱い」と断じかけていたわけです。これはフェアな勝負ではありません。

そこで、キーワード検索を pg_trgm から本物の BM25 に差し替えました。実装は、日本語を単語に分割する janome と、BM25 を計算する rank_bm25 の組み合わせ。どちらも純Pythonで、DBの拡張も追加サーバも要りません(コーパスが小さいので、検索のたびに索引を組み直す素直な実装で十分でした)。

真似する方へ: この「検索のたびに索引を組み直す」実装が許されるのは、コーパスが小さいからです。本番でデータが増えると、毎回のインデックス再構築が重くて破綻します。規模が大きくなるなら、rank_bm25 のような都度計算ではなく、Elasticsearch などの常設インデックス(検索エンジン)に寄せるのが定石です。

差し替えて、同じ8問をもう一度測ります。

指標

vector

keyword

hybrid

rerank

正解@1

1.000

1.000

1.000

1.000

(都市部の質問・BM25版)
○ keyword: 都市部への出張については、1泊あたり13000円を上限とする ← 正解に!
○ hybrid : 都市部への出張については、1泊あたり13000円を上限とする ← 釣られなくなった

▲ 実際の実行結果(第2幕・キーワード=BM25)。全方式が 1.000 に揃い、最下部の「方式で結果が割れた質問」が空=どの方式も間違えなくなった。

キーワード検索が 0.875 → 1.000。「都市部・13000」を自力で正しく引き当てました。弱いキーワードに引きずられていたハイブリッドも、当然 1.000 に戻ります。第1幕で「ハイブリッドはむしろ害」と結論づけかけた現象は、手法のせいではなく、私が選んだキーワード実装の弱さが生んでいたアーティファクトだったのです。

検証で気をつけたこと

余談ですが、この検証を無料の範囲で何度も回し切るために、意識したことがあります。

評価は「検索」と「生成」を分ける。 RAGは「検索(どの文章を拾うか)」と「生成(拾った文章から回答文を作る)」に分かれます。Geminiの無料枠は、生成が「1日あたり数十回」と少なく、デモですぐ枯れます。ですが、検索の精度評価は「正しい文章を拾えたか」だけを見るので、生成を使わずに測れます。この分離のおかげで、生成の枠が切れても検索の実験は何度でも回せました。今回の BM25 への差し替え検証も、生成枠をいっさい使わずに完結しています。

考察:評価は必要条件、でも十分条件ではない

今回の一件で、私の中の教訓は「評価を作ろう」から一段深まりました。

評価は、変更の良し悪しを数字で教えてくれる"必要条件"です。 実際、評価がなければ「ハイブリッドで精度が下がった」異変には気づけませんでした。ここは間違いなく価値がありました。

ですが評価は"十分条件"ではありません。 数字が「ハイブリッドは弱い」と告げても、それが「手法が弱い」のか「自分の実装が弱い」のかは、数字だけでは区別できません。私は危うく、pg_trgm という控えめな実装の責任を、"ハイブリッド検索という手法"に押しつけて記事にするところでした。「手法が悪い」と結論する前に、まず「自分の実装がその手法の代表として公平か」を疑う——これが今回いちばん高くついた学びです。

その上で、手法そのものへの評価は据え置きです。今回のようにデータが小さく、比較的整っていて、ベクトル検索だけで満点が出る状況では、BM25 を正しく積んでも、ハイブリッドやリランキングは"上乗せ"しませんでした(害にもなりませんでした)。ハイブリッドやリランキングが本当に効いてくるのは、データが大きく、ノイジーで、固有名詞や数値の厳密な一致が求められ、しかも1つのチャンクに答えが収まりきらない——そういう場面のはずです。今回のデータでは、その土俵にまだ立てていない、というのが正直なところです。

この検証の限界(正直に)

この記事の結論は、以下の限界の上に立っています。読む方が過大に受け取らないよう、明記しておきます。

  • 評価データが n=8 と小さい。 しかも正直に言うと、第1幕で下がった1問も、第2幕で戻った1問も、まったく同じ「都市部・13000円」の質問です。pg_trgm が落とし、BM25 が拾った——物語全体が、この1問の上で踊っています。1問で結論が反転してしまう規模のデータで「効く/効かない」を強く断定はできません。今回言えるのは「このデータ・この実装では、こう動いた」までです。
  • 当初のキーワード実装が BM25 ではなかった。 第1幕の「ハイブリッドは害」は、この実装の弱さが主因でした。裏を返せば、方式の比較は「各部品を、その方式の代表としてフェアに実装できているか」に強く依存します。
  • データが比較的きれい(かつ小さい)。 抽出後のノイズは残るものの、条文どうしは概ね分かれており、ベクトル検索が満点を取れてしまう難易度でした。

次にやること(Next Action)

限界の裏返しが、そのまま次の一手になります。

  1. 評価データを増やす(最優先)。 まずは 20〜30 問規模に増やし、トピックの言い換えや紛らわしい distractor を意図的に混ぜて、n=8 では見えなかった方式差を統計的に見えるようにします。「1問で反転する」状態から抜け出すのが先決です。
  2. 融合の仕方を工夫する。 素朴な RRF ではなく、方式ごとの信頼度に応じた重み付き RRF を試し、「弱い方式に引きずられにくい混ぜ方」を評価で比較します。
  3. もっと意地悪なデータで測る。 passage 単位・ノイジーで、1チャンクに答えが収まらない題材を用意し、ハイブリッドやリランキングが"上乗せ"に転じる条件を探します。

もう一段、上流にあった学び

ここまでは「実装を疑う」という事後の話でした。でも記事を書きながら、この遠回りはもっと手前で防げたのではないか、と考えるようになりました。

そもそも私は、キーワード検索を実装してもらうとき、「pg_trgm を使う」という前提を自分で握ったまま LLM に依頼していました。手軽さを優先した、私の引き出しの中の選択です。もし前提を外して、「キーワード検索の定番手法を、精度の観点から根拠付きで選定してほしい」と設計から任せていたら——LLM は最初から BM25 を提案できた可能性が高いと思います。pg_trgm を全文検索スコアの本命として推すことは、まずないはずだからです。

裏を返すと、今回の遠回りは「AI に実装を振り、設計選定は自分の狭い前提で固定した」ことから生まれました。人間が中途半端に設計を握ると、LLM が持っている"定番の知識"をかえって殺してしまう。 AI を活かすなら、人間の担当は「何を優先するか(精度か、軽さか)」を明示するところまでで、その先の「定番からの選定と根拠出し」は、むしろ先入観なく開いて任せたほうがいい——これが今回いちばん腹落ちした、もう一段上の学びでした。

まとめ

今回は、RAGの精度改善に取り組む中で、「派手な手法を足す前に、まず評価という地味な土台を作る」ことの大切さを実感しました。評価があったからこそ、「ハイブリッド検索で精度が下がった」という異変に気づけました。

ですが、その数字だけで「素朴なハイブリッドは害」と結論するのは早計でした。真犯人は手法ではなく、私が選んだキーワード実装(pg_trgm)の弱さ——さらにたどれば、その実装を自分の前提で握ったまま AI に振っていたことでした。手法・実装・前提と順に疑って初めて、数字は正しい方向を指してくれます。

RAGは「とりあえず動かす」ところまでは驚くほど簡単です。ですが、「本当に良くなっているか」を説明できる状態にするには、評価という物差しと、その数字を正しく読む慎重さの両方が欠かせません。

今後も、新しいAI技術や手法を、雰囲気ではなく数字で見極めながら(そして数字の裏側まで疑いながら)、お客様のニーズに迅速かつ的確に応える提案やプロダクトを提供していきたいです。

お気軽にご相談ください


ご不明な点はお気軽に
お問い合わせください

サービス資料や
お役立ち資料はこちら

DIVXブログ

テックブログ タグ一覧

人気記事ランキング

関連記事