上一文把RAG的问题拆成了检索证据和回答忠实性。接下来处理一个常见失败:正确文档已经被检索出来,却排在后面,最终拼给模型的前几段没有包含它。
例如用户问"预售订单取消后多久退款",候选里同时出现退款入口、普通订单到账时间、预售退款规则和支付失败说明。它们都带着相关词语,真正回答问题的却是预售规则。
**重排(Reranking)**在候选召回之后重新比较问题与片段,改变进入上下文的优先级。它适合解决"找到了,但没选好"的问题。
一、先区分两个数量
设第一阶段返回top_k个候选,第二阶段只选top_n个送给生成模型。例如先取20个,再选4个。
这两个数字控制不同的取舍。top_k决定重排能看到的范围;top_n决定最终上下文的大小。把top_n调大,会增加阅读材料,也可能把噪声一起放进去。把top_k调小,则可能在重排之前就丢掉关键证据。
候选中没有的证据,重排无法找回来 。因此调整前先检查召回列表,确认问题属于候选遗漏还是排序不佳。Sentence Transformers给出的检索与重排流程也采用两阶段组织方式。
二、CrossEncoder多看了什么
向量检索通常先分别编码问题与文档,再用向量距离选择候选**。CrossEncoder**接收一个问题与一个片段组成的文本对,在共同输入中评估它们的相关性。
对于"预售订单取消后多久退款",相关性判断需要同时处理预售、取消和退款时间三个约束。只包含"退款入口"的片段主题相关,却无法提供所需时间。
重排需要为候选逐对计算,开销随候选数量和文本长度变化。它通常用于缩小后的候选集合;离线文档向量缓存无法直接替代这种依赖当前问题的成对评分。

三、把输入、分数和文档身份绑在一起
下面使用四条虚构规则,先验证重排接入中最容易出错的排序与映射。分数是明确设置的测试输入,用于检查程序;模型接入放在下一节。
python
from math import isfinite
# 固定分数用于验证排序与ID映射;不代表模型推理结果。
candidates = [
{"id": "d1", "text": "退款申请入口位于订单详情页。"},
{"id": "d2", "text": "普通订单通过审核后,退款在3个工作日内到账。"},
{"id": "d3", "text": "预售订单取消后,退款在7个工作日内到账。"},
{"id": "d4", "text": "支付失败时请重新选择支付方式。"},
]
def select_top(candidates, scores, top_n):
if len(candidates) != len(scores):
raise ValueError("score count mismatch")
if len({x["id"] for x in candidates}) != len(candidates):
raise ValueError("duplicate document id")
if not 0 <= top_n <= len(candidates):
raise ValueError("invalid top_n")
rows = []
for position, (doc, value) in enumerate(zip(candidates, scores)):
score = float(value)
if not isfinite(score):
raise ValueError("non-finite score")
rows.append((score, position, doc))
rows.sort(key=lambda x: (-x[0], x[1]))
return [doc for _, _, doc in rows[:top_n]]
if __name__ == "__main__":
chosen = select_top(candidates, [0.2, 0.4, 0.9, -0.3], 2)
print("before:", [x["id"] for x in candidates[:2]])
print("after:", [x["id"] for x in chosen])
relevant = {"d3"}
for name, docs in [("before", candidates[:2]), ("after", chosen)]:
recall = len({x["id"] for x in docs} & relevant) / len(relevant)
print(f"{name} recall@2={recall:.1f}")
assert [x["id"] for x in chosen] == ["d3", "d2"]
try:
select_top(candidates, [1.0], 2)
except ValueError as ex:
print("rejected:", ex)
保存为rerank_demo.py,执行python rerank_demo.py,本次运行结果:
text
before: ['d1', 'd2']
after: ['d3', 'd2']
before recall@2=0.0
after recall@2=1.0
rejected: score count mismatch
这个小例子重点检查四件事。
第一,分数数量必须与候选一致。直接zip会在长度不一致时截断,遗漏候选而不报错,所以先做校验。
第二,排序时一起携带文档对象。只排序分数,再按排序后的索引访问原候选,容易把分数与证据配错。
第三,相同分数保留原顺序。这样可以稳定复现结果,避免相同输入在后续阶段出现无意义的抖动。
第四,最终保留来源ID。回答中的引用需要回到原始文档,不能用排序名次取代文档身份。
四、接入一个真实重排模型
以BAAI/bge-reranker-v2-m3 为例,官方模型卡支持通过Sentence Transformers的CrossEncoder接入,输入形式是问题与片段列表,输出每一对的相关性分数。模型及接口参见官方模型卡和CrossEncoder文档。
安装依赖后,可以将上一节的固定分数替换成模型输出:
bash
pip install sentence-transformers
python
from sentence_transformers import CrossEncoder
from rerank_demo import candidates, select_top
query = "预售订单取消后多久退款?"
model = CrossEncoder("BAAI/bge-reranker-v2-m3")
pairs = [(query, doc["text"]) for doc in candidates]
scores = model.predict(pairs)
selected = select_top(candidates, scores, top_n=2)
for doc in selected:
print(doc["id"], doc["text"])
首次加载需要下载模型权重。部署时固定依赖版本和模型revision,在同一评估集上比较升级前后的结果,避免把版本变化与检索参数变化混在一起。
分数用于排序,不能直接解释为"这段话有百分之多少概率正确"。即便使用sigmoid把数值映射到0到1,数值范围的变化也不等于获得了经过校准的正确率。
五、完整链路中,哪些步骤不能调换

先按用户权限和资料适用范围过滤,再形成候选,随后重排,最后完成去重与上下文预算控制。文档身份、来源、章节和版本信息要伴随片段传递。
权限过滤放在模型读取之前。不能先把无权访问的内容交给外部重排服务,再在生成阶段隐藏;内容在评分阶段已经离开权限边界。
时效规则单独处理。旧版预售规则可能语义上非常贴合问题,也能得到高相关性。是否已经废止、适用于哪个产品,应通过元数据规则决定,不能让相关性分数代替有效性判断。
去重保留覆盖面。如果前四名都是同一段话的重叠切片,top_n=4也只提供了一份证据。去重后应从后续候选补齐,或者按预算停止;不能把四个高度重复片段误认为四种独立支持。
六、长片段和分数阈值容易怎样误导
长文档被截断时,重要条件可能落在模型看不到的位置。预售规则若写在段落末尾,评分输入只保留开头,系统就会依据残缺文本排序。处理时应检查分词后的实际输入长度,按语义边界切分,并携带必要的小标题。
分数阈值也需要用评估数据确定。某个查询的最高分很低,仍可能是候选中最好的片段;另一个查询的最高分很高,也不保证片段足以回答全部问题。排序、是否有足够证据回答、是否需要继续检索,是三个不同决策。
对于需要多条证据的问题,应评价证据集合。例如"普通订单与预售订单分别多久到账",只选预售规则仍不完整。逐片段相关性高,并不自动保证组合后覆盖所有子问题。
七、怎样证明重排值得加入
保持问题集、召回候选和生成提示一致,比较加入重排前后的top_n上下文。先看关键证据是否进入上下文,再看回答是否正确使用它们。
| 检查项 | 能回答的问题 |
|---|---|
| 候选Recall@k | 召回阶段是否找到必需证据 |
| 最终Recall@n | 选入上下文的片段是否覆盖证据 |
| 首个相关结果的位置 | 用户或模型多久能看到核心材料 |
| 回答完整性与忠实性 | 证据进入后是否被正确使用 |
| 延迟分位数与请求成本 | 新增阶段是否符合服务目标 |
单个退款例子说明排序逻辑,实际评估集还需要包含同义问法、相似政策、过期版本、多条件问题和没有答案的提问。对每一类失败分别统计,才能决定应扩大候选、改善切分、换重排模型,还是修正生成提示。
重排服务超时后可以回退到原候选顺序,但要把回退记录进运行指标。否则业务看到的结果来自两条不同路径,质量波动却无法解释。
八、🧠 思维导图
#mermaid-svg-M2RCUjkv92PVyuOS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M2RCUjkv92PVyuOS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M2RCUjkv92PVyuOS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M2RCUjkv92PVyuOS .error-icon{fill:#552222;}#mermaid-svg-M2RCUjkv92PVyuOS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M2RCUjkv92PVyuOS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M2RCUjkv92PVyuOS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M2RCUjkv92PVyuOS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M2RCUjkv92PVyuOS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M2RCUjkv92PVyuOS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M2RCUjkv92PVyuOS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M2RCUjkv92PVyuOS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M2RCUjkv92PVyuOS .marker.cross{stroke:#333333;}#mermaid-svg-M2RCUjkv92PVyuOS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M2RCUjkv92PVyuOS p{margin:0;}#mermaid-svg-M2RCUjkv92PVyuOS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M2RCUjkv92PVyuOS .cluster-label text{fill:#333;}#mermaid-svg-M2RCUjkv92PVyuOS .cluster-label span{color:#333;}#mermaid-svg-M2RCUjkv92PVyuOS .cluster-label span p{background-color:transparent;}#mermaid-svg-M2RCUjkv92PVyuOS .label text,#mermaid-svg-M2RCUjkv92PVyuOS span{fill:#333;color:#333;}#mermaid-svg-M2RCUjkv92PVyuOS .node rect,#mermaid-svg-M2RCUjkv92PVyuOS .node circle,#mermaid-svg-M2RCUjkv92PVyuOS .node ellipse,#mermaid-svg-M2RCUjkv92PVyuOS .node polygon,#mermaid-svg-M2RCUjkv92PVyuOS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M2RCUjkv92PVyuOS .rough-node .label text,#mermaid-svg-M2RCUjkv92PVyuOS .node .label text,#mermaid-svg-M2RCUjkv92PVyuOS .image-shape .label,#mermaid-svg-M2RCUjkv92PVyuOS .icon-shape .label{text-anchor:middle;}#mermaid-svg-M2RCUjkv92PVyuOS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M2RCUjkv92PVyuOS .rough-node .label,#mermaid-svg-M2RCUjkv92PVyuOS .node .label,#mermaid-svg-M2RCUjkv92PVyuOS .image-shape .label,#mermaid-svg-M2RCUjkv92PVyuOS .icon-shape .label{text-align:center;}#mermaid-svg-M2RCUjkv92PVyuOS .node.clickable{cursor:pointer;}#mermaid-svg-M2RCUjkv92PVyuOS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M2RCUjkv92PVyuOS .arrowheadPath{fill:#333333;}#mermaid-svg-M2RCUjkv92PVyuOS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M2RCUjkv92PVyuOS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M2RCUjkv92PVyuOS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M2RCUjkv92PVyuOS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M2RCUjkv92PVyuOS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M2RCUjkv92PVyuOS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M2RCUjkv92PVyuOS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M2RCUjkv92PVyuOS .cluster text{fill:#333;}#mermaid-svg-M2RCUjkv92PVyuOS .cluster span{color:#333;}#mermaid-svg-M2RCUjkv92PVyuOS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M2RCUjkv92PVyuOS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M2RCUjkv92PVyuOS rect.text{fill:none;stroke-width:0;}#mermaid-svg-M2RCUjkv92PVyuOS .icon-shape,#mermaid-svg-M2RCUjkv92PVyuOS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M2RCUjkv92PVyuOS .icon-shape p,#mermaid-svg-M2RCUjkv92PVyuOS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M2RCUjkv92PVyuOS .icon-shape .label rect,#mermaid-svg-M2RCUjkv92PVyuOS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M2RCUjkv92PVyuOS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M2RCUjkv92PVyuOS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M2RCUjkv92PVyuOS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RAG重排
候选召回
先过滤权限
保留证据覆盖
CrossEncoder
问题与片段成对
输出相关性分数
选择上下文
稳定排序及去重
保留来源ID
效果验证
召回与排序分开
质量和延迟一起看
九、总结
总结要点
召回决定可选范围。重排只在已有候选中工作,先确认关键证据被找到,再优化排序。
分数与来源必须一起流转。数量校验、稳定排序、去重和ID保留,直接决定最终上下文有没有拿错材料。
相关性只是选择依据之一。权限、资料有效性、问题覆盖面与上下文预算,都需要明确规则。用固定评估集同时观察质量和延迟,才能判断改动是否有价值。
下一篇继续讨论RAG混合检索:关键词召回和向量召回怎样合并候选。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊