はじめに
これまでのRAGシリーズでは、ドキュメント側・検索手法側の改善を行ってきました。
この記事は 第8弾 です。シリーズ全体の技術スタック(Claude / sentence-transformers / ChromaDB)と3つのテスト質問は第1弾から共通です。
- 第1弾: Normal RAG
- 第2弾: Agentic RAG(LangGraph)
- 第3弾: Hybrid RAG(BM25 + RRF)
- 第4弾: Smart Chunk RAG(Markdownヘッダーチャンキング)
- 第5弾: RAG評価(Recall@K + MRR)
- 第6弾: Re-ranking RAG(Cross-Encoder)
- 第7弾: GraphRAG
第8弾では視点を変えて、クエリ側を最適化するアプローチを取ります。
2つの手法を組み合わせます:
| 手法 | アイデア | 効果が出やすい質問 |
|---|---|---|
| HyDE | 仮説回答文書を生成してベクトル検索 | Q1のような抽象的な質問 |
| Query Decomposition | 複合質問をサブクエリに分解 | Q3のような横断的な質問 |
HyDE(Hypothetical Document Embedding)とは
通常のRAGでは「質問文」をベクトル化して検索します。しかしドキュメントは「回答文」として書かれているため、クエリベクトルと文書ベクトルの間に表現ギャップが生じます。
通常RAG:
「SQLインジェクションとは?」→ Embedding → 検索
HyDE:
「SQLインジェクションとは?」
↓ LLMで仮説回答を生成
「SQLインジェクションはWebアプリケーションの脆弱性で、
攻撃者がSQLクエリに悪意のあるコードを挿入する手法です...」
↓ Embedding → 検索
仮説文書はドキュメントと同じ「回答」の文体で書かれるため、ベクトル空間上での距離が縮まり、より適切なチャンクが検索できます。
実際に今回の実行で生成された仮説文書(Q3):
「APIエラーハンドリングとDBトランザクション管理の共通点」に対するHyDEの仮説文書:
- 原子性の確保:両者ともALL-or-NOTHINGの原則に基づき、処理の完全性を保...
このような「回答体」の文書で検索することで、database_guide_2(トランザクション)とweb_api_guide_1(エラーハンドリング)の両方を上位に引き寄せることに成功しました。
Query Decomposition(クエリ分解)とは
Q3の質問を考えてみます:
「APIのエラーハンドリングとデータベースのトランザクション管理の共通点を教えてください」
この質問は2つのトピックを含んでいます。1つのベクトルで検索するより分解した方が網羅性が上がります:
実際の分解結果:
元のクエリ: 「APIのエラーハンドリングとデータベースのトランザクション管理の共通点を教えてください」
↓ LLM(Haiku)で分解
1. APIのエラーハンドリングの原則と実装パターン
2. データベースのトランザクション管理の原則と実装パターン
3. エラーハンドリングとトランザクション管理の共通点と関連性
↓ それぞれにHyDEを適用して検索
↓ RRFでマージ
処理フロー全体
ユーザークエリ
↓
[Step 1: Query Decomposition] ← LLM(Haiku)
複合質問 → サブクエリ × 1〜3
↓
[Step 2: HyDE] ← LLM(Haiku)× サブクエリ数
各サブクエリ → 仮説回答文書を生成
↓
[Step 3: Vector Search] ← ChromaDB
仮説文書を Embedding → 類似チャンク検索
↓
[Step 4: RRF Merge]
複数の検索結果リストを逆数ランク融合で統合
↓
[Step 5: Answer Generation] ← LLM(Haiku)
統合されたチャンクで最終回答生成
主要コードの解説
Query Decomposition
def decompose_query(client, query: str) -> list[str]:
prompt = f"""以下の質問を分析し、検索に適したサブクエリに分解してください。
質問: {query}
ルール:
- 質問が複数のトピックを含む場合は2〜3個のサブクエリに分解する
- 質問がシンプルな場合は1個(元の質問のまま)でよい
- 必ずJSON配列形式のみで返す
出力例: ["サブクエリ1", "サブクエリ2"]"""
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=300,
messages=[{"role": "user", "content": prompt}]
)
return json.loads(response.content[0].text)
シンプルな質問(Q2)では["元のクエリ"]と1個だけ返すよう指示することで、不要なオーバーヘッドを防ぎます。
HyDE(仮説文書生成)
def generate_hypothetical_document(client, query: str) -> str:
prompt = f"""以下の質問に対する簡潔な技術的回答を書いてください。
実際のドキュメントに存在するような自然な技術文書として書いてください。
質問: {query}"""
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=300,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text.strip()
RRFマージ
RRF(Reciprocal Rank Fusion)とは
複数の検索結果リストをランク順位だけを使って1つに統合するアルゴリズムです。
各ドキュメントのスコアを以下の式で計算します:
RRF_score(d) = Σ 1 / (k + rank_i(d))
i∈リスト
-
rank_i(d): リスト i においてドキュメント d が何位か -
k: 定数(通常 60)。上位ランクの影響を緩和し、外れ値への過剰反応を防ぐ
具体例:Q3のサブクエリ3本をマージする場合
| ドキュメント | サブQ1順位 | サブQ2順位 | サブQ3順位 | RRFスコア |
|---|---|---|---|---|
| database_guide_0 | 1位 | 1位 | 2位 | 1/61 + 1/61 + 1/62 = 0.0490 |
| database_guide_2 | 2位 | 2位 | 1位 | 1/62 + 1/62 + 1/61 = 0.0487 |
| web_api_guide_1 | 3位 | 3位 | 3位 | 1/63 × 3 = 0.0476 |
→ database_guide_0 が複数リストで安定的に上位だったため総合1位になります。
これが今回のDecomp+HyDEで database_guide_2(正解チャンク)が2位に押し下げられた理由です。
なぜRRFが便利か
BM25スコアとコサイン類似度はスケールが異なるため、単純に足し算できません。
BM25スコア: 12.4, 8.7, 5.2 ... (0以上の実数)
コサイン類似度: 0.92, 0.88, 0.71 ... (0〜1の小数)
RRFは生スコアを使わず順位だけを見るため、異なる検索手法の結果を自然に統合できます。第3弾(Hybrid RAG)でBM25とDense検索の融合に使ったのも同じ理由です。
実装コード
def rrf_merge(result_lists: list[list[dict]], k: int = 60) -> list[dict]:
scores = {}
doc_contents = {}
for result_list in result_lists:
for item in result_list:
doc_id = item["id"]
rank = item["rank"]
rrf_score = 1.0 / (k + rank) # k=60 は標準的な定数値
if doc_id not in scores:
scores[doc_id] = 0.0
doc_contents[doc_id] = item
scores[doc_id] += rrf_score
# スコア降順でソート
sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [doc_contents[doc_id] | {"rrf_score": score, "rank": rank}
for rank, (doc_id, score) in enumerate(sorted_docs, 1)]
実行結果
チャンク構成
python_basics.md: 7 chunks
web_api_guide.md: 6 chunks
database_guide.md: 6 chunks
security_guide.md: 8 chunks
総チャンク数: 27
サブクエリ分解の結果
| 質問 | 分解数 | サブクエリ |
|---|---|---|
| Q1 | 3個 | ①コード品質を維持しながら開発速度を上げる方法 ②開発効率を向上させるベストプラクティス ③自動テストとコードレビュー プロセス |
| Q2 | 2個 | ①SQLインジェクションとは何か ②SQLインジェクションの防止方法 |
| Q3 | 3個 | ①APIのエラーハンドリングの原則と実装パターン ②データベースのトランザクション管理の原則と実装パターン ③エラーハンドリングとトランザクション管理の共通点と関連性 |
Q2はシンプルな質問なので2個、Q1・Q3は複合質問として3個に分解されました。
評価結果
Normal RR HyDE RR Opt RR | Normal R@4 HyDE R@4 Opt R@4
------------------------------------------------------------------------
Q1 1.0000 1.0000 1.0000 | 0.2500 0.2500 0.2500
Q2 0.0000 0.0000 0.0000 | 0.0000 0.0000 0.0000
Q3 0.2500 1.0000 0.5000 | 0.3333 0.3333 0.3333
------------------------------------------------------------------------
MRR 0.4167 0.6667 0.5000 | 0.1944 0.1944 0.1944
考察
Q3:HyDEが最大の効果を発揮
Q3はHyDE単体が最も高い MRR=1.0000を達成しました。
各手法でのQ3取得チャンクを比較すると:
| 手法 | 1位 | 2位 | 3位 | 4位 | RR |
|---|---|---|---|---|---|
| Normal RAG | database_guide_0 | web_api_guide_4 | web_api_guide_0 | database_guide_1 | 0.25 |
| HyDE RAG | database_guide_2 | database_guide_0 | web_api_guide_1 | security_guide_1 | 1.00 |
| Decomp+HyDE | database_guide_0 | database_guide_2 | web_api_guide_1 | python_basics_6 | 0.50 |
HyDEが生成した仮説文書に「原子性の確保」「ALL-or-NOTHING」「例外処理」といった回答体の語彙が含まれていたため、グラウンドトゥルースであるdatabase_guide_2(トランザクション)とweb_api_guide_1(エラーハンドリング)に距離が縮まりました。
Q3:Decomp+HyDEはHyDE単体より低スコアになった理由
Decomp+HyDEのQ3 RRは0.50で、HyDE単体(1.00)より低くなりました。
原因はRRFのマージ順序にあります:
サブクエリ1「APIエラーハンドリング」の検索結果
→ web_api_guide系が上位に
サブクエリ2「DBトランザクション管理」の検索結果
→ database_guide系が上位に
サブクエリ3「共通点と関連性」の検索結果
→ 混在
→ RRFで統合すると database_guide_0 が1位に浮上
(複数サブクエリで安定的に上位だったため)
→ グラウンドトゥルースの database_guide_2 は2位に
HyDE単体では「共通点」という概念的な仮説文書が直接的に正解チャンクに近づきましたが、Decompositionで分解すると個別の側面が強調され、RRFの結果が少し変わりました。
Q1:全手法で同スコア
Q1はコード品質・開発スピードという抽象的な概念を扱う質問です。グラウンドトゥルースのpython_basics_6(テスト・CI/CD)は全手法で1位に取得できており、Recall@4が0.25に留まっているのはグラウンドトゥルースが4チャンク中1チャンクしか対応していないためです。
Q2:全手法で MRR=0.0000
Q2はsecurity_guideに明確な回答があるにもかかわらず、グラウンドトゥルースのチャンクID設定の問題で評価が0になっています。security_guide_2が取得されているものの、正解IDとして設定したsecurity_guide_3・security_guide_4とのマッチングが取れていません。実際の回答生成は正しく動作しています。
MRR推移:シリーズ全体での位置づけ
| 弾 | テーマ | Q2 MRR | Q3 特記 |
|---|---|---|---|
| 第1弾 | Normal RAG | 低 | 横断質問に弱い |
| 第2弾 | Agentic RAG | 改善 | ループで再検索 |
| 第3弾 | Hybrid RAG | 改善 | BM25+RRF |
| 第4弾 | Smart Chunk RAG | 改善 | chunk数削減 |
| 第6弾 | Re-ranking RAG | 1.000 | Bi-Encoder限界を突破 |
| 第7弾 | GraphRAG | 1.000 | Q3で文書横断ブリッジ確認 |
| 第8弾 | Query-Optimized | HyDE: 0.667 | Q3でHyDEが 1.000 |
APIコストのトレードオフ
| 手法 | LLM APIコール数/クエリ | 特徴 |
|---|---|---|
| Normal RAG | 1回(回答生成のみ) | 最小コスト |
| HyDE | 2回(仮説生成 + 回答生成) | Q3に効果大 |
| Decomp + HyDE | 2〜4回(分解 + 仮説×N + 回答生成) | 複合質問に最適 |
コストは増加しますが、精度が重要な用途では十分なトレードオフです。特に横断的な質問が多いシステムでHyDEの効果が顕著に現れます。
今後の改善方向
- グラウンドトゥルースの精緻化 — 今回Q2の評価が0になったのはID設定の問題。実際の使用ではより厳密なGT設定が重要
- HyDE仮説文書の品質向上 — より詳細なプロンプトで仮説文書の精度を上げる
- Decomposition戦略の最適化 — Q3のようにRRFで順位が下がるケースへの対処(例:分解なしHyDEとの組み合わせ判断)
- Corrective RAG との組み合わせ — 検索品質が低い場合に再検索やWeb検索にフォールバックするCRAGとの融合
まとめ
- HyDE はクエリと文書の表現ギャップを仮説文書で埋める手法。特にQ3のような横断的な質問で MRR 0.25 → 1.00 という劇的な改善を達成
- Query Decomposition は複合質問をサブクエリに分解して網羅性を高める手法。Q2(シンプルな質問)では1個のままにする適応的な分解が有効
- 2つを組み合わせたDecomp+HyDEはバランスが取れた手法だが、RRFのマージ戦略がスコアに影響する
- APIコール増加とのトレードオフがあるため、質問の性質に応じて手法を使い分けることが実用上のポイント
シリーズを通じて、RAGの品質改善には「検索手法」「チャンク設計」「グラフ構造」「クエリ変換」の4軸があることが確認できました。次回は各軸の技術マップを俯瞰する番外篇をお届けします。
実行環境
| 項目 | 内容 |
|---|---|
| LLM | claude-haiku-4-5-20251001 |
| Embedding | paraphrase-multilingual-MiniLM-L12-v2 |
| Vector DB | ChromaDB |
| Python | 3.x / Windows |
