RAG 进阶:混合检索 + Rerank 让准确率从 60% 到 90%

约 75 分钟L2L3T2T1

课程来源

[来源] FDE 实战课(fde-teach)

[收录说明] 本课未随文署名,按课程线署名。

本课目录(17 节)

🎯 本节目标

学完这一节,你能:

  • 配置混合检索(向量 + BM25 全文),解决"语义像但关键词丢"问题
  • 接入 Rerank 模型(BGE-Reranker),把准确率从 60% 拉到 90%
  • 用 RAGAS 评估 RAG 质量(faithfulness / answer relevancy)

🍎 生活化类比

单向量检索 = 只看"意思像不像"。混合检索 = 意思 + 关键词双保险。Rerank = 复赛精选。

  • 用户问"2024 年 Q3 营收":向量检索可能召回"2023 Q2 利润"(意思像但年份错)
  • 加 BM25 全文检索:"2024""Q3""营收"这些关键词必须命中
  • 两路结果合并后,Rerank 用更强的模型精排,挑出真正最相关的

📖 概念讲解

为什么单向量不够

检索方式强项弱项
纯向量(语义)理解同义词、语义关键词(年份、编号、专有名词)容易丢
纯全文(BM25)关键词精确不懂同义词
混合(生产必选)两者互补略复杂

Rerank:召回之后的精排

向量库一次召回 20-50 条(怕漏),再用更强的 Rerank 模型(BGE-Reranker / Cohere)逐对打分,挑出 top 3-5 塞给大模型。这一步能把准确率拉 20-30 个点。

RAGAS 四指标

指标含义目标
faithfulness回答是否忠于检索内容(不编)≥ 0.8
answer_relevancy回答是否切题≥ 0.8
context_precision检索的片段是否精准≥ 0.7
context_recall该召回的有没有漏≥ 0.7

🛠️ 动手实操:Dify 配置混合检索 + Rerank

(沿用模块 2 的 Dify 部署)

  1. 知识库设置:
  • 检索模式:混合检索(起步用向量 0.5 + 全文 0.5 对半配,这是默认起点;后面常见错误表会讲怎么按业务调,比如偏术语精确匹配的业务往全文加权重)
  • Rerank 模型:bge-reranker-v2-m3
  • Top K:召回 20,Rerank 后取 5
  1. 准备 20 个测试问题 + 标准答案
  2. 用 RAGAS 跑评估:
pip install ragas
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy
# 准备 dataset:question/answer/context/ground_truth
# result = evaluate(dataset, metrics=[faithfulness, answer_relevancy])
# 目标:faithfulness ≥ 0.8

✅ 这是智谱×公积金 12329 把正确率从 60% 拉到 90%+ 的核心方法。

⚠️ 常见错误

问题原因解决
混合检索后效果反而更差向量和全文权重没调好先各跑一遍看单路效果,再调权重(通常向量 0.6 + 全文 0.4)
Rerank 后 top 反而变了Rerank 模型和 Embedding 不匹配用同家族(BGE Embedding 配 BGE Reranker)
faithfulness 达不到 0.8检索召回的片段不准先治检索(context_precision),再测生成

✅ 自测题

<summary>1. 为什么纯向量检索不够,要加 BM25 全文?</summary>

向量懂语义但丢关键词。用户问"2024 Q3 营收",向量可能召回"2023 Q2 利润"(语义像但年份错)。BM25 保证"2024""Q3""营收"这些关键词精确命中。混合检索两者互补。

</details> <summary>2. Rerank 在 RAG 里起什么作用?为什么能提 20-30 个点?</summary>

向量库一次召回 20-50 条(怕漏),Rerank 用更强的模型逐对精排,挑出真正最相关的 top 3-5 给大模型。召回重、精排准,准确率自然上来。

</details>

📚 总结 & 下一节

混合检索 + Rerank 是生产 RAG 标配。下一节:Agent 实战——把 RAG 升级成能"干活"的智能体。

🏋️ 配套自练

  • Pinecone RAG 学习中心 — 业界最全的 RAG 教程,从分块到混合检索到 Rerank(免费、英文)
  • RAGAS 快速上手 — 装 RAGAS,给你的 RAG 跑一次 faithfulness 评分(免费)
  • 把本节的混合检索配置在 Dify 里跑一遍,对比开 Rerank 前后的准确率

🔗 延伸阅读


← 上一节:RAG 基础 | 下一节:Agent 实战 →


动手实验

实验改编自 ai-engineer-notebooks(MIT License)第 03 章 Hybrid Retrieval & Reranking(02-hybrid-and-reranking.ipynb)· github.com/calmrocks/ai-engineer-notebooks 运行方式:Colab 或本地 Jupyter 任选;模型用 Groq 免费 API(OpenAI 兼容),换成任何 OpenAI 兼容端点(含公司内部网关)只需改 base_url 与 key。

实验目标

把 BM25 关键词检索与向量检索用 RRF 融合成混合检索,再叠加一层 LLM 重排,并用逐阶段计时数据判断每一层「配不配得上它的成本」。对应本课时讲义「混合检索弥补词表失配」与「rerank 用更贵的精读换更准的相关性排序」。

前置条件

  1. 免费 GROQ_API_KEY(console.groq.com)。
  2. Colab 或本地 Jupyter;额外依赖 rank-bm25、sentence-transformers、numpy。
  3. 安装 aien 包(自带 groq 客户端)。

实验步骤

步骤 1:安装与初始化

%pip install -q "git+https://github.com/calmrocks/ai-engineer-notebooks.git@v0.1.0" requests sentence-transformers numpy rank-bm25

from aien import setup
client, MODEL = setup()

步骤 2:语料、切块与两路检索器

语料下载、清洗、切块、embedding 与上一实验(RAG 基础)完全一致,原 notebook 自包含复制了这些单元格,照搬即可。在此基础上加 BM25:

from rank_bm25 import BM25Okapi

def tokenize(text):  # 保留 Retry-After、Sec-WebSocket-Key 这类带符号标识符
    return re.findall(r'[a-z0-9][a-z0-9._-]*', text.lower())

bm25 = BM25Okapi([tokenize(t) for t in chunk_texts])

def bm25_search(query, k=5):
    scores = bm25.get_scores(tokenize(query))
    top = np.argsort(-scores)[:k]
    return [(float(scores[i]), int(i)) for i in top]
# vector_search 同上一实验:embedder 编码 query 后与 embeddings 点积取 top-k

步骤 3:两类查询,各有一个检索器赢

q1 = 'when does TCP send a RST segment?'  # 精确标识符:rst 是稀有词,BM25 碾压
q2 = 'how can a browser keep a two-way conversation open with a server?'  # 换说法:零关键词重叠,向量赢

for q in [q1, q2]:
    print(bm25_search(q)[:3])
    print(vector_search(q)[:3])   # 对比两个检索器各自的 top-3

预期:q1 上 BM25 直接命中定义 RST 行为的章节,向量检索只捞回「泛 TCP」文本;q2 上 BM25 几乎无词可抓(原文写的是 bidirectional / full-duplex),向量检索轻松落到 WebSocket RFC。生产查询流里两类都存在,所以两路都要。

步骤 4:RRF 融合

def rrf(rankings, k=60, top_n=20):
    """只用排名不用分数:每列贡献 1/(k+rank) 后累加;k=60 是标准常数。"""
    scores = {}
    for ranking in rankings:
        for rank, idx in enumerate(ranking):
            scores[idx] = scores.get(idx, 0.0) + 1.0 / (k + rank + 1)
    return sorted(scores.items(), key=lambda x: -x[1])[:top_n]

def hybrid_search(query, top_n=20, pool=50):
    v = [i for _, i in vector_search(query, pool)]
    b = [i for _, i in bm25_search(query, pool)]
    return rrf([v, b], top_n=top_n)

为什么不做分数加权平均:BM25 分数无界且依赖语料,余弦分数在 [-1,1],量纲不可比,任何加权和都是带旋钮的谎言;RRF 把分数扔掉只留排名。

步骤 5:LLM 重排与逐阶段计时

import time

def llm_score(query, passage):  # 点式重排:一次调用给一个段落打 0-10 分
    prompt = ('Rate how relevant this passage is for answering the query, '
              'on a scale of 0 (irrelevant) to 10 (directly answers it).\n\n'
              f'Query: {query}\n\nPassage:\n{passage[:1500]}\n\n'
              'Reply with a single integer 0-10 and nothing else.')
    resp = client.chat.completions.create(
        model=MODEL, max_tokens=8,
        messages=[{'role': 'user', 'content': prompt}])
    m = re.search(r'\d+', resp.choices[0].message.content)
    return min(int(m.group()), 10) if m else 0

def rerank(query, candidate_ids):  # 20 个候选 = 20 次 API 调用
    scored = [(llm_score(query, chunk_texts[i]), i) for i in candidate_ids]
    return sorted(scored, key=lambda x: -x[0])

q = 'how does a server tell a client to retry later?'
t0 = time.perf_counter(); v = vector_search(q, 50); t1 = time.perf_counter()
b = bm25_search(q, 50); t2 = time.perf_counter()
fused = rrf([[i for _, i in v], [i for _, i in b]], top_n=20); t3 = time.perf_counter()
reranked = rerank(q, [i for i, _ in fused]); t4 = time.perf_counter()
print(f'vector {(t1-t0)*1e3:.1f}ms  bm25 {(t2-t1)*1e3:.1f}ms  '
      f'rrf {(t3-t2)*1e3:.1f}ms  rerank {(t4-t3)*1e3:.0f}ms (20 次 API 调用)')

预期形态:BM25 与 RRF 近乎免费(微秒到毫秒级),LLM 重排串行要几十秒。典型结果里重排会把「真正定义行为的章节」提到「只是频繁提到该词」的章节前面——这正是词频和向量距离都看不见的 mention-vs-answer 区别。生产系统会用 cross-encoder(如 Cohere Rerank)把这个开销压低约两个数量级,概念完全相同。

自查问题

  1. 为什么 BM25 与向量「谁也替代不了谁」?各举一类只有对方能命中的查询。
  2. reranker 只跑融合后的 top-20 候选而不是全库,为什么?
  3. 「要不要加 rerank 层」为什么必须靠带已知答案的问题集和命中率来裁决,而不是看两个查询的眼缘?

随堂测

逐题作答,即时反馈

0 / 3 已作答
  1. 1. 为什么纯向量检索不够,需要加 BM25 全文检索?

  2. 2. Rerank 在 RAG 中的核心作用是什么?

  3. 3. 在 Dify 中混合检索后效果反而更差,正确的排查思路是?