RAG/Agent 记忆混合检索多路召回:RRF 算法与Chunk RRF、Document RRF如何决策TopK

单个 Chunk 相关,不代表整篇文档可信;同一文档命中多个 Chunk,也不代表应该把它们全部交给 LLM。企业级 RAG 的答案通常不是 Chunk RRF 和 Document RRF 二选一,而是"Chunk 召回、Document 聚合、集合级重排"的分层排序。

在混合检索系统里,Elasticsearch 擅长关键词、编号和实体,向量数据库擅长语义相似,结构化查询又擅长画像、约束和权限过滤。RRF(Reciprocal Rank Fusion)很适合把这些异构通道融合起来。

但当一篇文档被切成多个 Chunk 后,一个新问题会出现:RRF 到底应该排 Chunk,还是排 Document?

如果只排 Chunk,同一篇长文档可能占满 TopK;如果先把结果压成 Document,又可能丢失真正应该注入模型的局部证据。更合理的工程答案是:

text 复制代码
第一层:Chunk RRF
  解决"哪些局部片段可能相关"

第二层:Document Coverage Boost / Document Aggregation
  解决"哪个父文档获得了更充分的相关性证据"

第三层:Coverage-aware Rerank / MMR
  解决"在有限上下文中,最终选择哪组片段"

这三层优化的对象不同,不能用一个分数包办全部问题。

一、先说清 Chunk RRF 与 Document RRF

Chunk RRF:以片段为融合单元

假设 ES 和 Milvus 都返回 Chunk ID,标准加权 RRF 可以写成:

text 复制代码
ChunkRRF(c) = Σ_i w_i / (k + rank_i(c))

其中:

  • c 是一个 Chunk;
  • i 是检索通道,例如 ES、Milvus、PG Exact;
  • w_i 是通道权重;
  • rank_i(c) 是 Chunk 在通道中的名次;
  • k 是平滑常数,工程中常从 60 起步。

Chunk RRF 的优势是粒度细,适合定位具体答案。问题是同一篇长文档可能有多个相邻 Chunk 同时命中,造成:

  • Top10 中 7 条来自同一篇文档;
  • 相邻 Chunk 有大量重复文字;
  • 其他文档虽提供互补证据,却没有上下文名额;
  • 长文档因为 Chunk 数量多,天然获得更多"抽奖机会"。

Document RRF:以父文档为融合单元

Document RRF 通常有两种实现。

第一种是在每个通道内先把 Chunk 折叠成父文档,再做 RRF:

text 复制代码
rank_i(d) = min rank_i(c), c ∈ d
DocRRF(d) = Σ_i w_i / (k + rank_i(d))

它本质上采用 best-chunk 或 max-pooling 思想:一篇文档只要有一个强 Chunk,就能代表该文档参与融合。

第二种是先做 Chunk RRF,再把 Chunk 分数聚合为 Document 分数:

text 复制代码
BaseDocScore(d) = max ChunkRRF(c), c ∈ d

Document RRF 能有效控制"一篇文档霸榜",但它也有代价:一篇文档中究竟哪几个 Chunk 最值得进入 LLM,仍然需要回到 Chunk 层解决。

因此企业级实践不是二选一,而是:Chunk 保留证据粒度,Document 提供父级置信度与配额,集合重排负责最终组合。

二、第一层:Document Coverage Boost

它解决什么问题

假设某篇文档只有一个 Chunk 在 Milvus 排第 1,另一篇文档有 4 个互不相邻的 Chunk,分别在 ES 和 Milvus 的前列。后者通常说明查询主题在整篇文档中得到了更充分的覆盖。

Document Coverage Boost 要回答的是:

"这个 Document 是否获得了多处、跨通道、相互独立的相关性证据?"

一个直观公式是:

text 复制代码
DocScore(d) = BaseDocScore(d) + α · log(1 + Nchunk(d))

其中:

  • BaseDocScore(d) 建议使用最佳 Chunk 的 RRF 分数,或 Document RRF;
  • Nchunk(d) 不是该文档的总 Chunk 数,而是进入候选池且通过阈值的有效命中 Chunk 数;
  • log 用于抑制 Chunk 数量线性增长带来的长文档偏置;
  • α 控制覆盖度证据的强度。

原始公式必须加三道保护

如果直接使用命中 Chunk 数,长文档仍然占优势。生产实现建议改成:

text 复制代码
DocScore(d) = BaseDocScore(d)
            + α · log(1 + min(Ndistinct(d), C))
            + β · ChannelAgreement(d)
1. 只计算有效 Chunk

Chunk 至少满足以下条件之一才计数:

  • Chunk RRF 进入全局候选 TopM;
  • 至少一个通道进入该通道 TopK;
  • Reranker 预打分超过最低阈值;
  • 命中精确实体或必需关键词。

低质量尾部候选不能给文档"刷覆盖度"。

2. 相邻 Chunk 先合并为证据簇

采用 15% overlap 时,同一段文字可能同时出现在 Chunk 7 和 Chunk 8。它们应视作一个证据簇,而不是两个独立命中。

可以按以下规则折叠:

text 复制代码
同 document_id
且 chunk_index 相邻
且文本重合率或 embedding similarity > threshold
→ 合并为一个 distinct evidence span

因此公式里的 Ndistinct 应是独立证据簇数量,不是裸 Chunk 数量。

3. 设置上限 C

推荐 C=3~5。一篇文档命中 3 个独立片段已经是强信号,命中 20 个不应继续线性增加可信度。

α 如何设置

由于 RRF 分数通常很小,α 不能脱离分数量纲拍脑袋设置。更安全的初始化方式是:

text 复制代码
α = γ × median(BaseDocScore@Top20)

其中 γ 可从 0.05~0.15 起步。目标是让 Coverage Boost 只能在相关性接近的文档之间调整次序,而不能把一个弱相关长文档推到强相关短文档前面。

如果线上分数已经归一化到 [0,1],可以从 α=0.03~0.08 起步,并把单文档总 Coverage Boost 限制在 0.10~0.20

"文档可信"更准确地说是"相关性证据更充分"

Coverage 只能说明文档与查询存在多处关联,不能证明内容事实正确。真正的可信度还应由来源等级、发布时间、审批状态、ACL、版本有效性等 metadata 决定。

因此更完整的文档分数可以写为:

text 复制代码
DocScore(d) = Normalize(BaseDocScore)
            × MetadataFactor(d)
            + CoverageBoost(d)

其中 MetadataFactor 建议严格控制在 0.8~1.3。业务权重是微调项,不应该压过检索相关性。

三、第二层:Coverage-aware Rerank

Document Coverage Boost 优化单篇文档;Coverage-aware Rerank 优化的是整个结果集合。

它要回答的是:

"在已经选中的片段基础上,下一个 Chunk 能否补充新的知识,而不是重复已有内容?"

可以用增量覆盖公式:

text 复制代码
SelectScore(c | S) = Rel(c)
                   + η · NewCoverage(c, S)
                   + μ · DocScore(doc(c))
                   - ρ · Redundancy(c, S)

其中:

  • S 是已经选择的 Chunk 集合;
  • Rel(c) 是 Cross-Encoder/Reranker 或归一化 RRF 相关性;
  • NewCoverage 衡量新实体、新小节、新意图或新证据的增量;
  • DocScore 继承父文档置信度,但只应占较小权重;
  • Redundancy 衡量和已选 Chunk 的重复程度。

用户给出的简化形式:

text 复制代码
FinalScore = DocScore + λ · Coverage(d, S)

适合表达思想,但生产实现最好保留 Chunk 自身相关性,否则父文档分高会把该文档中的弱 Chunk 一并带入上下文。

推荐改成:

text 复制代码
FinalScore(c | S) = Rel(c)
                  + μ · DocScore(doc(c))
                  + λ · MarginalCoverage(c, S)

Coverage 可以怎么计算

不一定需要大模型。可以从简单到复杂逐步实现:

  1. Document Coverage:是否来自尚未入选的新文档;
  2. Section Coverage:是否来自新的标题路径或章节;
  3. Entity Coverage:是否包含查询要求但当前集合未覆盖的实体;
  4. Sub-query Coverage:是否命中尚未覆盖的子问题;
  5. Semantic Coverage:与已选集合的最大相似度是否较低;
  6. Evidence Type Coverage:定义、步骤、案例、限制条件是否互补。

对于多意图查询,例如"说明部署步骤、回滚方案和常见错误",Sub-query Coverage 往往比单纯的 embedding 多样性更有效。

四、MMR:一个成熟的集合级选择基线

Maximum Marginal Relevance(MMR)同时考虑相关性和冗余:

text 复制代码
MMR(c | S) = λ · Rel(c)
           - (1 - λ) · max Sim(c, s), s ∈ S

其中 λ 越大越偏向相关性,越小越强调多样性。

推荐从 λ=0.7~0.85 起步:

  • FAQ、客服:0.80~0.90,答案通常集中,优先准确;
  • 研发文档:0.70~0.80,需要覆盖步骤、限制、示例;
  • 电商导购:0.65~0.80,需要覆盖多个商品或卖点;
  • 多工具 Agent:0.75~0.85,避免把不相关工具说明带入上下文。

MMR 的 Sim 不应只看原始文本重合。可组合:

text 复制代码
Redundancy = 0.5 × embeddingSimilarity
           + 0.3 × tokenJaccard
           + 0.2 × sameSectionPenalty

相邻 overlap Chunk 可以直接设置较高 redundancy,避免重复注入。

五、贯穿式示例:从 5 个候选到最终 3 个 Chunk

下面用一个简化案例完整推演各层算法。为了方便手算,示例只保留少量候选;生产环境仍应使用完整 Recall Pool,并通过离线评测标定参数。

1. 查询与候选文档

用户查询:

text 复制代码
Redis 高可用有哪些组件?哨兵和集群模式分别解决什么问题?

候选库中有三篇文档:

Document 类型 Chunk 内容摘要
Doc-A 官方 Redis 高可用手册 A1 主从复制与 Sentinel 故障转移
Doc-A 官方 Redis 高可用手册 A2 Cluster 分片、主从与节点故障转移
Doc-A 官方 Redis 高可用手册 A3 Sentinel 故障转移详细步骤,与 A1 部分重叠
Doc-B 团队架构复盘 B1 Sentinel 与 Cluster 的选型对比
Doc-C 旧版博客 C1 Redis RDB 与 AOF 持久化

三个检索通道返回的名次如下:

Chunk ES Rank Milvus Rank Exact Rank
A1 1 4 1
A2 5 2 2
A3 - 5 -
B1 2 1 -
C1 3 - -

这里的 - 表示该 Chunk 没有进入对应通道的 TopK,因而该通道贡献为 0。

2. 第一步:计算 Weighted Chunk RRF

设定:

text 复制代码
k = 60
w_ES = 1.1
w_Milvus = 1.0
w_Exact = 1.2

计算 A1:

text 复制代码
ChunkRRF(A1)
= 1.1/(60+1) + 1.0/(60+4) + 1.2/(60+1)
= 0.01803 + 0.01563 + 0.01967
= 0.05333

计算 A2:

text 复制代码
ChunkRRF(A2)
= 1.1/(60+5) + 1.0/(60+2) + 1.2/(60+2)
= 0.01692 + 0.01613 + 0.01935
= 0.05240

其余候选同理:

Chunk ES 贡献 Milvus 贡献 Exact 贡献 Chunk RRF
A1 0.01803 0.01563 0.01967 0.05333
A2 0.01692 0.01613 0.01935 0.05240
B1 0.01774 0.01639 0 0.03413
C1 0.01746 0 0 0.01746
A3 0 0.01538 0 0.01538

这一步可以看到 RRF 的两个特点:

  1. A1 与 A2 在多个通道中共同出现,累计分数明显高于单通道候选;
  2. B1 虽然是 Milvus 第 1,但没有 Exact 命中,因此没有压过跨通道一致的 A1/A2。

如果直接取 Chunk Top3,结果是 A1、A2、B1,已经比较合理。但当 Doc-A 有十几个相似 Chunk 时,它仍可能占满 TopK,所以还需要父文档聚合。

3. 第二步:比较 Document RRF 与 max-pooling

方案 A:通道内先折叠 Document

每个通道只保留某文档排名最靠前的 Chunk:

Document ES Doc Rank Milvus Doc Rank Exact Doc Rank
Doc-A 1 2 1
Doc-B 2 1 -
Doc-C 3 - -

于是:

text 复制代码
DocRRF(Doc-A)
= 1.1/61 + 1.0/62 + 1.2/61
= 0.05383

DocRRF(Doc-B)
= 1.1/62 + 1.0/61
= 0.03413

DocRRF(Doc-C)
= 1.1/63
= 0.01746

注意,Document RRF 中 Doc-A 在 Milvus 的文档名次是 2,不再分别累计 A1、A2、A3。这样可以消除"同一文档切得越碎,RRF 分数越高"的问题。

方案 B:Chunk RRF 后做 max-pooling
text 复制代码
BaseDocScore(Doc-A) = max(0.05333, 0.05240, 0.01538) = 0.05333
BaseDocScore(Doc-B) = 0.03413
BaseDocScore(Doc-C) = 0.01746

两个方案的排序相同,但分数略有差异:

  • Document RRF 更强调"文档在多少个通道中排名靠前";
  • max-pooling 更容易复用已经算好的 Chunk RRF,也保留最佳片段的融合证据;
  • 不论采用哪一种,都不要把同文档所有 Chunk RRF 直接相加,否则长文档偏置会重新出现。

4. 第三步:计算 Document Coverage Boost

Doc-A 表面上命中了 A1、A2、A3 三个 Chunk,但 A3 与 A1 的 Sentinel 内容高度重叠。聚类后:

text 复制代码
Evidence Cluster 1:A1 + A3(Sentinel)
Evidence Cluster 2:A2(Cluster)

所以 Doc-A 的 Ndistinct=2,不是 3。Doc-B 和 Doc-C 都只有 1 个证据簇。

为方便展示,取:

text 复制代码
α = 0.006
C = 3
CoverageBoost = α × log(1 + min(Ndistinct, C))

得到:

Document BaseDocScore Ndistinct Coverage Boost DocScore
Doc-A 0.05333 2 0.00659 0.05992
Doc-B 0.03413 1 0.00416 0.03829
Doc-C 0.01746 1 0.00416 0.02162

Doc-A 获得更高加分,是因为它同时覆盖 Sentinel 和 Cluster 两个独立知识点,而不是因为它机械切出了三个 Chunk。

反例:为什么必须聚类和封顶

如果把 10 个高度重叠的相邻 Chunk 全部计入:

text 复制代码
错误 Boost = 0.006 × log(1 + 10) = 0.01439

它会比两个真实独立证据的 0.00659 高一倍以上。这样优化出来的不是 Coverage,而是"谁切片多谁赢"。

5. 第四步:加入 Metadata Boost

假设:

text 复制代码
Doc-A:官方且当前版本,MetadataFactor = 1.08
Doc-B:内部复盘,MetadataFactor = 1.00
Doc-C:旧版内容,MetadataFactor = 0.85

如果采用:

text 复制代码
FinalDocScore = BaseDocScore × MetadataFactor + CoverageBoost

则:

Document 计算过程 Final DocScore
Doc-A 0.05333 × 1.08 + 0.00659 0.06419
Doc-B 0.03413 × 1.00 + 0.00416 0.03829
Doc-C 0.01746 × 0.85 + 0.00416 0.01900

Metadata 只做温和微调,没有把弱相关内容推到强相关内容之前。

假如错误地给官方文档设置 MetadataFactor=3.0,一个正文几乎不相关的官方 Chunk 也可能压过真正回答问题的内部文档。这就是为什么建议将 Metadata Factor 限制在 0.8~1.3

6. 第五步:Cross-Encoder Rerank

Document 分数决定父级优先级,但最终仍需判断每个 Chunk 是否直接回答查询。假设 Cross-Encoder 给出:

Chunk Reranker Rel 解释
A1 0.94 直接说明 Sentinel
A2 0.89 直接说明 Cluster
B1 0.83 对比两种模式,提供选型视角
A3 0.78 与 A1 高度重复但更偏执行细节
C1 0.55 讨论持久化,不直接回答高可用组件

如果只按 Reranker 取 Top3,仍得到 A1、A2、B1。在这个简单例子里没有问题,但真实多 Chunk 场景中,Top5 可能全是同一段内容的近似改写,因此最后还要做集合级选择。

7. 第六步:用 MMR 选择最终组合

λ=0.75

text 复制代码
MMR(c | S) = 0.75 × Rel(c) - 0.25 × maxSimilarity(c, S)
第一次选择

集合为空,直接选择相关性最高的 A1:

text 复制代码
S = {A1}
第二次选择

假设候选与 A1 的相似度为:

text 复制代码
Sim(A2, A1) = 0.55
Sim(B1, A1) = 0.28
Sim(A3, A1) = 0.82
Sim(C1, A1) = 0.15

计算:

text 复制代码
MMR(A2) = 0.75×0.89 - 0.25×0.55 = 0.5300
MMR(B1) = 0.75×0.83 - 0.25×0.28 = 0.5525
MMR(A3) = 0.75×0.78 - 0.25×0.82 = 0.3800
MMR(C1) = 0.75×0.55 - 0.25×0.15 = 0.3750

B1 虽然原始相关性低于 A2,却因为提供了不同文档的选型视角,成为第二条:

text 复制代码
S = {A1, B1}
第三次选择

MMR 使用候选与已选集合的最大相似度。假设:

text 复制代码
maxSim(A2, {A1,B1}) = 0.55
maxSim(A3, {A1,B1}) = 0.82
maxSim(C1, {A1,B1}) = 0.20

得到:

text 复制代码
MMR(A2) = 0.5300
MMR(A3) = 0.3800
MMR(C1) = 0.3625

第三条选择 A2。最终结果是:

text 复制代码
A1:Sentinel 核心机制
B1:Sentinel 与 Cluster 的选型对比
A2:Cluster 核心机制

高度重复的 A3 被排除,不相关的 C1 也没有因为"多样性"混入答案。

8. 如果使用 Coverage-aware Rerank 会怎样

对于查询中的两个明确子问题,可以定义:

text 复制代码
Q1:Sentinel 解决什么问题
Q2:Cluster 解决什么问题

选择 A1 后,Q1 已覆盖,Q2 未覆盖。此时给能覆盖 Q2 的 A2 增加 NewCoverage=1

text 复制代码
SelectScore(c | S)
= Rel(c)
+ 0.10 × NewSubQueryCoverage(c,S)
- 0.20 × Redundancy(c,S)

则:

text 复制代码
A2 = 0.89 + 0.10×1 - 0.20×0.55 = 0.88
B1 = 0.83 + 0.10×1 - 0.20×0.28 = 0.874
A3 = 0.78 + 0.10×0 - 0.20×0.82 = 0.616

A2 会优先补齐 Cluster 子问题,随后 B1 再提供选型视角。这个结果与标准 MMR 略有不同,原因是 Coverage-aware Rerank 显式理解查询结构,而 MMR 只知道"相关"和"相似"。

因此:

  • 单意图查询,用 MMR 通常足够;
  • 多意图、多步骤查询,优先加入 Sub-query Coverage;
  • Coverage 奖励必须是增量奖励,某个子问题被覆盖后不能重复加分。

9. 本例每一层到底改变了什么

阶段 输入 输出或变化 解决的问题
Chunk RRF 三路 Chunk 排名 A1、A2 跨通道领先 异构分数不可直接比较
Document RRF Chunk → Document 同文档每通道只计一次 长文档多 Chunk 刷分
Coverage Boost 文档内命中 Doc-A 因两类独立证据加分 判断文档相关证据是否充分
Metadata Boost 来源和版本 官方当前版本小幅提升 业务可信元数据微调
Cross-Encoder Query + Chunk 文本 C1 被识别为弱相关 更精细地判断语义相关性
MMR Rerank 候选集合 A3 因重复被淘汰 最终上下文去冗余
Sub-query Coverage 查询子问题 + 已选集合 优先补齐 Sentinel/Cluster 保证多意图答案完整

这个推演说明,所谓"企业级排序"不是堆叠几个加权公式,而是让每一层只解决一个明确问题。

六、两层 Coverage 的区别

维度 Document Coverage Boost Coverage-aware Rerank / MMR
优化对象 单个 Document 整个结果集合
所在阶段 Chunk RRF 后的父级聚合 Rerank 后或 Context Builder 阶段
核心目标 证明一个 Document 获得多处相关证据 选择相关且互补的最佳 Chunk 组合
关注点 同文档多个有效 Chunk 不同知识点、文档、章节和子问题覆盖
数学思想 max pooling + 饱和覆盖奖励 贪心集合优化 / 次模思想
是否依赖已选结果
典型风险 长文档偏置 过度追求多样性而损失相关性
类似算法 best passage aggregation MMR、xQuAD、submodular selection

两者是上下游关系:Document Coverage 决定文档级优先级,Coverage-aware Rerank 决定最终上下文组合。

七、推荐的完整排序流水线

text 复制代码
Query Planner
  ↓
ES TopK + Milvus TopK + Exact/Profile TopK
  ↓
以 chunk_id 做 Weighted Chunk RRF
  ↓
回读权威库,做 ACL / deleted / frozen / hash 校验
  ↓
相邻 Chunk 聚类,计算 Ndistinct
  ↓
Document Coverage Boost,得到 DocScore
  ↓
按文档配额保留 Recall TopM
  ↓
Cross-Encoder Rerank TopN
  ↓
Coverage-aware Rerank / MMR 贪心选择
  ↓
相邻入选 Chunk 可按需合并
  ↓
按 Token Budget 注入 LLM TopK

伪代码

java 复制代码
List<Chunk> candidates = weightedChunkRrf(esHits, vectorHits, exactHits);
candidates = hydrateAndGovern(candidates);

Map<DocumentId, List<Chunk>> byDoc = groupByDocument(candidates);
for (DocumentGroup doc : byDoc.values()) {
    doc.clusterAdjacentChunks();
    doc.score(maxChunkRrf(doc)
        + alpha * Math.log1p(Math.min(doc.distinctEvidenceCount(), coverageCap)));
}

List<Chunk> rerankPool = applyDocumentQuota(byDoc, recallTopM);
rerankPool = crossEncoderRerank(query, rerankPool, rerankTopN);

List<Chunk> selected = new ArrayList<>();
while (selected.size() < finalTopK && tokenBudgetAvailable()) {
    Chunk next = argmax(rerankPool,
        c -> relevance(c)
           + docWeight * docScore(c.documentId())
           + coverageWeight * marginalCoverage(c, selected)
           - redundancyWeight * redundancy(c, selected));
    selected.add(next);
    rerankPool.remove(next);
}

八、Chunk Size 如何选:300、500 还是 800 Token

Chunk Size 不是越小越精准,也不是越大上下文越完整。它影响召回粒度、Embedding 表达、Reranker 成本和 LLM Token 预算。

Chunk Size 优点 风险 适用场景
300 Token 定位精准、单条成本低 上下文容易断裂,Chunk 数量大 FAQ、客服话术、错误码、API 参数
500 Token 精度与完整性较均衡 极长步骤仍可能被切断 通用知识库、会话记忆、产品文档
800 Token 语义完整,适合复杂章节 召回粒度粗、重排和注入成本高 法规条款、设计文档、长步骤说明

推荐默认值:正文 450~600 Token,标题和层级路径作为 metadata 附加,不计入正文切分预算。

真正的最佳实践是结构化切分优先:

text 复制代码
标题 / 小节 / 列表 / 代码块 / 表格边界
优先于
固定 Token 截断

对代码和表格不要机械从中间切断。长代码块可按函数、类或配置段切分,并附带文件路径、符号名和父标题。

一个简单选择方法

  1. 统计真实文档段落长度的 P50/P90;
  2. 统计真实查询答案跨越多少 Token;
  3. 用 300/500/800 三组离线实验比较 Recall@K、MRR、答案完整率;
  4. 同时记录平均候选数、Reranker 延迟和最终上下文 Token;
  5. 不要只以检索指标决定 Chunk Size,还要评估回答正确性。

九、Overlap 多大合适

固定切分的 overlap 通常从 10%~20% 起步:

  • 300 Token:重叠 30~50 Token;
  • 500 Token:重叠 50~80 Token;
  • 800 Token:重叠 80~120 Token。

不是所有内容都需要 overlap:

  • 按完整标题段落切分时,可降到 0~10%;
  • 叙述性长文可用 15%~20%;
  • FAQ、表格行、独立事实通常不需要 overlap;
  • 代码跨函数 overlap 的收益通常低于保留符号上下文。

Overlap 越大,索引体积、重复召回和 Coverage 虚高越严重。因此 Document Coverage 计算必须先把相邻重复 Chunk 聚类。

十、ES TopK 与 Milvus TopK 如何配置

TopK 的目的不是直接决定给 LLM 几条,而是保证融合阶段有足够候选。

推荐起点:

text 复制代码
ES TopK       = 30
Milvus TopK   = 30
Exact/Profile = 5~10
去重后 Recall Pool = 40~60
Rerank TopN   = 10~20
LLM Final TopK = 4~8

配置时要看通道互补性:

  • 查询包含编号、路径、版本、错误码:ES 可提高到 40~60,Milvus 保持 20~30;
  • 查询高度口语化、同义表达多:Milvus 可提高到 40~60;
  • 文档量小于几千 Chunk:两个通道 Top20 往往足够;
  • 百万级 Chunk:单纯扩大 TopK 会增加噪声,应先改善过滤条件、分区和 Query Planner。

多子查询时需要控制总候选量。若每个子查询都取 ES 30 + Milvus 30,3 个子查询理论上就是 180 条。应在通道内先按 Chunk ID 去重,并设置全局候选上限。

十一、RRF 的 k 为什么通常取 60

RRF 原始实践中常用 k=60,它已经成为一个稳健默认值。直觉上,k 控制头部名次差异的陡峭程度:

text 复制代码
k 小:Rank 1 与 Rank 10 差距明显,更相信单通道头部
k 大:排名贡献更平滑,更重视跨通道共同出现

以单通道贡献为例:

text 复制代码
k=60:Rank1 = 1/61,Rank10 = 1/70
k=10:Rank1 = 1/11,Rank10 = 1/20

k=60 并不是理论最优,只是通常不容易出错:它不会让单个通道的第一名压倒一切,也能奖励多个通道共同命中的候选。

调参建议:

  • 候选列表只有 Top10~20:可试 k=20~40
  • 每通道 Top30~100、通道质量差异大:从 k=60 起;
  • 希望强化头部精确命中:减小 k 或单独增加 Exact 通道权重;
  • 希望强化跨通道共识:增大 k,但要保证各通道都有足够召回深度。

十二、Channel Weight 如何初始化

不要一开始就根据感觉设置 ES=2.0、Vector=0.5。推荐三步:

第一步:等权基线

text 复制代码
ES = 1.0
Milvus = 1.0
Exact/Profile = 1.0

先确认各通道召回质量与数据完整性。

第二步:按离线贡献微调

用一组带相关性标注的真实查询,分别计算各通道 Recall@K、MRR、nDCG,并观察互补命中。常见起点:

text 复制代码
ES = 1.0~1.15
Milvus = 0.9~1.10
Exact/Profile = 1.15~1.30

Exact 权重高的前提是它真的精确,并经过用户、权限和字段类型约束。

第三步:按查询意图动态调整

text 复制代码
版本/IP/路径/错误码:提高 ES / Exact
概念解释/同义改写:提高 Milvus
用户偏好/明确约束:提高 Profile

建议把单通道权重限制在 0.7~1.4。如果必须设置到 2 倍以上才能工作,通常说明该通道的数据、分词、过滤或召回逻辑本身有问题。

十三、Metadata Boost 的合理范围

Metadata 可以表达:

  • 已确认事实;
  • 官方文档或高可信来源;
  • 当前有效版本;
  • 新鲜度;
  • 文档类型;
  • 当前 session 或当前项目。

推荐把乘法因子控制在:

text 复制代码
0.8 ~ 1.3

例如:

text 复制代码
confirmed fact       1.10
official source      1.08
current version      1.05
deprecated content   0.80
session summary      0.95
exact constraint     1.15

多个 metadata 不应无限连乘。可以采用封顶:

text 复制代码
MetadataFactor = clamp(product(factors), 0.8, 1.3)

权限、删除、冻结、租户隔离不是 Boost,而是硬过滤。不要用 deleted × 0.1 这种方式让不该出现的数据"低概率入选"。

十四、Reranker TopN 怎么设置

经典漏斗可以从以下配置开始:

text 复制代码
多通道原始候选:60~100
治理、聚合、去重后 Recall Top50
Cross-Encoder Rerank Top10~20
Coverage/MMR 选择 LLM Top5

用户给出的:

text 复制代码
Recall Top50 → Rerank Top10 → LLM Top5

是一个成本与效果均衡的默认值。但需要注意:如果 Reranker API 的 top_n=10 只返回十条结果,那么 MMR 只能在这十条中做多样性选择。复杂多意图问题可以保留 Rerank Top20,再选最终 Top5~8。

建议:

  • 简短事实问答:Rerank 10 → LLM 3~5;
  • 多步骤问题:Rerank 20 → LLM 6~8;
  • 长上下文模型也不要盲目扩大 TopK,优先受 Token Budget 控制;
  • Reranker 超时或失败时,必须退回 DocScore + RRF + MMR,而不是整条检索失败。

十五、不同行业的推荐参数

以下是冷启动建议,不是固定答案。最终应以真实查询集和答案指标校准。

场景 Chunk / Overlap ES / Vector TopK RRF k Rerank → LLM MMR λ 主要策略
客服 FAQ 250~400 / 10% 40 / 20 40~60 10 → 3~5 0.85 强 ES、问题同义词、答案完整块
研发文档 450~650 / 15% 40 / 40 60 20 → 5~8 0.75 路径/版本/错误码 Exact,章节覆盖
电商导购 300~500 / 10% 30 / 50 60 20 → 5~8 0.70 商品去重、属性覆盖、时效与库存硬过滤
Agent 工具知识 300~500 / 10% 30 / 30 40~60 12 → 4~6 0.80 工具名/参数 Exact,权限和版本优先
会话长期记忆 400~600 / 10%~15% 30 / 30 60 12 → 5~8 0.80 turn/session 配额、事实与原话并存

客服

客服答案通常集中在单个 FAQ 或标准话术,重点是准确和一致。应提高 ES/BM25 权重,Chunk 尽量保持"一问一答完整",不要为了多样性拼接多个相互冲突的话术。

研发文档

查询常含类名、文件路径、错误码、版本号,因此 ES 和 Exact 非常重要;同时一个问题可能需要定义、配置、示例和排障步骤,Coverage-aware Rerank 的收益最大。

电商导购

需要避免同一商品多个相似描述占满 TopK。Document 可以映射为商品,Chunk 映射为卖点、规格、评价摘要。先做商品级覆盖,再用 MMR 选择不同商品与不同属性。价格、库存、上下架状态必须走实时结构化数据,不能依赖向量库旧副本。

Agent

工具名称、参数 Schema、权限和版本是硬约束。召回到"语义相似但不是目标工具"的说明可能导致错误调用,因此 Exact 与 metadata filter 比单纯向量相似更重要。最终上下文还要控制工具文档数量,避免模型在近似工具之间摇摆。

十六、如何评估这套方案

不要只看 Recall@K。至少分四层指标:

Chunk 层

  • Recall@K
  • MRR / nDCG
  • 正确证据 Chunk 是否进入候选池

Document 层

  • 正确 Document Recall@K
  • TopK 文档唯一率
  • 长文档偏置:分数与文档 Chunk 总数的相关性

集合层

  • 子问题覆盖率
  • 重复率 / 平均最大相似度
  • 每个文档占用的最终 Chunk 数
  • 最终上下文 Token 利用率

答案层

  • Faithfulness:答案是否被入选证据支持
  • Completeness:多意图是否覆盖完整
  • Citation Accuracy:引用是否指向正确 Chunk
  • 延迟、Reranker 成本与降级成功率

必须做消融实验:

text 复制代码
Chunk RRF only
vs Chunk RRF + Document Coverage
vs Chunk RRF + MMR
vs Chunk RRF + Document Coverage + MMR

如果加 Coverage 后离线指标变好、答案却变差,通常是 Coverage 奖励过强、相邻 Chunk 未聚类,或者文档父分数错误地替代了 Chunk 相关性。

十七、最终建议

一套稳健的默认配置可以从这里开始:

text 复制代码
Chunk Size             500 Token
Overlap                60~80 Token(约 12%~16%)
ES TopK                30
Milvus TopK            30
Exact/Profile TopK     10
RRF k                  60
Channel Weight         ES 1.1 / Vector 1.0 / Exact 1.2
Metadata Factor        clamp 到 0.8~1.3
Coverage Cap           3 个独立证据簇
Coverage Boost         不超过基础文档分数的 10%~20%
Recall Pool            50
Rerank TopN            12~20
MMR λ                  0.75~0.85
LLM Final TopK         5~8,另受 Token Budget 限制

最关键的不是某个参数,而是保持三层职责清晰:

Chunk RRF 找局部证据,Document Coverage 判断父文档是否获得充分支持,Coverage-aware Rerank/MMR 选择最终最相关且互补的证据组合。

当排序系统开始同时考虑局部相关性、父文档证据和集合多样性时,RAG 才真正从"返回相似片段"进入"组织答案证据"的阶段。

相关推荐
倒流时光三十年15 小时前
第二阶段 15 · prefix / wildcard / fuzzy 模糊匹配
es
SelectDB19 小时前
当 PostgreSQL 面临性能瓶颈:80TB 电商业务迁移至 Apache Doris 的实践思考
postgresql
IvorySQL20 小时前
PG 日报|PG20 正式计划移除 refint 模块,官方指引迁移原生外键
数据库·人工智能·postgresql·开源·区块链
2601_9609067221 小时前
华为MateBook Pro S首发搭载麒麟XE90
华为·postgresql·sqlite·时序数据库·tdengine
叫我Paul就好21 小时前
RAG 入门到精通 - Rerank & Hybrid Search
人工智能·rag
丘丘用户思思澪1 天前
混合检索的极简主义:PostgreSQL + pgvector 生产级方案
数据库·postgresql·bm25·vectordb
依晨照旧1 天前
PostgreSQL 常用命令速查:MySQL 用户平滑上手,psql 元命令 + 库表管理 + 运维排查一篇通
后端·postgresql
AAA@峥1 天前
PostgreSQL 入门与实战指南|从基础概念到 CentOS 部署、运维、CRUD 完整教程
运维·postgresql·centos
CodexDave2 天前
PostgreSQL 明明有索引却选了 Nested Loop:从行数误判修正执行计划
数据库·postgresql·执行计划·扩展统计·nestedloop