检索、重排与降级编排

本节目标

  • 将硬过滤、召回、融合、重排和证据选择分成可观测阶段;
  • 区分 candidate window、rerank window 与最终 evidence;
  • 为超时、空响应和依赖故障设计安全 fallback;
  • 用总 deadline 控制并行、重试与尾延迟。

一条正确排序的候选链

可信身份 → tenant/ACL/状态/有效期过滤
         → sparse 与 dense 召回
         → 融合与 canonical 去重
         → 有限窗口 rerank
         → 上下文选择

这里有两个“排序”:

  1. 硬约束优先级:无权访问、已删除、未发布或已过期的文档先排除;
  2. 相关性排序:只在合法集合内比较“哪篇更能回答”。

不能用 reranker 的低分替代 ACL,也不能让高相关文档越过权限。

Filter 必须限制“可搜索集合”

将 tenant、角色、资源状态、有效期和授权 revision 作为检索请求的可信约束,而不是把全库近邻返回给应用后再做展示层过滤。可接受的实现包括由检索引擎执行的元数据 filter、按授权域隔离的 collection/namespace,或在受控服务端先求得可检索 ID 集;具体取舍取决于规模和策略复杂度。无论采用哪种方式,都要在 rerank、上下文和 citation 渲染前重检,因为 ACL、墓碑和有效期可能在请求期间或缓存生命周期内变化。

若第三方 reranker 会看到候选正文,它也是数据处理方:只发送当前主体可读、且该服务被允许处理的最小文本窗口,记录模型/区域/保留策略;绝不能以“排序服务需要输入”为由扩大可见数据域。

多路召回

Sparse

BM25/关键词对专名、编号、错误码和逐字短语通常很强,易解释,也可作为模型服务失败时的基线。

Dense

Embedding 检索擅长语义改写,但可能把“同主题却未回答”的内容排得很高。模型、归一化、距离、维度和索引版本必须一致。

结构化检索

时间、类别、产品、文档类型和实体关系有时更适合数据库、倒排过滤或图查询。RAG 不要求所有知识都变成向量。

融合时不要直接混分数

不同召回器的原始分数通常不在同一量纲。RRF(Reciprocal Rank Fusion)用 rank 融合:

其中 r_i(d) 是文档在第 i 个通道的排名,k 是平滑常数。它减少分数校准依赖,但 k、通道权重和候选深度仍需本地评测。

保存每个候选的:

  • source/document/chunk/canonical ID;
  • 通道、原 rank/score;
  • filter revision、index revision;
  • fusion rank/score;
  • 重排前后 rank 与 reranker revision。

Candidate 不等于 Evidence

名称作用常见规模
recall window第一阶段返回候选较大,优先高召回
rerank window送入昂贵模型的候选受延迟/成本限制
output top-n重排后的前排较小
selected context真正进入生成上下文的证据受预算、去重与覆盖限制

若 gold 文档不在 recall window,reranker 无法创造它;若它在重排前排但被 context budget 裁掉,则问题在上下文层。

降级不是“随便返回”

故障可接受的候选策略必须保留
Dense 超时只用已授权 sparsedegraded 标志与通道缺失
Sparse 失败只用已授权 dense关键编号类 query 的风险
Reranker 超时/畸形使用已评测的融合顺序同一安全候选集
一个分片失败视产品决定部分结果或拒答coverage/partial 标志
所有检索不可用明确暂不可检索不允许自由生成内部事实
生成服务失败可返回经授权原文片段或拒答产品策略和来源链接

Fallback 自己也要测 relevance、权限、延迟和容量。若只在事故时首次运行,它不是可靠方案。

缓存也是一条 fallback 路径

语义缓存、候选缓存和答案缓存能降低延迟,却会把旧的检索决定重新送回线上。缓存值至少要带入生成它的 authorization_revision、source/index generation(或等价快照)、路由/输出模式和到期策略;命中前仍要核验调用者的可见范围。撤权、删除、策略收紧与高风险来源更新应能使相关条目失效或拒绝命中。缓存无法被可靠地按授权和版本隔离时,宁可 miss,也不要返回可能来自另一主体或旧知识快照的答案。

总 deadline 与重试预算

假设端到端预算为 2 秒,可先分配而非写死:

阶段示例预算超时策略
路由/改写150 msoriginal query
sparse+dense 并行350 ms保留成功通道
rerank250 ms融合顺序
上下文与校验100 ms确定性本地处理
生成900 ms拒答/受控重试
余量250 ms网络抖动与序列化

这些数字只是分配示例,必须用自己的 p50/p95/p99 测量。重试要遵守剩余 deadline;无抖动的同步重试会在依赖故障时放大流量。

可诊断 trace 示例

{
  "retrieved": [
    {"id": "S8", "channel": "dense", "rank": 1, "score": 0.82},
    {"id": "S1", "channel": "bm25", "rank": 1, "score": 12.7}
  ],
  "reranked": [
    {"id": "S1", "rank": 1, "model_revision": "ce-v3"}
  ],
  "selected": [
    {"id": "S1", "reason": "top_relevant_current"}
  ],
  "fallback": null
}

JSON 不能写入合法行尾注释:retrieved 保存各召回通道自己的 rank/score,reranked 保存模型后的顺序与 revision,selected 说明真正进入上下文的原因,fallback 明确是否启用降级。不同通道的数值不能跨字段直接比较。

分数字段只在各自模型/查询语境内解释;不要看到 12.7 > 0.82 就断言 BM25 更相关。

动手练习

  1. 为 2 秒预算写出自己的阶段表与理由。
  2. 在离线项目中运行:
$env:PYTHONDONTWRITEBYTECODE = '1'  # 避免运行练习时在知识库写入 Python 缓存。
$script = '.\docs\RAG\examples\offline_cited_qa.py'  # 保存离线 RAG 项目脚本路径。
$fixture = '.\docs\RAG\examples\rag-fixture.json'  # 保存可复现的严格 fixture 路径。
 
python -B $script --fixture $fixture inspect --query-id Q-refund --operator-view  # 查看无故障时的受保护阶段 trace。
python -B $script --fixture $fixture inspect --query-id Q-refund --failure reranker_error --operator-view  # 模拟重排器失败,确认使用已授权融合顺序。
python -B $script --fixture $fixture inspect --query-id Q-refund --failure retrieval_error --operator-view  # 模拟检索失败,确认不会自由生成内部事实。
  1. 在受保护的 audit_trace 中对比 retrieved、reranked、selected、degraded 和 fallback;真实服务必须先做 operator 身份与审计授权,不能把 CLI 标志本身当作权限。
  2. 解释为何 retrieval_error 返回拒答,而不是让生成器凭记忆回答。

常见错误

  • 对未授权全库先检索、后过滤;
  • 混合 BM25、cosine 与模型 logit,却不做校准或 rank fusion;
  • 重排返回未知/重复 ID 时仍信任输出;
  • fallback 改用了更宽 ACL 或陈旧索引;
  • 每个并行分支都独立重试,超过总 deadline;
  • 只监控平均延迟,忽略 p99 与 fallback 比例。

自测

  1. 为什么硬过滤必须先于相关性评分?
  2. Candidate Recall@window 对 reranker 有什么上限约束?
  3. RRF 解决了什么,又没有解决什么?
  4. Reranker 失败时,为何保留融合顺序通常比返回空更合理?
  5. 哪些情况下应该停止回答而不是降级?
  6. 缓存命中前为什么仍要检查当前主体、授权 revision 与知识 generation?

小结与下一步

检索编排的目标是形成高召回、已授权、可降级、可诊断的候选链。候选仍不能直接塞满 prompt;下一节处理预算、去重、顺序与冲突:上下文选择与组装。

参考资料

来源获取日期:2026-07-22。论文中的特定性能数字依赖数据集与实现,本节不把它们外推为本地结论。