结论先说:去掉的不是「重排」这个动作,而是「每问必跑一次神经网络」的成本。用级联、双路召回和 RRF 把排序拆成三步,再用确定性业务加权收口;只有当 top1 与 top3 分数几乎一样时,才值得请一次轻量 LLM 重排。

起点:为什么一定要拆

部署目标是两台小机器:香港 2C2G 只跑 Nginx,广州 4C4G 跑完整后端。本地 BGE-M3 权重约 4.6GB,常驻内存 2GB+;Reranker 在 CPU 上单次 200–600ms。再加上 Milvus standalone 建议至少 8GB 内存,这台 4GB 机器没有可能同时留下它们。

而语料规模并不大:制度库 16 个部门约 400–700 个 chunk,案例库约 700–800 个 chunk,加起来 1700–2000 个向量。这个量级用 Chroma 嵌入式持久化足够,不需要 Milvus。真正需要重新回答的问题是:把本地模型全部拿掉之后,检索质量靠什么守住?

拆掉之后,排序会坏在哪

制度问答的难点不是「找不到」,而是「找到了但排不准」。16 个部门的条文高度相似,很多问题同时命中好几条;单一路 dense 余弦能处理「报销要几天 → 费用核销时限」这类语义泛化,却压不住专有名词、制度名和编号的精确命中。

原来的 Reranker 是收口:所有候选都进去,由它统一打分。拆掉它,等于把两种完全不同的信号直接扔给一个余弦阈值。常见两类失效:

问题类型单 dense 的风险补法
同义问法语义相近的条文挤在一起,顺序不稳扩大 dense 召回,交给 RRF 与业务加权
专有名词 / 编号字面精确命中被语义相似度压下去BM25 字符二元组召回,与 dense 并列
跨块答案答案被切在相邻两个 chunk 里相邻块合并,small-to-big

方案:先分级,再双路召回,最后 RRF

第一步不是换模型,而是减少进入向量检索的问题。现有架构本来就有四级:

L0 缓存
 → L1 FAQ 精确匹配(纯词面,无 embedding)
 → L1.5 域内意图(部门词表,sub-ms)
 → L2 向量检索(只处理长尾)

大量高频问题在 L1 就被拦掉,L2 只承担长尾。到了 L2,用两路召回:

  • Dense:云端 qwen3.7-text-embedding-flash(1024 维),召回 20–30 条,负责语义泛化。
  • Lexical:本地内存 BM25,中文按字符二元组切词,不引 jieba;500–700 个 chunk 下 <10ms,负责专有名词与编号。

融合用 RRF(Reciprocal Rank Fusion),只看名次,不看原始分数:

score(doc) = Σ_lane  1 / (60 + rank_lane(doc))

融合之后再叠加确定性业务加权:命中意图部门的 +1 档;「怎么 / 如何」类问题优先「操作细则」;同一来源最多保留 2 块;相邻块同时命中时合并上下文。最后取 top 3 给 LLM。

为什么是 RRF,不是分数加权

cosine 相似度和 BM25 分数的量纲不可比。加权融合需要每次归一化,而换模型、换语料、改切块都会让参数重新漂移。RRF 只依赖名次:某一路排第几,就拿对应的倒数分。换 embedding 模型或扩语料时,不用重新标定融合参数,这是拆掉 reranker 后最省心的一点。

代价是 RRF 忽略分数间距。两路都排第一的文档和「勉强第一」的文档在 RRF 里没有差别,所以它不能替代「拒答」判断,也不能解决 top1 与 top3 几乎同分的情况。

阈值必须重标,否则闸门失效

这是最容易漏的一步。旧阈值 0.18 / 0.20 / 0.27 是按哈希 embedding 的分数分布标定的;云端 embedding 的无关问句相似度通常在 0.3–0.5,直接沿用会让系统「什么都答」。

标定必须在「同一个 embedding 模型 + 同一份语料」上做:用案例语料加 20 条无关问题(食堂、天气、股票之类)构造标定集,取无关组 p95 与相关组最低分之间的间隔,重新设置 answer_min_score、low_confidence_threshold、answer_score_without_terms。标定结果写回配置,并注明语料规模与日期,避免下一个人把它当成恒定真理。

查询侧失败语义保持 fail-closed:embedding 或检索失败时返回 degraded,不编造结果;低分走「检索不到」,而不是让 LLM 自由发挥。

结果与代价

项改造前改造后
本地模型BGE-M3 4.6GB + Reranker无
Reranker 延迟200–600ms0(可选云端重排)
本地 BM25—<10ms
单次问答净变化—快约 200–500ms
启动内存2GB+ 模型常驻0.8–1.5GB

代码增量约 100 行 BM25 + 30 行 RRF,没有新增依赖。真正的成本不在实现,而在验证:必须用 30–50 条问答对看 Recall@3、无命中率和答非所问率,而不是凭一两个例子判断改造成功。

什么时候值得把重排请回来

只在一种情况下:top1 与 top3 分数几乎相同(margin < 0.02)。这时对 3 条候选做一次轻量 LLM listwise 重排,约 0.5–1s,能补上 RRF 缺失的分数间距。不做每问必排,否则等于把 reranker 的延迟换个形式请回来。

云端 rerank 也保留为可选项,失败自动回落 RRF。它是增强,不是主链路。

如果重来一次

  • 先建评测集,再改链路。30–50 条问答对不贵,但没有它,所有调参都是盲调。
  • 阈值和语料规模一起写进配置。阈值不是真理,是某次标定的产物。
  • 给每一路召回打日志。出问题时要知道是 dense 没召回、BM25 没命中,还是融合把顺序排错了。