单个 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 可以怎么计算
不一定需要大模型。可以从简单到复杂逐步实现:
- Document Coverage:是否来自尚未入选的新文档;
- Section Coverage:是否来自新的标题路径或章节;
- Entity Coverage:是否包含查询要求但当前集合未覆盖的实体;
- Sub-query Coverage:是否命中尚未覆盖的子问题;
- Semantic Coverage:与已选集合的最大相似度是否较低;
- 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 的两个特点:
- A1 与 A2 在多个通道中共同出现,累计分数明显高于单通道候选;
- 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 截断
对代码和表格不要机械从中间切断。长代码块可按函数、类或配置段切分,并附带文件路径、符号名和父标题。
一个简单选择方法
- 统计真实文档段落长度的 P50/P90;
- 统计真实查询答案跨越多少 Token;
- 用 300/500/800 三组离线实验比较 Recall@K、MRR、答案完整率;
- 同时记录平均候选数、Reranker 延迟和最终上下文 Token;
- 不要只以检索指标决定 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 才真正从"返回相似片段"进入"组织答案证据"的阶段。