# 诊断 Agent 场景下的 RAG 排序:从 K=3 规则加分,到多路召回与 RRF **日期**:2026-07-27 **范围**:知识检索排序、多路召回、分数融合、Rerank 选型 **读者**:需要在 Agent 系统里落地 RAG,而不是只做 Demo 问答的工程同学 **关联实现**:`Lookup_knowledge` 模块化链路(L0 hint → L1 向量 → 规则后处理 → 投影) --- ## 1. 引言:RAG 排序常被高估,也常被低估 在诊断 Agent 里,知识库检索很少是“搜一下、答一句”那么简单。一次 `lookup_knowledge` 往往要在有限工具预算里,尽快给出可引用的证据片段。这时排序质量会直接影响: - Agent 是否看得到正确 runbook / 排查步骤 - 是否被错误 domain 的文档带偏 - 是否在本就很少的 topK 里,把唯一有用的 chunk 挤掉 讨论排序时,团队里很容易出现两种极端: 1. **高估 Rerank** 一提质量问题就上 Cross-Encoder、商业 Rerank、LLM listwise。 但候选池只有 3 条时,精排只能在这 3 条里换座位,召不回的内容永远排不上来。 2. **低估融合** 觉得“都是相似度,加一加就行”。 但向量 L2、BM25、关键词加分根本不在同一尺度上,硬加权会把系统调成玄学。 本文基于一次针对真实诊断 Agent RAG 链路的讨论,整理一套可落地的判断框架: ```text 1. 候选太少时,复杂 rerank 价值有限 2. 多路召回解决“漏”,精排解决“噪” 3. 跨路不要硬加原始分,优先 RRF 用名次投票 4. L0 / 关键词适合做提示,不适合当最终裁判 5. 权重 w_i 是“路权”,不是文档原始分 ``` 目标不是证明某一种模型永远最优,而是回答工程上更常见的问题: > 现在的 K、现有的 L0/L1、现有的规则加分,下一步到底该扩召回、该融合,还是该上精排? ```mermaid flowchart TB P[排序/质量问题] --> A{候选池 K 多大?} A -->|K 很小 3~5| B[先扩召回 / 修去重 / 轻规则] A -->|K 中等 15~30| C[多路 + RRF + 可选轻精排] A -->|K 很大 50+| D[强 rerank 才划算] P --> E{跨路分数?} E -->|原始分硬加| F[尺度不同 · 易玄学] E -->|RRF 名次投票| G[推荐] ``` --- ## 2. 现状解剖:有“重排”,不等于有“强 Rerank” ### 2.1 一条典型主链路 以模块化 `lookup_knowledge` 为例,主路径大致是: ```text Agent query -> KnowledgeQueryTransformer # L0:domain / keyword hint,可选 category filter -> KnowledgeDocumentRetriever # L1:向量 topK -> KnowledgeEvidencePostProcessor # 归一化 + 规则 boost + 排序 + 去重 -> KnowledgeContextPacker # 字符预算打包 -> LookupResultAssembler -> RagResultProjector # 投影成 Agent 可见契约 ``` ```mermaid flowchart TB AQ[Agent query] --> L0[KnowledgeQueryTransformer
L0 hint / category filter] L0 --> L1[KnowledgeDocumentRetriever
L1 向量 topK] L1 --> POST[KnowledgeEvidencePostProcessor
归一化 + 规则 boost + 去重] POST --> PACK[KnowledgeContextPacker] PACK --> ASM[LookupResultAssembler] ASM --> PROJ[RagResultProjector] PROJ --> AGENT[Agent 可见结果] ``` 其中“重排”发生在后处理阶段,名字也常叫 rerank,但实现通常是: ```text baseScore = 向量距离归一化后的相似度 finalScore = baseScore + domain_match + entity_match + keyword_match + source_type_prior 再按 finalScore 降序 ``` ```mermaid flowchart LR BASE[baseScore
向量相似度] --> SUM[finalScore] D[+ domain] E[+ entity] K[+ keyword] S[+ source_type] D --> SUM E --> SUM K --> SUM S --> SUM SUM --> SORT[按 finalScore 降序] ``` 同时会留下 `rerankTrace`(base/final score、boost reasons),便于内部审计。 ### 2.2 这套做法解决了什么 它不是毫无意义: - 在小候选池里,能把更像目标域、更像 runbook 的结果往前推 - 有可解释的 boost 原因,方便 trace - 实现成本低,不引入额外模型服务 如果只是 Demo,或语料很小、query 很规范,这种轻规则重排往往够用。 ### 2.3 它没解决什么 真正的问题通常不在“3 条里谁排第一”,而在: 1. **K 太小** `topK=3` 时,重排空间极小。复杂精排模型也只能在这 3 条里微调。 2. **规则加分不稳定** 依赖 L0 词表和字符串 `contains`。短词、泛词、别名缺失都会让 boost 误触发或漏触发。 3. **L0 filter 可能误杀** 唯一 domain 时加 category filter,能降噪;一旦 L0 判错域,正确文档可能根本进不了候选。 4. **去重粒度若偏文档级** 同文档多个相关 chunk 可能被压成 1 条,召回了也会在后处理/投影阶段丢掉。 5. **Agent 侧看不到排序细节** 投影层常只保留 excerpt 与少量元数据,score / hitReasons / retrievalTrace 被裁掉。这对安全边界合理,但对模型“判断有多相关”不友好。 一句话概括现状: > 不是没有排序,而是在太小的候选池里,用不够稳的启发式做微调。 --- ## 3. 关键判断:候选池大小决定策略上限 排序策略必须和 K 匹配。可以先用下面这张表做决策: ```mermaid flowchart TB K{retrieve-k / 候选规模} K -->|3~5| S1[修去重 · 轻规则
双路径 dense 融合] K -->|15~30| S2[多路召回 + RRF
可选轻精排] K -->|50+| S3[强 Cross-Encoder / 托管 Rerank] S1 -.->|先别上| X1[商业 Rerank] S2 -.->|收益有限| X2[只继续调 keyword boost] S3 -.->|避免| X3[无评测堆模型] ``` | 召回规模 K | 更适合做什么 | 不太值得先做什么 | |---|---|---| | 3 ~ 5 | 修去重、轻规则、双路径 dense 融合 | Cross-Encoder / 商业 Rerank | | 15 ~ 30 | 多路召回 + RRF + 轻精排 | 只继续调 keyword boost | | 50+ | 强 rerank 才划算 | 无评测地堆模型 | 背后的原因很简单: ```text Rerank 的价值来自: 候选很多、噪声大 → 精排把好的顶到前面 如果好文档根本不在候选里: 再贵的 rerank 也救不回来 ``` 因此工程上应把两个参数拆开: ```text retrieve-k # 召回阶段多捞一些,例如 20 return-n # 最终给 Agent 的条数,例如 3~5 ``` 而不是始终: ```text topK = 3,召回、排序、返回都是 3 ``` **先让该进来的进来,再谈谁排前面。** --- ## 4. 多路召回:先分清“同模态双路径”和“跨维度多路” “多路召回”不是只有 BM25 + 向量这一种。可以分层理解。 ### 4.1 同模态双路径:filtered + unfiltered 很多系统已经有类似逻辑: ```text 若 L0 给出唯一 category: 先 filtered 向量检索 若结果差,再 unfiltered 重试 ``` ```mermaid flowchart TB Q[query + L0 category?] --> F[filtered dense] F --> BAD{结果差/空?} BAD -->|是| U[unfiltered retry] BAD -->|否| USE[采用 filtered] U --> REP[常见:整锅替换 filtered] ``` 这能工作,但常见实现是 **串行整锅替换**: - retry 成功后,直接丢掉第一次 filtered 的全部结果 - 没有“两边好结果都保留,再统一排序” 更稳的做法是把它升级为并行/双路融合: ```text 路 A:dense + category filter # 求准 路 B:dense + 无 filter # 求全 去重合并后一起排序 ``` ```mermaid flowchart LR Q[query] --> A[路A filtered dense] Q --> B[路B unfiltered dense] A --> M[去重合并] B --> M M --> RRF[RRF / 统一排序] RRF --> OUT[topN] ``` #### 为什么值得做? 因为两路解决的是不同失败模式: | 路径 | 优点 | 风险 | |---|---|---| | filtered | 更贴当前域,噪声少 | filter 错了会漏召回 | | unfiltered | 召回面宽,容错强 | 容易掺进其他域文档 | 典型场景: - L0 误判成 `mysql`,真正文档在 `redis` → 只走 filtered 会空或很差 → unfiltered 能救回 - L0 判对了 `mysql` → filtered 更干净 → unfiltered 可能带噪声 所以: > filtered 求准,unfiltered 求全;融合比二选一更稳。 #### 它算多路吗? 算,但要说清楚边界: ```text 这是同一 dense 检索器、不同过滤条件的双路径 属于多路召回的子集 还不是完整的跨维度多路 ``` 它的主要价值是: 1. 降低 L0 category 误杀 2. 保留“有 filter 时更准”的收益 3. 避免 retry 整锅替换导致好结果被误删 即便暂时不上 BM25,只做这一步,也常常比继续调 keyword boost 更有效。 ### 4.2 跨维度多路:Dense + BM25 更完整的多路,通常来自不同相关性维度: | 路 | 擅长 | 不擅长 | |---|---|---| | Dense(向量) | 语义相近、换说法、同义表达 | 罕见专有名词可能漂 | | BM25 / 关键词 | 错误码、类名、配置键、告警名、精确术语 | 换一种说法就容易漏 | 例子: ```text 用户说:连接池打满了 文档写:HikariCP pending threads high → dense 更容易搭上 用户说:SQLSTATE 08001 文档标题就含 08001 → BM25 / 字面匹配往往更稳 ``` 因此: ```text candidates = dense ∪ bm25 ``` 不是“BM25 替代向量”,而是“补向量漏掉的字面命中”。 ### 4.3 L0 能不能当一路召回? 可以,但要降权、控边界。 L0(文档 frontmatter 关键词 / domain hint)适合: - 决定要不要启用 filtered 路 - 提供精排时的弱特征 - 写出可解释 trace 不适合: - 关键词命中就直接当高置信事实证据 - 用 L0 粗分主导最终排序 一句话: > L0 是导航,不是裁判长。 --- ## 5. 融合为什么难:不是不会加权,是分数不可比 多路召回之后,第一反应常常是: ```text final = 0.7 * dense_score + 0.3 * bm25_score ``` 这在课堂上好讲,在工程上很脆。 ### 5.1 原始分为什么不能直接加 不同路的分数: - Dense:L2 距离或 cosine similarity,分布随 embedding 模型和语料变化 - BM25:另一套量纲,数值大小和 dense 完全不可比 - 规则 boost:+0.15 / +0.20 这种启发式加分,更像人工偏好,不是校准概率 把它们直接线性相加,等于默认“0.1 的向量分提升”和“2.0 的 BM25 提升”可以交换。这个默认通常不成立。 ### 5.2 规则 keyword boost 为什么不稳定 若最终分主要靠: ```text query.contains(keyword) || keyword.contains(query) ``` 再叠加固定加分,会出现: - 短词误命中 - 泛词普遍加分,区分度下降 - 词表一改,线上排序整体漂移 - 同义词没写进 frontmatter 就完全没帮助 所以: > 现在的“权重”如果本质是关键词启发式加分,稳定性天然有限。 这不代表规则无用,而是应把它放对层:路内微调或精排特征,而不是跨路主融合器。 --- ## 6. RRF:跨路融合时,优先用名次投票 ### 6.1 核心思想 RRF(Reciprocal Rank Fusion)的关键思想是: > 不管各路原始分是什么尺度,只看每条候选在各路里的名次,再投票。 基础公式: ```text RRF(d) = Σ 1 / (k + rank_i(d)) ``` 其中: | 符号 | 含义 | |---|---| | `d` | 某个候选文档或 chunk | | `i` | 第 i 路召回 | | `rank_i(d)` | d 在第 i 路中的名次(从 1 开始);未出现则该路贡献为 0 | | `k` | 常数,常用 60,用来缓和头部名次过强 | 直觉: - 某路第 1 名:贡献约 `1/61` - 某路第 2 名:贡献约 `1/62` - 多路都靠前的候选,融合分自然更高 - 只在一路偶然靠前的候选,不会单靠绝对分尺度“爆掉” ```mermaid flowchart TB subgraph paths["各路有序结果"] P1[dense ranks] P2[BM25 ranks] P3[filtered ranks · 可选] end P1 --> RRF["RRF(d) = Σ 1/(k + rank_i)"] P2 --> RRF P3 --> RRF RRF --> OUT[融合序 · 不依赖原始分尺度] L2[L2 原分] -.->|不直接相加| X[避免] BM[BM25 原分] -.-> X ``` ### 6.2 为什么适合 RAG 多路融合 RRF 特别适合下面这种现实约束: ```text dense 用 L2 bm25 用 BM25 filtered / unfiltered 虽同度量,但候选集合不同 暂时没有可靠的分数校准器 ``` 它把问题从: ```text 如何把不可比的分数对齐? ``` 简化成: ```text 各路是否都认为它靠前? ``` ### 6.3 加权 RRF:w_i 是路权,不是文档分 加权形式: ```text RRF_w(d) = Σ w_i / (k + rank_i(d)) ``` 这里的 `w_i` 非常容易被误解。 #### 正确理解 ```text w_i = 第 i 整路的话语权 ``` 例如: ```text w_dense = 1.0 w_bm25 = 1.5 # 想让 BM25 更重要,就提高这一路的 w w_filtered_dense = 1.0 ``` 含义是: - BM25 这一路投出的“名次票”更值钱 - 并不是把某个文档的 BM25 原始分 12.7 直接拿来和向量分相加 #### 错误理解 ```text 先算 keyword boost 得到一个大杂烩分 再把这个分塞进 RRF ``` 或: ```text RRF = f(L2原始分, BM25原始分, keyword加分) ``` 这都不是加权 RRF。 ### 6.4 分层心智模型 建议始终按三层理解分数角色: ```text 第 1 层:路内排序 dense 路用向量分排序 bm25 路用 BM25 分排序 各用各的分,互不直接相加 第 2 层:跨路融合 只吃各路 rank 普通 RRF 或加权 RRF(w_i 为路权) 第 3 层:精排(可选) 对融合后的 topM 再打分 这里才适合规则特征或 Cross-Encoder ``` 一句话记住: > 原始分只负责路内排名;RRF 只负责跨路投票;w_i 只调节哪一路更值钱;精排才做最终挑剔。 ### 6.5 手算例子 设: ```text k = 60 w_dense = 1.0 w_bm25 = 1.5 ``` 候选 A: - dense 第 1 名 - bm25 第 5 名 ```text RRF(A) = 1.0/(60+1) + 1.5/(60+5) = 1/61 + 1.5/65 ≈ 0.01639 + 0.02308 ≈ 0.03947 ``` 候选 B: - dense 第 4 名 - bm25 第 1 名 ```text RRF(B) = 1.0/(60+4) + 1.5/(60+1) = 1/64 + 1.5/61 ≈ 0.01563 + 0.02459 ≈ 0.04022 ``` 在这个设定下 B 更高,因为你主动提高了 BM25 路权,BM25 头名更吃香。 如果把 `w_bm25` 降到 `0.5`,同样名次下 dense 会重新占主导。 这就是路权的意义:调的是整路话语权,不是某个文档的原始分公式。 ### 6.6 w_i 怎么设 实操建议: 1. **起步全设 1.0** 先看纯 RRF,不要一上来调花活。 2. **再按评测微调** - 专有名词/报错码总靠 BM25 才找得到,却总被 dense 压下去 → 提高 `w_bm25`(如 1.2~1.5) - BM25 噪声大、泛词乱入 → 降低 `w_bm25`(如 0.6~0.8) - filtered 很准但有时过窄 → 可与 unfiltered 同权,或略高一点 3. **经验范围** `w_i` 常见落在 `0.5 ~ 2.0`。 若一路是 10、另一路是 0.1,基本等于放弃多路。 4. **没有回归集就不要谈“最优权重”** 路权应来自离线评测,而不是长期拍脑袋。 --- ## 7. 精排放在哪里:有了候选池,才配谈 Rerank ### 7.1 轻规则精排 在 RRF 融合出 top 10~15 后,可以用更稳的字段级规则做二次排序: ```text title 命中 > breadcrumb 命中 > body 覆盖 精确术语 > 泛词 runbook / case 来源轻微加分 与已选证据过相似则降权(MMR 思路) ``` 注意:这里的规则特征,最好作用于 **融合后的候选精排**,而不是重新发明一套跨路原始分加法。 ### 7.2 模型精排 当 `retrieve-k` 到 15~30,且评测证明融合后噪声仍高时,再考虑: ```text RRF topM -> Cross-Encoder / 托管 Rerank API -> 取 topN 给 Agent ``` 可选路线: - 开源 reranker(如 bge-reranker 一类) - 云厂商 / Cohere 等托管 rerank - LLM listwise(贵且不稳,一般不当首选) ### 7.3 什么时候不要上模型 rerank - 仍在 `K=3` 主路径上 - 还没修 chunk 级去重 - 还没有固定 query 回归集 - 延迟和工具预算已经很紧 否则花的是精排成本,换不到召回质量。 --- ## 8. 工程落地:比“换模型”更重要的顺序 ### 8.1 建议的目标形态 ```text Query ├─ dense unfiltered top 20 ├─ dense filtered top 10 # 有唯一 domain 时 └─ bm25 / keyword top 10 # 第二阶段 │ ▼ chunk 级去重(docId#chunkIndex) │ ▼ RRF / 加权 RRF 融合 │ ▼ 轻规则或模型精排,取 top 5 │ ▼ 每文档 chunk 上限 + context pack │ ▼ Agent projection ``` ```mermaid flowchart TB Q[Query] --> U[dense unfiltered top20] Q --> F[dense filtered top10] Q --> B[bm25/keyword top10] U --> DEDUP[chunk 级去重
docId#chunkIndex] F --> DEDUP B --> DEDUP DEDUP --> RRF[RRF / 加权 RRF] RRF --> RR[轻规则或模型精排 top5] RR --> CAP[每文档 chunk 上限 + pack] CAP --> AG[Agent projection] ``` ### 8.2 分阶段推进 ```mermaid flowchart LR P0[Phase0
chunk 去重
retrieve-k/return-n] --> P1[Phase1
filtered+unfiltered RRF] P1 --> P2[Phase2
BM25 跨维度] P2 --> P3[Phase3
可插拔精排] ``` #### Phase 0:先修前提 否则后面多路都会被吞: 1. 去重 key 从“文档/source”改为 `docId + chunkIndex`(fallback 可用向量主键) 2. 配置拆分: ```properties rag.retrieve-k=20 rag.return-n=5 rag.max-chunks-per-document=2 ``` 3. 明确 baseScore(可用性阈值)与融合分/精排分(排序)职责分离 #### Phase 1:同模态双路径 + RRF 1. filtered dense 与 unfiltered dense 都产出候选 2. 合并去重,不再整锅替换 3. RRF 融合后取 topN 4. trace 记录每路 rank 与 fused rank 这是最贴很多现有系统的一步,收益通常大于继续调 boost。 #### Phase 2:跨维度多路 1. 增加 BM25 / 关键词路 2. 继续 RRF,必要时给 `w_bm25` 微调 3. 增加字段级轻精排 #### Phase 3:可插拔模型 Rerank ```text interface Reranker { rerank(query, candidates, topN) -> rankedCandidates } ``` 实现可切换: - `NoopReranker` - `RuleReranker` - `HttpCrossEncoderReranker` 让精排成为插件,而不是写死在业务里。 ### 8.3 和 Agent 系统相关的额外约束 诊断 Agent 场景还有几个现实约束: 1. **工具预算有限** 检索本身只是工具循环的一环,延迟不能无限涨。 2. **证据要可引用** 最终给 Agent 的应是有界 excerpt,而不是内部全量 trace。 3. **投影会再裁一层** 即便内部排序很细,Agent 可见字段仍可能只有 document/source/title/breadcrumb/excerpt。 因此内部要保留完整 trace,外部保持契约稳定。 4. **安全发布与验真** 排序再好,也不能绕过证据引用与 guard;RAG 优化的是“更可能拿到对的证据”,不是“让模型自由发挥”。 --- ## 9. 评测:没有回归集,权重都是感觉 多路和 RRF 最怕“上线凭体感”。最少准备 15~30 条固定 query,覆盖: - 标准故障词 - 口语化换说法 - 专有名词 / 错误码 - 容易误判 domain 的问题 - 同文档多 chunk 才完整的流程题 - 负例:知识库本就没有答案 关注指标: | 指标 | 看什么 | |---|---| | Recall@5 | 该出现的文档/chunk 是否进前 5 | | nDCG@5 或人工 0/1/2 | 排序是否把更相关的放前面 | | filter 误杀率 | 唯一 domain 是否经常害人 | | 同文档多 chunk 保留率 | 去重是否过粗 | | P95 延迟 | 多路是否打爆预算 | | 无证据正确率 | 不该有答案时是否老实说没有 | 路权 `w_i` 的调整,应建立在这些数上,而不是单次手工 query。 --- ## 10. 反模式清单 下面这些做法看起来勤快,实际常把系统带偏: 1. **只在 K=3 上接昂贵 rerank** 候选池不够,精排没有舞台。 2. **L2 和 BM25 直接加权相加** 分数不可比,调参不可迁移。 3. **把 L0 contains 当最终裁判** 词表质量绑死线上排序。 4. **文档级去重吞掉同文档多 chunk** 多路召回也会在终点被自己吃掉。 5. **filtered 失败就整锅替换** 丢掉本可保留的好结果。 6. **无评测调 w_i** 今天的“最优权重”往往是过拟合某几条样例。 7. **L0 命中全文直接当高置信 evidence** 导航信号被抬成事实,诊断场景尤其危险。 --- ## 11. 可直接拿走的决策框架 遇到 RAG 排序问题时,按这个顺序问: ```text Q1. 正确答案是否经常连候选池都进不来? 是 → 先扩召回 / 多路,不要先上复杂 rerank Q2. 是否存在 filter 误杀? 是 → filtered + unfiltered 双路径融合 Q3. 是否大量依赖专有名词、错误码、配置键? 是 → 加 BM25 / 关键词路 Q4. 多路分数是否不可比? 是 → RRF,而不是原始分硬加 Q5. 融合后 topM 仍噪声大,且 K 已经够大? 是 → 再上规则精排或模型 rerank Q6. 有没有固定回归集? 没有 → 先建评测,再谈“最优权重” ``` 对应到一句话策略: > **先扩召回,再用名次融合,最后才模型精排。** --- ## 12. 结语 RAG 排序讨论很容易变成模型名词竞赛。但在诊断 Agent 这种真实系统里,更常见的瓶颈是: - 候选太少 - 过滤过猛 - 分数不可比 - 去重过粗 - 启发式加分承担了不该承担的最终裁决 RRF 的价值,不只是一个公式,而是一种工程态度: ```text 承认各路分数不可比 让每路先做好自己的排序 再用名次投票决定谁更值得进入下游 ``` 加权 RRF 也并不神秘:`w_i` 只是给整路调音量。 想让 BM25 更有话语权,就提高 `w_bm25`;它不会、也不应该要求你先把 BM25 分和向量分校准到同一宇宙。 如果只记住三句: 1. **K=3 时,复杂 rerank 不是第一优先级。** 2. **filtered + unfiltered 是值得做的同模态双路径;Dense + BM25 才是跨维度多路。** 3. **跨路融合优先 RRF;L0 做导航,精排做挑剔,原始分不要跨路硬加。** 按这个脉络演进,通常比“继续把 keyword boost 调大一点”更接近稳定、可解释、可评测的 RAG 排序系统。 --- ## 附录 A:术语对照 | 术语 | 含义 | |---|---| | L0 | 基于文档元数据/关键词的 query understanding 或弱召回 | | L1 | 向量语义召回 | | retrieve-k | 召回阶段候选数 | | return-n | 最终返回给 Agent 的证据数 | | baseScore | 向量相似度等主相关性分,常用于可用性阈值 | | finalScore / 精排分 | 用于排序的综合分 | | RRF | 基于名次的多路融合 | | w_i | 第 i 路在 RRF 中的路权 | | Rerank | 对已有候选做精排,不负责凭空召回新文档 | ## 附录 B:最小配置示例(示意) ```properties # 召回与返回分离 rag.retrieve-k=20 rag.return-n=5 rag.max-chunks-per-document=2 # RRF rag.fusion.method=rrf rag.fusion.rrf-k=60 rag.fusion.w-dense=1.0 rag.fusion.w-dense-filtered=1.0 rag.fusion.w-bm25=0.8 # 精排(先规则,后可插模型) rag.rerank.mode=rule # rule | model | off ``` 以上配置名是示意,重点在职责拆分,不在具体键名。 ## 附录 C:和本文讨论直接对应的实现关注点 阅读或改造现有代码时,可重点核对: 1. 后处理是否把“规则 boost”命名成了 rerank,却未做候选扩展 2. filtered 低质时是整锅替换,还是双路融合 3. 去重 key 是 source/文档级,还是 chunk 级 4. `topK` 是否同时承担召回、排序、返回三种职责 5. 内部 `rerankTrace` 是否可观测,Agent 投影是否有意裁剪 这些点决定了:你写在黑板上的 RRF,能不能在系统里真正跑起来。