知识库里明明有答案,AI却先引用了不相关的材料,问题可能出在检索结果的顺序上。比如搜索“出差住宿发票有什么要求”,系统先找到了设备采购报销,真正相关的住宿说明排在后面;如果只把前两条材料交给模型,回答就容易遗漏条件。
Reranking,中文通常叫重排,是对已经找到的候选材料重新打分、调整顺序。它适合解决“找得到,但排得不好”的问题。本文用一个报销资料的小练习,讲清召回、重排和最终回答的分工,帮助你判断知识库应该先改哪一步。
练习使用Python标准库,不需要账号、API密钥或下载模型。文中的分数是人为设置的教学数据,演示编号匹配和排序流程,不代表真实模型推理,也不代表任何检索效果测评。
一、先分清三个环节各自负责什么
第一步是召回:从资料库里找出一批可能相关的片段。它可以使用关键词检索、向量检索,也可以合并多个检索通道。这里追求的是候选覆盖,不能把真正需要的资料提前漏掉。
第二步是重排:把用户问题和候选片段一起交给评分机制,再选出更值得阅读的片段。常见实现是Cross-Encoder,它对问题与每个候选组成的输入对分别计算分数。
第三步是回答:把选中的证据、来源编号和问题交给生成模型,要求模型在证据范围内组织回答。回答里能否准确引用,还与提示词、上下文组织和模型行为有关,不能把所有问题都归给重排。
Sentence Transformers官方文档介绍了“先召回,再重排”的组合:先用较高效的检索得到候选,再用逐对计算的模型调整顺序。重排只能在现有候选里选择,不能凭空补回未被召回的资料。
二、用一份小资料集明确问题
假设员工问:“出差住宿的发票有哪些要求?”本次召回得到四条片段:
- d1:设备采购报销需要提交采购申请和发票。
- d2:差旅住宿报销的住宿发票必须填写单位抬头。
- d3:会议参与者需要在入口登记签到。
- d4:同一住宿订单的重复发票不得重复提交。
候选初始顺序是d1、d2、d3、d4。如果只取前两条,关于重复提交的要求就不会进入后续回答。对于这个问题,d2和d4应当优先,但这是我们对教学材料的人工判断,不是模型已经给出的结论。
再假设资料库里还有d5:“住宿报销还需要订单明细”,它没有被本次召回。无论重排多准确,四条候选中都不包含d5。此时应先检查资料是否正确入库、切分是否合适、检索条件是否过窄,而不是继续调整排序阈值。
三、准备环境并运行排序练习
先确认电脑已经安装可用的Python 3,本文只调用标准库中的math。以下用python作为命令名;如果你的Windows环境使用py,把命令前缀替换为实际可用的启动命令。先执行:
python --version在一个空文件夹里建立rerank_demo.py,把下面内容完整保存。文件编码使用UTF-8,避免中文材料出现乱码。
import math
candidates = [
{"id": "d1", "text": "设备采购报销:提交采购申请和发票。"},
{"id": "d2", "text": "差旅住宿报销:住宿发票必须填写单位抬头。"},
{"id": "d3", "text": "会议签到:参与者在入口登记。"},
{"id": "d4", "text": "差旅住宿报销:同一订单重复发票不得重复提交。"},
]
# 人工指定的练习分数,不是模型输出或相关概率。
scores = {"d1": 0.2, "d2": 0.9, "d3": 0.1, "d4": 0.8}
def apply_scores(items, score_map, k):
ids = [x["id"] for x in items]
if len(ids) != len(set(ids)):
raise ValueError("候选编号重复")
if set(ids) != set(score_map):
raise ValueError("候选与分数编号不匹配")
if type(k) is not int or not 1 <= k <= len(items):
raise ValueError("k超出候选范围")
for value in score_map.values():
if type(value) not in (int, float) or not math.isfinite(value):
raise ValueError("分数必须是有限数值")
ranked = sorted(items, key=lambda x: score_map[x["id"]], reverse=True)
return ranked[:k]
if __name__ == "__main__":
print("初始前两条:", [x["id"] for x in candidates[:2]])
print("重排前两条:", [x["id"] for x in apply_scores(candidates, scores, 2)])在该文件所在目录执行:
python rerank_demo.py这段自编练习已在本地执行,输出的初始前两条为d1、d2,重排前两条为d2、d4。围绕编号重复、编号漏配、非法数量、非有限数值、并列分数和不修改原列表等行为做了12项检查,均通过。验证的是排序程序本身,没有验证任何重排模型的准确率。
四、为什么不直接把分数列表和原文按位置拼起来
实际接入服务时,容易把“第一个分数”当成“第一条原文”的分数。但如果接口调整了返回顺序,或者某条输入失败,位置对应就可能错位。编号映射能让这种问题更容易被发现。
上面的函数要求候选编号集合与分数编号集合一致。缺少d4的分数、额外返回未知编号、候选里出现两个d2,都直接报错。面对异常,宁可暂停重排并记录原因,也不要用不对应的分数默默改动证据顺序。
相同分数时,Python排序保留原有顺序。这是一个明确且可复现的规则,但不表示原始顺序就是最佳顺序。正式系统可以再使用来源可信度、文档有效日期等规则,但必须记录规则,不能让读者误以为全部排序都来自模型。
五、接入真实模型前要确认的条件
练习里的scores,实际可以由重排服务或本地Cross-Encoder生成。接入时先确认输入是“同一个问题与不同候选分别配对”,再根据返回编号建立score_map;不要将不同用户问题的结果混在一起排序。
模型选择至少检查四项:支持的语言、允许的输入长度、运行依赖与资源要求、模型及服务的许可条款。中文办公资料不能因为英文示例能运行,就推断中文排序也可靠。候选截断后是否丢失金额、时间和否定条件,也需要单独评估。
分数不一定是0到1,更不一定是正确概率。Sentence Transformers当前Cross-Encoder用法文档明确说明,MS MARCO系列示例返回logits;使用Sigmoid可以改变数值范围,但不会因此把它变成经过业务校准的可信概率。不同模型和不同配置的分数也不能直接套用同一个阈值。
本文不提供未经本机验证的模型安装与硬件承诺。你可以先完成离线练习,再按官方文档选定一个模型或服务,用少量脱敏材料验证输入和输出,然后接入现有检索链路。
六、用同一批问题比较重排前后
先准备一组真实、脱敏且有人工答案依据的问题,每个问题记录“至少应该找到哪些片段”。不要只看系统是否写出一段通顺的文字。
第一项检查是候选覆盖:必要证据是否进入召回结果?如果没有,记录漏掉的资料编号,先排查入库和召回。
第二项检查是排序位置:在同一候选集合里,重排是否把必要证据放到了最终选取范围?比较时保持候选数量与最终选取数量一致,避免把“读了更多材料”误当成重排效果。
第三项检查是最终回答:是否完整说明单位抬头与重复发票要求,是否保留引用编号,是否加入材料里不存在的报销条件?排序改好后,生成阶段仍可能出错。
第四项检查是代价:记录候选数量、输入长度和重排耗时。逐对计算通常增加工作量,不能默认候选越多越好。先找出覆盖与延迟可接受的范围,再做扩展。
七、遇到这些现象,先检查对应环节
- 排得很准但答案仍缺条件:先看关键材料是否被截断,或最终只保留了一条片段。
- 相关材料完全不见:检查原始文档、入库、权限和召回,重排不能修复候选外的缺失。
- 相近片段挤满前几位:检查重复内容;重排不是自动去重器,去重后也要保留来源追溯。
- 分数突然整体变小:核对模型、激活函数和输入变化,不要仅凭绝对数值断言系统退化。
- 返回了NaN或数量不一致:停止使用这批分数,保留原始输入和错误记录后排查。
重排值得使用的前提,是你已经知道需要的证据大致在候选中,并且有可比较的人工标准。先把“有没有找到”“排在哪里”“回答是否引用正确”分别记录下来,才能把改动用在真正影响结果的位置。
八、做一张可以复查的评估记录
不必一开始就接入复杂平台。每次试验记录问题编号、候选编号列表、人工必要证据编号、重排前两条、重排后两条、回答引用编号和失败原因,就能发现不少问题。把资料版本也记录下来,否则文档更新后,旧的人工答案可能已经不再适用。
例如本篇问题可以记为q1,必要证据为d2与d4,原始前两条为d1与d2,练习重排后为d2与d4。这个记录只说明预设分数下排序符合预期;若改成真实模型,应重新运行并填入实际结果,不能复制练习结论。
还应加入没有答案的问题,检查系统是否愿意说明资料不足。把不相关片段排到第一名,并不意味着第一名已经足以回答问题。只有覆盖、排序与回答三项记录能对应起来,调整才有可追踪的依据。

还没有公开评论,欢迎分享你的想法。