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 段不落盘重开直接 InternalError9.4 万向量白嵌一遍对 9.4 万×512 维的规模精确点积本来就是秒级基准口径还少了 ANN 的近似噪声。查询侧双编码时带不带指令前缀bge 官方检索用法也做了对照MRR10 高的那侧0.5042 vs 0.5001定为召回层。主变量两个重排开/关、候选深度 N10/20/50。指标 Hit1/5/10前 k 条里至少有一个正例即记命中、MRR10首个正例的倒数排名均值金标注到段落级。全部 CPU6 核 12 线程单次运行。二、主结果精排救回的不是召回是排序500 题全量跑完主表长这样配置Hit1Hit5Hit10MRR10仅召回cosine 直接取 top-100.3640.6960.7800.5042召回 精排候选池 100.4860.7420.7800.5931召回 精排候选池 200.5060.7820.8400.6229召回 精排候选池 500.5040.8020.8560.6291第一候选池不扩只换排序器Hit1 就涨了 12.2 个点0.364→0.486。候选池是同样的 top-10召回阶段 cosine 把真答案埋在中后位、把不相干内容顶到最前CrossEncoder 重排一遍就把它挪到第一。这一档 Hit10 纹丝不动0.780——池子没变大只是重新排队。这部分收益不花一分钱检索成本是精排层最白赚的钱。第二候选池从 10 开到 20Hit10 才动了0.780→0.840Hit1 再抬到 0.506。前 20 名之外还压着真答案的题靠扩池捞进来再精排才够得着 top-10。第三20 到 50 基本是白烧候选翻 2.5 倍精排算力翻 2.5 倍Hit10 只再涨 1.6 个点Hit1 反而回落 0.002MRR 象征性涨 0.006。top-20 是这组实验的甜点。对照召回基线精排候选池 20把 Hit1 从 0.364 抬到 0.506、MRR10 从 0.504 抬到 0.623。重排的价值主要在 Hit1 和 MRR也就是“把对的排到最前面”它改变不了命中上限那个上限由召回层和候选池决定。按查询长度分层看收益不是均摊的短查询≤8 字214 题MRR10 从 0.5114 到 0.6193长查询286 题从 0.4988 到 0.6256。长查询的重排收益略大、终点更高——信息量大、噪声候选多排序层越有用。三、救不回的题死在哪先归因再上精排重排不是万能的。把 500 题按 rerank20精排候选池 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 6rerank20 命中 420 题 384 36。51 题召回缺失是最值得先看的一类。比如“为什么京东商城打不开”这种故障类查询正例在当年的语料里可能压根没有——召回层把 top-50 都翻遍了也找不到CrossEncoder 再强也没得排。工程含义很直接精排只能救“正例在池里但排不进来”的题救不了“正例根本不在池里”的题。上线重排之前先量这个占比占比高说明该修召回扩 N、换更强的嵌入模型、上混合检索而不是急着加精排。在这组数据里10.2% 的题精排连参与资格都没有。36 题救回是真金白银。举一个查询“北京机动车过户地点”召回没把任何正例送进 top-106 个正例全部压在池子深处rerank20 把 3 个正例分别提到第 1、4、10 位——材料准备、过户流程那种长段落词面 overlap 少cosine 抓不到交叉编码能读懂“查的是过户要去哪办、要带什么”。这种“词面不沾边、语义相关”的题就是重排存在的理由。6 题误杀提醒别把重排当免费午餐。例“would like 的回答”召回阶段正确答案在 top-10rerank20 排序后掉到第 17 位、跌出 top-10rerank10 时它还在第 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别默认“公开数据集都很短”。坑 2CrossEncoder 的分数不是置信度。这是我最想拿数据打脸的一个误用。bge-reranker-base 输出的分数落在 0~1正例对平均 0.967、负例对平均 0.567——看着分得开陷阱在分布里全部 25,000 对候选的分数 P50 是 0.73负例里有 56.7% 分数 ≥ 0.535.4% 分数 ≥ 0.9。为什么因为这是 BM25 难负例 召回高分段落组成的池子负例本来就“长得像答案”分数普遍虚高。真拿固定阈值当过滤闸门试试设 0.5会误杀 1.7% 的正例、同时放进 56.7% 的负例把闸门抬到 0.9误杀涨到 7.9%放进来的负例还有 35.4%。两头吃亏。这套分数只配组内排序用——“这段比那段更像答案”成立“这段分数超过 0.8 所以可信”不成立。五、成本口径精排的贵是按数量级算的项数值查询嵌入bge-smallCPU7.4 ms/查询余弦检索 top-5093,885 段2.7 ms/查询精排候选池 2020 对 pair约 5.9 s/查询精排候选池 5050 对 pair14.8 s/查询精排吞吐3.4 pair/sCPU模型体积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。换来的收益锚点就一句Hit1 从 0.364 到 0.506MRR 从 0.504 到 0.623——值不值拿你自己的查询分布和延迟预算套一下就有数。六、工程结论精排干的是“把对的往前挪”的活候选池不扩、只换排序器Hit1 就涨 12.2 个点它对已召回的题生效对召回上限无能为力。候选池 20 是甜点10→20 有肉Hit1 再 2、Hit10 620→50 基本白烧——先按你自己的数据扫一遍 N别默认越大越好。上精排前先做失效归因这组数据 10.2% 的题正例不在 top-50精排无米下锅召回缺失占比高时先修召回再上精排。精排会误杀评估看净收益救回 36、误杀 6净 30 才是它的真实贡献只报救回数是自欺。分数别当置信度难负例池里负例 35% 以上分数过 0.9固定阈值过滤两头吃亏rerank 分数只做组内排序。成本按数量级算召回 10 ms、精排 6 sCPU离线或 GPU 场景随便上在线 CPU 场景先压缩候选池。数据核验说明语料与查询来自 DuReader-retrieval dev splitHuggingFacezyznull/dureader-retrieval-ranking数据本体不入 gitsrc/download_data.py一条命令从 hf-mirror 拉取段落池 93,885 篇 全部正例 ∪ 负例去重正例在池内 100% 核验查询抽样固定种子 42500/2000可复现每查询正例平均 3.16 个全部实验单次运行无重复CPU6 核 12 线程口径嵌入 bge-small-zh-v1.5512 维余弦重排 BAAI/bge-reranker-basemax_length512检索为 numpy 精确余弦非 ANN 近似无近似噪声指标口径top-k 含 ≥1 正例记 hitMRR10 取首个正例排名倒数召回层采用带指令前缀一侧MRR10 0.5042 vs 无前缀 0.5001两侧数字均如实入库候选池非 866k 全库MRR 不与官方 leaderboard 直接对比池内为 BM25 难负例难度高于随机采样截断口径嵌入侧统计全语料段落、重排侧统计 500×50 25,000 对实际 pair均按 max_length512 token 判定42,467 字长尾段落计入超窗统计未单独归档归因口径以 rerank20 对照召回基线 top-10五类互斥计数500 题分配合计验证通过单次运行、n500报分层观察不做显著性检验。一条命令复现gitclone https://github.com/ethanliang2016/rag-labcdrag-lab pipinstall-rrequirements.txt python src/download_data.pypython src/prepare_dataset.py python src/build_index.pypython 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 代码治理都在这
上一篇/下一篇内容由系统自动关联
返回资讯列表 →