5.2 后处理与结果重排 漏斗图的最后一层是重排。本节承接 5.1 的返回结果:索引给出的排序是基于"快速距离"的近似序,要把质量再抬一截、把多样性补回来、把重复源合并掉,靠的是返回之后的后处理。这一环节常被当作可有可无的锦上添花,实际上它是检索质量的最后一道闸——前面所有环节都在追求"别漏",只有这一环在追求"排对"。 为什么近似序值得再修一遍 三个原因让重排成为标配。其一,近似索引的排序误差:3.2 说过量化粗筛的距离带有估计误差,排序层面的误差会让第十一名和第十名互换——对返回十条的业务,这个互换就是一线之差。其二,向量距离不等于业务相关度:向量捕捉语义相似,但业务要的可能是"最新""权威""在售",单一距离序融合不了这些信号。
漏斗图的最后一层是重排。本节承接 5.1 的返回结果:索引给出的排序是基于"快速距离"的近似序,要把质量再抬一截、把多样性补回来、把重复源合并掉,靠的是返回之后的后处理。这一环节常被当作可有可无的锦上添花,实际上它是检索质量的最后一道闸——前面所有环节都在追求"别漏",只有这一环在追求"排对"。
三个原因让重排成为标配。其一,近似索引的排序误差:3.2 说过量化粗筛的距离带有估计误差,排序层面的误差会让第十一名和第十名互换——对返回十条的业务,这个互换就是一线之差。其二,向量距离不等于业务相关度:向量捕捉语义相似,但业务要的可能是"最新""权威""在售",单一距离序融合不了这些信号。其三,同源霸屏:知识库里同一份文档被切成几十个片段,向量上彼此最近,TopK 轻易被同源片段包场,用户看到的是十条几乎重复的内容。
对应的四件工具:精确重打分——用原始向量(索引若用了量化)或更重的模型对超量候选重新算分,把近似序修回真实序;交叉编码器重排——把查询与候选拼在一起送进重排模型,精度远高于双塔的独立编码,代价是每个候选一次前向计算,所以只对几十条候选做;多样性重排——最常用的是最大边际相关算法,每选一条就惩罚与已选结果过近的候选,逼着结果覆盖不同侧面;同源去重与融合——按文档 ID 或来源分组取最优,混合检索则用倒数排名融合把两路排序合成一个。
import numpy as np # 倒数排名融合:把关键词路与向量路合成一个排序 def rrf(rank_lists, k=60): scores = {} for ranks in rank_lists: # 每路传入按序排列的 id 列表 for pos, doc_id in enumerate(ranks): scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + pos + 1) return sorted(scores, key=scores.get, reverse=True) # 最大边际相关:兼顾相关性打分与已选结果的多样性 def mmr(cand_vecs, query_vec, lambda_, m=10): sim_q = cand_vecs @ query_vec # 与查询的相关性 chosen, rest = [], list(range(len(cand_vecs))) while len(chosen) < m and rest: if not chosen: chosen.append(rest.pop(int(np.argmax(sim_q[rest])))) continue penalty = np.max([cand_vecs[rest] @ cand_vecs[c] for c in chosen], axis=0) gain = lambda_ * sim_q[rest] - (1 - lambda_) * penalty chosen.append(rest.pop(int(np.argmax(gain)))) return chosen fused = rrf([keyword_ids, vector_ids]) # 混合检索融合 final = mmr(np.array(cand_vectors), query_vector, 0.7, m=10) # 多样性裁剪
这段代码浓缩了两个工程要点:倒数排名融合只用名次不用分数,天然回避了两路分数量纲不可比的问题,混合检索的融合首选它;最大边际相关的权重是多样性旋钮,取值靠近一偏相关性、靠近零偏多样,问答场景常取偏高的值、资讯聚合场景取中间值,上线前用自己的数据扫一遍再定。
背景。 某企业知识库问答在召回侧表现尚可(人工抽检答案片段出现在 TopK 里的比例约九成),但用户满意度平平,典型抱怨是"答案明明在结果里,模型却引错了段落"。定位发现:TopK 片段里同源片段扎堆,且排序末尾的关键片段常被挤在模型读取窗口之外。
操作。 分三步改造:第一,把取回量从二十条放宽到八十条,给后处理留料;第二,按源文档分组取每源最优的两条,破掉同源霸屏;第三,在剩余候选上叠一层轻量重排模型精排,取前十交给生成模型。全程只动查询侧,索引与数据未动。
结果。 离线评测的答案命中质量显著抬升,首屏出现正确片段的比例从约六成升到八成五;因为精排只对八十条做,端到端延迟增加了可控的十几毫秒,用户几乎无感。
解读。 这组数字里最值得咀嚼的是"召回九成、首屏六成"的落差:召回率只保证答案在名单里,不保证它排在模型读得到的位置。重排解决的是排序质量问题,它不能救没召回的(那是第三章的事),但能把已召回的排到位——两件事必须分开度量、分开投入。
变式。 同样的后处理骨架可按场景换装:电商搜索把精排换成业务分(转化率、库存)与语义分的加权融合;推荐流把多样性重排换成品类与创作者的双维打散;去重场景把分组取最优换成阈值合并近似重复簇。工具箱不变,业务规则换装。
用"换底"实验量化:固定同一批查询与同一份召回结果,分别用"按向量距离排"与"加重排模型"两种排序喂给下游,对比业务指标(点击率、答案采纳率、人工评估合格率)。收益显著大于重排引入的延迟与成本,就值得上。经验规律是:召回源越单一、候选语义越接近,重排的边际收益越大;反之召回本身漏掉了正确答案,再重的重排也无力回天——重排投入前先确认召回率已经达标,钱要花在短板上。另外注意延迟预算的分配:重排模型的前向计算通常按毫秒数十计,只在几十条候选上做才划算,这个"只对头部做"的约束本身也是设计输入。
从损耗率倒推。假设过滤会滤掉七成、重排会淘汰一半,取回量就要是最终 K 的六倍左右才有足够存活者。把两级损耗率分别量出来(过滤通过率、重排保留率),取回量等于 K 除以两者乘积再乘安全系数。取回量的成本不是线性的——它同时抬高索引扫描目标与重排计算量,所以别无脑放大;扫描参数(探测数、候选队列)要与取回量联动调整,5.3 的扫描脚本里把取回量也作为一维即可。
返回质量有了保障,下一节进入最经典的调优话题:索引参数怎么调才有依据。