【随笔】RAG重排怎样挑证据:先扩大候选,再把相关片段排到前面

上一文把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混合检索:关键词召回和向量召回怎样合并候选。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

相关推荐
知赛官方1 小时前
DeepSeek 又偷偷升级了:我实测了 V4.1-Flash-1017,一句话生成可玩游戏,但有个坑会吃掉你 90% 的预算
人工智能
宋艳_AI应用开发1 小时前
页面有时间,AI却说没有:排查一个Bug,搞懂了AI应用的调试逻辑
人工智能
用户610719266631 小时前
我踩了4个坑才搞懂 AI 视频模型该怎么选
人工智能
doujiaozh1 小时前
从 0 到 1:我用 300 行代码给 AI 记忆加上了“不可篡改的收据”(附开源实现)
人工智能·开源
云和数据.ChenGuang1 小时前
python大小整数范围解析
java·开发语言·python
果霸大叔1 小时前
AI 网关第 9 篇:运行时处理模型——请求进到数据面后底层经历了什么
人工智能·自然语言处理·ai-native
zhidian_AI1 小时前
AI 智能体改 BUG,占满 CPU 内存?3 道资源护栏兜底
人工智能
Morning的Tech1 小时前
LangGraph 的 Swarm 多智能体机制
人工智能·agent
probably1211 小时前
基于水利部技术标准的山洪灾害科学防治体系建设探析
大数据·人工智能