Rerank 重排模型怎么用:让 RAG 回答更准的最小实现
2026/9/6 13:06:54 网站建设 项目流程

向量检索已经能找回“差不多”的内容,为什么回答还是不准?一个典型现象是:问题问“远程办公申请需要谁审批”,Top-5 里既有远程办公制度、考勤制度、出差制度,也有一段旧 FAQ;正确段落确实出现了,但排在第四。把这五段全交给模型,它可能被第一段相似但不适用的文字带偏。把k从 5 提到 20 通常也不是答案,只会把更多噪声和重复文本塞进上下文。

Rerank 的角色正好在这里:第一阶段用向量检索快速从大量文档中召回几十个候选;第二阶段对“问题—候选片段”逐对做更细粒度的相关性判断,再把少量最相关的片段交给生成模型。它不是替代向量数据库,也不是让模型凭空知道新知识,而是把有限的计算花在已经缩小的候选集上。

最重要的一句话是:先确认正确证据能被召回,再谈重排。若正确片段连 Top-50 都进不来,重排模型没有机会把它变到第一名。Rerank 解决的是候选排序问题,不是文档解析、权限过滤、切分失真或知识缺失的万能药。

一、双阶段检索到底发生了什么

embedding 检索常用双塔(bi-encoder)模型:问题单独编码成一个向量,文档单独编码成一个向量,离线索引后可以用近邻搜索快速比较。它适合海量候选,但为了让文档向量可预计算,模型无法在计算文档向量时看到当前问题的每个词。

Cross-Encoder 则把问题和候选文本一起输入,输出一个相关性分数。由于两段文本可以在模型内部交互,它通常在精细排序上更强;代价是每个候选都要再做一次推理。因此正确架构不是用 Cross-Encoder 扫全库,而是“先广召回、后精排序”。Sentence Transformers 的官方文档也明确指出 Cross-Encoder 更慢,通常用于重排 bi-encoder 的 top-k 结果。

用户问题

Bi-Encoder 编码

向量索引

召回 top_n 候选

ACL/元数据过滤

Cross-Encoder: 问题+每个候选

按相关分数排序

截取 top_m 上下文

LLM 基于证据回答

从这张图还能看出权限的位置。权限和文档状态过滤必须早于重排;不能为了获得“更好排序”把无权文档送给重排服务。即使 reranker 不输出文本,敏感文本已离开了本该受控的边界。若重排调用的是第三方 API,还要确认其数据保留、地域和合规要求。

二、开始前:不要把相关性分数误读成概率

许多 Cross-Encoder 模型输出的是 logit 或未校准的相关性值,可能是负数,也不一定在 0 到 1 之间。这个数最可靠的用法是对同一个问题的候选排序,而不是解释成“87% 正确”。不同模型、不同版本甚至同一模型不同任务的分数分布都可能不同。若要设拒绝阈值,必须在自己的已标注问题集上校准,而不是从别人文章里复制一个 0.5。

还要区分“相关”与“支持”。一段关于“差旅审批”的文字对“远程办公审批”很相关,但不支持答案。Reranker 的训练目标通常是相关性,不会自动理解企业制度中的时间有效性、部门权限或版本优先级。这些条件应通过 metadata 过滤和文档治理先保证,再交给语义排序。

本文示例用sentence-transformersCrossEncoder展示本地重排。模型名称只是一个能运行的示例;中文业务文档应优先选择在中文/多语数据上适配、许可证允许使用的模型,并在自己的评测集验证。第一次部署时下载权重需要网络和磁盘空间,离线服务器则应通过受控制品库预置模型,别在生产进程启动时临时下载。

python-mvenv .venv# Windows PowerShell.venv\Scripts\Activate.ps1 pipinstallsentence-transformers torch

这里假定你的第一阶段检索器已经返回字典列表,每项具有idtextsourcemetadata。无论底层是 Chroma、Elasticsearch、pgvector 还是托管向量库,这个边界都足够小:重排函数不应关心数据库 SDK 的细节。

三、一个最小且可运行的本地重排函数

下面代码加载一个公开 Cross-Encoder 模型,对问题和候选段落组成的 pairs 调用predict,按得分降序返回前若干项。为了可检查性,函数不修改输入字典,而是生成包含rerank_score的新字典。对空候选、过长问题、过长候选作了防御性处理;长度截断并不是语义完美方案,却比让异常输入把请求拖垮要好,真实上限应结合所选模型的最大序列长度设定。

# rerank.pyfrom__future__importannotationsfromcollections.abcimportSequencefromtypingimportAnyfromsentence_transformersimportCrossEncoder MODEL_NAME="cross-encoder/ms-marco-MiniLM-L6-v2"defrerank(query:str,candidates:Sequence[dict[str,Any]],model:CrossEncoder,top_n:int=5,max_chars:int=3000,)->list[dict[str,Any]]:ifnotquery.strip():raiseValueError("query 不能为空")iftop_n<=0:raiseValueError("top_n 必须大于 0")ifnotcandidates:return[]normalized=[]foritemincandidates:text=str(item.get("text","")).strip()ifnottext:continuenormalized.append({**item,"text":text[:max_chars]})ifnotnormalized:return[]pairs=[(query[:1000],item["text"])foriteminnormalized]scores=model.predict(pairs,show_progress_bar=False)ranked=sorted(({**item,"rerank_score":float(score)}foritem,scoreinzip(normalized,scores)),key=lambdaitem:item["rerank_score"],reverse=True,)returnranked[:top_n]if__name__=="__main__":reranker=CrossEncoder(MODEL_NAME)rows=[{"id":"a","text":"远程办公申请由部门负责人审批,并同步人力资源部门。"},{"id":"b","text":"出差前应在系统提交出差申请。"},]result=rerank("远程办公需要谁批准?",rows,reranker,top_n=1)assertlen(result)==1andresult[0]["id"]=="a"print(result)

这个断言只验证示例数据的排序,不能代替业务效果评测。还要注意模型分数:某些模型原始输出是 logits,加入 sigmoid 只会改变数值尺度,不会改变同一批候选的排序。不要为了让分数“好看”而擅自把它展示为置信度。

四、接到向量检索后,而不是替换向量检索

以下代码用一个小型内存候选集模拟向量库返回值。在项目里,把vector_search替换为实际集合的query即可。真实查询建议先按租户、部门、文档状态、有效期过滤,再向量召回candidate_k=20100的候选;具体数值必须由延迟预算和离线评测决定,不能把这里的数字当成通用参数。

# pipeline.pyfrom__future__importannotationsfromsentence_transformersimportCrossEncoderfromrerankimportrerank,MODEL_NAMEdefvector_search(question:str,allowed_departments:set[str],candidate_k:int=20)->list[dict]:# 用来演示接口形状;生产实现应由向量库完成相似度检索和服务端 ACL 过滤。all_rows=[{"id":"policy-1","department":"hr","source":"hr/remote.md","text":"远程办公申请须经直属部门负责人审批,并抄送人力资源部门。"},{"id":"policy-2","department":"finance","source":"finance/travel.md","text":"出差申请应在出发前提交,费用按差旅标准报销。"},{"id":"policy-3","department":"hr","source":"hr/attendance.md","text":"员工应按规定完成每日考勤打卡,异常考勤在三个工作日内说明。"},]return[rowforrowinall_rowsifrow["department"]inallowed_departments][:candidate_k]defretrieve(question:str,departments:set[str],model:CrossEncoder)->list[dict]:candidates=vector_search(question,departments,candidate_k=20)# ponytail: 先用固定候选池;只有评测显示正确证据常在池外时再提升 candidate_k 或增加召回路。returnrerank(question,candidates,model,top_n=4)if__name__=="__main__":model=CrossEncoder(MODEL_NAME)hits=retrieve("远程办公由谁审批?",{"hr"},model)asserthitsandhits[0]["source"]=="hr/remote.md"print(hits)

在代码里把allowed_departments作为函数参数,是为了强调它来自认证后的服务端身份,而不是请求体中的一个任意字符串。生产接口也不要把所有候选文本打印到日志。调试时常有人把完整候选和 prompt 输出到 APM,后来才发现日志平台的访问范围比文档库宽得多。

五、候选数、返回数与延迟预算

双阶段的两个数字经常被混为一谈。candidate_k是第一阶段召回多少条交给 reranker;top_n是重排后保留多少条给生成模型。前者越大,正确证据留在池中的机会越高,但 Cross-Encoder 推理越慢;后者越大,生成模型能看到更多证据,却可能被冗余上下文干扰并增加 token 成本。

合理的做法是画出候选数—Recall@k 和候选数—P95 延迟两条曲线。假设正确证据在 top-20 的召回已接近 top-50,而 P95 在 50 时明显上升,就没有理由默认传 50 条。又假如 rerank 后 top-3 的证据已足够支持大多数问题,top-8 只增加重复,则优先传 top-3。这里的“假如”是评测设计的例子,不是未经测量的性能承诺。

重排可以做批处理。CrossEncoder.predict接受 pairs 列表,避免为每个候选单独发起 Python 调用。并发较高时,要为模型推理设置队列、最大批量和超时;CPU 上跑大模型会显著拉长尾延迟,GPU 也可能因显存竞争抖动。不要在每一次请求里重新CrossEncoder(MODEL_NAME):权重加载既慢又占内存,应在进程生命周期内加载一次,并通过健康检查确认模型已就绪。

若服务端跑多个 worker,每个 worker 都会加载一份模型。对显存敏感的部署可先用单模型服务,应用以 HTTP 或 RPC 调用它;这增加了一个服务,但比每个 Web worker 各自抢 GPU 更容易控制。是否拆服务不该从架构图开始,而应由实际并发、显存和故障隔离需求决定。

六、评测要证明“排序变好”,不是只看回答更像人

建立对照非常简单:保持文档、切分、embedding、过滤规则和 candidate_k 不变,只改变是否使用 rerank。对每个问题保存第一阶段候选 ID 和重排后 ID,计算 Recall@k、MRR、nDCG,或者至少记录正确证据的排名变化。若你没有分级相关性标注,MRR 往往比 nDCG 更容易解释。

以下是一个最小的 MRR 对比脚本。它输入两份结果文件,比较同一个金标准集合;没有足够标注时,它不会替你“自动判分”。gold_ids可包含多个同等有效的证据 ID。

# compare_rerank.pyfrom__future__importannotationsimportjsonfrompathlibimportPathdefrows(path:str)->dict[str,dict]:return{r["id"]:rforrinmap(json.loads,Path(path).read_text(encoding="utf-8").splitlines())}defmrr(gold:dict[str,dict],ranked:dict[str,dict],k:int=10)->float:total=0.0forqid,labelingold.items():targets=set(label["gold_ids"])ids=ranked.get(qid,{}).get("ids",[])[:k]position=next((ifori,item_idinenumerate(ids,1)ifitem_idintargets),None)total+=0.0ifpositionisNoneelse1.0/positionreturntotal/len(gold)ifgoldelse0.0if__name__=="__main__":gold=rows("gold.jsonl")before,after=rows("vector.jsonl"),rows("reranked.jsonl")vector_mrr,rerank_mrr=mrr(gold,before),mrr(gold,after)assert0<=vector_mrr<=1and0<=rerank_mrr<=1print(f"vector MRR@10={vector_mrr:.3f}; reranked MRR@10={rerank_mrr:.3f}")

除了数值,还要保留失败样本。一个非常有价值的失败模式是“词面相同、语义不同”:问“审批人”,候选里出现了“审批流程说明”却没有实际角色;另一个是“时间冲突”:旧制度因词汇更接近而被排第一。后者不该单纯靠 reranker 解决,应在候选过滤中排除失效版本。还有“跨段证据”:正确答案需要两段同时出现,reranker 把其中一段升到第一却把另一段挤掉,最终生成仍会遗漏条件。

生成层也应单独评估。随机抽取重排前后相同问题,要求评审只依据原始资料判断答案是否忠实、引用是否支持每个结论、无答案时是否拒绝,而不是凭文风好坏评分。为了减少期望效应,可以隐藏“是否使用重排”的版本标签。不要让模型自评后把分数当作唯一事实,特别是制度和合规场景。

七、分数阈值、拒答与多样性

常见需求是“分数低于某值就不要回答”。这里有两个风险。第一,Cross-Encoder 的原始分数未必可跨问题比较,同一模型对短问题和长问题的尺度可能不同;第二,阈值通过后也不意味着候选完整支持结论。更稳妥的第一步是:以标注集绘制正负样本的分数分布,再选择满足业务风险的阈值,并把“低分拒答”视为可测量的分类规则,而非一句 prompt。

另外,排序最前面的三条很可能来自同一节相邻窗口。可以在 rerank 之后加入一个轻量的来源多样性约束:同一(source, section)最多保留两条,其余从后续候选补齐。它不适用于所有问题——有的问题确实需要同一章节的连续三段——所以应该在评测中验证,而不是强行套用。

defdiversify(rows:list[dict],limit_per_section:int=2)->list[dict]:selected,counts=[],{}forrowinrows:key=(row.get("source"),row.get("metadata",{}).get("section"))ifcounts.get(key,0)>=limit_per_section:continuecounts[key]=counts.get(key,0)+1selected.append(row)returnselected

这段函数故意简单:它只减少明显重复,并不声称实现了最优的多样性排序。如果离线结果显示某类跨段问题被它伤害,就关闭它或只针对文档窗口高度重叠的集合启用。工程上最危险的不是简单规则,而是没有指标却把规则当成普遍真理。

八、模型选择与安全边界

选择 reranker 时至少检查四件事:语种/领域是否匹配;最大输入长度是否覆盖你的 chunk;许可证是否允许商业或内部使用;推理资源是否满足延迟目标。通用英文 MS MARCO 模型可以验证代码路径,不应自动被当成中文企业制度的最佳模型。若候选文档混合中英,最好在相同中文问题集、相同切分条件下比较少量候选模型,而非只看模型榜单。

模型下载同样是供应链问题。生产部署应固定模型 revision 或校验哈希,使用受控镜像或模型仓库;不要让线上服务在启动时按一个浮动名称联网拉取最新权重。对于远端重排 API,除了 API key 管理,还需评估是否允许将候选原文传出。对包含个人信息、合同、源代码的资料,答案不是“脱敏一下再说”,而是先由数据负责人确认处理边界。

对输入要有长度、频率和格式限制。攻击者可以提交超长问题,迫使系统构造大量 pairs;也可以用大量低价值请求耗尽推理队列。接口层需要认证、限流、超时和请求体大小上限;业务层需要按租户隔离索引;日志层需要对问题和片段做最小化记录。Reranker 只是模型组件,不应绕开现有的安全控制。

九、何时不该加 Rerank

如果知识库只有几百个短且结构良好的 FAQ,embedding 检索的 top-3 在评测中已经稳定命中,加入 rerank 只会增加部署、延迟和排障面。若主要问题是文档 OCR 错误、过时版本未清理或权限 metadata 缺失,重排也是在给脏数据排序。若正确答案必须跨多个系统实时计算,例如库存余额或审批状态,应调用受权限控制的业务 API,而不是把昨天导出的报表交给 reranker。

一个更朴素的替代方案是先改进切分与 metadata,再取稍大的候选池,并在 UI 中显示来源片段让用户判断。只有当离线日志显示“正确证据经常在候选池内但名次不够前”,并且延迟预算允许第二阶段推理时,rerank 才是有证据的投资。

十、从分数到决策:离线校准比硬编码阈值更可靠

如果业务确实需要“没有足够证据就拒答”,就把它当作一个分类问题做校准。收集一批已标注的问题—候选对,标注每个候选是“可直接支持”“相关但不能支持”还是“不相关”。对每个问题记录重排最高分、第一与第二名的分差、前几名来源是否一致、候选的文档状态。然后分别统计不同阈值下的误答率和拒答率,而不是只挑一个能让准确率看上去最高的点。

阈值取舍必须由场景决定。内部搜索助手可以接受更多“请查看来源后确认”的回答,因而阈值不必过严;涉及报销、合规或安全操作的机器人则宁可多拒答,也不能把相关片段当作依据。更稳妥的输出可分三级:证据充分时给简要结论和引用;证据相关但不完整时仅展示相关条款并提示确认;无证据或冲突时明确拒绝下结论。把这些状态变成响应字段,而非让前端从一句自然语言猜测模型态度。

第一名与第二名的分差有时也很有用。如果最高分很高但第二名紧随其后且来源冲突,可能表示问题存在歧义;如果最高分远高于其余候选且文档有效,证据通常更集中。但分差仍受模型和候选池影响,只能作为特征的一部分。不要把“分差大”直接写成“模型 99% 确定”。

十一、跨语言、缩写与专有名词:先确认召回再选模型

企业资料常混有中英文产品名、工号、接口字段、缩写和部门别称。问题写“VPN 申请”,文档写“远程接入权限”;问题写“报销单”,文档写“费用申请单”。这类差异有两层:向量召回是否能把两种说法放到同一候选池,重排模型是否能在上下文中判断哪个概念真正对应。若第一层已经漏掉,替换 reranker 不会有任何效果。

可以按真实问题建立一个小型同义表达集,但不要在客户端悄悄把用户问题替换成某种推测。更可控的做法是把受治理的同义词作为额外检索查询或 metadata 别名,记录展开后的查询;然后保留原问题进入 rerank,避免改写带来意外语义。对于型号、合同号、错误码等精确标识,向量相似度未必优于关键词检索,常见做法是把关键词召回和向量召回合并、去重,再统一重排。

这也解释了为什么“只用向量库”有时很脆弱。关键词匹配擅长精确 token,向量擅长同义表达,二者在候选阶段互补;reranker 负责在合并后的几十条里做细排。是否需要混合检索依然要靠失败样本证明:若评测中专有名词问题频繁漏召回,它是低风险、可解释的下一步;若问题全是自然语言制度问答,先保持简单更好。

十二、与文档版本和过滤规则协同

排序模型通常只看问题和文本,无法天然知道“本制度已失效”或“该条只适用于上海分公司”。把版本、生效日期、地域、产品线、部门和保密级别当作 metadata,在第一阶段完成硬过滤。硬约束不应该交给 soft score:一段非常相关但已失效的旧制度,分数再高也不应进入结果。

有些规则不是简单过滤,例如“当前版本优先,但用户明确询问历史版本”。这类需求可以把查询模式写成受控参数:默认仅 active;审计角色可以指定历史日期;界面明确展示版本。不要让 LLM 从问题中自行决定可以越过版本边界,更不要用“请优先最新文档”这样的提示词代替数据库条件。模型会犯错,过滤规则应该可测试、可审计。

重排输出也应保留第一阶段分数与所有 metadata。排查一次错答时,若只能看到最后三段文本,无法知道它是因为正确内容没召回、被权限过滤、在重排中降级还是被最终上下文截掉。按请求 ID 保存最小必要诊断记录,既能支持评测,也能发现一个新版本发布后出现的排序回归。

十三、服务化时的资源管理与降级策略

本地调用很适合验证逻辑,线上则要面对模型加载、显存、并发和升级。推理服务启动后先完成权重加载和一次健康检查,未就绪就不要接收流量;设置输入最大长度、候选最大数、批量大小和超时;记录队列等待时间与推理时间,避免把排队误判为模型变慢。CPU 方案成本低但尾延迟可能不稳定,GPU 方案吞吐高但要避免多个进程各自复制权重。

遇到模型服务不可用时,最安全的降级一般是回退到“第一阶段检索 + 明确来源”,并在响应中标记排序服务暂不可用;不要自动扩大候选并让生成模型在更多噪声中自由发挥。是否允许这种降级要由业务风险决定:高风险领域可以直接返回暂不可用,低风险知识搜索可显示原始候选供用户自行判断。无论哪一种,监控都应将降级次数单独统计,防止它长期悄悄成为常态。

模型升级也应走影子验证。新模型对同一批匿名问题生成排序,但暂不影响用户;比较它与线上模型的 MRR、拒答行为、延迟和关键失败样本,再决定是否灰度。固定模型 revision、容器镜像和依赖版本能让回滚真正可行。只记录一个浮动模型名,发生回归时很可能连上一个可复现的权重都找不到。

十四、如何读懂“重排没有提升”的结果

没有提升并不意味着实验失败。它可能说明当前 embedding、切分和候选数量已经足以完成任务,新增一个模型只是增加成本;也可能说明评测集主要是精确关键词问题,Cross-Encoder 的优势没有被触发;还可能是你选用的模型语种不匹配、输入被过度截断、金标准未标注跨段证据。与其为了证明投入合理而继续调参,不如把原因写进实验报告。

尤其警惕平均数掩盖伤害。整体 MRR 略升,但“例外条款”“否定问题”或“最新版本”类别明显下降,可能比均值更重要。按问题类型、文档类型、语言和权限范围切分结果,才能发现模型偏好。对于关键问题建立回归集:每次改 embedding、rerank、chunk 或 prompt 都跑一次,任何关键条目退化都需要解释或阻止发布。

同样,不要把生成答案的文采变化误当作检索提升。重排评估首先看证据排名,生成评估再看是否忠实引用;两者分开记录。这样即使某个模型让答案更简洁,你也能知道它没有偷偷丢掉关键条件。

十五、最小发布清单

把 reranker 接进服务前,可以用一张很短的清单收尾:确认正确证据在第一阶段候选池的召回率足够;确认 ACL、租户和有效版本在任何模型调用前过滤;确认模型许可证、权重来源、语言能力和最大长度已审核;确认候选上限、输入上限、超时、限流和降级路径可观测;确认评测同时记录排序、引用和高风险问题的拒答;确认日志不写入不必要的全文和密钥。清单并不花哨,却能避免最常见的“本地很准,上线很危险”。

如果还没有一份标注问题集,先不要着急把模型服务部署到 GPU。用几十个有依据的真实问题建立基线,通常比连续试十个开源模型更快告诉你是否需要重排。若候选池内的正确证据已经稳定排第一,最好的 reranker 就是暂时不加;若正确证据常在第十名附近,才值得为它分配延迟预算。

需要解释每次排序的原因时,不要编造模型的“思考过程”。可解释的事实是:该候选来自哪份有效文档、通过了哪些过滤、在向量阶段和重排阶段分别排第几、最终是否被截断;这些记录足以支持工程复盘,也不会把不可验证的内部推理伪装成证据。

对同一个问题重复请求时,候选顺序若偶尔变化,也要先检查索引是否在更新、服务是否使用了不同模型 revision、并发批处理是否影响了浮点计算,再判断是模型不稳定。把请求时的索引版本与模型版本写入诊断记录,才能把偶发问题从“感觉不稳定”变成可定位的事实。

还应为关键查询保留一次人工复核入口。技术指标可以告诉你排名变了,却不能替业务负责人确认“这条制度解释是否该由系统自动给出”。人机协同不是失败兜底,而是高风险信息系统应有的最后一道边界。

十六、小结

Rerank 的价值在于把“语义大致相近”的候选重新排成“最能回答当前问题”的少量证据。它应位于权限过滤之后、LLM 生成之前;第一阶段负责召回,Cross-Encoder 负责精排,最终只传递有限且多样的上下文。把分数当作排序信号,不要贸然解释成置信度;把改进建立在固定数据集的排名对照上,不要凭一两个演示问题下结论。

最小实现只需要一个批量predict、一个不可变的排序函数和一份问题—证据评测集。后续是否扩大候选池、加多样性、做阈值拒答、拆独立推理服务,都应该由失败样本、P95 延迟和合规边界推动。这样做的好处不仅是“答案更准”,还在于你清楚它为什么变准,以及何时不值得继续加复杂度。

参考资料

  • Sentence Transformers:Cross-Encoder Usage
  • Sentence Transformers:Rerankers
  • Sentence Transformers CrossEncoder API
  • Chroma:Collection Query API
  • OWASP Top 10 for LLM Applications

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询