feat(harness,rag): dual LLM audit fields, run conclusion, and hybrid quality
Persist provider reasoning and assistant text separately on agent_reasoning_audit (DeepSeekAssistantMessage path), extract diagnosis_run.conclusion, enrich RAG tool audit (step_id/query/qualityScore), gate empty mysql tools, drop devtools, and align MVP docs after live E2E verification.
This commit is contained in:
@@ -0,0 +1,888 @@
|
||||
# 诊断 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<br/>L0 hint / category filter]
|
||||
L0 --> L1[KnowledgeDocumentRetriever<br/>L1 向量 topK]
|
||||
L1 --> POST[KnowledgeEvidencePostProcessor<br/>归一化 + 规则 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<br/>向量相似度] --> 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[修去重 · 轻规则<br/>双路径 dense 融合]
|
||||
K -->|15~30| S2[多路召回 + RRF<br/>可选轻精排]
|
||||
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 级去重<br/>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<br/>chunk 去重<br/>retrieve-k/return-n] --> P1[Phase1<br/>filtered+unfiltered RRF]
|
||||
P1 --> P2[Phase2<br/>BM25 跨维度]
|
||||
P2 --> P3[Phase3<br/>可插拔精排]
|
||||
```
|
||||
|
||||
#### 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,能不能在系统里真正跑起来。
|
||||
Reference in New Issue
Block a user