人工智能实践(语言智能)
第4讲:RAG

4.1 检索基础:稀疏、稠密与混合

从 BM25 的精确词汇匹配到 DPR / BGE 的稠密语义检索,再到用 RRF 融合两者——RAG 的检索侧基本功

为什么检索是 RAG 的上限

生成侧再强,也只能在"送进来的上下文"里做文章。检索环节一旦漏掉答案所在的段落,后续的提示工程和 LLM 再精致也无力回天。因此,理解三类主流检索方法并在混合检索中做好权重调度,是构建任何一个严肃 RAG 系统的起点。

经验法则:在工业场景中,Retriever 的召回率是 RAG 质量的硬上限。90% 的 RAG 失败案例源自检索侧(漏检、噪声检索、query-document 失配),而不是生成侧。


稀疏检索:BM25

BM25 是经典的词袋 + 词频检索算法,至今仍是最强的零样本基线之一。其评分函数为:

BM25(q,d)=tqIDF(t)f(t,d)(k1+1)f(t,d)+k1(1b+bdavgdl)\text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{f(t,d) \cdot (k_1 + 1)}{f(t,d) + k_1 \cdot \left(1 - b + b \cdot \frac{|d|}{\text{avgdl}}\right)}

其中 f(t,d)f(t, d) 是词 tt 在文档 dd 中的词频,d|d| 是文档长度,avgdl\text{avgdl} 是平均文档长度。默认 k1=1.2k_1 = 1.2b=0.75b = 0.75

何时 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-BERT2019Siamese 结构,通用句子嵌入
Contriever2022无监督对比学习,零样本迁移强
E5 / GTE2022-2024弱监督预训练 + 对比微调,小模型高性能
BGE / BGE-M32023-2024MTEB 榜首,统一稠密/稀疏/多向量
ColBERT2020晚交互范式,token 级 late interaction

对于中文 RAG,BGE 系列(BAAI/bge-small-zhBAAI/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) 合并两路排序:

RRF(d)=iR1k+ranki(d)\text{RRF}(d) = \sum_{i \in \mathcal{R}} \frac{1}{k + \text{rank}_i(d)}

其中 R\mathcal{R} 是多路排序集合(如 BM25 和 dense),ranki(d)\text{rank}_i(d) 是文档 dd 在第 ii 路中的排名,kk 通常取 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 RewritingLLM 改写为更规整的检索查询用户表达模糊Recall +5-10%
HyDELLM 先生成假设答案,用答案的嵌入检索零样本稠密检索显著提升(原文 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] → Generator
from 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% 的场景。