はじめに
以前、【データサイエンティスト入門】RAG(Retrieval-Augmented Generation)とは?生成AIはなぜ最新情報を知らないのか調べてみた という記事で、RAGは「LLMの外部に知識源を用意し、質問に関連する情報を検索して、その情報をもとにLLMへ回答させる仕組み」であることを整理しました。
ただ、前回は、
検索
↓
関連文書を取得
↓
LLMへ渡す
という大枠の流れを紹介しただけで、
- その「検索」は具体的にどうやって精度を上げるの?
- 文書はそのままベクトルDBに入れるの?
- ベクトル検索だけで十分なの?
- 検索結果の順番はどうやって決めるの?
- RAGの品質はどう評価すればいいの?
といった、実際にRAGを作るときに気になる部分までは踏み込めていませんでした。
そこで今回は、RAGの中でも特に重要な「検索」の部分を中心に、チャンク分割・ハイブリッド検索・リランキング・クエリ書き換え・評価方法などをもう一歩深掘りしてみました。
この記事はこんな方におすすめ
- 前回の記事でRAGの概要を理解した方
- RAGを実装してみたものの、思ったより回答精度が上がらず悩んでいる方
- チャンク分割やハイブリッド検索、リランキングという言葉が気になる方
- RAGの品質をどう評価すればいいのか知りたい方
- Naive RAG・Advanced RAG・Modular RAGの違いを整理したい方
まず前回のおさらい
前回の記事のポイントを簡単に振り返ります。
- LLMは、学習済みの知識だけでは最新情報や社内情報などを十分に扱えないことがある
- RAGは、質問に関連する文書を外部の知識源から検索し、その内容をLLMへ渡して回答させる仕組み
- 検索ではEmbeddingを使ったベクトル検索がよく利用される
- ファインチューニングとは異なり、モデル自体を再学習せずに外部情報を回答へ反映できる
前回のイメージはこんな感じでした。
質問
│
▼
関連文書を検索
│
▼
質問+検索結果
│
▼
LLM
│
▼
回答
この流れ自体は今回調べても変わりません。
ただし、実際のRAGでは、
「何を検索するか」
「どう検索するか」
「何件取得するか」
「取得した結果をどう並べるか」
「何をLLMへ渡すか」
といった部分に多くの工夫の余地があります。
今回はそこを順番に見ていきます。
チャンク分割:文書をどう区切るか
RAGでは、長い文書をそのまま1つのデータとして検索対象にするのではなく、**チャンク(chunk)**と呼ばれる小さな単位に分割して扱うことがよくあります。
例えば、
社内マニュアル 100ページ
を1つの巨大なEmbeddingにしてしまうのではなく、
社内マニュアル
│
├─ チャンク1
├─ チャンク2
├─ チャンク3
└─ ...
のように分割し、それぞれをEmbeddingへ変換して検索対象にします。
なぜ分割するのでしょうか。
例えばユーザーが、
「有給休暇の申請期限は?」
と質問したとします。
100ページ分を1つのベクトルにしてしまうと、文書全体の意味が混ざってしまい、「有給休暇の申請期限」というピンポイントな情報との類似度が弱くなる可能性があります。
一方、該当箇所を小さなチャンクとして分けておけば、
質問
↓
「有給休暇は取得希望日の3日前までに申請する」
のような部分を検索しやすくなります。
チャンク分割にはいくつかの方法があります。
| 戦略 | 特徴 |
|---|---|
| 固定長分割 | 一定の文字数やトークン数で分割する。実装が簡単 |
| オーバーラップ分割 | 隣接するチャンクに重複部分を持たせ、境界で文脈が切れるのを防ぐ |
| 構造ベース分割 | 見出し・段落・文章など文書構造に沿って分割する |
| セマンティックチャンク | 意味や話題の変化をもとに分割する |
| Parent-Child型 | 小さいチャンクで検索し、LLMにはより大きな親チャンクを渡す |
イメージすると、
元の文書
│
▼
チャンク分割
│
├─ Chunk A
├─ Chunk B
├─ Chunk C
└─ Chunk D
│
▼
それぞれEmbedding
│
▼
検索用インデックスへ保存
という流れです。
チャンクサイズは何トークンが正解?
ここで気になるのが、
じゃあ何トークンで区切ればいいの?
という点です。
調べてみると、すべてのRAGに共通する絶対的な最適値はありません。
文書の種類によって適切なチャンクサイズは変わります。
例えば、
FAQ
なら質問と回答を1セットのチャンクにする方が自然です。
一方で、
技術マニュアル
であれば、見出し単位や段落単位で分割した方が意味を保ちやすいかもしれません。
さらに、
契約書
なら条項単位で分割した方が検索しやすい可能性があります。
つまり、
400 tokens が正解
といった固定値で考えるより、
文書の構造
+
質問の粒度
+
Embeddingモデル
+
検索件数
+
LLMへ渡せるコンテキスト量
を見ながら調整する必要があります。
チャンクが小さすぎると、
検索には引っかかる
↓
でも前後の文脈が足りない
という問題が起こります。
逆に大きすぎると、
必要な情報
+
関係ない情報
+
関係ない情報
が1チャンクに混ざり、検索結果にノイズが増える可能性があります。
そのため、
まず妥当そうなサイズで作る
↓
評価する
↓
チャンクサイズを変更
↓
もう一度評価する
という形で調整していくのが現実的です。
実際にオーバーラップ付きで分割してみる
簡単な固定長+オーバーラップ分割なら、Pythonでも実装できます。
def split_into_chunks(
text: str,
chunk_size: int = 400,
overlap: int = 60
):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
document = "(ここに長い文書のテキストが入る)" * 100
chunks = split_into_chunks(
document,
chunk_size=400,
overlap=60
)
print(f"チャンク数: {len(chunks)}")
この例は文字数ベースの簡易実装です。
Chunk 1
──────────────
└───── 重複部分
──────────────
Chunk 2
のように、前のチャンクの末尾と次のチャンクの先頭を少し重複させます。
これにより、重要な文章がちょうどチャンク境界にまたがった場合でも、どちらかのチャンクにある程度まとまった文脈を残しやすくなります。
ただし実際のLLMではコンテキスト量をトークン単位で扱うため、本番実装では文字数だけではなく、使用するEmbeddingモデルのTokenizerを基準に分割した方が扱いやすい場合があります。
ベクトル検索だけでは苦手な検索もある
RAGではEmbeddingを使ったベクトル検索がよく使われます。
例えば、
質問:
「社員が会社を辞めるときの手続きは?」
に対して、
文書:
「退職時には退職届を提出してください」
のように、単語が完全一致していなくても意味的に近い文書を見つけられるのがベクトル検索の強みです。
しかし、ベクトル検索には苦手なものもあります。
例えば、
ABC-1234
v2.13.7
ERROR_CODE_5021
のような、
- 製品型番
- エラーコード
- バージョン番号
- 固有名詞
- ID
などです。
こうした情報は「意味が近いか」よりも、
文字列として正確に一致しているか
が重要になることがあります。
ハイブリッド検索:意味とキーワードの両方で探す
そこで使われる方法の一つが、**ハイブリッド検索(Hybrid Search)**です。
一般的には、
ベクトル検索
+
キーワード検索
を組み合わせます。
キーワード検索では、BM25などのランキング手法が使われることがあります。
イメージすると、
┌─ ベクトル検索
│ 意味の近さ
質問 ─────────────┤
│
└─ キーワード検索
単語の一致
│
▼
結果を統合
│
▼
検索結果
となります。
例えば、
「ABC-1234の接続エラーについて」
という質問なら、
ベクトル検索では、
接続
ネットワーク
エラー
といった意味に近い文章を拾えます。
一方、キーワード検索では、
ABC-1234
という完全一致に近い情報を強く評価できます。
両方を組み合わせることで、
意味的にも近い
+
固有名詞も一致している
文書を上位へ持ってきやすくなります。
ただし、ハイブリッド検索が必ずベクトル検索単体より優れるとは限りません。
抽象的な質問が中心で、型番や固有名詞がほとんど出てこないデータなら、ベクトル検索だけで十分なケースもあります。
そのため、
検索対象のデータと実際の質問を使って比較評価する
ことが重要です。
リランキング:検索した後でもう一度並べ替える
RAGでは、検索で取得した候補をそのままLLMへ渡すとは限りません。
検索結果をさらに評価し直す、**Reranking(リランキング)**という処理を挟むことがあります。
イメージすると、
質問
↓
高速な検索
↓
候補を広めに取得
↓
Reranker
↓
関連度をより細かく評価
↓
上位だけLLMへ渡す
という流れです。
例えば最初の検索で、
Top 50 chunks
を取得し、Rerankerで再評価したあと、
Top 5 chunks
だけをLLMへ渡す、といった構成です。
最初のRetrieverは、
大量の文書から候補を高速に絞る
役割を担当します。
一方Rerankerは、
少数の候補に対して、質問との関連性をより細かく評価する
役割を担当します。
Retrieval
=
広く拾う
Reranking
=
精密に並べる
という役割分担で考えると分かりやすいです。
ただし、リランキングにも追加の処理時間やコストがかかります。
そのため、
Rerankingあり
と、
Rerankingなし
で実際に検索精度・レイテンシ・コストを比較して採用を判断するのが良さそうです。
Query Rewriting:検索しやすい質問へ変換する
検索精度を改善する方法は、文書側だけではありません。
ユーザーの質問自体を検索に適した形へ変換する、Query Rewritingという方法もあります。
例えばユーザーが、
「これっていつから?」
と質問したとします。
会話の文脈を見れば、
「新しい料金プランはいつから適用されますか?」
という意味だとしても、検索エンジンへそのまま、
これっていつから?
と渡しても、何を探せばいいのか分かりません。
そこでLLMなどを使って、
ユーザーの質問
「これっていつから?」
↓
会話履歴を考慮
↓
検索用クエリ
「新料金プラン 適用開始日」
のように変換します。
これだけでもRetrieverが必要な文書を見つけやすくなる場合があります。
HyDE:仮の回答を作ってから検索する
Query Rewritingの一種として知られている面白いアプローチが、**HyDE(Hypothetical Document Embeddings)**です。
HyDEでは、質問をそのままEmbeddingにするのではなく、まずLLMに仮想的な回答文書を生成させます。
流れは次のようになります。
ユーザーの質問
│
▼
LLM
│
▼
仮想的な回答文書
│
▼
Embedding
│
▼
ベクトル検索
│
▼
実際の関連文書
例えば、
質問:
「TransformerのSelf-Attentionとは?」
に対して、まずLLMに、
Self-Attentionは、各トークンが他のトークンとの関係を...
のような仮想文書を生成させます。
その仮想文書をEmbeddingして、実際の文書群から近いものを検索します。
HyDEの元論文では、生成した架空の文書そのものを正解として利用するわけではなく、そのEmbeddingを使って実際の文書を検索する方法が提案されています。
重要なのは、
仮の回答
=
そのままユーザーへ返す答え
ではないという点です。
あくまで、
検索用の中間表現
として使います。
質問文と実際の回答文書では文章の形が大きく違う場合があるため、仮想的な回答文書を間に挟むことで検索しやすくなる場合があります。
ただし、HyDEも万能ではありません。
追加でLLMを呼び出すため、
- コスト
- レイテンシ
が増えます。
さらに、生成された仮想文書が質問の意図からずれていれば、検索も誤った方向へ引っ張られる可能性があります。
そのため、実際の評価データで効果を確認したうえで採用する必要があります。
Agentic RAG:検索方法そのものをAIに判断させる
さらに複雑な構成として、Agentic RAGと呼ばれる考え方もあります。
通常のRAGでは、
質問
↓
検索
↓
回答
という処理フローがあらかじめ決まっています。
一方Agentic RAGでは、LLMやエージェントが、
そもそも検索する必要があるか?
どの検索方法を使うか?
検索結果は十分か?
もう一度検索した方がいいか?
などを判断します。
例えば、
「AWS LambdaとFargateの違いを、
公式ドキュメントをもとに比較して」
という質問なら、
質問
↓
Agent
│
├─ Lambdaを検索
│
├─ Fargateを検索
│
├─ 情報が足りない項目を再検索
│
└─ 十分な情報が集まったと判断
↓
比較回答を生成
のような処理が可能です。
単純なRAGでは検索が1回でも、Agentic RAGでは質問に応じて検索回数や検索対象を変えられます。
一方で、
検索
↓
LLM
↓
再検索
↓
LLM
↓
さらに検索
と処理が増える可能性があります。
そのため、
- 最大検索回数
- 最大ツール呼び出し回数
- タイムアウト
- トークン上限
- コスト上限
などのガードレールを設けることが重要です。
RAGの評価:なんとなく良くなった、で終わらせない
RAGを作ると、
前より回答が良くなった気がする
という感覚で評価してしまいがちです。
しかし実務では、
検索部分と生成部分を分けて評価する
ことが重要です。
そのためのフレームワークの一つがRagasです。
Ragasには現在さまざまな評価指標がありますが、RAGを理解するうえで代表的なものとして、
- Context Precision
- Context Recall
- Faithfulness
- Answer Relevancy
があります。Ragas自体にはこの4つ以外にもAnswer CorrectnessやContext Relevanceなど複数の指標が用意されています。(Ragas)
Context Precision
Context Precisionは、関連性の高い検索結果が上位に並んでいるかを見る指標です。
例えば、
検索結果
1位:正解に必要な文書
2位:正解に必要な文書
3位:無関係
4位:無関係
なら比較的良い状態です。
一方、
1位:無関係
2位:無関係
3位:正解に必要
4位:正解に必要
では、必要な文書自体は取得できていても順位が悪くなっています。
RagasのContext Precisionは、関連するチャンクが検索結果の上位に配置されているかを評価する指標です。(Ragas)
Context Recall
Context Recallは、
回答に必要な情報を検索で取りこぼしていないか
を見る指標です。
例えば正解に、
A
B
C
という3つの情報が必要なのに、検索結果には、
A
B
しか含まれていなければRecallが低くなります。
Ragasでは、referenceに含まれる主張のうち、検索されたコンテキストでどれだけ裏付けられるか、という形で評価できます。(Ragas)
Faithfulness
Faithfulnessは、
生成された回答が、渡されたコンテキストにきちんと基づいているか
を見る指標です。
例えばコンテキストには、
料金は月額1,000円です
としか書いていないのに、LLMが、
料金は月額1,000円で、
初月無料です
と答えた場合、
初月無料
という情報はコンテキストにありません。
このような、検索結果にはない内容を生成していないかを見るのがFaithfulnessです。Ragasでは、回答中の主張が与えられたコンテキストから推論できるかを評価します。(Ragas)
Answer Relevancy
Answer Relevancyは、
ユーザーの質問に対して回答がきちんと関連しているか
を見る指標です。
例えば、
質問:
「返品期限は何日ですか?」
に対して、
返品期限は30日です。
なら質問に直接答えています。
一方、
当社では商品の品質を重視しており、
返品制度も用意しています。
だけでは、質問に十分答えていません。
RagasのAnswer Relevancyは、回答がユーザー入力の意図にどれだけ沿っているかを評価します。なお、これは事実の正しさそのものを測る指標ではありません。(Ragas)
検索と生成を分けて考える
4つを整理すると、ざっくり、
| 指標 | 主に見るもの |
|---|---|
| Context Precision | 検索結果の順位・ノイズ |
| Context Recall | 必要な情報を検索できたか |
| Faithfulness | 回答が取得したコンテキストに忠実か |
| Answer Relevancy | 質問に対して適切に答えているか |
という形になります。
例えば、
回答が悪い
という結果だけでは原因が分かりません。
しかし、
Context Recallが低い
なら、
必要な文書をRetrieverが取れていない
可能性があります。
その場合、
- チャンク分割
- Embeddingモデル
- Top K
- Hybrid Search
- Query Rewriting
などを改善候補として考えられます。
一方、
Context Recallは高い
Faithfulnessが低い
なら、
必要な情報は検索できている
↓
でもLLMが検索結果に基づいて答えていない
可能性があります。
その場合は、
- プロンプト
- モデル
- コンテキストの渡し方
などを見直します。
このように、
Retriever
と
Generator
を分けて評価できるのが重要です。
RAGASだけで完璧に評価できるわけではない
ここも注意したいポイントです。
RagasのようなLLMベース評価は非常に便利ですが、
Ragasのスコアが高い
=
本番で絶対に問題ない
とは限りません。
例えば社内FAQなら、
社員が必要な回答を得られたか
という実ユーザーの評価も重要です。
ECサイトなら、
問い合わせ解決率
などの業務指標が重要かもしれません。
そのため実務では、
自動評価
+
人間による確認
+
実際の業務KPI
を組み合わせるのが安全そうです。
Naive RAG・Advanced RAG・Modular RAG
ここまで紹介した仕組みを整理するうえで便利なのが、
- Naive RAG
- Advanced RAG
- Modular RAG
という分類です。
RAGのサーベイ論文でも、この3つのパラダイムとして整理されています。(arXiv)
Naive RAG
最もシンプルなRAGです。
文書
↓
Chunk
↓
Embedding
↓
Vector DB
質問
↓
Vector Search
↓
関連文書
↓
LLM
↓
回答
という構成です。
前回の記事で紹介したRAGは、ほぼこのイメージでした。
Advanced RAG
Naive RAGに対して、
検索前
↓
Query Rewriting
検索
↓
Hybrid Search
検索後
↓
Reranking
などの工夫を加えます。
つまり、
Query
↓
前処理
↓
Retrieval
↓
後処理
↓
LLM
という形で、検索前後を強化したRAGです。
Modular RAG
さらに発展すると、
検索
生成
メモリ
ルーティング
評価
ツール
などをモジュールとして組み合わせ、質問に応じて処理の流れそのものを変更する構成も考えられます。
例えば、
質問
↓
Router
│
├─ 社内規定 → Vector DB
├─ 売上情報 → SQL
└─ 最新情報 → Web Search
のように、問い合わせ内容に応じて検索先を切り替えることもできます。
このような柔軟なRAG構成を、Modular RAGとして整理する研究があります。(arXiv)
RAGは複雑にすればするほど良いわけではない
ここまで見ると、
Hybrid Search
+
Reranking
+
HyDE
+
Query Rewriting
+
Agent
を全部入れれば最強のRAGが作れそうに見えます。
しかし実務では、機能を追加するほど、
コスト
レイテンシ
実装量
障害ポイント
運用負荷
も増えます。
例えば、
Vector Searchだけ
で十分な精度が出るなら、わざわざHyDEやAgentを追加する必要はありません。
逆に検索精度が足りなければ、
Naive RAG
↓
Hybrid Search
↓
Reranking
↓
Query Rewriting
のように一つずつ追加して、そのたびに評価する方が原因を追いやすくなります。
個人的には、
まずシンプルに作る
↓
評価する
↓
問題点を特定
↓
必要な機能だけ追加
という進め方が一番実務的だと感じました。
実務で気をつけたいポイント
ここまで調べてきて、RAGを実務で扱ううえで意識したいポイントを整理します。
チャンクサイズに万能な正解はない
「400トークンが正解」「オーバーラップは15%が正解」といった固定値はありません。
データと質問パターンを使って評価しながら決める必要があります。
Hybrid Searchはユースケース次第
固有名詞・製品番号・エラーコードなどを扱うならキーワード検索を組み合わせる価値が高まりますが、すべてのRAGで必須ではありません。
Rerankingにもコストがかかる
検索候補を増やしてRerankerへ大量に渡せば精度が上がる可能性はありますが、処理時間や料金も増えます。
Top Kも評価対象として調整した方が良さそうです。
HyDEは生成内容に引っ張られる可能性がある
仮想文書が質問からずれれば、そのEmbeddingを使った検索もずれる可能性があります。
検索精度が実際に改善するか、評価データで確認する必要があります。
Agentic RAGにはガードレールを付ける
検索回数をAIに自由に決めさせると、想定以上にツール呼び出しやLLM呼び出しが増える可能性があります。
最大ステップ数などを決めておく方が安全です。
評価指標を1つだけ見ない
例えばContext Recallだけを最大化すると、関係する可能性のある文書を大量に取得することでRecallは上げられても、ノイズが増える可能性があります。
Recall
Precision
Faithfulness
Relevancy
Latency
Cost
などをまとめて見る必要があります。
個人的に調べてみて思ったこと
前回は、
RAG
=
検索してからLLMに答えさせる
という理解でした。
今回調べてみると、その「検索」という一言の中に、
Document
↓
Chunking
↓
Embedding
↓
Indexing
↓
Query Rewriting
↓
Retrieval
↓
Hybrid Search
↓
Reranking
↓
Context Selection
↓
LLM
という、かなり多くの処理が入る可能性があることが分かりました。
特に印象的だったのが、RAGではLLMそのものを変えなくても、検索部分を改善するだけで回答品質を上げられる可能性があるという点です。
例えば回答が悪かったときに、
LLMが悪い
とすぐ考えるのではなく、
必要な文書を検索できている?
↓
良い順番で取得できている?
↓
ノイズが多すぎない?
↓
必要な情報をLLMへ渡せている?
↓
その情報に基づいて回答している?
と分解して考えることが重要だと感じました。
また、Ragasの評価指標を調べたことで、
なんとなく良くなった
ではなく、
Context Recall
0.62 → 0.81
のように改善を数値で比較できるのも面白いところでした。
ただし、自動評価そのものにも誤差があるため、最終的には人間による確認や実際のユースケースでの評価も必要です。
RAGは「LLMに検索機能を付ければ完成」ではなく、検索システムとして継続的に改善していくもの、と考えると理解しやすくなりました。
まとめ
今回はRAGについて、前回踏み込めていなかった検索・評価周辺を深掘りしました。
- 長い文書はチャンクへ分割して検索対象にすることが多い
- チャンクサイズやオーバーラップに万能な最適値はなく、データに合わせて評価する必要がある
- ベクトル検索は意味検索に強い一方、型番や固有名詞ではキーワード検索が有効な場合がある
- Hybrid Searchでは、ベクトル検索とキーワード検索を組み合わせる
- Rerankingでは、取得した候補をさらに精密に並べ替える
- Query Rewritingではユーザーの質問を検索しやすい形へ変換する
- HyDEでは仮想的な回答文書を作り、そのEmbeddingを使って検索する
- Agentic RAGでは、検索方法や再検索の判断そのものをエージェントに任せられる
- RagasではContext Precision・Context Recall・Faithfulness・Answer Relevancyなどを使ってRAGを評価できる
- RAGの評価はRetrieverとGeneratorを分けて考えることが重要
- Naive RAG・Advanced RAG・Modular RAGという発展段階として整理する考え方がある
- 高度な機能を全部入れるのではなく、Naive RAGから始めて評価しながら必要な機能を追加する方が扱いやすい
前回は、
質問
↓
検索
↓
LLM
↓
回答
という理解でしたが、今回の記事を通して、
文書
↓
Chunking
↓
Embedding
↓
Index
質問
↓
Query Processing
↓
Vector / Keyword Search
↓
Reranking
↓
必要なContext
↓
LLM
↓
回答
↓
Evaluation
というところまで、RAGの内部をかなり具体的にイメージできるようになりました。
次は実際に、
Python
+
Embedding API
+
Vector DB
+
Reranking
+
Ragas
を組み合わせて、小さなRAGパイプラインを実装しながら精度を改善する記事も書いてみたいと思います。
最後まで読んでいただき、ありがとうございました!
参考サイト
OpenAI「Retrieval」
https://platform.openai.com/docs/guides/retrieval
OpenAI「File Search」
https://platform.openai.com/docs/guides/tools-file-search
Ragas「Context Precision」
https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/context_precision/
Ragas「Context Recall」
https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/context_recall/
Ragas「Faithfulness」
https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/faithfulness/
Ragas「Answer / Response Relevancy」
https://docs.ragas.io/en/latest/concepts/metrics/available_metrics/answer_relevance/
Ragas「Available metrics / v0.4」
https://docs.ragas.io/en/stable/howtos/migrations/migrate_from_v03_to_v04/
Gao et al.「Retrieval-Augmented Generation for Large Language Models: A Survey」
https://arxiv.org/abs/2312.10997
Gao et al.「Precise Zero-Shot Dense Retrieval without Relevance Labels(HyDE)」
https://arxiv.org/abs/2212.10496