Day 26 / 共 68 天 · 阶段 4 RAG
rerank & 混合检索:让捞回来的结果更准
昨天(Day 25)你学会了用余弦相似度取 top-k——这一步又快又便宜,但排在最前面的不一定最对。今天升级两招:rerank(精排)先粗召回一批、再用更强的模型精挑;混合检索把"关键词(BM25)"和"语义向量"两条渠道合起来,互相补漏。这是把 RAG 准确率从"能用"推到"好用"的关键;明天(Day 27)我们学怎么用指标量出到底准了多少。
📍 你在阶段 4(RAG D21-28)的位置
D24 向量库→
D25 检索→
D26 rerank&混合→
D27 评测→
D28 实战
💡 用一个类比兜住今天(今天全程沿用「公司招人筛简历」的世界观)
提升检索 = 优化一次招聘。粗召回(向量 top-k) = HR 海选,快速从上万份简历里捞出 50 份"看着像"的;rerank 精排 = 部门主管坐下来把这 50 份逐一细读、重新排名,选出最合适的 5 份;向量检索 = 看"气质/经历是否对味"(语义),但可能漏掉写了精确技能关键词的人;BM25 关键词检索 = 按"简历里有没有写 Python/K8s 这些词"精确匹配;混合检索 = 两个招聘渠道同时开、结果合并,谁都不漏。今天你从"只会海选的 HR"升级成"海选+面试双把关"。
L01
粗召回:快,但不够准
🤔 痛点昨天的向量 top-k,有时排第 1 的段落其实不是最该用的,真正的答案排在第 4、第 5,甚至被 k 的边界挡在门外。为什么?
💡 本质向量检索为了快做了妥协:它把整段文字压成一个向量再比,细节被"压扁"了。像 HR 海选只扫一眼简历排版和关键词密度,能快速筛掉大部分,但排名粗糙——会漏细节、排序不精。
向量检索的正式名字叫 bi-encoder(双塔):问题和文档各自独立编码成向量再比,快但粗。它的对手 rerank 用的是另一种更精但更慢的方式,下一讲讲。
📝 举个例子:粗排的翻车
问:
怎么申请年假? 向量粗召回排序可能是:①「年假的历史由来」(0.86,像但没用) ②「请假系统操作步骤」(0.84,才是真答案) ……真正该排第 1 的"操作步骤"却排在第 2。粗召回给了候选,但顺序需要有人重排——这就是 rerank 登场的地方。L02
两阶段:先海选,再精排
🤔 痛点那为什么不一上来就用"最准的方法"给全库排序?——因为最准的方法太慢,几万段挨个精算,用户等到花儿都谢了。
💡 本质业界标准答案是两阶段:①召回用快的向量检索,从全库捞出候选一批(比如 top-50);②精排(rerank)只对这 50 个用"慢而准"的模型重新打分排序,取最终 top-5。快的负责"广撒网别漏",准的负责"在小池子里挑精"。
图注:召回负责"别漏"(宽进),精排负责"挑精"(严出)。这是几乎所有严肃搜索/推荐系统的通用架构。
L03
rerank 是怎么做到更准的
🤔 痛点rerank 凭什么比向量检索准?不都是算相似度吗?
💡 本质关键区别:向量检索是问题、文档各编各的再比(省事但粗);rerank 用的 cross-encoder 把"问题 + 这段文档"拼在一起一整个喂进模型,让它们充分"对视"后直接打一个相关性分。像面试官把 JD 和这份简历并排逐字比对——慢,但准得多。
# rerank 用 cross-encoder:问题和每个候选"配对"后打分
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") # 一个开源精排模型
query = "怎么申请年假?"
candidates = ["年假的历史由来", "请假系统操作步骤", "食堂菜单"] # 来自①召回
# 把 (问题, 候选) 一对对喂进去,得到相关性分数
pairs = [[query, c] for c in candidates]
scores = reranker.predict(pairs) # 每对一个分,越高越相关
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
for text, s in ranked:
print(round(float(s), 2), text)
# → "请假系统操作步骤" 被精排提到第 1(粗排里它才第 2)
👶 那为啥不全程用 rerank因为 cross-encoder 要把"问题+文档"成对喂,几万段就得算几万次,慢到没法用。所以只让它处理召回后的几十个候选——快模型管广度、慢模型管精度,各司其职,这就是两阶段的精髓。云服务(如各家 rerank API)也是这个原理,你以后调 API 就行,不必自己训。
L04
纯向量的软肋:关键词对不上
🤔 痛点用户搜精确的产品型号
X-200 或专有名词 ERP-9527,向量检索有时反而找不准——它太"讲语义"了,对这种"必须一字不差匹配"的词不敏感。💡 本质向量擅长"意思相近"(把"电脑坏了"和"计算机故障"认成一伙),但对精确关键词、罕见术语、编号容易失手。招人时只看"气质对味",可能漏掉简历里白纸黑字写着"精通 K8s"的那个人——你需要一个会"抠关键词"的搭档。
📝 举个例子:语义 vs 关键词各擅胜场
查
查
笔记本电脑发热严重:向量能召回"散热差、烫手"等不同措辞的段(语义强)。查
错误码 E-404 含义:向量可能把它和一堆"错误、报错"泛泛的段混在一起,而关键词检索一眼锁定含 E-404 字样的那段(精确匹配强)。两者互补,缺一漏一半。L05
BM25:经典的关键词检索
🤔 痛点那"抠关键词"这条渠道用什么?总不能自己写字符串匹配吧。
💡 本质BM25 是几十年来最经典好用的关键词检索算法(搜索引擎、Elasticsearch 都用它)。直觉:一段文本里命中查询词越多、这些词越罕见(越有区分度)、这段又不过分冗长,就打越高分。不懂公式没关系,记住"它是精准抠词的老法师"即可。
from rank_bm25 import BM25Okapi # pip install rank-bm25
# 文档先分词(中文可用 jieba,这里用英文空格分词演示原理)
docs = ["error code E-404 means not found",
"how to apply for annual leave",
"the canteen menu today"]
tokenized = [d.split() for d in docs] # 切成词列表
bm25 = BM25Okapi(tokenized) # 建索引
query = "E-404".split()
scores = bm25.get_scores(query) # 给每段打关键词匹配分
for d, s in sorted(zip(docs, scores), key=lambda x: x[1], reverse=True):
print(round(s, 2), d)
# → 含 "E-404" 的那段分最高,精确命中
👶 BM25 和向量谁更好没有谁绝对更好,它俩擅长的场景不同:向量强在"换个说法也认得",BM25 强在"精确词/术语/编号"。所以聪明的做法不是二选一,而是都要——这就是下一讲的混合检索。
L06
混合检索:两条渠道结果融合
🤔 痛点向量一条榜、BM25 一条榜,两份排名不一样,最后到底信谁、怎么合成一份?
💡 本质混合检索(hybrid search) = 向量和 BM25 各出一份候选榜,再融合成一份总榜。最常用的融合叫 RRF(倒数排名融合):一段在两份榜里排得越靠前,合计分越高;两榜都看好的段自然冒到最上面。像招聘时两个渠道都推荐的候选人,优先约面。
图注:"E-404段"在 BM25 里第 1、向量里第 2,融合后稳居榜首——单靠任一渠道都可能把它排偏。
# RRF 融合:一段在某榜排名 r,贡献 1/(k+r) 分;两榜相加
def rrf(rank_lists, k=60): # rank_lists = [向量榜, BM25榜],每个是文档id的有序列表
scores = {}
for lst in rank_lists:
for rank, doc_id in enumerate(lst, start=1): # rank 从 1 开始
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True) # 合计分降序
vec_rank = ["A", "B", "C"] # 向量榜(A最像)
bm25_rank = ["B", "C", "A"] # BM25榜(B关键词最匹配)
print(rrf([vec_rank, bm25_rank])) # → 两榜都靠前的会排在最前
👶 小白:那我做 RAG 是不是必须又 rerank 又混合,全都上?
👨🏫 老师:别一上来全堆。先用最简单的纯向量 top-k 跑通,再看毛病对症下药:发现"换种说法就搜不到"→ 上混合检索补关键词;发现"候选里有对的但排序乱"→ 上 rerank 精排。每加一层都要用明天(Day 27)的评测数据证明"确实变好了"再留下,否则只是徒增复杂度和成本。先测量,再优化——这是本课反复强调的信条。
L07
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- 为什么向量 top-k 是"粗召回"?它粗在哪?
- 两阶段(召回→精排)各自负责什么?为什么不全程用最准的方法?
- rerank 的 cross-encoder 和向量检索的 bi-encoder 最大区别是什么?
- 纯向量检索的软肋是什么?BM25 恰好补了哪一块?
- 混合检索为什么比单渠道好?RRF 融合的直觉是什么?
✋ 动手 10 分钟:跑一次 BM25 + 感受精排
装库,把 L05 的 BM25 例子跑通,再自己加两段文档、换几个含"精确词"的查询,观察 BM25 是不是比"泛泛语义"更能锁定关键词:
python -m venv .venv && source .venv/bin/activate # 建并进车间
pip install rank-bm25 # 关键词检索库
# 新建 day26.py,粘入 L05 代码,运行:
python day26.py
进阶(选做):再 pip install sentence-transformers,把 L03 的 rerank 也跑一遍,亲眼看它把"粗排第 2"提到第 1。
明日预告 · Day 27:今天说了好几次"要用数据证明变好了"——明天就学怎么量。RAG 评测分两层:检索层(hit rate / MRR / NDCG / precision@k——该捞的段有没有捞到、排得够不够前)和生成层(faithfulness 答案有没有瞎编、relevancy 答得切不切题)。有了尺子,你的每一次优化才不是碰运气。明天还会给你一个去《LlamaIndex 20 天精讲》深潜评测工具的入口。