RAG从入门到实战(九):重排序与结果多样性——Cross-Encoder 和 MMR 怎么用?
文章目录
- RAG从入门到实战(九):重排序与结果多样性——Cross-Encoder 和 MMR 怎么用?
- 前言
- 一、从召回到重排序
- 二、为什么需要重排序?
- 三、Bi-Encoder 与 Cross-Encoder 的区别
- 3.1 Bi-Encoder:分别编码,便于大规模召回
- 3.2 Cross-Encoder:联合判断查询与候选
- 3.3 两者不是互斥关系
- 四、Cross-Encoder 重排序流程
- 4.1 把排序器接入候选结果
- 4.2 输入长度和截断
- 4.3 分数不能直接当概率
- 4.4 外部排序服务与隐私
- 五、MMR:同时考虑相关性与多样性
- 5.1 为什么需要多样性?
- 5.2 相似度和相关性必须考虑尺度
- 5.3 多样性不是越高越好
- 5.4 代码示例:简化 MMR 选择
- 5.5 在向量库中使用 MMR 检索器
- 六、Cross-Encoder 与 MMR 怎么组合?
- 七、候选数量如何选择?
- 八、如何判断重排序真的有用?
- 九、总结
- 参考资料
前言
上一篇介绍了稠密、稀疏和混合检索。多路召回后,系统可能得到几十个候选片段,但它们不一定按“最能回答当前问题”的顺序排列。
同时,前几个结果也可能来自同一段反复切分的文本:看起来相关,却缺少其他必要信息。这时可以考虑:
- 用 **重排序(Reranking)**重新评估查询与候选的相关性。
- 用MMR等方法在相关性与结果多样性之间取得平衡。
本文介绍二者的作用、差异、成本及评估方法,并给出一个不依赖模型的 MMR 可运行示例。
本文中的制度和候选片段均为虚构示例。算法结果依赖模型与数据,不能保证一定提升最终答案质量。
一、从召回到重排序
一个典型流程:
用户问题 ↓ 稠密 / 稀疏 / 混合检索 ↓ 候选片段(例如 30 条,仅为示意) ↓ 去重 → 重排序 → 必要时控制多样性 ↓ 少量证据片段 → 上下文组装 → 回答第一阶段的主要任务是尽量不要漏掉有用证据;第二阶段的主要任务是在候选中把更有用的内容排得靠前。两阶段的候选数量应依据测试结果与预算调整,图中的数字不是通用最佳值。
需要强调:重排序通常只能处理已经召回的候选。如果正确片段根本没有进入候选集,排序器不会凭空恢复它;应先排查解析、切分、查询和检索策略。
二、为什么需要重排序?
假设用户问:
入职满一年后申请年假,需要提前多久提交?
初次检索可能返回:
- 年假可以按制度申请。
- 差旅申请需要提前三个工作日提交。
- 年假申请需提前三个工作日提交,并经直属主管审批。
- 年假相关常见问题汇总。
第 2 条包含“提前三个工作日”和“申请”,但主题是差旅;第 3 条同时满足“年假”和“提前多久”的问题条件,通常更直接。重排序的目的就是结合完整问题和候选内容,重新判断它们之间的关系。
不过,即使排序靠前,也不能保证文档是最新版本,或者该片段包含全部适用条件。排序分数不是事实正确性认证。
三、Bi-Encoder 与 Cross-Encoder 的区别
3.1 Bi-Encoder:分别编码,便于大规模召回
向量检索常用的双编码器思路是分别编码问题与文档:
问题 → 查询向量 ─┐ ├→ 向量相似度 → 大规模候选检索 文档 → 文档向量 ─┘文档向量可以提前计算并建立索引,因此适合在大量文档中快速查找。但单个向量需要压缩文本信息,精确比较问题和文档中的多个条件可能存在局限。
3.2 Cross-Encoder:联合判断查询与候选
Cross-Encoder 将问题和一个候选片段一起输入模型,输出一个相关性分数或其他模型定义的结果:
[问题,候选 A] → 分数 A [问题,候选 B] → 分数 B [问题,候选 C] → 分数 C模型可以在一次前向计算中考虑问题与候选之间更细粒度的交互,因此在一些任务中适合用于候选重排序。代价是需要对每个查询—候选对计算,通常不能像预计算文档向量那样直接对整个大库做同等规模的搜索。
不同模型对输入格式、最大长度、输出分数和语言有不同要求;不要假定所有 Cross-Encoder 分数都处于[0,1]或可直接跨查询比较。
3.3 两者不是互斥关系
常见搭配是:
Bi-Encoder / BM25:从大量资料中召回候选 Cross-Encoder:对候选逐一打分并排序是否值得增加排序器,应由真实问题集决定。如果候选已经很少或延迟预算很紧,也可以先使用更简单的排序方法。
四、Cross-Encoder 重排序流程
一种通用实现流程:
- 召回候选片段并执行权限过滤。
- 合并不同来源的结果,依据可信片段 ID 去重。
- 为每个候选构造
(question, text)输入对。 - 使用排序模型批量计算分数。
- 按分数排序并选择前若干条。
- 检查证据是否足够,保留原始来源和版本。
标题或章节有时可以帮助排序模型理解片段,但也可能增加噪声或占用长度。应确保必要内容不被截断,并通过对照实验决定输入结构。
4.1 把排序器接入候选结果
下面把“问题—候选文本配对、批量打分、按分数重排”封装成与服务商无关的函数。调用方提供的score_pairs负责调用已配置的远程 Cross-Encoder 重排序服务,并按输入顺序返回分数;示例不下载本地模型,也不假定某一家服务的接口格式。示例中的虚拟打分仅用于检查排序流程,不代表语义模型效果。
fromcollections.abcimportCallable,Sequencefrommathimportisfinitedefrerank(question:str,documents:Sequence[dict],score_pairs:Callable[[list[tuple[str,str]]],Sequence[float]],top_k:int=3)->list[dict]:iftop_k<0:raiseValueError("top_k 不能为负数")ifnotdocumentsortop_k==0:return[]pairs=[(question,doc["content"])fordocindocuments]scores=list(score_pairs(pairs))iflen(scores)!=len(documents)ornotall(isfinite(score)forscoreinscores):raiseValueError("排序服务必须为每个候选返回一个有限数值")ranked=[{**doc,"rerank_score":float(score)}fordoc,scoreinzip(documents,scores)]returnsorted(ranked,key=lambdadoc:doc["rerank_score"],reverse=True)[:top_k]if__name__=="__main__":candidates=[{"id":"A","content":"申请年假需提前三个工作日。"},{"id":"B","content":"出差报销需提供票据。"},]# 假分数只验证流程;替换为按所用服务文档实现的真实远程调用。print(rerank("年假需要提前多久申请?",candidates,lambdapairs:[0.9,0.2]))集成真实服务时,先完成权限过滤,再按服务商的请求和响应格式实现score_pairs,正确恢复输入顺序;密钥从受控环境变量读取,不要写入代码或公开日志。不同排序器的分数范围和意义不同,不能照搬统一的“分数大于 0”阈值。远程服务示例在此未做真实调用;将问题和候选原文发给第三方之前,需确认数据授权和处理政策。
4.2 输入长度和截断
如果文档片段超过模型支持的最大长度,关键条件可能被截掉。要明确模型实际采用的截断方式,并在切分、输入构造和测试时覆盖“关键信息位于末尾”的情况。
4.3 分数不能直接当概率
例如某模型输出4.2和1.7,它们可能只是该模型的排序分数。不能不经校准就说“第一个答案正确概率为 4.2”或“阈值 0.8 对所有模型通用”。
排序器也可能偏爱主题相近但缺少条件的内容。需要比较原始文档、引用和最终答案,不能只看分数。
4.4 外部排序服务与隐私
如果将用户问题和候选原文发送到外部排序 API,这些内容会离开本地系统。使用前应确认数据授权、敏感信息处理、服务商政策和传输安全;本地部署也仍需权限隔离与访问审计。
五、MMR:同时考虑相关性与多样性
5.1 为什么需要多样性?
假设查询“年假怎么申请”,召回前三条:
A:年假申请需提前三个工作日提交。 B:申请年假需要提前三个工作日提交。 C:年假申请要提前三个工作日提交。这三条几乎重复。如果回答还需要“谁审批”,那么它们可能挤占了审批规则的上下文位置。
MMR(Maximal Marginal Relevance,最大边际相关性)逐步选择候选,同时考虑其与问题的相关性,以及它与已选结果的相似程度:
MMR(d) = λ × relevance(query, d) - (1 - λ) × max similarity(d, 已选片段)其中λ通常取[0, 1]:
- 越接近 1,越偏重问题相关性。
- 越接近 0,越偏重减少重复。
当已选集合为空时,可以约定冗余项为 0;λ > 0 时,第一步选择相关性最高的候选。λ = 0 时,按此约定首轮分数全为 0,需要明确平分规则或单独规定初始化方式。不同实现的初始化和相似度定义可能有所区别。
5.2 相似度和相关性必须考虑尺度
公式中的相关性与冗余度可能来自不同模型,例如 Cross-Encoder 分数与向量余弦相似度。它们的取值范围未必一致,直接代入会导致某一项主导结果。
实际使用应:
- 了解分数的方向、范围及是否可比较。
- 使用适当的归一化或校准方法,并在相同测试集上验证。
- 处理零向量、重复片段、缺失值和跨版本 ID。
- 不要假定某个
λ对所有领域都适合。
5.3 多样性不是越高越好
如果问题只需要一个精确事实,强行选择差异大的片段可能引入不相关资料。多样性是为了覆盖必要信息,而不是为了让所有结果看起来不同。
5.4 代码示例:简化 MMR 选择
下面代码使用人工定义的相关性和片段间相似度,展示选择过程。它不是 Embedding 模型,也不连接向量数据库。
defselect_mmr(candidates,relevance,similarity,limit,lambda_=0.5):"""按 MMR 逐个选择 ID;相似度输入应使用可比较的预处理分数。"""ifnot0<=lambda_<=1:raiseValueError("lambda_ 必须在 [0, 1] 内")iflimit<0:raiseValueError("limit 不能为负数")iflen(set(candidates))!=len(candidates):raiseValueError("候选 ID 不能重复")remaining=list(candidates)selected=[]whileremainingandlen(selected)<limit:best_id=Nonebest_score=float("-inf")forcandidateinremaining:redundancy=max((similarity(candidate,prior)forpriorinselected),default=0.0,)score=lambda_*relevance[candidate]-(1-lambda_)*redundancy# 平分时保留输入顺序,便于复现。ifscore>best_score:best_id,best_score=candidate,score selected.append(best_id)remaining.remove(best_id)returnselectedif__name__=="__main__":relevance={"A":0.95,"B":0.90,"C":0.80}pair_similarity={("A","B"):0.95,("A","C"):0.10,("B","C"):0.15,}defsimilarity(a,b):returnpair_similarity.get((a,b),pair_similarity.get((b,a),0.0))print(select_mmr(["A","B","C"],relevance,similarity,limit=2,lambda_=0.5))结果是['A', 'C']:A 的相关性最高,先被选择;B 和 A 非常相似,因此第二步的 MMR 分数低于 C。
本例刻意把相关性和相似度放在相近尺度,真实系统不能直接假定这一点。它也没有实现权限过滤、排序模型或证据充分性判断。
5.5 在向量库中使用 MMR 检索器
如果已经使用第二篇的InMemoryVectorStore,可以把检索方式配置成 MMR,而不必手写上面的选择循环。以下是接入片段:store是已建立并写入文档的向量库,需继承同一权限范围,不能单独复制运行。
retriever=store.as_retriever(search_type="mmr",search_kwargs={"k":3,"fetch_k":12,"lambda_mult":0.5},)results=retriever.invoke("年假申请需要提前多久?")fordocumentinresults:print(document.metadata,document.page_content)fetch_k控制送入 MMR 的候选数量,k控制最终返回数;lambda_mult通常越大越重视相关性,越小越重视多样性。支持的参数及权限过滤能力因向量库实现和版本而异,不能假设每个后端完全相同。这里仍会调用已配置的 Embedding 服务,但不需要下载本地排序模型。注意:InMemoryVectorStore的 MMR 检索路径依赖numpy,未安装时会直接抛出 ImportError;如果第二篇的最小环境没有安装 numpy,需要先执行pip install numpy。
Cross-Encoder 则是另一条可选步骤:实际使用时可以对候选调用组织已配置的远程重排序服务;每个服务的字段、认证、长度限制和分数含义不同,须以其文档为准。外发问题和文档前必须先确认授权。MMR 与 Cross-Encoder 解决的问题不同,组合次序和候选数都应通过评估决定。
六、Cross-Encoder 与 MMR 怎么组合?
两者解决的问题不同:
| 方法 | 重点 | 输入 | 常见代价 |
|---|---|---|---|
| Cross-Encoder | 问题与候选的细粒度相关性 | 问题和每个候选组成的文本对 | 候选越多,模型推理成本越高 |
| MMR | 相关性和已选结果的多样性 | 相关性分数与候选间相似度 | 需要计算或取得候选间相似度 |
一种可能的流程:
混合召回 → 去重 → Cross-Encoder 重排序 → MMR 选取 → 组装上下文也可以先使用向量相似度做 MMR、再做重排序;顺序会影响结果和成本,没有无需评估的通用答案。特别是对需要多个证据的问题,最终应检查证据覆盖,而不只追求排序或多样性得分。
七、候选数量如何选择?
需要关注至少三个数量:
- 每路检索取多少候选。
- 多少候选进入排序器。
- 最终给模型多少证据片段。
如果召回数量太小,相关证据可能不在候选中;太大则增加排序器计算与噪声。最终片段过多可能导致输入成本增加、重复信息增加或关键信息被稀释。
建议用同一测试集尝试不同组合,并记录:
- 候选召回是否包含所需证据。
- 排序后相关证据是否进入前 K。
- 重复片段比例和多证据问题的覆盖情况。
- 排序器延迟、吞吐、模型费用和最终答案质量。
八、如何判断重排序真的有用?
建立对照:
基线:混合检索 → 直接取前 K 实验 A:混合检索 → Cross-Encoder → 取前 K 实验 B:混合检索 → Cross-Encoder → MMR → 取前 K确保各实验使用相同的候选数据、权限范围和测试问题。可以观察 MRR、nDCG、Recall@K 等指标,但也要检查:
- 错误片段是否被排在正确证据前面。
- 多条件问题是否缺少某个必要条件。
- 是否因多样性过强把相关片段挤掉。
- 引用是否支持最终结论。
- 成本与延迟是否满足业务目标。
无答案、跨文档、长片段、时间版本冲突和权限隔离等情况都应进入测试集;排序分数高并不意味着可以忽略拒答策略。
九、总结
- 召回决定哪些候选有机会进入后续流程;重排序改善候选顺序。
- Cross-Encoder 联合评估问题与候选,通常适合在较小候选集上使用。
- MMR 在相关性和去重、多样性之间进行取舍,但需要注意分数尺度。
- 最终仍应检查来源、版本、权限和证据充分性,而不是把排序得分当作事实保证。
下一篇将继续介绍检索后的上下文优化,包括片段提取、压缩、去重、父块扩展与引用。
参考资料
- MMR 原始论文:https://doi.org/10.1145/290941.291025
- Sentence Transformers 重排序器文档:https://sbert.net/docs/cross_encoder/usage/usage.html