4.1 检索基础:稀疏、稠密与混合
从 BM25 的精确词汇匹配到 DPR / BGE 的稠密语义检索,再到用 RRF 融合两者——RAG 的检索侧基本功
为什么检索是 RAG 的上限
生成侧再强,也只能在"送进来的上下文"里做文章。检索环节一旦漏掉答案所在的段落,后续的提示工程和 LLM 再精致也无力回天。因此,理解三类主流检索方法并在混合检索中做好权重调度,是构建任何一个严肃 RAG 系统的起点。
经验法则:在工业场景中,Retriever 的召回率是 RAG 质量的硬上限。90% 的 RAG 失败案例源自检索侧(漏检、噪声检索、query-document 失配),而不是生成侧。
稀疏检索:BM25
BM25 是经典的词袋 + 词频检索算法,至今仍是最强的零样本基线之一。其评分函数为:
其中 是词 在文档 中的词频, 是文档长度, 是平均文档长度。默认 ,。
何时 BM25 更优:
- 精确匹配场景:型号(
Qwen3-1.7B)、编号(GB/T 1.1-2020)、专有名词 - 冷启动、零样本场景(无标注训练数据)
- 语料规模极大、对延迟敏感(倒排索引查询非常快)
from rank_bm25 import BM25Okapi
import jieba
# 中文需先分词
corpus_tokenized = [list(jieba.cut(doc)) for doc in corpus]
bm25 = BM25Okapi(corpus_tokenized)
query_tokens = list(jieba.cut("研究生学位论文答辩流程"))
scores = bm25.get_scores(query_tokens)
top_k_idx = scores.argsort()[::-1][:5]稠密检索:从 DPR 到 BGE-M3
稠密检索用**双编码器(bi-encoder)**将查询与文档独立编码为稠密向量,通过内积或余弦相似度匹配。
稠密检索模型的演进路径:
| 模型 | 年份 | 关键贡献 |
|---|---|---|
| DPR (Karpukhin et al.) | 2020 | 监督学习的双编码器,Open-Domain QA 的起点 |
| Sentence-BERT | 2019 | Siamese 结构,通用句子嵌入 |
| Contriever | 2022 | 无监督对比学习,零样本迁移强 |
| E5 / GTE | 2022-2024 | 弱监督预训练 + 对比微调,小模型高性能 |
| BGE / BGE-M3 | 2023-2024 | MTEB 榜首,统一稠密/稀疏/多向量 |
| ColBERT | 2020 | 晚交互范式,token 级 late interaction |
对于中文 RAG,BGE 系列(BAAI/bge-small-zh、BAAI/bge-m3)是目前的首选。使用时一般会在查询前加入前缀以激活检索模式:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
# 查询侧加前缀(BGE 特有约定)
query = "为这个句子生成表示以用于检索相关文章:" + user_query
q_emb = model.encode([query], normalize_embeddings=True)
d_emb = model.encode(documents, normalize_embeddings=True)
# 归一化后内积 = 余弦相似度
scores = q_emb @ d_emb.T混合检索:稀疏与稠密的 RRF 融合
稀疏检索擅长精确匹配,稠密检索擅长语义泛化——两者的错误模式几乎互补。生产环境中的默认策略是混合检索,用 Reciprocal Rank Fusion(RRF) 合并两路排序:
其中 是多路排序集合(如 BM25 和 dense), 是文档 在第 路中的排名, 通常取 60。RRF 的优点是不需要对不同来源的分数做归一化——它只看排名。
def rrf_fuse(rankings: list[list[str]], k: int = 60, top_k: int = 5):
"""rankings: 多个已排序的文档 id 列表"""
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
fused = rrf_fuse([bm25_ranking, dense_ranking], k=60, top_k=5)实践经验:BM25 + Dense + RRF 的三件套能稳定比单一方法提升 5-15% 的 Recall@5,几乎没有副作用。这是研究生课程和工业落地都应该默认启用的配置。
查询变换:让用户问题更"可检索"
用户的自然语言问题往往对检索不友好——过短、歧义、或缺少关键概念。以下四种变换技术在实践中被广泛使用:
| 技术 | 原理 | 适用场景 | 典型提升 |
|---|---|---|---|
| Query Rewriting | LLM 改写为更规整的检索查询 | 用户表达模糊 | Recall +5-10% |
| HyDE | LLM 先生成假设答案,用答案的嵌入检索 | 零样本稠密检索 | 显著提升(原文 ACL 2023) |
| Query Decomposition | 把复杂问题拆成子问题并行检索 | 多跳推理 | 跨文档召回 |
| Step-back Prompting | 先抽象为上位概念再检索 | 知识图谱式问答 | MMLU 物理 +7% |
HyDE 的核心思路非常优雅——既然 doc-doc 的嵌入对齐比 query-doc 更好,那就让 LLM 伪造一个 doc:
def hyde_retrieve(query: str, top_k: int = 5):
# 1. 让 LLM 生成假设性答案(即使不准确也无妨)
hypothetical = llm.generate(
f"请直接回答以下问题,即使你不确定:\n{query}"
)
# 2. 用假设答案的嵌入去检索
hyde_emb = embed_model.encode(hypothetical, normalize_embeddings=True)
return faiss_index.search(hyde_emb, top_k)Reranker:后处理的精排
召回阶段要求快且广(Recall 优先),重排阶段要求准(Precision 优先)。交叉编码器(cross-encoder)把 query 和 candidate 一起送入 Transformer,得到更精确但更慢的相关性分数。典型流水线:
Query → [粗检索:BM25+Dense Top-20] → [交叉编码器重排 Top-5] → Generatorfrom sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-base")
pairs = [[query, c] for c in candidates]
rerank_scores = reranker.predict(pairs)本节小结
| 方法 | 优势 | 劣势 |
|---|---|---|
| BM25 | 精确匹配、零样本、极快 | 不懂同义词和语义 |
| Dense | 理解语义、跨语言 | 对编号/型号不敏感、需嵌入模型 |
| Hybrid + RRF | 两者互补、稳定提升 | 实现稍复杂 |
| HyDE | 零样本强 | 依赖 LLM 生成质量 |
| Reranker | 精度高 | 延迟大、需要双阶段 |
默认配置建议:中文 RAG 起步就用 BM25 + BGE-m3 + RRF + bge-reranker,足以应对 80% 的场景。