
1. 从整体链路看召回与重排1.1 先想清楚一个问题你为什么要单独做重排很多人一上来就追着问重排模型用哪个cross-encoder 是不是吊打 bi-encoder但我想先泼一盆冷水如果你的召回阶段没做好重排只是给一堆垃圾排序。RAG 的整体链路可以拆成索引、召回、重排、生成四段。索引解决的是文档怎么存召回解决的是候选从哪来重排解决的是谁最该被送进大模型生成解决的是答案怎么说。前两段决定了天花板后两段决定了下限。重排确实能把召回结果里被埋没的相关内容捞回来但它永远无法找回召回阶段就已经丢掉的文档。我见过不少项目在召回阶段用了一个最简单的 BM25然后花大力气调重排模型结果线上指标纹丝不动。为什么因为 BM25 召回的候选里相关文档压根没进来重排再准也只能对着错误集合打分。所以做召回与重排的第一原则是先盘点你的召回候选集质量再谈重排精度。那是不是说重排不重要不是。重排的价值体现在召回的候选集够宽但不够准的时候。现实场景里我们通常会取回 50 到 100 条候选送进重排然后只留 Top 5 到 Top 10 给生成阶段。为什么不让大模型直接看 100 条因为上下文窗口有限、token 成本高、噪音还会稀释注意力。重排就是那个从 100 缩到 10的关键角色。这篇实战笔记我会围绕召回与重排的完整链路展开涉及的方案包括稀疏检索、稠密检索、混合召回、cross-encoder 重排、LLM 重排以及它们背后的选型逻辑和工程落地方案。适合正在做 RAG 应用、被召回不准、重排无效困扰的开发者和算法工程师参考。1.2 先建立一套统一评估口径动手之前我强烈建议你先定义清楚两个指标召回率Recall和命中率Hit Rate。很多项目做完召回和重排发现效果好像好了但一问怎么评估的答案往往是看起来差不多——这种靠感觉调系统的做法大概率会在上线后翻车。我给团队定的评估口径是这样指标定义作用RecallK前 K 条结果中相关文档数 / 总相关文档数衡量召回能力Hit RateK前 K 条结果中是否至少包含一条相关文档衡量系统可用性MRR第一个相关文档排名的倒数均值衡量排序质量NDCGK考虑位置权重的排序质量衡量重排精细度实践中我通常会先盯 Recall50确保召回侧能把相关文档捞进候选集。如果 Recall50 都不到 80%说明索引或召回策略有问题这时候调重排纯粹是浪费算力。等 Recall 达标了再盯 NDCG10这时候重排的收益才会真实体现出来。这个评估口径是整个技术方案的地基后面所有调优都得回到这个坐标系里看。有了统一口径你才能判断某个改动到底有没有用而不是靠感觉更准了这种玄学。2. 召回技术实战稀疏、稠密与混合召回2.1 BM25 为什么至今没有退出历史舞台说到召回很多人第一反应是现在都 2025 年了谁还用 BM25——我还在用而且我不觉得这有什么丢人的。BM25 属于稀疏检索核心逻辑是词频加逆文档频率的加权匹配。它的优点是快、稳、可解释缺点是词汇鸿沟用户问怎么退款文档里写的是退货流程字面不匹配它就召回不到。但恰恰是这个缺陷让它和稠密检索形成了互补关系。BM25 擅长精确匹配专有名词、代码片段、型号、编号比如CUDA 12.1 编译报错这种查询稠密检索反而容易因为语义向量把关键信息磨平了。我现在做 RAG 召回的第一版永远是先上 BM25因为它能在五分钟内给我一个不丢脸的基线。工程实现上我常用 Elasticsearch 或者 OpenSearch 的 BM25 实现。关键参数是k1和b。默认值k11.2, b0.75在大多数场景都够用但如果你发现长文档总是排太靠后可以把b调低到 0.5 左右减弱文档长度归一化的惩罚。如果查询词频偏高k1可以往 2.0 调让词频的贡献更饱和一些。这些参数不用死记理解原理后根据数据特征微调即可。BM25 还有一个容易被忽略的用法对 query 本身做预处理。我一直坚持对查询做分词、停用词过滤、同义词扩展。同义词这块初期可以维护一个小的同义词词典比如退款对应退货退钱refund后期再考虑用词向量自动挖掘。这些小改动在字面匹配场景下的收益往往比换一个更大的模型更直接。2.2 稠密检索向量相似度背后的三个隐性成本稠密检索解决的是语义匹配问题。它通过 embedding 模型把 query 和文档映射到同一个向量空间然后用余弦相似度或内积找最近邻。原理不复杂但落地时容易被三个隐性成本坑到。第一个是向量索引的构建成本。别以为随便调个 embedding API 就完事了你需要决定用哪种索引结构。小规模数据百万级以内用暴力搜索Flat就行精确但不快大规模数据必须用 HNSWHierarchical Navigable Small World这类近似最近邻索引。HNSW 的M每个节点的最大连接数和efConstruction构建时的动态列表长度直接影响召回精度和内存占用。我一般先设M16efConstruction100然后在评估集上试不同参数看 Recall10 的变化不会一上来就追求极致参数。第二个是embedding 模型的数据适配。通用 embedding 模型在垂直领域往往表现一般。我踩过的坑是直接用某个开源通用模型处理客服工单结果电池鼓包和电池保养被算成高度相似因为它们在通用语义空间里太接近了。后来我在领域数据上做了微调效果才明显改善。如果你没有微调条件至少要做领域词典增强把核心术语拼进文本再做 embedding。第三个是向量相似度≠业务相关度。余弦相似度 0.85 在技术问答里可能很相关在商品检索里可能完全不相关。所以我会根据场景调整分数阈值技术上可以尝试对向量做归一化后使用点积语义上则要结合业务规则做后置过滤。这一点到重排阶段会更明显。2.3 混合召回不是简单拼接分数把 BM25 和向量检索结果合并就是我说的混合召回。但很多人做混合召回时有个误区把两个分数直接相加或者简单做个加权和。我试过效果通常不稳定因为 BM25 分数和余弦相似度根本不在一个量纲里。更靠谱的做法是归一化后加权。我会先对两个通道的分数分别做 min-max 归一化或 z-score 归一化再按权重合并。权重怎么定不要拍脑袋直接在验证集上搜。我常用的方式是网格搜索alpha从 0.1 到 0.9步长 0.1取验证集 Recall50 最高的组合。很多时候你会发现alpha0.3即 BM25 占三成、向量占七成比 0.5 均衡效果好因为向量检索贡献了更多的语义多样性。另一个更稳妥的方案是RRFReciprocal Rank Fusion。它不依赖分数只依赖排名。对每个候选文档统计它在两个通道里的排名位次然后按1/(krank)累积得分。k一般取 60 左右。RRF 的好处是鲁棒不需要调权重但代价是没法精细控制通道贡献度。我的建议是冷启动用 RRF有评估数据后再切到归一化加权。混合召回的候选集合并之后还需要做一步去重。特别要注意近似段落重复的问题比如同一篇文档里多个段落描述相似内容去重时既要去掉完全重复也要考虑按滑动窗口切分造成的近乎重复段落。我习惯用 MinHash 做一次粗粒度去重再用 SimHash 做细粒度过滤能有效减少后续重排的计算浪费。2.4 召回阶段的分块策略容易被低估的杠杆召回效果不好很多人先怀疑模型其实分块策略才是第一杠杆。块太大语义被稀释向量检索精度下降块太小上下文不完整BM25 匹配率下降。我的经验是从 256 到 512 token 开始试配合 50 到 100 token 的重叠。为什么要有重叠举个例子某份文档在中间第 400 token 处定义了一个专业名词如果块切在第 350 token 结束下一块从 400 开始那这个定义就和前文分开了语义链断裂。重叠能让边界处的上下文有一定延续性。更进阶一点的做法是结构感知分块。PDF 或 HTML 转出来的文档天然有标题、段落层级按层级切块往往比按固定 token 数切块效果好。我在处理长文档时会先把 Markdown 标题作为切分边界再对超长章节做二次切分。索引时除了存正文还要把标题层级路径作为元数据一起存召回后可以拼进上下文重排和生成阶段都会受益。另一个细节是块 ID 与来源映射。给每个块一个全局唯一 ID同时记录它属于哪篇文档、哪个章节、原始页码。后面做重排、引用溯源、答案校验都需要这些信息别等到上线才发现查不到来源。3. 重排技术实战从 cross-encoder 到 LLM 重排3.1 cross-encoder 为什么比 bi-encoder 更准召回阶段用的 bi-encoder 把 query 和文档分别编码成向量再算相似度好处是文档向量可以离线算好在线只算 query 向量速度快。但它有个致命缺陷query 和文档在编码阶段完全没有交互信息损失发生在编码那一刻。cross-encoder 就不一样了。它把 query 和文档拼接成一句话送进模型注意力机制可以在 token 级别让两边充分交互。所以 cross-encoder 的排序精度通常显著优于 bi-encoder。代价是速度慢——每个候选对都要过一次模型100 条候选就是 100 次推理。工程上我一般用这样的链路召回阶段用 bi-encoder 快速取回 50 到 100 条候选重排阶段用 cross-encoder 精排到 Top 5 到 10。召回负责宽重排负责准各司其职这是目前性价比最高的组合。选 cross-encoder 时要注意模型的输入长度限制。很多经典模型最长只支持 512 token如果你的文档块超过这个长度需要截断或者选支持更长的变体。另外要注意别把重排模型的分数当概率用它只是一个相对排序分数不同 query 之间的分数没有可比性更不能直接拿去和生成阶段的阈值做硬比较。3.2 重排模型微调数据怎么来标注怎么做预训练的 cross-encoder 在通用领域表现尚可但要真正贴合业务微调几乎不可避免。问题来了标注数据从哪来我有几个实践路径。第一从线上日志挖。用户点击过哪些文档、点赞过哪些回答这些隐式反馈是最廉价的数据源。第二用大模型生成伪标注。拿一批 query 和候选文档让大模型判断相关/部分相关/不相关然后蒸馏成交叉编码器的训练数据。这个方法有噪音但成本低、迭代快。第三人工精标。数量不用多几千条高质量样本就够关键是要覆盖边界案例——那些看起来相关但其实不相关的坑爹样本。微调时我用的是排序损失常见的有 pairwise 的 RankNet loss或者 listwise 的 ListMLE。数据格式是(query, doc_pos, doc_neg)三元组。一个容易踩的坑是负样本太简单模型学不到东西。我会刻意加入难负样本——比如共享关键词但语义不相关的文档逼模型学会区分字面相似和语义相关。训练轮数别太多我一般 2 到 3 轮就够了多了容易过拟合。学习率用 1e-5 到 3e-5 之间配合线性衰减。评估时不要只看整体准确率要分 query 类型看短查询、长查询、含专有名词的查询它们的重排表现可能差异很大。3.3 LLM 重排什么时候用怎么控制成本用大模型做重排是近两年的热门方案。常见的做法是让 LLM 对候选文档做打分、排序或摘要式筛选。效果确实好因为它理解复杂指令和隐含语义但成本也高得惊人。我只有在三种情况下才用 LLM 重排一是 query 特别复杂、歧义性强二是文档之间差异微妙、需要深层推理三是候选数量少10 条以内且对延迟不敏感。LLM 重排有一种很实用的轻量模式让 LLM 用 相关/不相关 做粗过滤而不是直接排序。给它一个 query 和一批候选文档让它逐条判断相关与否输出 JSON。这样能从 30 条候选里快速剔除 20 条垃圾剩下 10 条再交给轻量模型或直接进生成。这个模式在延迟和效果之间取得了不错的平衡。真正让 LLM 直接对候选列表进行排序时要注意位置偏差。LLM 倾向于高估排在前面的文档。一种缓解方法是让 LLM 先打乱候选顺序再排序或者采用两两比较 归并排序的策略。但后者意味着多次调用成本倍增实际项目中我用得不多。再提醒一点LLM 重排的输入模板一定要反复测试。有一次我把模板里的字段顺序写错了结果模型把文档 ID 当成排序依据输出了一堆莫名其妙的顺序。这种问题在离线评估时很难发现因为评估集太小。所以上线前一定要人工抽检至少 100 条真实 query。3.4 重排结果的后处理Score 校准与阈值设定重排模型输出的分数是否能直接用我的答案是不要直接当概率用。不同模型、不同域下的分数分布差异很大你需要做校准。一种简单有效的方法是温度缩放Temperature Scaling。在验证集上学习一个温度参数T把 logits 除以T后再过 softmax使得校准后的分数更接近真实概率分布。这个方法实现成本极低效果却立竿见影——尤其当你要用分数做阈值截断时。阈值的设定不要拍脑袋。我会画出分数 vs 相关率的校准曲线找出分数和实际相关率的对应关系再决定该在哪个分数处截断。比如0.65 分以上的文档 85% 是相关的那阈值就可以设在 0.65 左右然后根据业务对召回和精度的偏好微调。另外一个容易被忽略的细节是重排后的 Top 文档分数差距很小的时候顺序并不稳定。比如第 1 名和第 2 名只差 0.01换个模型或改个参数排名就变了。这时候我不强求 Top 1 的准确性而是把 Top 3 都送进生成阶段让生成模型自己综合判断。实践中这个策略能让答案质量更稳。4. 工程落地与评估体系搭建4.1 链路搭建从接口设计到缓存策略有了模型和算法工程上需要把它们串起来。我先说说标准的 RAG 召回与重排接口长什么样。召回模块的输入是 query输出是候选列表。接口设计上我倾向于做成可插拔的管道召回 BM25 召回 向量召回中间通过一个抽象接口统一输出(doc_id, score, source)。这样后面加新的召回通道不需要改业务代码。重排模块的输入是候选列表输出是排序后的列表。这里要特别注意批量推理的吞吐优化。cross-encoder 一次可以处理多个样本我用的是动态 batch根据候选数量和 GPU 显存自动调整 batch size避免 OOM 也避免浪费。缓存策略是另一个大头。用户的 query 往往有重复缓存结果能显著降低延迟和成本。我的方案是对规范化后的 query 计算哈希缓存召回和重排结果同时设置过期时间比如 10 分钟。更细一点还可以做已见文档分数缓存——同一个文档在同一 session 内重复出现时直接复用重排分数。关于延迟预算我通常按这个量级分配向量检索 50ms稀疏检索 20ms混合融合 5ms重排 100ms 到 200ms20 条候选总延迟控制在 300ms 上下。如果你的场景要求 100ms 以内就得考虑缩小重排候选集或换更轻量的重排模型。4.2 离线评测集建设别再用感觉验证效果离线评测对召回与重排项目来说就像地基对房子一样重要。但很多团队直到上线前一星期才想起来要攒数据集然后靠大模型一口气生成几百条效果自然离谱。我推荐的离线评测集建设流程是这样的。首先从真实业务日志里抽 500 到 1000 条 query确保覆盖高频、中频、长尾和特殊类型。然后对每条 query 标注相关文档。标注标准要细化严格相关、部分相关、不相关三个层级。最后把数据分成训练集、验证集、测试集注意避免同一文档出现在多个集合中否则评估结果会虚高。有了评测集我习惯每次改动就跑一遍完整评估召回侧看 Recall50融合侧看 Recall10重排侧看 NDCG5。如果改动让召回指标上升但重排指标下降我会重点检查是不是候选集产生了高相似度但低相关性的噪声或者融合权重需要重调。这里再提供一个教训离线数据的时间分布要与线上一致。我曾经拿三个月前的数据评估一个新闻类 RAG当时指标不错上线后发现热点话题完全召不回。原因是词分布变了。所以离线评测集要定期滚动更新至少每季度重建一次。4.3 线上观测指标埋点与阈值报警离线评估只是第一步线上观测才是检验真理的唯一标准。我从一开始就坚持在 RAG 链路上埋点记录这些信息query 原文、召回通道各自的返回数、重排候选数、重排后 Top 5 文档的分数分布、生成阶段的引用来源。有了这些日志我就能做失败样本分析。常见的问题是某类 query 的召回结果为空或者重排分数普遍偏低。通过分维度统计我可以快速定位是召回阶段漏了还是重排阶段错了。如果没有日志你连问题出在哪都不知道只能盲改。我会给关键指标设报警阈值。比如Top 1 文档的平均重排分数低于某个值说明系统整体相关度下降需要立刻查。再比如无召回率超过 2%说明索引或词典出问题了。报警不是越多越好我通常只盯三到五个核心指标避免报警疲劳。5. 常见问题与排查技巧5.1 召回率低问题可能在索引而不在模型遇到召回率低我第一反应不是调模型而是检查索引内容。最常见的问题有三个一是文档没做正常清洗HTTP 标签、乱码、空行充斥着文本导致分块和 embedding 质量极差二是分块策略不合适块太大导致相似度被稀释块太小导致语义断裂三是索引与查询不匹配比如文档是中文的但用的 embedding 模型是英文为主的效果自然大打折扣。排查顺序是随便挑几条线上查不到的 query打开检索调试面板看看候选集里到底有没有相关文档。如果相关文档压根没进候选集那就是索引或召回的问题如果相关文档进候选了但排很后面那才是重排的问题。这就叫定位不能跳过。5.2 重排后效果反而变差这是怎么回事重排后效果变差这种情况我见过很多次。原因往往是重排分数把原本合理的排序打乱了。比如 BM25 把精确匹配的文档排第一cross-encoder 因为语义模型被长文档误导把该文档排到第五结果生成阶段用了更差的内容。解决思路是给重排加一个保护机制。一种做法是让召回阶段的分数参与最终排序的软融合比如把重排分数和召回分数各自归一化后按比例合并防止重排完全推翻召回的合理判断。另一种做法是只在候选集内部做重排而不是把重排分数和全库的向量分数混在一起排序。还有一个小细节确保重排模型的训练数据与线上数据分布一致。我踩过坑训练数据全是长文档线上 query 大量是短查询结果重排对短查询的表现明显偏弱。后来我在训练数据里刻意加了短查询样本问题才缓解。5.3 延迟超预算怎么在不牺牲太多效果的前提下减负延迟是召回与重排落地中最现实的敌人。我常用的优化手段有以下几种按效果从高到低排序。第一缩小重排候选集。从 100 条降到 50 条或者降到 30 条能明显降低 cross-encoder 推理量。前提是召回阶段要把质量做扎实确保相关文档集中在前面。第二模型蒸馏和量化。把大 cross-encoder 蒸馏成小模型或者用 INT8 量化推理速度快一倍以上精度损失通常可以接受。第三批量推理和 GPU 调度。不要一条条调用重排模型而是攒批处理。如果延迟峰值集中在某一时段可以考虑队列缓冲来削峰。最后如果延迟要求极其苛刻干脆放弃 cross-encoder改用双塔模型加后期交互。虽然精度略低但速度可以快一个数量级。这个方案适合检索量极大、对延迟极敏感的场景。5.4 数据更新后效果骤降索引同步是隐形杀手这个问题很隐蔽但破坏力极大。当文档库发生增量更新时如果新的 embedding 向量没有及时写入向量索引或者旧的文档没有及时删除系统就会返回过期甚至错误的内容。我的做法是建立一套索引版本管理机制。每次更新都生成新的索引版本先在灰度环境验证 Recall50达标后切一部分流量上线。同时记录每篇文档的 embedding 版本一旦发现某个 embedding 模型升级了就要全量重建索引——只增量更新会导致新旧向量语义空间不一致混合在一起会大幅拉低召回质量。另外注意向量索引的删除操作不是实时的。 HNSW 索引在删除后可能留下残留节点需要定期做 compact 操作。很多线上故障排查到最后才发现是删除的文档还在被召回。这种问题靠肉眼很难发现一定要有对账工具定期校验索引里的文档数和文档库里的总数是否一致。6. 我的一点体会召回与重排做了这几年我最大的感受是别把模型当万能药先把链路和数据想清楚。一个调好的 BM25 加一个通用的向量召回配上合理的重排规模往往能胜过一套花哨但没调优的全深度学习方案。评估、数据、迭代机制才是整个系统能持续变好的核心动力。如果你正在搭 RAG 系统我建议按这个顺序推进先拿 BM25 搭基线再逐步加向量召回、混合融合、重排。每一步都用同一套评估口径量化收益收益不明显就保持现状。做技术最怕的不是慢而是方向错了还拼命加速。希望这篇实战笔记能帮你少走一些弯路。最后别忘了把评估集当资产一样维护它是你后续所有改动的唯一裁判。