RAG 实战第三课:reranker 精排 + 给知识库打分,从能跑变测得准
生命不息,折腾不止。前两课把知识库做到「能存能查」了,但还差两口气:检索回来的候选到底谁该排第一?调来调去,怎么证明自己越调越好?这一课补上 reranker 精排 + 评测打分,让知识库从「能跑」变成「测得准」。
上一课我们把 BM25 + 向量检索用 RRF 融合,能同时召回「精确命中」和「语义相关」的 chunk。但一个新问题冒出来了:召回是宽进宽出,一次给你 top50、top100 个候选,谁最相关、谁该先喂给大模型,纯靠原来的分数排序其实拿不准。另外,你改个切分参数、换了个 embedding,到底有没有变好,不能靠感觉,得有个数字说话。
这就是这一课要干的活:加一层 reranker 精排 + 写一个评测脚本。
一、先搞清楚:召回和精排是两个阶段
RAG 检索里,模型分两种思路,成本天差地别:
- Bi-Encoder(双塔,召回用):query 和文档各自单独编码成一个向量,用余弦/点积算相似度。优点是快、能预计算海量文档向量;缺点是「各算各的」,query 和文档之间没发生真正的交互,精细的相关性判断不准。
- Cross-Encoder(交叉编码,精排用):把 query 和文档拼接成一段一起丢进 transformer,直接输出一个相关性分数。优点是准得多;缺点是没法预计算、一次只能处理一对,慢。
所以业内的标准玩法是两段式:先用双塔(上一课的 FAISS + BM25)把全库成千上万篇快速召回成 top50~100 个候选,再用 Cross-Encoder 把这个小集合精排一遍,取最靠前的几个塞给大模型。一句话记住:
召回负责「别漏」,精排负责「别乱」。
你不用担心 Cross-Encoder 慢——它只对着几十个候选跑,不是对着全库跑,时间完全可控。
二、上一个 reranker:一个 CrossEncoder 就够
精排最省事的是直接用现成的 reranker 模型。中文场景我推荐 BAAI/bge-reranker-v2-m3,多语言、中文效果稳,就长这样:
1 | from sentence_transformers import CrossEncoder |
用法就是:先用混合检索召回 top100,再把 top100 丢给 reranker 精排出 top8,最后喂给大模型的就这 8 个。代码逻辑是:
1 | top100 = hybrid_search(query, k=100) # 上一课的 BM25 + 向量 + RRF |
其他可选的 reranker 也顺手列一下,按需换:
BAAI/bge-reranker-base、BAAI/bge-reranker-large:同门的更小/更大号,精度阶梯。cross-encoder/ms-marco-MiniLM-L6-v2:纯英文、只有 22M 参数,跑得快,适合英文语料想省资源的场景。- Cohere / Jina 的 Rerank API:不想本地跑模型,就调云 API,按调用计费。
记一个关键差异:向量 cosine 是在「各算各的向量」上比相似,Cross-Encoder 是把两段文字塞一起精算,所以精排后的排名通常明显更顺。光嘴上说没用,下一节教你怎么用数字证明它。
三、给 RAG 打分:先搞懂这几个指标
要评测,得先有一份测试集:一组 (问题, 标准答案chunk),比如「ERR_500 怎么处理」该命中哪几个 chunk id,手工标好。有了标准,才谈得上打分。
检索层只看「该找的找到没、排没排对」,最常用这五个:
- Recall@k(召回率):标准相关的 chunk 里,有多少进了 top-k。这是 RAG 最关键的指标——相关 chunk 没进上下文,大模型再怎么牛也答不对。目标一般 ≥ 0.8(k=5)。
- Precision@k(精确率):top-k 里有多少是真的相关。它管的是「别塞一堆垃圾进上下文」,k 越大越容易掉。
- Hit@k(命中率):top-k 里只要命中至少一个相关 chunk 就算 1,否则 0,最后取平均。最直观的「这题答没答到点」。
- MRR(平均倒数排名):看第一个相关 chunk 排在第几位,取排名倒数再平均。排第 1 得分 1,排第 5 得分 0.2。衡量「好东西排得靠不靠前」。
- NDCG@k(可选):带分级相关度(相关/非常相关)的排序质量,需要更细的标注才值得用,初学先跳过。
一句话区分盲点,方便你选指标:
| 指标 | 回答的问题 | 盲点 |
|---|---|---|
| Recall@k | 该找的都找到没 | 不管排名、不管混进来的噪音 |
| Precision@k | 找回来的干净不干净 | 不管是不是漏了相关 chunk |
| Hit@k | 至少命中一个没 | 只认「有没有」,不认「排第几」 |
| MRR | 第一个相关的排得靠前不 | 只盯第一个命中,后面的不管 |
四、写一个一劳永逸的评测脚本
下面这个脚本把上面五样指标一次性算出来,而且能对比精排前后,让你亲眼看到 reranker 到底有没有用:
1 | def evaluate(test_set, retrieve_fn, rerank_fn=None, k=5): |
跑一版,你会看到类似这样的结果(示意值):
1 | 无 reranker: {'recall@5': 0.71, 'precision@5': 0.46, 'hit@5': 0.78, 'mrr': 0.52} |
数字一摆,精排值不值得上,一目了然。改切分、换 embedding、调 k,都用这同一份脚本跑,改动好坏不再靠玄学。
五、这几个坑,踩过才知道
- 精排前召回要留够。reranker 是「从候选里挑」,候选没召回全,它再神也白搭。所以精排一定放在「宽召回 top50~100」之后,别图省事把召回 k 也设成 8。
- reranker 只对候选跑。别犯「对全库每个 chunk 都跑一遍 CrossEncoder」的错,速度会教你做人。
- 评测要分层。retrieval 层(recall/MRR/hit)和 generation 层(忠实度、答案正确性)是两码事,得分开测。recall 低是检索的问题,recall 高但答案胡扯是生成的问题,混在一起只会误导你。
- 指标涨了要回看样本。一个 aggregate 数字看不出门道,把「加了 rerank 反而掉」的 query 挑出来看几眼,往往能发现测试集标注或切分的问题。
- 测试集要跟线上分布一致。你拿技术文档的测试集测出来 recall 0.9,上线用户问的是口语化问题,指标就全骗了你。
六、小结
这一课给知识库补上了「眼睛」和「尺子」:用 CrossEncoder 精排,让最相关的 chunk 稳坐头名;用 recall/MRR/hit 这些指标,把每次改动的好坏量化出来。到这一步,你的 RAG 检索端算是「测得准、调得动」了。
但还有个关没打通:把检索到的上下文变成真正给人看的答案——生成层还没接上。
生命不息,折腾不止。下一篇 RAG 实战第四课:把召回的上下文喂给大模型,生成带引用来源的答案,并补上 faithfulness(忠实度)、答案正确性这些生成层指标——让整条「召回 → 精排 → 生成」链路真正闭环,答得不光有据,还能摊开给你看出处。回见。