news 2026/9/12 19:40:54

RAG 重排序实测:双编码召回 + CrossEncoder 精排,500 条真实查询上的收益与代价

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG 重排序实测:双编码召回 + CrossEncoder 精排,500 条真实查询上的收益与代价

本文是「RAG 链路实测」第 2 篇 · 上篇:RAG 分块策略实测

后端做了多年了,推荐、搜索都碰过,对这套分层不陌生:召回用便宜的向量检索把十万候选筛到几百,精排用贵的模型把几百排到几十。RAG(检索增强生成,Retrieval-Augmented Generation)的「双编码召回 + CrossEncoder(交叉编码器)重排」是同一件事换了相似度函数——双编码把 query 和段落各压成一个向量,离线可算、在线只做点积,快但糙;交叉编码把 query 和段落拼起来过一遍模型,准但每对都要现算。这篇用公开基准把精排层单独拆出来测:它到底能救回多少分,救不回的题死在哪,每次查询要多付多少毫秒——不,是几秒。

一、实验怎么搭的

为什么换公开数据集:第 1 篇用自建语料和自建题,结论外推性存疑。这篇直接上 DuReader-retrieval——百度/BAAI 出的中文段落检索数据集,查询来自真实百度搜索,正例段落人工标注。测试集不是“我出的题”,是别人标的金标准,对读者更硬。

数据口径(从data/dureader/三个 jsonl 可完整复现):

查询dev split 2000 条中固定种子抽 500 条(真实搜索 query,平均 9 字)
语料池全部正例∪负例去重段落 93,885 篇(平均 328 字 / 中位 275 字 / 最长 42,467 字)
标注每查询平均 3.16 个正例段落;负例为 BM25 难负例

诚实口径:语料池是 dev 候选池,不是 866k 全库——池内全是难负例,难度高于随机采样,MRR 不能跟官方全库 leaderboard 直接对比。候选池口径的好处是 CPU 跑得动,而且难负例把重排的价值空间真实压了出来。

管线

查询 → bge-small-zh-v1.5 双编码召回 top-N → [bge-reranker-base 交叉编码重排] → 指标

召回层与第 1 篇同款(bge-small-zh-v1.5,余弦)。9.4 万段落的嵌入只算一次、向量矩阵落盘复用——重排实验换的只是排序层,不碰索引。检索用 numpy 精确余弦而非 ANN(近似最近邻)近似:这里踩了个真实的坑,Windows 上 Chroma PersistentClient 进程正常退出后 HNSW 段不落盘,重开直接 InternalError,9.4 万向量白嵌一遍;对 9.4 万×512 维的规模,精确点积本来就是秒级,基准口径还少了 ANN 的近似噪声。查询侧双编码时带不带指令前缀(bge 官方检索用法)也做了对照,MRR@10 高的那侧(0.5042 vs 0.5001)定为召回层。

主变量两个:重排开/关、候选深度 N(10/20/50)。指标 Hit@1/5/10(前 k 条里至少有一个正例即记命中)、MRR@10(首个正例的倒数排名均值),金标注到段落级。全部 CPU(6 核 12 线程)单次运行。

二、主结果:精排救回的不是召回,是排序

500 题全量跑完,主表长这样:

配置Hit@1Hit@5Hit@10MRR@10
仅召回(cosine 直接取 top-10)0.3640.6960.7800.5042
召回 + 精排(候选池 10)0.4860.7420.7800.5931
召回 + 精排(候选池 20)0.5060.7820.8400.6229
召回 + 精排(候选池 50)0.5040.8020.8560.6291

第一,候选池不扩,只换排序器,Hit@1 就涨了 12.2 个点(0.364→0.486)。候选池是同样的 top-10,召回阶段 cosine 把真答案埋在中后位、把不相干内容顶到最前,CrossEncoder 重排一遍就把它挪到第一。这一档 Hit@10 纹丝不动(0.780)——池子没变大,只是重新排队。这部分收益不花一分钱检索成本,是精排层最白赚的钱。

第二,候选池从 10 开到 20,Hit@10 才动了(0.780→0.840),Hit@1 再抬到 0.506。前 20 名之外还压着真答案的题,靠扩池捞进来再精排,才够得着 top-10。

第三,20 到 50 基本是白烧:候选翻 2.5 倍,精排算力翻 2.5 倍,Hit@10 只再涨 1.6 个点,Hit@1 反而回落 0.002,MRR 象征性涨 0.006。top-20 是这组实验的甜点。

对照召回基线,精排(候选池 20)把 Hit@1 从 0.364 抬到 0.506、MRR@10 从 0.504 抬到 0.623。重排的价值主要在 Hit@1 和 MRR,也就是“把对的排到最前面”;它改变不了命中上限,那个上限由召回层和候选池决定。

按查询长度分层看,收益不是均摊的:短查询(≤8 字,214 题)MRR@10 从 0.5114 到 0.6193,长查询(286 题)从 0.4988 到 0.6256。长查询的重排收益略大、终点更高——信息量大、噪声候选多,排序层越有用。

三、救不回的题死在哪:先归因,再上精排

重排不是万能的。把 500 题按 rerank@20(精排候选池 20)对照召回基线 top-10 做失效归因,结果分五类:

类别题数占比含义
召回缺失5110.2%正例压根不在 top-50,精排无米下锅
精排救回367.2%召回没进 top-10,精排拉进来了
精排误杀61.2%召回在 top-10,精排后掉出去了
排不进去234.6%正例在 top-50 池里,但两轮都没进 top-10
两轮都在 top-1038476.8%无争议命中

五类分配合计严丝合缝:召回基线 top-10 命中 390 题 = 384 + 6;rerank@20 命中 420 题 = 384 + 36。

51 题召回缺失是最值得先看的一类。比如“为什么京东商城打不开”这种故障类查询,正例在当年的语料里可能压根没有——召回层把 top-50 都翻遍了也找不到,CrossEncoder 再强也没得排。工程含义很直接:精排只能救“正例在池里但排不进来”的题,救不了“正例根本不在池里”的题。上线重排之前先量这个占比:占比高,说明该修召回(扩 N、换更强的嵌入模型、上混合检索),而不是急着加精排。在这组数据里,10.2% 的题精排连参与资格都没有。

36 题救回是真金白银。举一个:查询“北京机动车过户地点”,召回没把任何正例送进 top-10(6 个正例全部压在池子深处),rerank@20 把 3 个正例分别提到第 1、4、10 位——材料准备、过户流程那种长段落,词面 overlap 少,cosine 抓不到,交叉编码能读懂“查的是过户要去哪办、要带什么”。这种“词面不沾边、语义相关”的题,就是重排存在的理由。

6 题误杀提醒别把重排当免费午餐。例:“would like 的回答”,召回阶段正确答案在 top-10,rerank@20 排序后掉到第 17 位、跌出 top-10(rerank@10 时它还在第 9 位——扩池捞进更多候选,反而把它挤了出去)。误杀率低(1.2%),但不是零——精排的输出是模型主观排序,会犯错。评估时别只看救回了多少,净收益(36 − 6 = 30 题)才是精排的真实贡献。

四、两个预埋的坑,都踩实了

坑 1:双窗截断,这次两边占比都不高,但长尾是真凶。bge-small 嵌入窗 512 token、CrossEncoder 的 pair 也是 512 token,超窗静默截断。实测:嵌入侧全语料 93,885 段里 7,173 段超窗,占 7.64%;重排侧 25,000 对 pair 里 1,173 对超窗,占 4.69%。比第 1 篇 fixed-1024 那个 90% 的截断率温和得多——DuReader 段落中位才 275 字。但最长的那段 42,467 字是必然超窗的,语料里这种百倍于窗口的长尾段落一旦被查询命中,嵌入侧和重排侧都在截断后的内容上打分。段落级语料同样要查max_seq_length,别默认“公开数据集都很短”。

坑 2:CrossEncoder 的分数不是置信度。这是我最想拿数据打脸的一个误用。bge-reranker-base 输出的分数落在 0~1,正例对平均 0.967、负例对平均 0.567——看着分得开?陷阱在分布里:全部 25,000 对候选的分数 P50 是 0.73,负例里有 56.7% 分数 ≥ 0.5,35.4% 分数 ≥ 0.9。为什么?因为这是 BM25 难负例 + 召回高分段落组成的池子,负例本来就“长得像答案”,分数普遍虚高。真拿固定阈值当过滤闸门试试:设 0.5,会误杀 1.7% 的正例、同时放进 56.7% 的负例;把闸门抬到 0.9,误杀涨到 7.9%,放进来的负例还有 35.4%。两头吃亏。这套分数只配组内排序用——“这段比那段更像答案”成立,“这段分数超过 0.8 所以可信”不成立。

五、成本口径:精排的贵,是按数量级算的

数值
查询嵌入(bge-small,CPU)7.4 ms/查询
余弦检索 top-50(93,885 段)2.7 ms/查询
精排候选池 20(20 对 pair)约 5.9 s/查询
精排候选池 50(50 对 pair)14.8 s/查询
精排吞吐3.4 pair/s(CPU)
模型体积bge-small 约 92 MB / bge-reranker-base 约 1.06 GB
索引构建(一次性,93,885 段)约 74 分钟,21.1 段/s

这组数把“贵”讲清楚了:召回全流程加起来 10 毫秒,精排一个 query 的 20 对候选要近 6 秒——约 600 倍,快三个数量级。模型体积也从 92 MB 跳到 1.06 GB,参数规模差约一个量级(24M vs 278M)。索引那 74 分钟是一次性成本,嵌入只算一次,之后换排序层、调 N 都不碰它。

值不值,取决于场景:离线精排(先粗排 200 条存下来,夜里把 top-20 精排好)几乎白赚;在线实时 RAG 要掂量,CPU 上 6 秒对交互不可接受,得上 GPU(批量时吞吐能拉上去)或者把候选池压到 10。换来的收益锚点就一句:Hit@1 从 0.364 到 0.506,MRR 从 0.504 到 0.623——值不值,拿你自己的查询分布和延迟预算套一下就有数。

六、工程结论

  1. 精排干的是“把对的往前挪”的活:候选池不扩、只换排序器,Hit@1 就涨 12.2 个点;它对已召回的题生效,对召回上限无能为力。
  2. 候选池 20 是甜点:10→20 有肉(Hit@1 再 +2、Hit@10 +6),20→50 基本白烧——先按你自己的数据扫一遍 N,别默认越大越好。
  3. 上精排前先做失效归因:这组数据 10.2% 的题正例不在 top-50,精排无米下锅;召回缺失占比高时,先修召回再上精排。
  4. 精排会误杀,评估看净收益:救回 36、误杀 6,净 +30 才是它的真实贡献;只报救回数是自欺。
  5. 分数别当置信度:难负例池里负例 35% 以上分数过 0.9,固定阈值过滤两头吃亏;rerank 分数只做组内排序。
  6. 成本按数量级算:召回 10 ms、精排 6 s(CPU);离线或 GPU 场景随便上,在线 CPU 场景先压缩候选池。

数据核验说明

  • 语料与查询来自 DuReader-retrieval dev split(HuggingFacezyznull/dureader-retrieval-ranking),数据本体不入 git,src/download_data.py一条命令从 hf-mirror 拉取;段落池 93,885 篇 = 全部正例 ∪ 负例去重,正例在池内 100% 核验;
  • 查询抽样固定种子 42,500/2000,可复现;每查询正例平均 3.16 个;
  • 全部实验单次运行(无重复),CPU(6 核 12 线程)口径;嵌入 bge-small-zh-v1.5(512 维,余弦),重排 BAAI/bge-reranker-base(max_length=512);检索为 numpy 精确余弦(非 ANN 近似),无近似噪声;
  • 指标口径:top-k 含 ≥1 正例记 hit,MRR@10 取首个正例排名倒数;召回层采用带指令前缀一侧(MRR@10 0.5042 vs 无前缀 0.5001),两侧数字均如实入库;
  • 候选池非 866k 全库,MRR 不与官方 leaderboard 直接对比;池内为 BM25 难负例,难度高于随机采样;
  • 截断口径:嵌入侧统计全语料段落、重排侧统计 500×50 = 25,000 对实际 pair,均按 max_length=512 token 判定;42,467 字长尾段落计入超窗统计,未单独归档;
  • 归因口径:以 rerank@20 对照召回基线 top-10,五类互斥计数,500 题分配合计验证通过;
  • 单次运行、n=500,报分层观察,不做显著性检验。

一条命令复现

gitclone https://github.com/ethanliang2016/rag-lab&&cdrag-lab pipinstall-rrequirements.txt python src/download_data.py&&python src/prepare_dataset.py python src/build_index.py&&python src/run_rerank.py

模型自动从 hf-mirror 下载,全部结果(含逐查询命中记录、失效归因明细、截断与 logit 统计)在results/rerank_results.json。跑批支持断点续跑(--resume),算到一半断电不丢进度。


留个问题:你们的 RAG 现在上精排了吗?候选池开多大、延迟预算给多少?评论区聊聊,下一篇我拿生成层来补完这条链路。

相关实测

  • 《RAG 分块策略实测:固定窗口 vs 递归切分 vs 结构感知,用 100 道题说话》—— 分块榨不出分,才要往下游要,这是本系列第 1 篇
  • 《让大模型给 6,208 条答案打分:判对率 94%,逐字正确率 6%》—— 效果好不好,先得有个可信的尺子

完整导航:博客导航|agent 安全 / RAG 实测 / AI 代码治理,都在这

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 19:40:45

LangChain Model I/O架构与多模型调用实践

1. LangChain Model I/O 核心架构解析LangChain的Model I/O模块是整个框架与各类大模型交互的核心枢纽。它通过标准化的接口设计,实现了对不同模型提供商的统一接入能力。这种设计模式让开发者能够以相同的方式调用OpenAI、Anthropic、Google等不同厂商的模型服务。…

作者头像 李华
网站建设 2026/9/12 19:40:10

AD8065AR基线噪声超标根因与实战整改指南

1. 项目概述:为什么AD8065AR的输出端基线噪声会“悄悄爬高” 我第一次在高速数据采集板上看到AD8065AR的输出波形时,心里就咯噔一下——示波器底噪本该是干净平滑的一条线,结果它像被微风吹皱的湖面,始终浮着一层细密、均匀、不随…

作者头像 李华
网站建设 2026/9/12 19:38:52

AIGC技术如何提升在线教育教师备课效率

1. 在线教育教师的备课痛点与AIGC机遇在线教育行业近年来呈现爆发式增长,但教师备课效率低下成为普遍痛点。根据行业调研数据显示,教师平均每周需要花费15-20小时在课程准备上,其中约60%时间消耗在内容搜集、课件制作和教学材料整理等重复性工…

作者头像 李华
网站建设 2026/9/12 19:38:28

3C零件厚度测量传感器怎么选?MLD25激光位移传感器选型与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 19:36:04

LubanCat RK3588 实时 Linux 开发(二):picocom 串口调试完整指南

摘要:介绍 /dev/ttyUSB0 的识别、LubanCat 常用串口参数、picocom 快捷键和网络失效时的串口排障方法。 适用对象:需要通过 USB-TTL 串口维护 LubanCat 的开发者。串口电平、接线和设备名必须按实际硬件确认。 文章目录本篇要解决什么问题串口调试链路为…

作者头像 李华
网站建设 2026/9/12 19:33:01

【读点论文】SAM 2: Segment Anything in Images and Videos

SAM 2: Segment Anything in Images and Videos abstract 我们提出了 Segment Anything Model 2 (SAM 2),这是一个用于解决图像和视频中可提示的视觉分割的基础模型。我们构建了一个数据引擎,通过用户交互改进模型和数据,以收集迄今为止最大的…

作者头像 李华