☰
RAG翻车救星:Rerank重排如何让向量检索的正确答案精准进入Prompt
2026/10/24 4:34:09 网站建设 项目流程

1. 从一个翻车现场说起:检索到了,答案还是错的

我之前给人调一个 RAG 问答系统,对方很兴奋地给我演示:他们接好了向量数据库,Embedding 模型也换了更贵的,问题随便问,看起来都有模有样。但我随手抛了一个业务场景问题,答案立刻露馅——系统检索出来的文档片段里,明显有正确的段落,而且排名还不低,可最终大模型生成的内容引用了一个八竿子打不着的条款,做出了一段“看起来合理但根本不是用户要的”回答。

这其实是特别典型的 RAG 翻车现场:不是没检索到,是检索到了,但正确答案没有进入最终拼接给大模型的那一小段上下文里。或者更准确地说,它混在一堆“相关但无用”的噪声片段里,被生成模型无视了。从那之后我就一直跟人说,RAG 这条流水线上,最容易被低估也最值得优先补的环节,就是 Rerank。

RAG 这个名字这两年几乎被说烂了,但多数团队搭的第一个版本都长一个样:问一句 -> 用向量检索召回 TopK 个片段 -> 全部塞进 Prompt -> 让大模型生成答案。这套流程跑起来很容易,但想让它稳定好用,几乎一定会撞上同一个问题:召回回来的 TopK,不等于质量排序正确的 TopK。这也是我后来把所有 RAG 项目都改成“召回-重排-生成”三段式的原因,Rerank 在这条链路里不是锦上添花,是必需品。

1.1 一次最典型的 RAG 事故

先还原一下那个现场。知识库里有几百篇合同模板和条款说明,问题问的是“这个违约金条款在跨区域履约时应该优先适用哪个法律依据”。从向量召回的结果看,系统其实已经把一份写明了适用法律条款的文档片段排到了 Top10,得分也不低。

但问题是它排在了第7位。系统当时设置的 TopK 是 5,于是拼接进 Prompt 的就只有前5个片段,正确的那条被硬生生挡在门外。大模型根本不知道有这个片段存在,只能靠前5个碎片里那些“违约金”“适用法律”之类的零散词硬凑一个答案,结果自然不对。

这类事故最气人的地方在于:检索层没做错什么,向量分数也确实把正确内容找出来了,纯粹是“排序不对”导致正确答案没被送进生成阶段。这种问题靠调 Embedding 模型很难解决,因为你无论怎么调向量,Top1、Top2 都被一些在文本表面上更相似、但实际回答不了问题的段落占着。

1.2 朴素的 RAG 流程里,问题到底出在哪一环

可以把最初的 RAG 流水线拆成四步:向量化、召回、拼接、生成。大部分团队花大力气优化第一步和第二步,比如换更好的中文 Embedding 模型、给文档做更细的切分、接混合检索召回更多候选。这些优化当然有效,但它们解决的是“能不能找到正确内容”的问题,而不是“正确内容能不能排在最前面”的问题。

第三步“拼接”看似没有技术含量,实际上才是决定生成质量的关键。大模型的输入上下文是一块有限的地皮,你往里面塞什么、顺序怎么样、噪声比例有多高,直接决定它回答质量的上限。如果在拼接之前没有人对召回结果做一次“精排”,大模型就成了一个被迫阅读杂乱的资料堆然后强行总结的人。

这里有个很直观的感受:上下文越长,大模型注意力越分散,对中间位置的片段记忆越含糊。哪怕你把 TopK 从 5 扩大到 20,正确片段确实进了上下文,但它被夹在十几个无关段落里,生成时很容易被更高分贝的噪声带跑。所以问题的核心不在召回数量,而在拼接窗口这一小块地皮里,你能不能保证最要紧的几段真的在最显眼的位置上。

1.3 结论前置:Rerank 就是那道门槛

所以 Rerank 在 RAG 里的角色可以概括成一句话:在召回结果里,用更精细的模型重新给每个片段打一遍分,把“可能有用”排前面,把“只是有点像但不是答案”的踢下去,保证拼进 Prompt 的每一段都是精挑细选出来的。

它不是替代向量检索,也不是给大模型提供额外知识,它只是在召回和生成之间加了一道过滤和排序的门槛。这个门槛看着简单,但对最终效果的影响,往往比换一个更大的向量模型还明显。我在后面会详细拆解里面的原理、数据、选型和坑。

2. 向量召回的两个结构性短板:粗筛逻辑天生无法精准排序

很多人第一次听到“Rerank 有必要吗”这个问题时,第一反应是:向量检索已经算出相似度了,再排一次不是多此一举吗?如果你也这么想,那要先理解一个事实——向量检索的设计目标从来就不是“精准排序”,而是“快速捞回”。

2.1 Embedding 是一次“语义压缩”,细节必然丢失

先看 Embedding 的原理。无论你用的是哪个开源向量模型,它的工作方式都类似:把一个 query 或一段文档编码成一个固定长度的稠密向量,常见的有 768 维、1024 维。这意味着,一篇 500 字的法律条款,最终被压缩成几百个浮点数,用这些浮点数来表示它的“语义”。

这一步压缩一定是有损的。模型在训练时学到的,是“什么样的文本在语义上更接近”,它擅长捕捉主题、领域、关键词分布这些粗粒度特征,但很难精确记住“这句话里到底哪个限定条件最关键”。打个比方,向量检索就像招聘专员只看简历摘要来筛选候选人,他能判断这个人是不是做技术的、工作年限够不够,但没法凭摘要确定这个人能否解决一个具体的技术难题。

这类粗粒度匹配在“找主题类似的内容”时非常高效,但在“找一个能精确回答某个具体问题的证据片段”时就不够用了。RAG 要求的恰恰是后者。因为你的用户不会问“有没有关于违约金的资料”,他会问一个带着具体约束条件的业务问题,这时候仅有主题相似远远不够。

2.2 相似不等于有用:召回分数的三个陷阱

我碰到的检索结果里,最常见的误判有三种。

第一种是关键词重叠陷阱。假设问的是“儿童退烧药的服用间隔”,知识库里有一篇长文叫“退烧药使用注意事项”,里面大量出现“退烧药”“服用”“间隔”这些词。向量模型算相似度时,这些词的重叠会拉高分数,但这篇文章主要讲成人用法,真正能回答儿童问题的段落反而排在后面。

第二种是主题接近但不对口。问的是“合同解约后的赔偿标准”,召回结果里有一篇关于合同违约类型的文章,主题同属“合同纠纷”,看起来关联很强,但里面根本没有回答赔偿标准的条款。主题接近和能回答问题,在向量空间里是两回事。

第三种是召回分数没有经过任务级校准。向量模型训练用的数据通常是通用图文对或搜索点击数据,它的分数含义和“这段文本能否正确回答当前问题”不是一回事。所以你会发现,有时候一个完全答非所问的片段,余弦相似度竟然有 0.8,而正确答案反而只有 0.72。

这些情况不是哪个向量模型不好,而是“相似度匹配”这个机制本身的上限决定的。它在召回阶段足够可靠,在排序阶段远远不够。

2.3 召回阶段的目标是 Recall 而不是 Precision

理解 RAG 系统设计,有个关键点:召回阶段和排序阶段的目标函数是不同的。

召回阶段追求的核心指标是 Recall@K,也就是“正确答案有没有被捞进前 K 个候选里”。为了把 Recall 拉高,你会不自觉地调大 K,比如从 Top5 调到 Top20、Top50。K 越大,正确答案漏掉的可能性越低,但随之而来的是:噪声片段的绝对数量也在上升。原本只要应付5个候选里的2个噪声,现在要应付50个候选里可能40个噪声。

这就在“拼接给大模型的窗口”层面制造了一个矛盾:正确答案确实进了召回集,但它在几十个低质候选里根本排不上号。如果直接把 Top50 塞给大模型,且不说 token 成本爆炸,注意力也会被大量干扰项稀释,生成质量不升反降。

Rerank 的存在,本质上就是解决这个矛盾的:召回阶段负责“广撒网保 Recall”,Rerank 阶段负责“精准打分保 Precision”。两件事分开做,才符合 RAG 这类工程系统“先粗筛后精排”的经典漏斗设计。想靠一个环节同时做好两件事,通常两头都做不漂亮。

3. Rerank 的本质:用交叉编码器把“粗筛”变成“精读”

如果只用一句话解释 Rerank 模型为什么能比向量检索更准,那就是:它把 query 和文档放在同一个模型里,让它们充分交互,而不是各自独立编码后再用向量点积来近似。

3.1 双塔和交叉编码器的根本区别

向量检索用的通常是双塔结构(也叫 Bi-Encoder)。它把 query 编码成一个向量,把文档编码成另一个向量,两边彼此独立,最后只在输出层做一个简单的余弦相似度。这种设计的好处是文档向量可以离线算好、存进向量库,在线查询时只要算一个 query 向量,然后去做近似最近邻搜索,速度极快,可以支撑百万级甚至亿级规模的召回。

Rerank 模型用的则是交叉编码器(Cross-Encoder)。它会把你问的问题和待评分的文档拼成一个文本对,作为一个序列一起输入模型。比如:

[CLS] 儿童退烧药的服用间隔是多少 [SEP] 退烧药使用注意事项:儿童应在体温超过38.5度时服用,间隔4-6小时 [SEP]

Transformer 在计算注意力时,query 里的每一个 token 都能直接观察到文档里每一个 token 的信息,反过来也一样。这意味着模型能做词级交互,能捕捉“退烧药”和“儿童”“间隔”之间细微的限定关系,而不是像双塔那样,两边各自读完再隔着房间喊一句话。

两种结构的差异,我用一个比较生活化的类比来解释:双塔模式像是两个人都提前在家看了简历,见面后只交换一下结论;交叉编码器模式像是两个人拿着一沓材料面对面逐行讨论,任何细节都能当场对质。后者当然更精确,代价就是每来一个问题,所有候选文档都要跟着重新算一遍,不能离线缓存,计算量自然大得多。

3.2 Rerank 的分数从哪来

交叉编码器最后输出的通常是一个 logit 值,再经过 sigmoid 之类的激活函数转成一个 0 到 1 之间的相关性分数。

这个分数代表的是“在当前这个问题下,这段文档跟问题的相关程度有多高”,而不是语义相似度。所以你会发现一个有意思的现象:同一个文档,换一个问法,Rerank 给的分可能差别很大,因为它真正建模的是 query 和文档之间“问答对”级别的相关性,而不是两个文本“长得像不像”。这正是 RAG 场景最需要的能力。

需要提醒的是,不同 Rerank 模型输出的分数分布不一样。有的模型输出集中在 0.1 到 0.9,有的则聚在 0.9 以上,所以当你拿某个模型跑出来的阈值经验去套另一个模型时,基本都会踩坑。最好的做法是让模型先跑一批真实的 query 和文档,自己看一遍分数分布再定阈值,别硬套别人的经验值。

3.3 “把 Top10 都喂给 LLM 让模型自己挑”为什么不行

还有一种常见思路是:既然大模型理解能力强,干脆就靠它,把召回 Top10、Top20 全部拼进去,让 LLM 自己判断哪些相关。听起来好像没必要专门做一个 Rerank,但实际一测就露馅了。

大模型的上下文窗口确实越做越大,但长上下文不等于每个 token 都会被同等关注。我做过一组简单的对比实验,同一批问答,一个方案是把向量召回 Top5 直接塞进 Prompt,另一个方案是先召回 Top20,用 Rerank 挑出最相关的 Top3 再塞进去。结果挺打脸的:前者不仅回答正确率更低,还更容易在答案里“引用”一些看似相关但其实是噪声的段落,产生幻觉。

原因也很简单。第一,上下文越长,注意力越发散,模型对排在中间位置的片段记忆很模糊,容易被开头结尾的强势段落带偏。第二,噪声片段本身可能包含部分正确的词句,生成时会诱导模型把这些词句组织成貌似合理的错误答案。与其让大模型在杂乱的草稿里挑重点,不如在把草稿交出去之前就整理好,这才是 Rerank 存在的意义。

4. 哪些场景真正离不开 Rerank:一张判断清单

Rerank 不是“所有 RAG 都必须上”的银弹,但确实有一批典型场景,不加它几乎必然翻车。我按自己实际接触过的项目,总结出四类最容易受益的场景,以及两个可以暂缓的反例。

4.1 四种典型场景

场景为什么离不开 Rerank
知识库问答/客服助手知识库上万条甚至几十万条,向量召回必然混入大量主题相近但答非所问的片段,Top1 直接决定回答质量
多路召回合并同时用了稀疏检索、向量检索甚至关键词检索,各路分数量纲不一致,必须有一个统一的精排模型重新打分
结果直接展示给用户比如“相关文档推荐”,用户第一眼看到的就是前三个结果,排序质量即产品口碑
医疗、金融、合同类对错误零容忍的场景正确答案排在Top5之外就等于没检索到,必须用精排把正确片段压到最前面再交给生成模型

第一类场景最直观。只要你的知识库规模大过“几十条文档、每次都全塞进 Prompt”的量级,向量召回后的 Top1 就未必是真正能回答问题的片段。这时候 Rerank 负责把正确答案从 Top20 里捞到 Top3,对比特别明显。

第二类场景容易被忽略。有些项目为了提升召回率,把 BM25 稀疏检索、向量稠密检索、甚至 ES 的布尔查询结果合并起来,这时你会发现各路分数根本不是同一个体系:BM25 的 20 分和向量的 0.8 相似度没法直接比较。简单做法是做个归一化再加权求和,但这类启发式方法在不同数据集上波动很大。用交叉编码器统一重打一遍分,是最省心、也最可解释的做法。

4.2 两个不太需要 Rerank 的反例

也有不需要上 Rerank 的场合。如果你的知识库非常小,比如只有几十段标准操作流程,每次查询都能把全部内容塞进 Prompt,那确实不需要 Rerank,因为所有内容都已经进上下文了,排序意义不大。另一种情况是文档内容高度同质,比如只检索某一类固定格式的产品参数,噪声本身就少,向量召回的 Top5 基本足够,Rerank 带来的提升有限。

所以更准确的说法是:当“正确片段能被召回,但排不到前几位”这个问题在你的数据上真实存在时,Rerank 就完全是必需品。判断方法也很简单:抽几组真实问题,人工看一下向量召回 Top10 里正确答案排在第几位。如果正确答案经常在 Top5 之后、Top10 之内,Rerank 带来的收益几乎立竿见影。

4.3 五分钟判断你的 RAG 是否需要 Rerank

我判断一个项目值不值得加 Rerank,一般不做复杂分析,就做一件事:取 20 条真实的用户问题,分别跑向量召回 Top10,人工标注正确答案的位置,然后统计。

如果正确答案落在 Top3 的比例已经超过 90%,那说明你的知识库和向量模型配合得足够好,Rerank 可以先放一放。但如果这个比例只有 60% 到 70%,甚至更低,那 RAG 的生成质量就会非常不稳定——因为每三次回答里就有一次,正确答案根本没进 Prompt。这个“正确答案能否进入前几位”的比例,才是决定要不要上 Rerank 的核心指标,而不是“检索感觉准不准”。

5. 实操接入:一套可复现的最小配置

讲了这么多原理,接下来给一套可以直接照抄的最小接入方案。Rerank 的服务化部署其实很轻量,关键是选对模型、写好调用逻辑、定好阈值。

5.1 模型选型与运行方式

目前开源领域里我用的比较多的是 bge-reranker 系列,中文场景表现稳定,而且有 base、large 和 v2 多语言版本可选。英文场景也可以用常见的 cross-encoder 系列模型。选型大致可以参考这个思路:

需求建议型号说明
快速实验、CPU 部署base 或 m3 版本体积小,推理快,效果已经够用
追求更高质量、有 GPUlarge 版本精度更高,但显存占用和推理耗时也要跟上
多语言/中英混合知识库v2 多语言版本跨语言场景下比纯中文模型稳

跑 Rerank 最好单独起一个推理服务,把模型加载到 GPU 上,然后给主服务提供一个 HTTP 接口。不要把它和向量召回的接口混在一块,因为它和 Embedding 模型是两套权重、两种推理模式,混用会让内存管理和热更新都变得别扭。

5.2 一个跑通的调用示例(Python)

如果你只想最快跑通,推荐直接用开源的 FlagEmbedding 工具库,它封装了加载和打分逻辑:

from FlagEmbedding import FlagReranker # 这里填你本地拉取到的模型路径或模型名 reranker = FlagReranker("bge-reranker-base", use_fp16=True) query = "儿童退烧药的服用间隔是多少" documents = [ "儿童腋温超过38.5度时可服用退烧药,两次间隔不少于4小时。", "成人退烧药建议每6小时服用一次,24小时内不超过4次。", "退烧药应放在儿童无法触及的地方,以免误服。", ] pairs = [[query, doc] for doc in documents] scores = reranker.compute_score(pairs, normalize=True) results = sorted( zip(documents, scores), key=lambda x: x[1], reverse=True ) for doc, score in results: print(f"{score:.4f}\t{doc}")

如果不想引入额外依赖,也可以用 transformers 直接加载模型来做,本质是一样的:

from transformers import AutoModelForSequenceClassification, AutoTokenizer model_name = "bge-reranker-base" # 替换成实际模型路径 tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name) pairs = [ ("儿童退烧药的服用间隔是多少", "儿童腋温超过38.5度时可服用退烧药,两次间隔不少于4小时。"), ("儿童退烧药的服用间隔是多少", "成人退烧药建议每6小时服用一次,24小时内不超过4次。"), ] inputs = tokenizer( pairs, padding=True, truncation=True, max_length=512, return_tensors="pt", ) logits = model(**inputs).logits.view(-1).float() scores = logits.sigmoid() # 转成0到1的分数 print(scores)

跑完之后你会得到一个分数数组,按分数降序取前 N 个作为最终上下文即可。需要注意的是,normalize=True和sigmoid给出的分数范围不一样,但排序效果一致。个人习惯是统一用 0 到 1 的分数,方便后面定阈值。

5.3 接入到完整 RAG 链路中

实际接入时我的链路一般是这样的:

用户问题 → 向量召回 Top 50 → 去重 → Rerank 打分 → 阈值过滤 → 拼接 Top3 → 生成

以下是一个简化的伪代码结构:

def build_context(query, top_n=50, max_keep=3, threshold=0.4): # 1. 向量召回候选 candidates = vector_search(query, top_n) # 2. 去重(按正文hash或embedding相似度) candidates = dedup(candidates) # 3. Rerank打分 scores = reranker.compute_score([[query, doc] for doc in candidates], normalize=True) # 4. 按分数降序,并过滤低分片段 ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) kept = [doc for doc, score in ranked if score >= threshold][:max_keep] # 5. 拼进prompt return format_context(kept)

这串逻辑看起来简单,但每个环节都有坑。下面单独开一章讲我踩过的和调优过的细节。

6. 调参与避坑:我实际验证过的几个关键细节

Rerank 模型本身不用训练,直接拿来打分就行,但要想在真实项目里稳定落地,有几个细节值得花时间调。

6.1 先做去重,再做重排

有一次我把一批长文本切成几百个片段喂进 RAG,Rerank 出来的结果让人哭笑不得:Top10 里有一大半是同一个来源的不同切块,因为它们在语义上都很接近。去重这一步如果省了,重排后取 Top3 很可能会取到三段内容几乎一样的片段,等于整个上下文白给了。

最简单的方式是对片段内容做 hash 去重。如果想要更细一点,就先用向量算一下片段之间的相似度,把相似度超过 0.9 的合并成一组,每组只保留最有代表性的一个。注意去重要在 Rerank 之前做,否则重复片段会把真正有价值的其他段落挤出去。

6.2 长文本必须切块截断,别让模型硬扛

交叉编码器对输入长度有上限,常见的是 512 个 token。你如果直接把一个超长段落丢进去,后面部分会被截掉,Rerank 看到的可能只是这段材料的开头,打分自然不准。

所以文档进入知识库之前就要做好切块。一个很现实的经验是:切块质量对 Rerank 效果的影响,可能比模型选择还大。如果一刀切成固定 512 字符,很容易把关键信息从中间截断;如果切成完整的小节或段落,相关性信号会更完整。我一般会按语义边界切,尽量保证每个块是一个能独立回答问题的最小单元,再对超长块做进一步拆分或折叠处理。

6.3 阈值别盲设 0.5,先看分数分布

Rerank 分数到底多少算高、多少算低,跟模型、语料、文本长度都有关。直接拿一个网上抄来的阈值套到自己项目上,几乎必踩坑,因为不同模型的分数分布差异太大了。

我现在的做法是上线之前先收集 100 组真实 query 和对应候选片段,用 Rerank 跑一遍,人工标出哪些片段是真正相关的,然后画出相关与不相关片段的分数分布。一般来说,相关片段会集中在高分区间,不相关片段会散在低分区,中间会有一个相对清晰的界线。阈值就选在这个界线上,再留一点余量。这样比拍脑袋定 0.5 靠谱得多。

另外要提醒的是:阈值设太高会把正确答案过滤掉,阈值设太低又会让噪声混进来。你要结合“召回与精排后的取舍”来权衡,宁可让 Rerank 多放一两个低分段落,也不要让正确答案被阈值误杀。

6.4 Rerank 之后,片段的顺序和格式也要讲究

Rerank 只解决“哪些片段更重要”的问题,不解决“片段以什么形式进入 Prompt”的问题。同样是 Top3,直接按分数降序堆给大模型,和先加一句引导语、再按“最相关→次相关”排列,效果有明显差别。

我的习惯是把最高分片段放在 Prompt 的靠前位置,并且在每段前面加一个标题式引导,比如“参考材料一:关于儿童退烧药服用间隔的规定”。这一步看似小,但对大模型的提取效率有帮助,尤其在上下文片段较多时,等于给它指了一条路。还有一个小技巧:如果 Top1 的分数明显高于其他片段,可以在 Prompt 里明确说“优先依据第一段材料回答”,实测下来能进一步减少跑偏概率。

6.5 一组对照实验的数据参考

我之前在一个模拟客服知识库项目上做过对照实验,知识库约 5 万条片段,用 200 个问答对评估,结果是这样的:

方案Top1 命中率(正确答案排第一)最终回答正确率
直接向量召回 Top5 拼 Prompt52%72%
向量召回 Top20 + Rerank 选 Top384%91%
向量召回 Top50 + Rerank 选 Top581%88%

注意最后一行,Top50 + Rerank 选 Top5 反而比 Top20 + Rerank 选 Top3 低了几个点。这里的原因不复杂:候选里噪声绝对数量太大,即使有了 Rerank,Top5 的数量也摊薄了上下文的质量。所以不要以为 Rerank 之后 TopK 就可以无脑调大,精排后保留的片段数量,通常控制在 3 到 5 条最优,再多只会稀释注意力。

这也是我反复跟人强调的一点:Rerank 不是万能药,它的作用是把“排序”这个环节做对,但“选多少条进 Prompt”这件事还得你来定,得根据实际数据测试。

6.6 性能开销:重排到底有多“贵”

最后聊一个大家都会关心的问题:交叉编码器比双塔慢得多,加一个 Rerank 是不是会让接口延迟涨到不可接受?

说实话,Rerank 的计算量确实比向量检索大,但它只作用于召回后的候选集,一般就是几十条,不会对全量文档跑。以 base 规模的 Rerank 模型为例,在普通 GPU 上跑 50 条候选的批量打分,耗时通常是几十毫秒到一两百毫秒的量级。如果你的 Rerank 是独立服务,这个耗时只占总链路的一小部分——和动不动一两秒的大模型生成相比,根本不是瓶颈。

真正要注意的反而是候选数。如果你为了“保险”把候选集调到几百条甚至上千条,Rerank 的耗时和资源占用会被明显拉高。我一般建议候选数控制在 20 到 100 之间,跨过一个量级之后,收益几乎不再增长,成本却是线性往上走的。

根据我个人做 RAG 项目的体会,重排环节是最值得先投入的优化点之一。很多人一开始会花大量时间换更大的向量模型、调更复杂的切分逻辑,但其实只要在召回后面加一个 Rerank,正确率就能往上走一大截。而且 Rerank 本身的接入成本并不高,开源模型足够用了,真正花时间的反而是去重、切块、阈值这些看起来不起眼的细节。如果看完这篇你也准备给 RAG 加上重排这步,我的建议是:先从一个小批量候选集跑起,拿真实的 query 看上几组分数分布,再决定阈值和保留条数,效果一定看得见。

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

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

立即咨询