RAGパイプライン チェックリスト
検索拡張生成(RAG)の構築とレビューのためのチェックリスト。取り込み、チャンク分割、埋め込み、索引の種類、BM25とのハイブリッド検索、リランキング、引用付きプロンプトの組み立て、評価、監視を扱います。
このガイドは、検索拡張生成(RAG)システム、つまり自分の文書からパッセージを取得して言語モデルに渡し、回答をそれに根拠付けるパイプラインを構築またはレビューする人のためのものです。RAGの失敗の大半は検索の失敗であり、検索の失敗の大半は取り込みの段階で決まります。段階を順番に確認してください。/projects/tiny-rag-local-embeddings/ のプロジェクトでは小さなRAGを端から端まで作り、/learn/beginner/08-embeddings-and-similarity/ では埋め込みの側を扱います。
チェックリスト
| 段階 | 確認項目 | 出発点となる既定値 | なぜ重要か |
|---|---|---|---|
| 取り込み | すべての文書に安定したID、ソースURLまたはパス、タイトル、最終更新日、アクセス権限がある | チャンクごとに1つのメタデータレコードをベクトルの隣に保存 | 記録しなかったものは引用も更新も削除もフィルタもできない |
| 取り込み | 表とスキャンページを含む10文書で、テキスト抽出を目視確認した | 見出しは残し、ボイラープレート(ナビゲーション、フッター)は捨て、表の行は1行ずつ残す | 索引の中のゴミは、完全な自信を持って取得される |
| 取り込み | コンテンツハッシュによる重複排除と、埋め込み類似度が約0.95を超える近似重複の排除 | 最新のコピーを残す | 重複は上位k件を同じパッセージで埋めてしまう |
| チャンク分割 | 200〜500トークンのチャンクに10〜20%の重なり、固定長より先に見出しと段落で分割 | 300トークン、重なり50、各チャンクの先頭に見出しテキストを付加 | 小さすぎると文脈を失い、大きすぎると埋め込みとプロンプト予算が薄まる |
| チャンク分割 | 各チャンクが単独で意味をなす | 文書タイトルとセクションパスを先頭に付加 | モデルが見るのはチャンクであり、文書ではない |
| 埋め込み | 言語とドメインに合わせてモデルを選び、最大シーケンス長がチャンクサイズを上回る | 英語のプロトタイプにはall-MiniLM-L6-v2(384次元、256トークン上限)のような汎用オープンモデル、本番にはより大きなモデル | モデルの上限より長いチャンクは黙って切り詰められる |
| 埋め込み | 文書とクエリに同じモデルと正規化を使い、モデルが求めるなら非対称の接頭辞を付ける | モデル名とバージョンを索引と一緒に記録 | バージョンが混在すると比較不能なベクトルになる |
| 索引 | 約100万ベクトルまではフラット(厳密)検索、それ以上はHNSW、メモリが厳しいならIVF | フラット、正規化ベクトルと内積 | 近似索引は速度と引き換えに再現率を失う。損失を計測すること |
| 索引 | メタデータフィルタ(テナント、日付、権限)は検索の後ではなく検索の中で適用 | 事前フィルタ、またはkを大きくした事後フィルタ | 上位10件の事後フィルタは何も返さないことがある |
| ハイブリッド | ベクトル索引の隣にBM25(語彙)索引を置き、相互ランク融合で統合 | 同じ重みで、それぞれ50件を取得 | 密検索は正確な名前、コード、バージョン番号、まれな語を取りこぼす |
| リランキング | クロスエンコーダが上位30〜100件の候補を、プロンプトに入れる3〜8件まで絞り込む | 小さなオープンのクロスエンコーダリランカー | バイエンコーダは大まかな話題でランク付けする。リランカーはクエリとパッセージを一緒に読む |
| プロンプト | パッセージに番号を付け、ソースと日付を添え、指示はパッセージの前、質問は最後 | 「パッセージに基づいて答えてください。[n]の形で引用してください。パッセージに答えがなければそう述べてください。」 | 引用によって出力が検証可能になり、失敗が見えるようになる |
| プロンプト | トークン予算を強制: 収まるようにパッセージを切り詰め、指示は決して切らない | コンテキストの3分の1を回答用に確保 | 末尾からの切り詰めは質問を落とす |
| 評価 | 正解パッセージIDと参照回答を持つ、少なくとも100問のラベル付きセット | 実際のクエリから作り、文書の所有者とともにラベル付け | 正解パッセージがなければ、検索の誤りと生成の誤りを切り分けられない |
| 評価 | 正解パッセージに対する検索のrecall at kとMRRを、変更ごとに報告 | recall at 5とrecall at 20 | 正解パッセージが上位k件になければ、下流では何も直せない |
| 評価 | 回答の忠実性(すべての主張が引用パッセージに支持される)と回答の正確さを、サンプルは人が、残りはモデルが判定 | ルーブリック、検証済みの審判、信頼区間 | 流暢だが間違ったパッセージを引用する回答が、よくある失敗 |
| 監視 | すべてのクエリを、取得したID、スコア、プロンプトのハッシュ、モデルのバージョンとともにログに記録 | 回答ごとに再生可能な記録 | これがなければ1週間後の苦情は役に立たない |
| 監視 | 索引の鮮度、埋め込みモデルのバージョン、空結果の割合、上位スコアの平均、段階ごとのレイテンシ | 空結果と上位スコアのドリフトでアラート | ドリフトはユーザーが報告する前にここに現れる |
| 監視 | 文書の変更時に再取り込み、埋め込みモデルの変更時には索引全体を再構築 | ソースとの定期的な差分取得 | 古いパッセージは、もうそう書かれていない文書を引用する |
12行のフラットコサイン索引
フラット検索は行列積です。正規化したベクトルなら内積がコサイン類似度になり、100万チャンク以下なら十分に速く、しかも厳密です。ここから始め、必要性を計測してからHNSWに移ってください。
import numpy as np
from sentence_transformers import SentenceTransformer
docs = [
"The training loop calls zero_grad, backward and step in that order.",
"Gradient clipping bounds the global norm before the optimiser step.",
"A learning-rate scheduler is stepped once per epoch, after the optimiser.",
"Mixed precision keeps weights in fp32 and runs matmuls in bf16.",
"Chunk documents to 200 to 500 tokens with a small overlap.",
"HNSW indexes trade memory for fast approximate nearest neighbours.",
"BM25 scores exact lexical matches and complements dense retrieval.",
"A reranker scores query and passage together and reorders the top 50.",
"Recall at k measures whether the gold passage made it into the context.",
"Faithfulness checks that every claim in the answer is supported by a passage.",
"Log the retrieved ids with every answer so failures can be replayed.",
"Rebuild the index when the embedding model changes, never mix versions.",
]
query = "how do I measure whether retrieval found the right passage"
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2", device="cpu")
index = model.encode(docs, normalize_embeddings=True) # (12, 384), unit length
q = model.encode([query], normalize_embeddings=True)[0] # (384,)
scores = index @ q # cosine, because both are unit vectors
top = np.argsort(-scores)[:3]
for rank, i in enumerate(top, start=1):
print(f"{rank}. {scores[i]:.3f} {docs[i]}")
1. 0.479 BM25 scores exact lexical matches and complements dense retrieval.
2. 0.394 Recall at k measures whether the gold passage made it into the context.
3. 0.358 Log the retrieved ids with every answer so failures can be replayed.
正しいパッセージ(recall at k)は2位で、「retrieval」という語を共有するが質問には答えていない文の後ろにいます。これは小さなバイエンコーダのごく普通の振る舞いであり、リランカー段階が必要な理由でもあります。クエリと各候補を一緒に読むクロスエンコーダなら、2番目の文を1位にするでしょう。top-1の正解率ではなくrecall at 5が最初に調整すべき検索指標である理由も、ここにあります。
チャンク分割をもう少し詳しく
| 戦略 | 使う場面 | トレードオフ |
|---|---|---|
| 固定トークン数と重なり | 構造のない散文、手早い最初の索引 | 文や表を途中で切る |
| 構造的(見出し、段落、リスト項目)、その後に固定長のフォールバック | ドキュメント、マニュアル、ポリシー | 形式ごとにまともなパーサが必要 |
| 文ウィンドウ: 単一の文を埋め込み、周囲のウィンドウを返す | 精密な質問応答 | 管理の手間は2倍、適合率は向上 |
| 親子: 小さなチャンクを埋め込み、親セクションを返す | 長い技術的セクション | プロンプトが大きくなる |
| 行またはレコード単位 | 表、チケット、製品カタログ | ヘッダのない行は無意味なので、先頭に付加する |
チャンクサイズは文字数ではなく埋め込みモデルのトークン数で測ります。分割後の分布を確認してください。5トークンのチャンク(見出しだけ、ページ番号)の長い裾は、スプリッタを直す必要があるサインです。
索引の選択
| 索引 | 検索 | メモリ | 構築 | 使う場面 |
|---|---|---|---|---|
| フラット | 厳密、線形走査 | ベクトルのみ | 不要 | 約100万ベクトル以下、または他の索引を計測するための正解が必要なとき |
| HNSW | 近似、グラフ探索、低レイテンシで高い再現率 | ベクトルとグラフ、生のベクトルの約1.5〜2倍 | 数分から数時間、大規模では挿入が遅い | 数百万ベクトル、読み取り中心、メモリに余裕あり |
| IVF(直積量子化付き) | 近似、多数のクラスタのうち少数を検索 | PQによりはるかに小さい | サンプルベクトルでの学習パスが必要 | 数千万ベクトル、メモリ制約あり、ある程度の再現率低下を許容 |
どの索引であれ、信頼する前に自分のクエリでフラット索引に対する再現率を計測してください。ベンチマークで0.99の再現率を出すHNSWの既定設定が、自分のデータでは0.9になることがあります。
出典
- Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, 2020
- Karpukhin et al., Dense Passage Retrieval for Open-Domain Question Answering, 2020
- Robertson and Zaragoza, The Probabilistic Relevance Framework: BM25 and Beyond, 2009
- Malkov and Yashunin, Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs, 2016
- Cormack et al., Reciprocal Rank Fusion outperforms Condorcet and individual rank learning methods, 2009
- Nogueira and Cho, Passage Re-ranking with BERT, 2019
- Es et al., RAGAS: Automated Evaluation of Retrieval Augmented Generation, 2023
- sentence-transformersドキュメントとall-MiniLM-L6-v2モデルカード
- Faiss wiki: 索引選択のガイドライン