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:
@@ -53,6 +53,18 @@ docs/
|
||||
1. [分析笔记目录](analysis/) - 代码分析和问题分析
|
||||
2. [临时报告目录](reports/) - 修复和验证报告
|
||||
|
||||
### RAG 设计讨论(docs 根目录)
|
||||
1. [RAG 排序:多路召回与 RRF](RAG排序-多路召回与RRF.md) - K、融合、L0 边界
|
||||
2. [Hybrid 之后的 qualityScore 与后处理](RAG-Hybrid质量分与后处理.md) - 上一代问题、L2 伪装、统一归一化
|
||||
3. [Agent 如何读 relevance_level](RAG-Agent如何读relevance_level.md) - 粗相关度标签的含义与误读
|
||||
4. [RAG 离线评测:讨论、设计与落地](RAG离线评测-基线设计.md) - Golden/Fixture、流程图、hybrid 对齐与闸门修复
|
||||
5. [Milvus hybrid 接入清单](Milvus-Hybrid接入清单.md)
|
||||
6. [RAG Trace / 审计(架构)](../mvp/architecture/RAG检索可观测性与审计.md) - 请求内 trace、tool_invocation、Trace API
|
||||
|
||||
### 诊断全流程(E2E 导读)
|
||||
1. [一次诊断到底发生了什么](一次诊断全流程-E2E导读.md) - SUCCESS 全流程:阶段拆解、token、timeline、字段词典
|
||||
2. [RAG 审计补丁 E2E:step_id + query](RAG审计补丁-stepid-query-E2E验收.md) - 审计字段 live 验收(含业务 FALLBACK 样本)
|
||||
|
||||
---
|
||||
|
||||
## 📚 学习笔记 (learning/)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
**前提**:旧 Milvus SDK 直连检索路径后续废弃,不作为长期实现基础
|
||||
**目标**:在现有 `lookup_knowledge` pipeline 上接入 dense + sparse/BM25 混合检索,融合优先走服务端 RRF
|
||||
**关联文档**:
|
||||
- `docs/rag-ranking-multipath-retrieval-and-rrf.md`(排序与多路召回判断框架)
|
||||
- `docs/RAG排序-多路召回与RRF.md`(排序与多路召回判断框架)
|
||||
- 本文后续实现讨论以本节 **「交付拆分:分块去重 + Hybrid 同规划」** 为基线
|
||||
|
||||
---
|
||||
@@ -12,7 +12,7 @@
|
||||
## 1. 结论先说
|
||||
|
||||
可以接,而且和前面讨论的多路召回 / RRF 高度一致。
|
||||
但当前项目 **还不具备 hybrid 运行条件**,缺的不是“再调一次 search”,而是:
|
||||
但(写作当时)项目 **还不具备 hybrid 运行条件**,缺的不是“再调一次 search”,而是:
|
||||
|
||||
```text
|
||||
1. schema 只有 dense,没有 sparse/BM25 字段
|
||||
@@ -32,7 +32,27 @@
|
||||
- 分块去重与 hybrid 同规划、分里程碑交付(先共用地基,再开 hybrid)
|
||||
```
|
||||
|
||||
## 实现状态(2026-07-27)
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph gaps["写作时的缺口"]
|
||||
G1[无 sparse/BM25 schema]
|
||||
G2[写入只有 dense]
|
||||
G3[单路 similaritySearch]
|
||||
G4[后处理伪融合]
|
||||
G5[source 级去重吞 chunk]
|
||||
end
|
||||
|
||||
subgraph principles["接入原则"]
|
||||
P1[端口抽象 · 不堆旧 SDK]
|
||||
P2[融合下沉向量库 RRF]
|
||||
P3[应用层:filter/dedup/return-n/投影]
|
||||
P4[先地基后 hybrid 分里程碑]
|
||||
end
|
||||
|
||||
gaps --> principles
|
||||
```
|
||||
|
||||
## 实现状态(2026-07-27,后续已完成)
|
||||
|
||||
| 里程碑 | 状态 | 说明 |
|
||||
|---|---|---|
|
||||
@@ -40,17 +60,42 @@
|
||||
| 交付 2a 应用层 multi-path+RRF | **已完成并归档** | `2026-07-27-rag-hybrid-search-rrf`(已被 2b 取代为生产路径) |
|
||||
| 交付 2b 真 BM25 hybrid + 废弃 SDK | **已完成并归档** | `2026-07-27-rag-bm25-hybrid-drop-sdk` |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
D1[交付1<br/>chunk 身份/去重] --> D2a[交付2a<br/>app RRF]
|
||||
D2a --> D2b[交付2b<br/>真 BM25 hybrid]
|
||||
D2b --> NOW[生产:V2 store + hybrid mode]
|
||||
```
|
||||
|
||||
**当前生产知识路径:**
|
||||
|
||||
```text
|
||||
VectorIndexService / VectorSearchService
|
||||
-> MilvusHybridKnowledgeStore (MilvusClientV2 only)
|
||||
collection: milvus.collection (default biz_hybrid)
|
||||
collection: milvus.collection (default biz)
|
||||
mode: retrieval.search.mode = dense | hybrid
|
||||
hybrid: dense ANN + BM25 sparse ANN + RRFRanker
|
||||
```
|
||||
|
||||
**运维必做:** 全量重灌知识库到 `biz_hybrid`;旧 `biz` collection 不再被知识路径使用。
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph write["写入"]
|
||||
UP[upload / init / rebuild] --> VIS[VectorIndexService]
|
||||
VIS --> STORE[MilvusHybridKnowledgeStore]
|
||||
end
|
||||
|
||||
subgraph read["检索"]
|
||||
LK[lookup_knowledge] --> VSS[VectorSearchService]
|
||||
VSS -->|dense| SD[searchDense]
|
||||
VSS -->|hybrid| SH[searchHybrid + RRF]
|
||||
SD --> STORE
|
||||
SH --> STORE
|
||||
end
|
||||
|
||||
STORE --> COL[(Milvus collection biz<br/>dense + BM25 schema)]
|
||||
```
|
||||
|
||||
**运维必做:** 全量重灌知识库到 hybrid schema collection(配置名以 `milvus.collection` 为准,常见 `biz`);旧纯 dense collection 不能直接当 hybrid 用。
|
||||
|
||||
---
|
||||
|
||||
@@ -83,6 +128,19 @@ VectorIndexService / VectorSearchService
|
||||
-> return-n / Agent 投影
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
HIT[检索命中] --> ID[候选身份<br/>docId / chunkIndex / evidenceKey]
|
||||
ID --> DEDUP[chunk 去重 · 每文档上限]
|
||||
DEDUP --> FUSE[排序融合 · 库内 RRF]
|
||||
FUSE --> RET[return-n]
|
||||
RET --> PROJ[Agent 投影]
|
||||
|
||||
ID -.->|交付1 地基| D1[chunk identity]
|
||||
DEDUP -.-> D1
|
||||
FUSE -.->|交付2| D2[hybrid]
|
||||
```
|
||||
|
||||
若拆开且顺序错误:
|
||||
|
||||
| 只做一项 | 后果 |
|
||||
@@ -305,6 +363,27 @@ VectorIndexService / VectorSearchService
|
||||
LookupResult / Projector
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph write_port["写入边界"]
|
||||
W[KnowledgeWritePort<br/>upsert / deleteByDocId]
|
||||
end
|
||||
|
||||
subgraph search_port["检索边界"]
|
||||
S[KnowledgeSearchPort<br/>mode DENSE / HYBRID]
|
||||
end
|
||||
|
||||
W --> VDB[(Vector DB<br/>dense + BM25 sparse<br/>metadata filter)]
|
||||
S --> VDB
|
||||
|
||||
UP[upload/init] --> W
|
||||
LK[lookup_knowledge] --> S
|
||||
S --> RET[DocumentRetriever]
|
||||
RET --> POST[PostProcess<br/>dedup / cap / threshold / pack]
|
||||
POST --> PROJ[LookupResult / Projector]
|
||||
PROJ --> AG[Agent]
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- **Port** 是应用边界,实现可换成 Spring AI、Milvus 新客户端、或其他封装。
|
||||
@@ -385,6 +464,19 @@ category/kb_scope/doc_id -> 标量过滤索引(如需要)
|
||||
- 稳定 chunk id(doc_id + chunk_index 派生)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CHUNK[DocumentChunk] --> EMB[dense embed]
|
||||
CHUNK --> ST[search_text<br/>title+path+content]
|
||||
CHUNK --> META[docId/chunkIndex<br/>category/kb_scope]
|
||||
EMB --> ROW[upsert row]
|
||||
ST --> ROW
|
||||
META --> ROW
|
||||
ROW --> FN[BM25 Function<br/>search_text → sparse]
|
||||
ROW --> COL[(collection)]
|
||||
FN --> COL
|
||||
```
|
||||
|
||||
### 5.3 注意
|
||||
|
||||
- embedding 文本可以继续拼 `Title/Path/Content`
|
||||
@@ -429,6 +521,16 @@ SearchHit {
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
REQ[SearchRequest<br/>query · retrieveK · mode<br/>filter · rrfK] --> PORT[KnowledgeSearchPort]
|
||||
PORT -->|DENSE| D[dense ANN only]
|
||||
PORT -->|HYBRID| H[dense + BM25 + RRF]
|
||||
D --> HIT[SearchHit 列表<br/>id/docId/chunk · content · ranks]
|
||||
H --> HIT
|
||||
HIT --> APP[后处理 / 投影]
|
||||
```
|
||||
|
||||
### 6.2 `VectorSearchService` 怎么演进
|
||||
|
||||
短期:
|
||||
@@ -0,0 +1,396 @@
|
||||
# Agent 如何读 `relevance_level`:它是什么、不是什么
|
||||
|
||||
**日期**:2026-07-28
|
||||
**范围**:`lookup_knowledge` 投影给 Agent 的粗粒度相关度标签
|
||||
**读者**:要在 Diagnosis Agent / 工具契约里正确使用知识库结果的工程与提示词同学
|
||||
**关联**:
|
||||
|
||||
- ACI 契约:`RagToolResult.relevance_level` / `RagRelevanceLevel`
|
||||
- 计算:`KnowledgeEvidencePostProcessor` → `qualityScore` 阈值
|
||||
- 质量统一:`docs/RAG-Hybrid质量分与后处理.md`
|
||||
- 多路与 RRF:`docs/RAG排序-多路召回与RRF.md`
|
||||
- **运行时 Trace / 审计**:`mvp/architecture/RAG检索可观测性与审计.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话定义
|
||||
|
||||
**`relevance_level` 是对「这一次 `lookup_knowledge` 调用整体有多相关」的粗档标签,不是某一条 evidence 的分数,也不是 0~1 的相似度。**
|
||||
|
||||
Agent 真正写诊断、做引用时,仍应以 `evidence[]` 里的 excerpt 为准;`relevance_level` 只帮助判断:**这批评据大概有多硬、还要不要再查。**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph tool["lookup_knowledge 结果"]
|
||||
ES[evidence_status]
|
||||
EV[evidence excerpt]
|
||||
RL[relevance_level]
|
||||
TR[truncated]
|
||||
end
|
||||
|
||||
ES --> DEC{Agent 决策}
|
||||
EV --> DEC
|
||||
RL --> DEC
|
||||
TR --> DEC
|
||||
|
||||
DEC --> A1[写结论 / 引用]
|
||||
DEC --> A2[再查 / 换工具]
|
||||
DEC --> A3[证据不足降级]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 它出现在哪里
|
||||
|
||||
成功(或有结果)的知识库工具投影里,典型形状:
|
||||
|
||||
```json
|
||||
{
|
||||
"evidence_status": "EVIDENCE_FOUND",
|
||||
"tool_call_id": "call-…",
|
||||
"query": "用户/Agent 的检索句",
|
||||
"evidence": [
|
||||
{
|
||||
"document_id": "doc#chunk-0",
|
||||
"source": "…",
|
||||
"title": "…",
|
||||
"breadcrumb": "…",
|
||||
"excerpt": "…"
|
||||
}
|
||||
],
|
||||
"returned_count": 3,
|
||||
"relevance_level": "PRECISE",
|
||||
"truncated": false
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- JSON 字段名是 **`relevance_level`**(snake_case)
|
||||
- Java 枚举:`RagRelevanceLevel`(`PRECISE` / `HIGHLY_RELEVANT` / `REFERENCE`)
|
||||
- 无可用证据或质量不够时,字段常为 **null / 省略**(`NON_NULL`)
|
||||
- Agent **看不到** raw L2、RRF 分、`retrievalTrace`、`rerankTrace`(ACI 有意裁掉)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph internal["内部 LookupResult(审计/调试)"]
|
||||
QS[qualityScore / topSimilarity]
|
||||
RT[retrievalTrace / rerankTrace]
|
||||
CP[contextPack 全文]
|
||||
SC[raw score / scoreLabel]
|
||||
end
|
||||
|
||||
subgraph project["RagResultProjector"]
|
||||
P[裁剪与规范化]
|
||||
end
|
||||
|
||||
subgraph agent["Agent 可见 RagToolResult"]
|
||||
A1[evidence_status]
|
||||
A2[tool_call_id / query]
|
||||
A3[evidence excerpt 列表]
|
||||
A4[relevance_level 可选]
|
||||
A5[returned_count / truncated]
|
||||
end
|
||||
|
||||
internal --> P --> agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 枚举值怎么理解
|
||||
|
||||
| 值 | 产品语义 | Agent 侧更合理的用法 |
|
||||
|----|----------|----------------------|
|
||||
| **PRECISE** | 整体很贴:当前 top 证据质量高,继续同题检索不太可能更准 | 优先依据 `evidence[]` 组织结论;避免无意义的重复 `lookup_knowledge` |
|
||||
| **HIGHLY_RELEVANT** | 高度相关(枚举保留) | 与 PRECISE 类似,略保守表述即可 |
|
||||
| **REFERENCE** | 可作参考,但不到「已精准命中」 | 可引用,但结论留余地;缺维度时换 query 再查或叠日志/指标 |
|
||||
| **null / 不出现** | 没有可报的粗相关档(无证据或 top 质量偏低) | **不要**当成知识库已证实;按证据不足处理 |
|
||||
|
||||
### 和 `evidence_status` 的分工
|
||||
|
||||
| 字段 | 回答的问题 |
|
||||
|------|------------|
|
||||
| `evidence_status` | 这次有没有合法、可引用的证据(如 `EVIDENCE_FOUND` / `NO_EVIDENCE`) |
|
||||
| `relevance_level` | **有证据时**,整体有多贴(粗档) |
|
||||
| `evidence[]` | 具体可以引用哪些片段 |
|
||||
|
||||
没有证据时,不应指望靠 `relevance_level`「升级」出结论;契约上也不会用 level 把空结果扮成有证据。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
ES{evidence_status}
|
||||
ES -->|NO_EVIDENCE| N1[不要当知识库已证实]
|
||||
ES -->|EVIDENCE_FOUND| RL{relevance_level}
|
||||
|
||||
RL -->|PRECISE| U1[优先引用 excerpt · 少重复检索]
|
||||
RL -->|REFERENCE| U2[可引用 · 结论留余地]
|
||||
RL -->|null / 缺省| U3[有块但质量偏低 · 慎用强结论]
|
||||
RL -->|HIGHLY_RELEVANT| U4[与 PRECISE 类似 · 略保守]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 它是怎么算出来的(实现口径)
|
||||
|
||||
### 4.1 只看「本轮第一名」的 qualityScore
|
||||
|
||||
后处理在完成排序、去重、截断之后:
|
||||
|
||||
```text
|
||||
取 originalRank 最优(排序后第一条)的 qualityScore
|
||||
≥ highly-relevant-threshold (默认 0.75) → PRECISE
|
||||
≥ reference-threshold (默认 0.5) → REFERENCE
|
||||
否则 / 无可用证据 → null
|
||||
```
|
||||
|
||||
配置(`application.yml`):
|
||||
|
||||
```yaml
|
||||
retrieval:
|
||||
normalization:
|
||||
max-l2-distance: 2.0
|
||||
highly-relevant-threshold: 0.75
|
||||
reference-threshold: 0.5
|
||||
```
|
||||
|
||||
因此:
|
||||
|
||||
- level 描述的是 **整次调用的 top 质量**,不是每条 evidence 各打一档
|
||||
- 列表里第 2、第 3 条即使偏弱,只要 top1 够高,整次仍可能是 `PRECISE`
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
RET[检索有序候选] --> POST[后处理:保 originalRank · 去重 · 截断]
|
||||
POST --> TOP[取排序后第一条 qualityScore]
|
||||
TOP --> T1{≥ 0.75?}
|
||||
T1 -->|是| PRECISE[PRECISE]
|
||||
T1 -->|否| T2{≥ 0.5?}
|
||||
T2 -->|是| REF[REFERENCE]
|
||||
T2 -->|否| NULL[null / 不报档]
|
||||
```
|
||||
|
||||
### 4.2 qualityScore 从哪来(和检索 mode 绑定)
|
||||
|
||||
统一经 `RetrievalScoreNormalizer.toQualityScore`:
|
||||
|
||||
| `retrieval.search.mode` | top qualityScore 含义 |
|
||||
|-------------------------|------------------------|
|
||||
| **dense** | top1 的 L2 归一化:约 `1 - L2 / maxL2Distance` |
|
||||
| **hybrid** | **优先**同 id 的 `denseDistance`(绝对 L2 质量,供闸门/level);无 dense 邻域时 **rank 回退** |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph dense_mode["mode=dense"]
|
||||
L2[score = L2] --> QD[quality = 1 - L2/max]
|
||||
end
|
||||
|
||||
subgraph hybrid_mode["mode=hybrid"]
|
||||
RRF[RRF 序 = originalRank] --> SORT[列表顺序]
|
||||
DD[denseDistance 可选] --> QH{有 dense?}
|
||||
QH -->|是| QL[quality = L2 归一化]
|
||||
QH -->|否| QR[quality = rank 映射]
|
||||
end
|
||||
|
||||
QD --> LV[relevance_level]
|
||||
QL --> LV
|
||||
QR --> LV
|
||||
```
|
||||
|
||||
读 level 时注意:
|
||||
|
||||
> **dense 下的 PRECISE ≈「向量足够近」**
|
||||
> **hybrid 下的 PRECISE ≈「top1 的绝对/回退 quality 跨过了 0.75」**;排序仍跟 RRF,不是「又变回只信 L2 排序」。
|
||||
|
||||
不要把 hybrid 的 level 读成与 dense **完全同一把尺子**,但也不要再假设「rank1 永远 PRECISE」(在附带 denseDistance 后,远邻 decoy 可以很低分并触发 filter fallback)。
|
||||
### 4.3 关于 HIGHLY_RELEVANT
|
||||
|
||||
枚举和旧文档里仍有三档。历史上大致是:
|
||||
|
||||
```text
|
||||
高分 + L0 hint 支撑 → PRECISE
|
||||
高分但无 hint → HIGHLY_RELEVANT
|
||||
中等分 → REFERENCE
|
||||
```
|
||||
|
||||
质量分统一之后,当前实现是:**≥ 0.75 直接 PRECISE**,不再要求 L0 contains 才能精准。
|
||||
因此运行时 **很少再单独产出 HIGHLY_RELEVANT**;读旧 trace / 旧快照时仍可能见到。
|
||||
|
||||
---
|
||||
|
||||
## 5. Agent 应该怎么读(建议协议)
|
||||
|
||||
### 5.1 推荐读法
|
||||
|
||||
```text
|
||||
1. 先看 evidence_status
|
||||
2. 再读 evidence[] 的 excerpt(唯一可引用正文)
|
||||
3. 用 relevance_level 调节「敢多敢少」与「要不要再查」
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
START[收到 RagToolResult] --> S1{evidence_status}
|
||||
S1 -->|无证据| FAIL[不编造 · 换工具或安全降级]
|
||||
S1 -->|有证据| S2[精读 evidence excerpt]
|
||||
S2 --> S3{relevance_level}
|
||||
S3 -->|PRECISE| C1[结论可较硬 · 少重复同 query 检索]
|
||||
S3 -->|REFERENCE| C2[结论留余地 · 可换问法或叠日志指标]
|
||||
S3 -->|缺省| C3[慎用强结论 · 优先补查]
|
||||
C1 --> CITE[引用必须落在 excerpt]
|
||||
C2 --> CITE
|
||||
C3 --> CITE
|
||||
```
|
||||
|
||||
| 组合 | 建议行为 |
|
||||
|------|----------|
|
||||
| FOUND + PRECISE | 以 excerpt 为主写结论;少重复同 query 检索 |
|
||||
| FOUND + REFERENCE | 可引用,表述保守;缺关键事实则改写 query 或换工具 |
|
||||
| FOUND 但 level 空 | 有块但质量闸门偏低:慎用强结论,优先补查 |
|
||||
| NO_EVIDENCE | 不编造知识库依据;走其他证据工具或安全降级 |
|
||||
|
||||
### 5.2 明确不要这样读
|
||||
|
||||
1. **不要当逐条相关度**
|
||||
没有 `evidence[i].relevance_level`;不能说「第 2 条是 REFERENCE」。
|
||||
|
||||
2. **不要当连续分数**
|
||||
没有 0.83;只有粗档。不要在推理里假装有精确分。
|
||||
|
||||
3. **不要在 hybrid 下当成绝对语义相似度**
|
||||
序数 quality 下 PRECISE 很常见,表示「本轮第一够格」,不等于「全局语义必近」。
|
||||
|
||||
4. **不要代替 excerpt 引用**
|
||||
level 不能当证据正文;Gatekeeper / EvidenceGuard 认的是可核对片段与引用约束。
|
||||
|
||||
5. **不要和 Harness 验真混为一谈**
|
||||
level 是检索侧粗标;工具生命周期、`evidence_status`、守卫校验是另一层。
|
||||
|
||||
6. **不要用它驱动跨 mode 对比**
|
||||
同一 query 切 dense/hybrid 时,比命中集合与排名;别只比「是不是都 PRECISE」。
|
||||
|
||||
---
|
||||
|
||||
## 6. Agent 看不见、但会影响 level 的内部量
|
||||
|
||||
便于排查「为什么突然全是 PRECISE / 总是 null」:
|
||||
|
||||
| 内部量 | 作用 | Agent 是否可见 |
|
||||
|--------|------|----------------|
|
||||
| `qualityScore` / `topSimilarity` | 定 level、低质 retry | 否 |
|
||||
| `originalRank` | 排序权威;hybrid quality 输入 | 否 |
|
||||
| `score` + `scoreLabel` | dense=L2 / hybrid=融合侧 | 否 |
|
||||
| L0 domain/keyword | 现仅 hitReasons 解释,不改序、不抬 level | 否(reasons 也可能被投影裁掉) |
|
||||
| `completenessHint` | 内部完整度文案 | 通常否 |
|
||||
| category filter + unfiltered retry | 低质时可能换一批 evidence 再定 level | 过程 trace 否 |
|
||||
|
||||
投影原则(ACI):模型只要能理解与引用结果;**不给 raw score、阈值、trace。**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph pipe["检索管道内部"]
|
||||
STORE[Milvus hybrid/dense]
|
||||
NORM[toQualityScore]
|
||||
POST[PostProcessor]
|
||||
STORE --> NORM --> POST
|
||||
end
|
||||
|
||||
POST --> LV[relevance_level]
|
||||
POST --> EB[evidenceBlocks]
|
||||
POST --> TR[traces · 通常不投影]
|
||||
|
||||
EB --> PROJ[RagResultProjector]
|
||||
LV --> PROJ
|
||||
PROJ --> AGENT[Agent Observation]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 和相邻概念的边界
|
||||
|
||||
```text
|
||||
evidence_status 有没有证据
|
||||
relevance_level 有的话有多贴(粗)
|
||||
evidence[].excerpt 贴在哪一段文字上(细、可引用)
|
||||
truncated 列表是否被预算截断(可能还有更好的没展示)
|
||||
information_gain 等 诊断环路里「这轮工具对任务有没有增益」(另一契约)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph fields["同一次工具结果里的分工"]
|
||||
ES[evidence_status<br/>有没有]
|
||||
RL[relevance_level<br/>有多贴·粗]
|
||||
EX[excerpt<br/>说什么·细]
|
||||
TC[truncated<br/>是否被截断]
|
||||
end
|
||||
|
||||
ES --> USE[Agent 使用]
|
||||
RL --> USE
|
||||
EX --> USE
|
||||
TC --> USE
|
||||
|
||||
USE --> NOTE[结论锚在 excerpt<br/>level 只调力度]
|
||||
```
|
||||
|
||||
`truncated=true` 时:即使 `PRECISE`,也只说明 **已返回子集里的 top 很强**,不保证库内没有更相关却被截掉的块。
|
||||
|
||||
---
|
||||
|
||||
## 8. 提示词 / 产品文案可用的短说明
|
||||
|
||||
可直接给模型或文档的精简版:
|
||||
|
||||
```text
|
||||
relevance_level 是本次知识库检索的整体相关度粗标:
|
||||
- PRECISE:当前证据整体很贴,优先引用 evidence 写结论,避免无意义重复检索
|
||||
- REFERENCE:仅供参考,结论需留余地,必要时换问法或改用其他工具
|
||||
- 缺省:不要把本次结果当作高置信知识库证实
|
||||
|
||||
务必以 evidence 中的 excerpt 为唯一引用依据;不要编造未出现的文档内容。
|
||||
在 hybrid 检索下,PRECISE 更多表示「本轮排序第一档」,不是精确相似度分数。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见误读示例
|
||||
|
||||
| 误读 | 更正 |
|
||||
|------|------|
|
||||
| 「PRECISE 所以三条 evidence 都精准」 | 只保证 top 质量跨线;其余条只是同批返回 |
|
||||
| 「没有 relevance_level 就是工具失败」 | 更可能是无证据或质量偏低;看 `evidence_status` |
|
||||
| 「hybrid 全是 PRECISE 说明召回完美」 | 可能只是 rank1→quality=1.0 的档位特性 |
|
||||
| 「REFERENCE 的 excerpt 不能引用」 | 可以引用,但结论强度应下调 |
|
||||
| 「level 高就可以跳过 excerpt」 | 不可;引用与验真仍看正文 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 结语
|
||||
|
||||
`relevance_level` 是检索链路送给 Agent 的 **粗粒度驾驶辅助**:
|
||||
|
||||
- 告诉模型这批评据大概硬不硬
|
||||
- **不**替代 excerpt,**不**暴露打分细节,**不**等于逐条标注
|
||||
|
||||
在 dense 模式下,它更接近「向量有多近」;
|
||||
在 hybrid 主路径下,它更接近「本轮融合第一名是否跨过质量门槛」。
|
||||
|
||||
读的时候记住三句即可:
|
||||
|
||||
```text
|
||||
1. 先 status,再 excerpt,最后才看 level
|
||||
2. level 管「敢多敢少」,excerpt 管「说了什么」
|
||||
3. hybrid 的 PRECISE ≠ 绝对语义满分
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:代码与规格锚点
|
||||
|
||||
| 项 | 位置 |
|
||||
|----|------|
|
||||
| Agent 结果契约 | `RagToolResult` / `RagRelevanceLevel` |
|
||||
| 投影 | `RagResultProjector` |
|
||||
| 等级计算 | `KnowledgeEvidencePostProcessor.computeRelevance` |
|
||||
| 质量分 | `RetrievalScoreNormalizer` |
|
||||
| 规格 | `openspec/specs/aci-evidence-tool-contracts`、`rag-retrieval-quality-score` |
|
||||
| 架构 | `mvp/architecture/RAG知识检索架构.md` §9 |
|
||||
@@ -0,0 +1,592 @@
|
||||
# 混合检索上线之后:为什么还要统一 qualityScore,以及上一代后处理错在哪
|
||||
|
||||
**日期**:2026-07-28
|
||||
**范围**:hybrid 检索后的分数语义、后处理排序、相关度闸门、scoreLabel 约定
|
||||
**读者**:已经(或准备)上 dense+BM25+RRF,却发现「召回变了、质量判断还拧着」的工程同学
|
||||
**关联实现**:
|
||||
|
||||
- `lookup_knowledge` 模块化链路
|
||||
- `MilvusHybridKnowledgeStore`(dense / hybrid)
|
||||
- `RetrievalScoreNormalizer` / `KnowledgeEvidencePostProcessor`
|
||||
- OpenSpec / devflow:`rag-quality-score-unify`
|
||||
- 前置讨论:`docs/RAG排序-多路召回与RRF.md`
|
||||
- 架构:`mvp/architecture/RAG知识检索架构.md` §6
|
||||
|
||||
---
|
||||
|
||||
## 1. 引言:融合排好了序,不等于质量链路闭环了
|
||||
|
||||
上一篇文章(《诊断 Agent 场景下的 RAG 排序:从 K=3 规则加分,到多路召回与 RRF》)回答的是:
|
||||
|
||||
> 候选太少时别急着上精排;跨路不要硬加原始分;优先 RRF;L0 只做导航。
|
||||
|
||||
那一轮讨论之后,工程上陆续落地了:
|
||||
|
||||
1. **chunk 级证据身份与去重**(`docId#chunkIndex`,同文档多片段可并存)
|
||||
2. **真 hybrid**:Milvus 服务端 dense ANN + BM25 sparse + `hybridSearch` + `RRFRanker`
|
||||
3. **单一知识后端**(`MilvusClientV2`),去掉 sdk/spring 多路由主路径
|
||||
4. **mode 开关**:`hybrid` 线上主路径,`dense` 同库对照评测
|
||||
|
||||
主缺口从「假 hybrid / 粗去重」变成了另一件事:
|
||||
|
||||
```text
|
||||
库内:RRF 已经决定谁先谁后
|
||||
应用:后处理仍假装每条 score 都是 L2
|
||||
再用 L0 关键词 contains 加分改序
|
||||
```
|
||||
|
||||
于是出现一种很拧的现象:
|
||||
|
||||
- 检索层已经是 **混合检索的世界**
|
||||
- 质量层还活在 **单路 dense + 规则 boost 的世界**
|
||||
|
||||
本文记录的,就是这次对「拧」的拆解、拍板与落地口径:
|
||||
**统一 qualityScore,废止 hybrid 的 L2 伪装,去掉关键词 boost 改序。**
|
||||
|
||||
目标不是再推一套更复杂的模型,而是回答:
|
||||
|
||||
> hybrid 上线之后,排序权威和质量闸门到底听谁的?后处理还该不该拿关键词打分?
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph retrieval["检索层 · 已 hybrid"]
|
||||
Q[query] --> D[dense ANN]
|
||||
Q --> B[BM25 sparse]
|
||||
D --> RRF[hybridSearch + RRF]
|
||||
B --> RRF
|
||||
RRF --> ORD[RRF 序]
|
||||
end
|
||||
|
||||
subgraph post_old["后处理 · 仍 L2 世界"]
|
||||
ORD --> FAKE[伪装 / 回填 L2]
|
||||
FAKE --> BOOST[关键词 boost 改序]
|
||||
BOOST --> GATE[阈值 / level / retry]
|
||||
end
|
||||
|
||||
post_old --> PAIN[排序与闸门拧巴]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 上一代(hybrid 刚落地时)到底长什么样
|
||||
|
||||
### 2.1 检索侧:已经是真混合
|
||||
|
||||
`mode=hybrid` 时大致是:
|
||||
|
||||
```text
|
||||
query
|
||||
├─ dense ANN(query embedding) → vector / L2
|
||||
└─ BM25 sparse(EmbeddedText) → sparse_vector / BM25
|
||||
│
|
||||
▼
|
||||
Milvus hybridSearch + RRFRanker(k)
|
||||
│
|
||||
▼
|
||||
融合后的 hit 列表(RRF 序)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q[query] --> EMB[embedding]
|
||||
Q --> TXT[raw text]
|
||||
EMB --> DA[dense ANN<br/>vector / L2]
|
||||
TXT --> BA[BM25 ANN<br/>sparse]
|
||||
DA --> HS[Milvus hybridSearch]
|
||||
BA --> HS
|
||||
HS --> RR[RRFRanker]
|
||||
RR --> HITS[有序 hits]
|
||||
```
|
||||
|
||||
这比应用层 sparse-lite / 伪 hybrid 前进了一大步:词面与语义在**库内**融合,chunk 身份也不会在后处理被文档级折叠吞掉。
|
||||
|
||||
### 2.2 分数侧:仍在「骗」后处理
|
||||
|
||||
后处理历史契约默认:
|
||||
|
||||
```text
|
||||
score ≈ L2 距离(越小越好)
|
||||
baseScore = 1 - clamp(L2) / maxL2Distance # 越大越好
|
||||
再 + domain/entity/keyword boost
|
||||
按 finalScore 重排
|
||||
用 baseScore 定 relevance_level / 是否低质 retry
|
||||
```
|
||||
|
||||
为了迁就这套契约,hybrid 路径做了补丁:
|
||||
|
||||
```text
|
||||
hybrid 融合结果
|
||||
-> 再跑一路 dense
|
||||
-> 按 id 把 L2 回填到 score,label 改成 l2_distance
|
||||
-> 仅 BM25 命中、dense 没命中:
|
||||
score = maxL2Distance
|
||||
label = bm25_only_no_dense
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
H[hybrid RRF 结果] --> P[并行 dense 探测]
|
||||
P --> M{同 id 有 L2?}
|
||||
M -->|是| O1[score=L2 · label=l2_distance]
|
||||
M -->|否| O2[score=maxL2 · label=bm25_only]
|
||||
O1 --> N[normalizeL2 + boost 重排]
|
||||
O2 --> N
|
||||
N --> BAD[BM25-only 好证据被当成最差]
|
||||
```
|
||||
|
||||
意图是好的:让 `normalizeL2` 和 0.75/0.5 阈值「还能用」。
|
||||
副作用也很清楚:
|
||||
|
||||
| 现象 | 后果 |
|
||||
|------|------|
|
||||
| RRF 决定顺序,L2 决定「好不好」 | 两套真理,互相打架 |
|
||||
| BM25-only 好证据被标成最远 L2 | quality≈0,像低质,甚至触发 unfiltered retry |
|
||||
| `bm25_only_no_dense` 像第三种 label | 概念膨胀:mode 其实只有 dense/hybrid |
|
||||
| 多打一路 dense 只为回填 | 延迟与复杂度,换来的是语义自洽的假象 |
|
||||
|
||||
一句话:
|
||||
|
||||
> **不是拿 RRF 分错误地套了 L2 公式,而是排序信 RRF,打分/闸门仍假装大家都是 L2。**
|
||||
|
||||
### 2.3 后处理侧:关键词 boost 改主序
|
||||
|
||||
典型逻辑:
|
||||
|
||||
```text
|
||||
baseScore = normalizeL2(score)
|
||||
finalScore = baseScore
|
||||
+ domain_match (+0.15)
|
||||
+ entity_match (+0.20)
|
||||
+ keyword_match (+0.10)
|
||||
+ source_type (+0.05)
|
||||
按 finalScore 降序
|
||||
```
|
||||
|
||||
在 **还没有库内 BM25** 时,这套东西多少能补一点词面。
|
||||
在 **已经 hybrid** 之后,问题变成:
|
||||
|
||||
1. **双重计分**
|
||||
BM25 已经在 RRF 里投过票;后处理再用 contains 加分,等于词面再抬一次。
|
||||
|
||||
2. **contains 比 BM25 更糙**
|
||||
无 IDF、无文档长度、短词子串误命中——正好制造「词频/词面高、相关度低却排前面」。
|
||||
|
||||
3. **冲掉 RRF 序**
|
||||
花了 hybrid 买到的融合序,被 L0 词表二次改写。
|
||||
|
||||
4. **PRECISE 还绑 hint**
|
||||
高质量还要 `hasHintSupport`(同样是 contains),把导航层信号抬成等级门槛。
|
||||
|
||||
结合上一篇文章的判断——**L0 / 关键词适合做提示,不适合当最终裁判**——hybrid 上线后,后处理 boost 改序已经从「可接受的轻启发式」滑向「明确的设计债」。
|
||||
|
||||
---
|
||||
|
||||
## 3. 问题清单:chunk 去重 + hybrid 之后,还剩什么
|
||||
|
||||
可以分成四层(本次主要收口前两层):
|
||||
|
||||
### 3.1 正确性 / 契约(本次主战场)
|
||||
|
||||
1. hybrid **没有**独立的融合分归一化,只有 L2 兼容补丁
|
||||
2. 后处理关键词打分不合理,会抬升词面热、语义冷的片段
|
||||
3. `scoreLabel` 语义混乱:`l2_distance` / `rrf_fused` / `bm25_only_*` 混用
|
||||
4. `mode=dense` 与 hybrid 内部 dense 子路概念易混(mode 是整次查询算法,不是「第三套库」)
|
||||
|
||||
### 3.2 质量上限(未在本 change 做完)
|
||||
|
||||
- 无固定 RAG 评测报表驱动阈值标定
|
||||
- 无邻块扩展、真 query rewrite、cross-encoder 精排
|
||||
- 中文 analyzer / 分词策略未产品化钉死
|
||||
|
||||
### 3.3 工程债(部分清理、部分保留)
|
||||
|
||||
- 应用层 `LexicalRanker` / 自研 `RrfFusion` 可能仍像「还有 app-layer hybrid」
|
||||
- Spring AI starter 仍可作 sidecar,但不是知识主路径(starter 至 2.0.0 仍无 BM25 hybrid)
|
||||
- 写入先删后插,非强 upsert;Milvus / MySQL / L0 三方一致性靠流程
|
||||
|
||||
### 3.4 运维边界
|
||||
|
||||
- hybrid 依赖 BM25 Function + sparse index
|
||||
- 全量 rebuild 受 embedding 与写入延迟约束
|
||||
- `totalVectors` 一类统计可能仍不可信
|
||||
|
||||
本次 change(`rag-quality-score-unify`)**有意只收口 3.1**:
|
||||
让 hybrid 的排序权威与质量闸门重新对齐,而不是同时上精排模型。
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键澄清:L2 是什么,它是不是「后处理」本身
|
||||
|
||||
讨论中容易把「L2」和「后处理打分流程」混成一个词。需要拆开:
|
||||
|
||||
### 4.1 L2 是度量
|
||||
|
||||
**L2 = 欧氏距离**,dense ANN 常用 metric:
|
||||
|
||||
- 越小越相似
|
||||
- 单位向量场景下可用 `maxL2Distance≈2` 做上界
|
||||
- 归一化相似度:`1 - clamp(L2) / maxL2`
|
||||
|
||||
### 4.2 后处理是流水线
|
||||
|
||||
后处理消费的是「约定好的 score」,历史上**假定**它是 L2,于是:
|
||||
|
||||
```text
|
||||
score(L2) → normalizeL2 → baseScore → (+boost) → finalScore → 排序/等级
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph metric["度量层"]
|
||||
L2[L2 距离]
|
||||
end
|
||||
|
||||
subgraph pipe["后处理流水线"]
|
||||
N[normalize]
|
||||
B[可选 boost]
|
||||
S[排序 / 截断]
|
||||
G[等级 / 闸门]
|
||||
N --> B --> S --> G
|
||||
end
|
||||
|
||||
L2 -.->|历史上假定输入是 L2| N
|
||||
RRF[RRF 融合分] -.->|量纲不同 · 不能直接套| N
|
||||
```
|
||||
|
||||
所以:
|
||||
|
||||
- L2 ≠ 后处理
|
||||
- L2 = dense 路径的自然距离
|
||||
- 后处理 = 把某种 score 变成 quality / 等级 / 截断结果的流程
|
||||
|
||||
hybrid 的问题是:**流程还在,输入契约已经不再总是 L2。**
|
||||
|
||||
### 4.3 category 降级也不是 L2 存在的唯一理由
|
||||
|
||||
filtered → unfiltered retry 用的是:
|
||||
|
||||
```text
|
||||
isLowQuality = 无可用证据 或 topSimilarity < referenceThreshold
|
||||
```
|
||||
|
||||
`topSimilarity` 来自归一化后的质量分。
|
||||
unfiltered 只是**再检一次**,尺子本来就该是统一 quality,而不是「专为降级准备的 L2」。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
F[带 category 的检索] --> Q{isLowQuality?}
|
||||
Q -->|是| U[unfiltered retry]
|
||||
Q -->|否| K[采用本次结果]
|
||||
U --> M[合并/替换为 retry 结果]
|
||||
K --> OUT[后处理出口]
|
||||
M --> OUT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计拍板:统一成什么
|
||||
|
||||
### 5.1 两层概念,不要混
|
||||
|
||||
| 层 | 只有什么 | 不是什么 |
|
||||
|----|----------|----------|
|
||||
| **检索 mode** | `dense` \| `hybrid` | 不是三套库 |
|
||||
| **一级 scoreLabel** | `dense` \| `hybrid` | 不是 `bm25_only` 第三种模式 |
|
||||
|
||||
- `mode`:整次查询怎么跑(配置 `retrieval.search.mode`)
|
||||
- `scoreLabel`:这条 hit 的 `score` 怎么解释
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
CFG[retrieval.search.mode] --> M1[dense 整次只跑 ANN]
|
||||
CFG --> M2[hybrid 整次 dense+BM25+RRF]
|
||||
|
||||
M1 --> L1[scoreLabel=dense]
|
||||
M2 --> L2[scoreLabel=hybrid]
|
||||
|
||||
L2 -.->|不是| L3[bm25_only 第三种 mode]
|
||||
```
|
||||
|
||||
`bm25_only_no_dense` **不是第三种检索**,只是旧链路里「这条 hybrid 命中没有 dense L2 可回填」的补丁标签。统一后应降级为历史别名(canonicalize → `hybrid`),不再一级发射。
|
||||
### 5.2 检索回来带什么
|
||||
|
||||
建议最小契约:
|
||||
|
||||
```text
|
||||
rank (originalRank) // 1 最好;hybrid = RRF 序;dense = ANN 序
|
||||
score // 引擎主分;量纲由 label 解释
|
||||
scoreLabel // dense | hybrid
|
||||
rawScore? // 可选调试
|
||||
denseDistance? // hybrid 可选:同 id 的 L2,仅供闸门
|
||||
```
|
||||
|
||||
| label | score 含义 |
|
||||
|-------|------------|
|
||||
| `dense` | L2 距离(越小越好) |
|
||||
| `hybrid` | 引擎融合分可放 raw/score;**排序不看其量纲** |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph emit["Store 发射"]
|
||||
LAB[scoreLabel<br/>dense | hybrid]
|
||||
SCR[score / rawScore]
|
||||
RNK[列表序 → originalRank]
|
||||
DD[denseDistance? 仅 hybrid]
|
||||
end
|
||||
|
||||
LAB --> NORM
|
||||
SCR --> NORM
|
||||
RNK --> SORT
|
||||
DD --> NORM
|
||||
NORM[toQualityScore] --> QS[qualityScore]
|
||||
SORT[按 rank 排序] --> LIST[evidence 顺序]
|
||||
QS --> GATE[level / isLowQuality]
|
||||
```
|
||||
|
||||
### 5.3 唯一归一化点
|
||||
|
||||
```text
|
||||
qualityScore = toQualityScore(label, score, rank, batchSize, maxL2, denseDistance?)
|
||||
// 输出统一:[0,1],越大越好
|
||||
```
|
||||
|
||||
分支只允许出现在这里:
|
||||
|
||||
```text
|
||||
dense → 1 - clamp(L2)/maxL2
|
||||
hybrid → 优先 denseDistance 的 L2 归一化(绝对质量 / 闸门)
|
||||
无 dense 时 rank 线性回退
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
IN[label + score + rank + denseDistance?] --> C{canonicalize label}
|
||||
C -->|dense| L2[l2ToQuality score]
|
||||
C -->|hybrid| H{denseDistance?}
|
||||
H -->|有| L2H[l2ToQuality denseDistance]
|
||||
H -->|无| RK[rankToQuality]
|
||||
L2 --> OUT[qualityScore 0..1]
|
||||
L2H --> OUT
|
||||
RK --> OUT
|
||||
```
|
||||
|
||||
**演进说明:** 切片 1 曾用纯 rank 做 hybrid quality;eval 发现 rank1 恒高会杀死 L0 filter fallback。
|
||||
现行约定:**排序仍纯 RRF;闸门可用 denseDistance 绝对质量**,且 **不得** 再把主分/label 伪装成 L2。
|
||||
|
||||
### 5.4 后处理:统一流程,不要按 label 再分叉业务
|
||||
|
||||
```text
|
||||
candidates
|
||||
→ 每条 toQualityScore(...) ← 唯一认 label 的地方
|
||||
→ qualityScore + originalRank
|
||||
→ 统一:按 rank 排序 / 去重 / 每文档 chunk 上限 / return-n
|
||||
→ 统一:relevance_level、isLowQuality(只看 qualityScore)
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
CAND[candidates] --> QS[toQualityScore 每条]
|
||||
QS --> SORT[sort by originalRank ASC]
|
||||
SORT --> DEDUP[evidenceKey 去重]
|
||||
DEDUP --> CAP[max-chunks / return-n]
|
||||
CAP --> REL[relevance_level]
|
||||
CAP --> LOW[isLowQuality → filter retry]
|
||||
CAP --> OUT[EvidenceBlocks]
|
||||
```
|
||||
|
||||
可以记成:
|
||||
|
||||
> **Label 只活在进后处理之前的适配器里;后处理是 label-agnostic 的。**
|
||||
> **排序听 rank;闸门听 qualityScore(hybrid 可含 denseDistance)。**
|
||||
### 5.5 后处理还改不改?——要改,而且和归一化同一刀
|
||||
|
||||
后处理合理职责是 **裁剪与装配**,不是第二套检索:
|
||||
|
||||
| 保留 | 去掉或降级 |
|
||||
|------|------------|
|
||||
| evidenceKey 去重 | domain/entity/keyword **加分改序** |
|
||||
| max-chunks-per-document | contains 当相关度代理 |
|
||||
| return-n | PRECISE 强制 hint support |
|
||||
| excerpt 截断、EvidenceBlock | |
|
||||
| 统一 qualityScore 闸门 | |
|
||||
|
||||
L0 仍可:
|
||||
|
||||
- 导航:category filter(失败 unfiltered retry)
|
||||
- 解释:`hitReasons` 记 `l0_keyword_overlap` 等(**零分值**)
|
||||
|
||||
词面该不该高:交给 **BM25 子路 + RRF**。
|
||||
语义该不该近:交给 **dense 子路**(融合时已参与;dense-only mode 对照时单独看)。
|
||||
|
||||
### 5.6 mode=dense 还要不要
|
||||
|
||||
要,但定位清楚:
|
||||
|
||||
| 模式 | 定位 |
|
||||
|------|------|
|
||||
| **hybrid** | 线上主路径 / 默认 |
|
||||
| **dense** | 同库对照、评测、排障——看「去掉 BM25+RRF 后差在哪」 |
|
||||
|
||||
注意:
|
||||
|
||||
- hybrid **入库**数据完全适用于 dense 查询(每条都写了 `vector`)
|
||||
- hybrid **内部**仍有 dense 子路——那是融合的一部分,≠ `mode=dense`
|
||||
- 对照时固定 `retrieve-k` / `return-n` / filter / query 集,只切 mode
|
||||
- 优先比命中集合与排名;`relevance_level` 在 hybrid 下是序数 quality,慎作跨 mode 绝对值对比
|
||||
|
||||
---
|
||||
|
||||
## 6. 目标数据流(落地后)
|
||||
|
||||
```text
|
||||
VectorSearchService (mode=dense|hybrid)
|
||||
-> hits{ originalRank, score, scoreLabel=dense|hybrid, rawScore?, denseDistance? }
|
||||
-> KnowledgeDocumentRetriever / SearchPort
|
||||
-> KnowledgeEvidencePostProcessor
|
||||
qualityScore = RetrievalScoreNormalizer.toQualityScore(...)
|
||||
sort by originalRank ASC
|
||||
evidenceKey dedup / max-chunks / return-n
|
||||
relevance_level & topSimilarity from qualityScore
|
||||
L0 overlap → hitReasons only
|
||||
-> ContextPack / Assembler / Projector
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Q[query] --> VSS[VectorSearchService]
|
||||
VSS -->|mode=dense| SD[searchDense]
|
||||
VSS -->|mode=hybrid| SH[searchHybrid + 可选 denseDistance]
|
||||
SD --> PORT[KnowledgeSearchPort / Adapter]
|
||||
SH --> PORT
|
||||
PORT --> POST[KnowledgeEvidencePostProcessor]
|
||||
POST --> PACK[ContextPacker]
|
||||
POST --> ASM[LookupResultAssembler]
|
||||
ASM --> PROJ[RagResultProjector]
|
||||
PROJ --> AGENT[Agent 可见契约]
|
||||
```
|
||||
|
||||
与上一代对比:
|
||||
|
||||
| 环节 | 上一代 | 现在 |
|
||||
|------|--------|------|
|
||||
| hybrid score | 常被 L2 覆盖 | 保持融合侧;label=`hybrid` |
|
||||
| BM25-only | maxL2 + `bm25_only_*` | 普通 hybrid hit,quality 看 rank |
|
||||
| 归一化 | 一律当 L2 | 按 label 唯一转换 |
|
||||
| 排序 | finalScore(含 boost) | originalRank |
|
||||
| L0 关键词 | +分改序 | 仅解释 |
|
||||
| 质量闸门 | baseScore(L2 兼容) | qualityScore |
|
||||
|
||||
---
|
||||
|
||||
## 7. 行为变化:必须说清楚的协议调整
|
||||
|
||||
这是**有意的行为变化**(对内检索质量语义;Agent ACI 字段名可不变):
|
||||
|
||||
1. hybrid 下证据顺序更贴近 **RRF**,不再被 contains 抬到前面
|
||||
2. 「词面很准、dense 略远」的命中,不再被默认打成低质占位
|
||||
3. `relevance_level` / category unfiltered retry 的触发分布可能变化
|
||||
4. hybrid 的 quality 是**本轮序数分**,跨 query 绝对值不可比;阈值可能需后续标定
|
||||
5. dense 对照模式:质量仍走 L2 归一化,行为更接近旧 dense 主路径
|
||||
|
||||
未改:
|
||||
|
||||
- Agent 可见字段结构(evidence 列表、relevance 枚举名等)
|
||||
- Milvus hybrid schema / 不必为本次 rebuild
|
||||
- `mode=dense` 开关本身
|
||||
|
||||
---
|
||||
|
||||
## 8. 和上一篇文章的衔接:阶段进度
|
||||
|
||||
对照 `RAG排序-多路召回与RRF.md` 的推进顺序:
|
||||
|
||||
| 阶段 | 内容 | 状态(截至 2026-07-28) |
|
||||
|------|------|-------------------------|
|
||||
| Phase 0 | chunk 去重、retrieve-k/return-n、身份 | **已落地** |
|
||||
| Phase 1~2 | 多路 + RRF;真 BM25 hybrid | **已落地**(库内 hybrid,非 app-layer 伪融合) |
|
||||
| 分数职责分离 | 排序 vs 可用性/质量闸门 | **本次收口**(qualityScore 统一) |
|
||||
| 去掉 L0 当裁判 | 关键词不改主序 | **本次收口** |
|
||||
| Phase 3 | 可插拔模型 Rerank | **未做**(候选池与评测闭环仍优先) |
|
||||
| 邻块 / query rewrite | 上下文与问句改写 | **未做** |
|
||||
|
||||
因此,本次文章不是推翻上一篇,而是补上上一篇写到「融合之后」却还没写完的半截:
|
||||
|
||||
> 融合解决「谁进来、谁先排」;
|
||||
> 归一化与后处理决定「算不算够好、会不会被规则再次打乱」。
|
||||
|
||||
---
|
||||
|
||||
## 9. 实现锚点(便于对照代码)
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `RetrievalScoreLabels` | `dense` / `hybrid` + 旧别名 canonicalize |
|
||||
| `RetrievalScoreNormalizer` | 唯一 `toQualityScore` |
|
||||
| `MilvusHybridKnowledgeStore` | 发射 label;hybrid 不 L2 覆盖 |
|
||||
| `VectorSearchService` | mode 路由;SearchResult 契约注释 |
|
||||
| `KnowledgeEvidencePostProcessor` | rank 保序、去 boost 改序、quality 闸门 |
|
||||
| 架构 §6.0 | mode 用途:hybrid 主路径 / dense 对照 |
|
||||
|
||||
验证(单测,非 live E2E):
|
||||
|
||||
- normalizer:L2 边界、rank 单调、别名
|
||||
- post-process:rank 不被 keyword 打乱;caps/return-n
|
||||
- lookup tool:保序;context pack 元数据仍在
|
||||
|
||||
已知未验证:真实 Milvus 联调对照、阈值标定。
|
||||
|
||||
---
|
||||
|
||||
## 10. 实践清单:以后别再踩的坑
|
||||
|
||||
1. **不要**为了复用旧 `normalizeL2`,把 hybrid 结果伪装成 L2。
|
||||
2. **不要**在已经 BM25 hybrid 之后,再用 L0 contains 大额加分改主序。
|
||||
3. **不要**把 `bm25_only` 当成第三种检索模式。
|
||||
4. **不要**把 hybrid 内部的 dense 子路,和 `mode=dense` 整次查询混为一谈。
|
||||
5. **要**让 label 差异停在适配器;后处理只认 qualityScore + rank。
|
||||
6. **要**用 dense mode 做召回对照,而不是第二套长期线上策略。
|
||||
7. **要**接受:hybrid 序数 quality 与绝对阈值之间,需要观测后再调,而不是再发明一层伪装。
|
||||
8. **下一步再考虑**精排模型——在契约掰直、有固定 query 回归集之后。
|
||||
|
||||
---
|
||||
|
||||
## 11. 结语
|
||||
|
||||
混合检索落地,解决的是「漏」和「跨路硬加分」里很大一块。
|
||||
但若后处理仍活在 L2 + 关键词 boost 的旧世界,hybrid 买到的 RRF 序和质量信号会被悄悄改写,甚至惩罚「只在 BM25 路很强」的好证据。
|
||||
|
||||
这次收口的核心就三句:
|
||||
|
||||
```text
|
||||
1. 一级 label 只有 dense / hybrid
|
||||
2. 唯一 toQualityScore;后处理统一、保 rank
|
||||
3. L0 关键词可以解释,不可以再当排序裁判
|
||||
```
|
||||
|
||||
它不是 RAG 的终点,而是 hybrid 从「能跑」变成「分数语义自洽」的必要一步。
|
||||
在此之后,评测闭环、阈值标定、邻块与精排,才有干净的基线可谈。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:术语
|
||||
|
||||
| 术语 | 含义 |
|
||||
|------|------|
|
||||
| L2 | 欧氏距离;dense ANN 常用;越小越相似 |
|
||||
| RRF | Reciprocal Rank Fusion;用名次融合多路,不融合原始分 |
|
||||
| scoreLabel | 一级分数语义:`dense` \| `hybrid` |
|
||||
| qualityScore | 归一化后的 0~1 质量分(越大越好),供等级与低质闸门 |
|
||||
| originalRank | 检索返回名次;后处理排序权威 |
|
||||
| mode=dense | 整次只跑 dense ANN(对照) |
|
||||
| mode=hybrid | dense+BM25+RRF(主路径) |
|
||||
| L0 | query hint / 可选 category filter;不作事实证据、不改主序 |
|
||||
|
||||
## 附录 B:相关材料
|
||||
|
||||
| 材料 | 路径 |
|
||||
|------|------|
|
||||
| 多路与 RRF 讨论 | `docs/RAG排序-多路召回与RRF.md` |
|
||||
| 当前架构 | `mvp/architecture/RAG知识检索架构.md` |
|
||||
| OpenSpec 归档 | `openspec/changes/archive/2026-07-28-rag-quality-score-unify/` |
|
||||
| 主规格 | `openspec/specs/rag-retrieval-quality-score/spec.md` |
|
||||
| devflow | `devflow/projects/2026-07-28-rag-quality-score-unify/` |
|
||||
@@ -0,0 +1,163 @@
|
||||
# RAG 审计补丁 E2E:`step_id` + query(含业务 FALLBACK 样本)
|
||||
|
||||
**日期**:2026-07-28
|
||||
**状态**:审计字段 live 验收记录
|
||||
**关联主文档**:[一次诊断到底发生了什么(SUCCESS 全流程)](一次诊断全流程-E2E导读.md)
|
||||
|
||||
> 主文档只保留 **SUCCESS 完整诊断** 与 **现行审计能力说明**。
|
||||
> 本页单独记录:改造后的一次 live 验收——**审计字段 PASS**,业务因 Milvus 空结果走了 **FALLBACK**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 样本身份
|
||||
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| `session_id` | `e2e-audit-20260728171858` |
|
||||
| `run_id` | `0be605f6-e036-40d3-b364-11b73672241f` |
|
||||
| 入口 | `POST /api/chat`(与 SUCCESS 样例同构的 RAG-only 约束题) |
|
||||
| 冷启动 | 含 `AgentStepAuditTracker` 等补丁后的 `mvn spring-boot:run` |
|
||||
| 业务结局 | `release_outcome=FALLBACK`,`content_type=SAFE_FALLBACK` |
|
||||
| 工具 | 1× `lookup_knowledge` |
|
||||
|
||||
本地产物(若仍在):`target/e2e-audit-sse.txt`、`target/e2e-audit-session.txt`、`target/e2e-audit-run.txt`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 验收目标 vs 非目标
|
||||
|
||||
| 目标 | 是否本页重点 |
|
||||
|------|----------------|
|
||||
| `tool_invocation.step_id` = 发出 tool_call 的 `agent_step.id` | **是** |
|
||||
| `input_params` 含安全 `query` 预览 | **是** |
|
||||
| Trace / timeline 带回 `step_id` | **是** |
|
||||
| 业务必须 SUCCESS | **否**(本 run 为 FALLBACK,归因环境) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 审计结果:PASS
|
||||
|
||||
### 3.1 `agent_step`
|
||||
|
||||
| id | step_index | has_tool_call | 说明 |
|
||||
|----|------------|---------------|------|
|
||||
| **962** | 0 | 1 | 发出 `lookup_knowledge` |
|
||||
| 963 | 1 | 0 | 无证据后的收尾轮 |
|
||||
|
||||
### 3.2 `tool_invocation`(id=860)
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| `step_id` | **962**(= step0) |
|
||||
| `tool_name` | `lookup_knowledge` |
|
||||
| `success` | 1(工具跑完;无证据也算执行成功) |
|
||||
| `search_mode` | hybrid |
|
||||
| `evidence_status` | `NO_EVIDENCE` |
|
||||
| `candidate_count` | 0 |
|
||||
| `relevance_level` | null |
|
||||
|
||||
**`input_params` 实值:**
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "MySQL connection pool exhausted HikariCP diagnosis",
|
||||
"step_id": 962,
|
||||
"tool_call_id": "call_00_LireJzbiFEsbZ9ZVvgYp8330",
|
||||
"request_bytes": 62
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Trace API
|
||||
|
||||
- `toolInvocations[0].stepId = 962`
|
||||
- `inputParams.query` 有值
|
||||
- timeline `TOOL_INVOCATION.details.step_id = 962`
|
||||
|
||||
### 3.4 对照表
|
||||
|
||||
| 检查项 | 预期 | 实际 | 判定 |
|
||||
|--------|------|------|------|
|
||||
| `tool_invocation.step_id` | = agent_step.id | 962 | **PASS** |
|
||||
| `input_params.query` | 有预览 | 有 | **PASS** |
|
||||
| `input_params.step_id` | 与列一致 | 962 | **PASS** |
|
||||
| Trace `stepId` | 非空 | 962 | **PASS** |
|
||||
| timeline `step_id` | 非空 | 962 | **PASS** |
|
||||
| `search_mode` | hybrid | hybrid | **PASS** |
|
||||
| 业务 outcome | (非本页 KPI) | FALLBACK | 见 §4 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S0[agent_step 962<br/>r1 tool_call] --> TI[tool_invocation 860<br/>step_id=962]
|
||||
TI --> IP[input_params.query]
|
||||
TI --> TR[Trace / timeline]
|
||||
TI --> RAG[hybrid candidate_count=0]
|
||||
RAG --> FB[FALLBACK<br/>NO_EVIDENCE]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 业务 FALLBACK 原因(与审计无关)
|
||||
|
||||
| 现象 | 说明 |
|
||||
|------|------|
|
||||
| 日志 | `found=false`,`evidenceBlocks=0` |
|
||||
| audit | `attempts[].usable=false`,`candidate_count=0` |
|
||||
| warm 检索 | 同期 `GET /api/search/similar` 亦失败 |
|
||||
| 日志噪音 | Milvus channel 未正确 shutdown 等提示(环境/客户端生命周期) |
|
||||
|
||||
**结论**:hybrid **路径进了**,但当次 **0 候选** → 无证据可写报告 → `SAFE_FALLBACK` / `INSUFFICIENT_EVIDENCE`。
|
||||
这不否定 `step_id` / `query` 落库;完整 SUCCESS 业务故事见主文档。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Q[同一 RAG-only 题] --> LK[lookup_knowledge]
|
||||
LK --> AUD[审计: step_id + query PASS]
|
||||
LK --> HIT{有候选?}
|
||||
HIT -->|SUCCESS 主文档 run| OK[PRECISE → DIAGNOSIS_REPORT]
|
||||
HIT -->|本页 run| NO[0 候选 → FALLBACK]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实现索引(补丁代码)
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `AgentStepAuditTracker` | run 级 bind/current/clear `agent_step.id` |
|
||||
| `HarnessAgentAuditHook` | `beforeModel` 落 step 后 `bind` |
|
||||
| `ToolBoundary.auditSafely` | 读 tracker,带上 `stepId` + `requestJson` |
|
||||
| `JpaToolInvocationAuditSink` | 写 `step_id` 列;安全展开 `input_params` |
|
||||
| `TraceAuditEvents.toolInvocation` | timeline details 的 `step_id` |
|
||||
| `JpaChatRunStore.finish` | `clear(runId)` |
|
||||
|
||||
**`input_params` 安全规则摘要**:
|
||||
|
||||
- 始终:`tool_call_id`、`request_bytes`;有则:`step_id`
|
||||
- 顶层 string/number/boolean/纯字符串数组;文本 ≤160 字符
|
||||
- 键名含 password/token/secret/apikey 等 → 不落库
|
||||
|
||||
更完整的字段词典与 SUCCESS 阶段拆解见主文档 §3.4 / §3.4.1。
|
||||
|
||||
---
|
||||
|
||||
## 6. 复盘 SQL
|
||||
|
||||
```bash
|
||||
python scripts/query_mysql.py "SELECT id, step_id, tool_name, relevance_level, CAST(input_params AS CHAR) AS inputp, LEFT(CAST(retrieval_details AS CHAR), 500) AS details FROM tool_invocation WHERE run_id = '0be605f6-e036-40d3-b364-11b73672241f'"
|
||||
|
||||
python scripts/query_mysql.py "SELECT id, step_index, agent_name, has_tool_call, token_count FROM agent_step WHERE run_id = '0be605f6-e036-40d3-b364-11b73672241f' ORDER BY step_index"
|
||||
|
||||
python scripts/query_mysql.py "SELECT run_id, status, release_outcome, tool_call_count, total_token_count FROM diagnosis_run WHERE run_id = '0be605f6-e036-40d3-b364-11b73672241f'"
|
||||
```
|
||||
|
||||
```http
|
||||
GET /api/diagnosis/e2e-audit-20260728171858/trace?runId=0be605f6-e036-40d3-b364-11b73672241f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 修订记录
|
||||
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-07-28 | 从主 walkthrough 拆出:专门记录 audit 验收 run(FALLBACK 业务 + step_id/query PASS) |
|
||||
@@ -39,6 +39,18 @@
|
||||
|
||||
> 现在的 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”
|
||||
@@ -57,6 +69,17 @@ Agent query
|
||||
-> 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
|
||||
@@ -69,6 +92,20 @@ finalScore = baseScore
|
||||
再按 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 这套做法解决了什么
|
||||
@@ -110,6 +147,18 @@ finalScore = baseScore
|
||||
|
||||
排序策略必须和 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 |
|
||||
@@ -157,6 +206,15 @@ topK = 3,召回、排序、返回都是 3
|
||||
若结果差,再 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 的全部结果
|
||||
@@ -170,6 +228,16 @@ topK = 3,召回、排序、返回都是 3
|
||||
去重合并后一起排序
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q[query] --> A[路A filtered dense]
|
||||
Q --> B[路B unfiltered dense]
|
||||
A --> M[去重合并]
|
||||
B --> M
|
||||
M --> RRF[RRF / 统一排序]
|
||||
RRF --> OUT[topN]
|
||||
```
|
||||
|
||||
#### 为什么值得做?
|
||||
|
||||
因为两路解决的是不同失败模式:
|
||||
@@ -333,6 +401,23 @@ RRF(d) = Σ 1 / (k + rank_i(d))
|
||||
- 多路都靠前的候选,融合分自然更高
|
||||
- 只在一路偶然靠前的候选,不会单靠绝对分尺度“爆掉”
|
||||
|
||||
```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 特别适合下面这种现实约束:
|
||||
@@ -553,8 +638,29 @@ Query
|
||||
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:先修前提
|
||||
|
||||
否则后面多路都会被吞:
|
||||
@@ -0,0 +1,583 @@
|
||||
# RAG 离线评测:讨论、设计与落地
|
||||
|
||||
**日期**:2026-07-28
|
||||
**范围**:`eval/rag-retrieval` 离线 baseline、fixture 生成、与 hybrid/quality 主路径对齐
|
||||
**读者**:要维护或扩展知识库回归评测的工程同学
|
||||
**关联实现 / 变更**:
|
||||
|
||||
- 目录:`eval/rag-retrieval/`
|
||||
- 脚本:`scripts/eval_rag_retrieval.py`、`generate_rag_lookup_snapshots.ps1`、`prepare_rag_eval_seed.ps1`
|
||||
- OpenSpec / devflow:`rag-eval-hybrid-baseline`(已归档)
|
||||
- 前置:`docs/RAG-Hybrid质量分与后处理.md`、`docs/RAG-Agent如何读relevance_level.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要单独谈评测
|
||||
|
||||
hybrid、chunk 去重、qualityScore 统一之后,工程上仍缺一块:
|
||||
|
||||
> **改检索之后,用什么可重复的信号判断「变好了还是变坏了」?**
|
||||
|
||||
完整诊断 E2E(多工具 + 最终回答)太重、太噪。需要一层**只盯 `lookup_knowledge` 召回与管道行为**的回归。
|
||||
|
||||
本文汇总讨论中形成的:
|
||||
|
||||
1. 离线评测是什么、不是什么
|
||||
2. Golden / Fixture 怎么设计、比什么
|
||||
3. 项目现状是否符合定义
|
||||
4. 改造复杂度与 sm-flow 落地(含 apply 中发现的闸门问题)
|
||||
5. 指标、报告、纪律
|
||||
|
||||
---
|
||||
|
||||
## 2. 离线评测:概念边界
|
||||
|
||||
### 2.1 离线 vs 在线
|
||||
|
||||
| 说法 | 含义 |
|
||||
|------|------|
|
||||
| **在线** | 真跑检索:embedding、Milvus、完整 `lookup_knowledge` |
|
||||
| **离线** | **不再访问检索栈**;用事先冻住的结果快照,和标准答案比对 |
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph online["在线(贵、真、偶发)"]
|
||||
S[Seed 语料] --> G[真 lookup / 检索管道]
|
||||
G --> F[写入 Fixtures]
|
||||
end
|
||||
|
||||
subgraph offline["离线(便宜、稳、可 CI)"]
|
||||
C[Golden cases] --> E[比对脚本]
|
||||
F2[已提交的 Fixtures] --> E
|
||||
E --> R[报告 / baseline / diff]
|
||||
end
|
||||
|
||||
F -.->|提交入库| F2
|
||||
```
|
||||
|
||||
可以记成:
|
||||
|
||||
```text
|
||||
在线:考试现场答题(环境会变)
|
||||
离线:用标准答卷复印件批改(环境冻结)
|
||||
```
|
||||
|
||||
### 2.2 离线适合 / 不适合回答的问题
|
||||
|
||||
**适合:**
|
||||
|
||||
- 契约有没有破(结构、关键字段、行为路径)
|
||||
- 在「同一份检索结果」假设下,期望文档/关键词/attempt 是否仍满足
|
||||
- 相对上一版 baseline 的回归 diff
|
||||
|
||||
**不适合单独承担:**
|
||||
|
||||
- hybrid 是否比 dense 更好 → 需要**同一时期**在线双跑
|
||||
- 阈值 0.75 是否合适 → 需要在线统计 level 分布
|
||||
- 换 embedding 后召回如何 → 必须重刷 fixture 或 live
|
||||
- Agent 最终诊断对不对 → 诊断 E2E
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q1[契约 / 期望回归] --> OFF[离线 baseline]
|
||||
Q2[当前召回是否正确] --> ON[在线生成 fixture 或 live]
|
||||
Q3[dense vs hybrid 增益] --> CMP[同期双 mode 对照]
|
||||
Q4[诊断是否正确] --> E2E[诊断 harness]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 三块积木
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph golden_box["① Golden(薄)"]
|
||||
GQ[query]
|
||||
GE[期望:doc / keyword / attempt / …]
|
||||
end
|
||||
|
||||
subgraph fixture_box["② Fixture(冻)"]
|
||||
FM[meta: caseId, searchMode, kbScope, time]
|
||||
FL[lookupResult 结构化输出]
|
||||
end
|
||||
|
||||
subgraph judge_box["③ 裁判(规则)"]
|
||||
A[按 golden 字段断言]
|
||||
M[衍生 Hit / rank / 通过率]
|
||||
REP[json + md + 可选 diff]
|
||||
end
|
||||
|
||||
golden_box -->|caseId 对齐| judge_box
|
||||
fixture_box --> judge_box
|
||||
```
|
||||
|
||||
| 积木 | 是什么 | 不是什么 |
|
||||
|------|--------|----------|
|
||||
| **Golden** | 问什么 + **应该**怎样 | 不是整包线上成功 JSON 原样当期望 |
|
||||
| **Fixture** | 某次跑完**实际**怎样 | 不是每次离线评测都要重跑检索 |
|
||||
| **裁判** | 关键字段比对 + 指标 + 报告 | 不是两个大 JSON deep equal |
|
||||
|
||||
时间线:
|
||||
|
||||
```text
|
||||
① 设计 Golden
|
||||
② (改检索 / 换库 / 换 mode 时)在线跑 → 写/更新 Fixtures
|
||||
③ 日常:Fixtures × Golden → 报告(多数时候只做这一步)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 为什么不全量字段对比
|
||||
|
||||
讨论中的直觉「分数不固定」是对的,但原因不止这一条:
|
||||
|
||||
| 原因 | 说明 |
|
||||
|------|------|
|
||||
| 分数不稳定 | L2 / RRF / embedding 会漂 |
|
||||
| 实现细节会变 | trace 结构、reason 文案、时间戳、tool_call_id |
|
||||
| 截断与预算会变 | excerpt 长度、packedText |
|
||||
| 目标是「对不对」 | 不是字节级一致 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
F[Fixture JSON] --> K[只抽取关键字段]
|
||||
G[Golden 期望] --> K
|
||||
K --> P{断言}
|
||||
P -->|通过| OK[Pass + 指标]
|
||||
P -->|失败| FAIL[Fail + 原因列表]
|
||||
|
||||
F -.->|不做| FULL[全量 deep equal]
|
||||
```
|
||||
|
||||
**全量对比**偶尔可用于「紧挨着两次生成器输出的工程 diff」,那不是 golden 质量标准。
|
||||
|
||||
---
|
||||
|
||||
## 5. Golden / Fixture 字段设计
|
||||
|
||||
### 5.1 Golden:一行长什么样(分层)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
ID[身份: caseId, scenario/tags, notes]
|
||||
IN[输入: query]
|
||||
P0[P0 命中: expectedDocIds/Sources, expectedKeywords]
|
||||
P1[P1 行为/展示: attempt, fallback, status, breadcrumb]
|
||||
P2[P2 细粒度: evidenceKey, minCount, firstRankMax]
|
||||
P3[P3 观察: relevanceLevel — 慎作硬门禁]
|
||||
NEG[负例: mustNotDocIds/Sources]
|
||||
|
||||
ID --> IN --> P0 --> P1
|
||||
P1 --> P2
|
||||
P1 --> NEG
|
||||
P1 --> P3
|
||||
```
|
||||
|
||||
| 优先级 | 比什么 | 作用 |
|
||||
|--------|--------|------|
|
||||
| P0 | docId / source、keywords | 召回对不对、段是否有用 |
|
||||
| P1 | selectedAttempt、fallback、evidenceStatus | 路径有没有坏 |
|
||||
| P1 | mustNot* | 硬负例 / decoy |
|
||||
| P2 | chunk / evidenceKey、条数、首条相关 rank | 身份与排序 |
|
||||
| P3 | relevance_level | 观察用;hybrid 下易松 |
|
||||
|
||||
**原则:** 期望对齐「用户/Agent 可感知的对错」,少锁实现细节。
|
||||
|
||||
### 5.2 Fixture:最少保留什么
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph fix["fixture"]
|
||||
META["meta<br/>caseId, query, retrievedAt<br/>searchMode, kbScope?"]
|
||||
RES["result / lookupResult<br/>有序 evidence[]<br/>attempt / fallback / status<br/>contextPack? / traces?"]
|
||||
DBG["debug 可选<br/>rawScore, denseDistance…"]
|
||||
end
|
||||
|
||||
META --> RES
|
||||
RES --> DBG
|
||||
```
|
||||
|
||||
每条 evidence 最少:
|
||||
|
||||
```text
|
||||
docId 或可对齐的 source
|
||||
excerpt / content(关键词断言需要)
|
||||
顺序 = rank(数组下标即可)
|
||||
evidenceKey / chunkIndex(多 chunk case 需要)
|
||||
title / breadcrumb(按需)
|
||||
```
|
||||
|
||||
**故意不锁:** score 全文、完整 trace、tool_call_id、packedText 全文(除非单独立项)。
|
||||
|
||||
### 5.3 Golden → Fixture 取值对照
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph g["Golden"]
|
||||
g1[expectedDocIds]
|
||||
g2[expectedKeywords]
|
||||
g3[expectedSelectedAttempt]
|
||||
g4[expectedFallbackReason]
|
||||
g5[mustNotDocIds]
|
||||
end
|
||||
|
||||
subgraph f["Fixture"]
|
||||
f1[evidence[].docId/source]
|
||||
f2[evidence[].excerpt 拼接]
|
||||
f3[retrievalTrace.selectedAttempt]
|
||||
f4[retrievalTrace.fallbackReason]
|
||||
f5[evidence 全表扫描]
|
||||
end
|
||||
|
||||
g1 --> f1
|
||||
g2 --> f2
|
||||
g3 --> f3
|
||||
g4 --> f4
|
||||
g5 --> f5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 指标与报告
|
||||
|
||||
### 6.1 两层指标
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph gate["门禁主信号"]
|
||||
PASS[逐 case Pass/Fail]
|
||||
RATE[通过率 / 按 tag 通过率]
|
||||
end
|
||||
|
||||
subgraph quality["质量刻度(报告展示)"]
|
||||
HIT[Hit@n / hitLevel strong·medium·weak·miss]
|
||||
RANK[first relevant rank / MRR]
|
||||
KW[keyword coverage]
|
||||
FB[fallback rate]
|
||||
LV[level 直方图 — 观察]
|
||||
end
|
||||
|
||||
PASS --> RATE
|
||||
HIT --> RANK
|
||||
```
|
||||
|
||||
| 指标 | 含义 |
|
||||
|------|------|
|
||||
| **Pass/Fail** | golden 声明的 expected* 是否全部满足 |
|
||||
| **hitLevel** | strong / medium / weak / miss(项目已有) |
|
||||
| **Recall@K** | strong+medium 算命中 |
|
||||
| **firstExpectedRank** | 第一条期望文档的排名 |
|
||||
| **fallback / attempt** | 行为路径 |
|
||||
| **level 分布** | 宜观察,hybrid 下慎作硬门禁 |
|
||||
|
||||
### 6.2 报告长什么样
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
EVAL[离线评测] --> J[baseline.json<br/>机器可读]
|
||||
EVAL --> M[baseline.md<br/>人读表格]
|
||||
EVAL --> D[baseline-diff.*<br/>相对上一版]
|
||||
|
||||
J --> CI[CI / 脚本解析]
|
||||
M --> HUM[人看失败原因]
|
||||
D --> REV[改代码还是改期望]
|
||||
```
|
||||
|
||||
工程上的「得出结果」=:
|
||||
|
||||
1. **门禁**:must-pass 是否全绿
|
||||
2. **诊断**:谁红、红在哪类断言
|
||||
3. **趋势**:相对旧 baseline 变好还是变差
|
||||
|
||||
---
|
||||
|
||||
## 7. 项目现状审计(改造前)
|
||||
|
||||
讨论结论:**模型符合定义,内容偏旧(约 70%)**。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph ok["已符合"]
|
||||
A1[golden × fixture × key-field]
|
||||
A2[seed + kb_scope=rag-eval]
|
||||
A3[Hit 分层 + recall + baseline diff]
|
||||
A4[离线不连库]
|
||||
end
|
||||
|
||||
subgraph gap["缺口"]
|
||||
B1[生成器仍传 vector-store.mode=spring]
|
||||
B2[fixture 无 searchMode/kbScope]
|
||||
B3[无 dense/hybrid 双目录对照]
|
||||
B4[无 mustNot / chunk 硬期望]
|
||||
B5[快照停在 boost 改序时代]
|
||||
end
|
||||
|
||||
ok --> gap
|
||||
```
|
||||
|
||||
| 维度 | 符合度 |
|
||||
|------|--------|
|
||||
| 三件套架构 | 高 |
|
||||
| 关键字段比对 | 高 |
|
||||
| 报告 / diff | 高 |
|
||||
| 与 hybrid 主路径同步 | 低(改造前) |
|
||||
| chunk / 负例 / mode 矩阵 | 弱或无 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 改造策略与复杂度
|
||||
|
||||
### 8.1 两刀切分
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
K1["第一刀(已落地)<br/>search.mode 接线<br/>fixture meta<br/>README<br/>重刷 hybrid baseline"]
|
||||
K2["第二刀(未做)<br/>fixtures/hybrid vs dense<br/>对照表<br/>golden tags/mustNot"]
|
||||
|
||||
K1 --> DONE[可门禁当前主路径]
|
||||
K2 --> CMP[可回答 hybrid 增益]
|
||||
```
|
||||
|
||||
**复杂度判断:中低。** 不必重写框架;成本在联调环境与 baseline 纪律,不在算法。
|
||||
|
||||
| 工作 | 复杂度 |
|
||||
|------|--------|
|
||||
| 改生成参数 / meta / README | 低 |
|
||||
| seed + 重刷 fixture | 中低(看环境) |
|
||||
| 双目录对照 | 中低(第二刀) |
|
||||
| 换框架 / LLM judge | 高(不建议现在) |
|
||||
|
||||
### 8.2 sm-flow 落地范围(已确认)
|
||||
|
||||
- **仅第一刀**
|
||||
- **接线必交**;fixture 刷新尽力(本次环境可用,已刷绿)
|
||||
|
||||
Change:`rag-eval-hybrid-baseline`(已归档)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 落地后的主链路(当前)
|
||||
|
||||
### 9.1 日常离线
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
GC[golden-cases.json] --> PY[eval_rag_retrieval.py]
|
||||
FX[fixtures/*.json] --> PY
|
||||
PY --> BR[reports/baseline.*]
|
||||
```
|
||||
|
||||
```bash
|
||||
python scripts/eval_rag_retrieval.py
|
||||
```
|
||||
|
||||
### 9.2 改检索后的完整环
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Eng as 工程师
|
||||
participant Seed as prepare_rag_eval_seed
|
||||
participant Gen as generate_rag_lookup_snapshots
|
||||
participant Tool as LookupKnowledgeTool
|
||||
participant Off as eval_rag_retrieval.py
|
||||
participant Git as 仓库 baseline
|
||||
|
||||
Eng->>Seed: 导入 seed-docs (kb_scope=rag-eval)
|
||||
Seed-->>Eng: MySQL/L0/Milvus 就绪
|
||||
Eng->>Gen: -SearchMode hybrid
|
||||
Gen->>Tool: 每条 golden.query
|
||||
Tool-->>Gen: LookupResult
|
||||
Gen->>Gen: 写 fixture + searchMode/kbScope
|
||||
Eng->>Off: fixtures × golden
|
||||
Off-->>Eng: pass/fail + 指标
|
||||
Eng->>Git: 意图变更则更新 baseline(带 diff 原因)
|
||||
```
|
||||
|
||||
### 9.3 生成器配置(改造后)
|
||||
|
||||
| 参数 | 默认 | 含义 |
|
||||
|------|------|------|
|
||||
| `SearchMode` | `hybrid` | `retrieval.search.mode` |
|
||||
| `KbScope` | `rag-eval` | 评测语料隔离 |
|
||||
| (已删除) | — | `vector-store.mode=spring\|sdk` |
|
||||
|
||||
```powershell
|
||||
.\scripts\prepare_rag_eval_seed.ps1
|
||||
.\scripts\generate_rag_lookup_snapshots.ps1 # hybrid
|
||||
.\scripts\generate_rag_lookup_snapshots.ps1 -SearchMode dense -Fixtures ... -SkipEval # 对照用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Apply 中发现的关键问题:filter fallback 与 quality 闸门
|
||||
|
||||
### 10.1 现象
|
||||
|
||||
重刷 hybrid fixtures 后,`chat-l0-filter-fallback` 变红:
|
||||
|
||||
- 只命中 decoy(`overfilter-decoy`)
|
||||
- `selectedAttempt=FILTERED_VECTOR`,**没有** unfiltered retry
|
||||
- 根因:hybrid **纯 rank→quality** 时 rank1 恒为 ~1.0 → `isLowQuality` 永不成立
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph before["纯 rank quality(有问题)"]
|
||||
H1[hybrid RRF 序] --> R1[rank1 quality=1.0]
|
||||
R1 --> N1[isLowQuality=false]
|
||||
N1 --> X1[不 retry · decoy 留下]
|
||||
end
|
||||
|
||||
subgraph after["denseDistance 闸门(已修)"]
|
||||
H2[hybrid RRF 序 · 排序不变] --> D2[并行 dense 填 denseDistance]
|
||||
D2 --> Q2[quality = L2 归一化]
|
||||
Q2 --> L2{top quality < 0.5?}
|
||||
L2 -->|是| RET[UNFILTERED_VECTOR_RETRY]
|
||||
L2 -->|否| KEEP[保留 filtered 结果]
|
||||
end
|
||||
```
|
||||
|
||||
### 10.2 设计取舍(必须记清)
|
||||
|
||||
| 信号 | 用途 |
|
||||
|------|------|
|
||||
| **RRF / originalRank** | **排序权威**(谁在前) |
|
||||
| **denseDistance → quality** | **绝对质量闸门**(要不要 retry、level 档) |
|
||||
| **不**再:用 L2 覆盖 hybrid 主分 / label | 避免回到「伪装成 L2」 |
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph sort["排序"]
|
||||
RRF[RRF 返回序]
|
||||
end
|
||||
|
||||
subgraph gate["质量闸门"]
|
||||
L2[dense L2 若有]
|
||||
RK[rank 回退若无 dense]
|
||||
L2 --> QS[qualityScore]
|
||||
RK --> QS
|
||||
end
|
||||
|
||||
RRF --> LIST[evidence 列表顺序]
|
||||
QS --> LV[relevance_level]
|
||||
QS --> FB[isLowQuality → filter fallback]
|
||||
```
|
||||
|
||||
这与 quality 统一文的精神一致:**排序与闸门分信号**;只是 hybrid 闸门不能**只**靠序数分。
|
||||
|
||||
验证(归档时):
|
||||
|
||||
- offline **7/7 pass**
|
||||
- fallback case:`UNFILTERED_VECTOR_RETRY` + `filtered_vector_low_quality`
|
||||
|
||||
---
|
||||
|
||||
## 11. Baseline 纪律
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
RED[离线变红] --> Q{实现退步还是预期变了?}
|
||||
Q -->|退步| CODE[改代码]
|
||||
Q -->|预期变了| GOLD[改 golden / 重刷 fixture]
|
||||
GOLD --> NOTE[写清原因 · 更新 baseline]
|
||||
Q -->|禁止| BLIND[不看 diff 整锅覆盖]
|
||||
```
|
||||
|
||||
README 原话仍然成立:fixture 对不上,要么修链路,要么改期望——**二选一要显式**。
|
||||
|
||||
---
|
||||
|
||||
## 12. 与完整评测体系的位置
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph L1["L1 离线契约 — 已有且已对齐 hybrid"]
|
||||
OFF[fixtures × golden]
|
||||
end
|
||||
|
||||
subgraph L2["L2 在线召回 — 生成器已接线"]
|
||||
LIVE[seed → snapshot hybrid]
|
||||
end
|
||||
|
||||
subgraph L3["L3 对照与标定 — 部分未做"]
|
||||
DD[dense vs hybrid 双目录表]
|
||||
CAL[level/阈值直方图标定]
|
||||
end
|
||||
|
||||
subgraph L4["L4 诊断 E2E — 另一套"]
|
||||
DIAG[多工具 · 最终回答]
|
||||
end
|
||||
|
||||
L1 --> L2
|
||||
L2 --> L3
|
||||
L2 -.-> L4
|
||||
```
|
||||
|
||||
| 层 | 状态 |
|
||||
|----|------|
|
||||
| L1 离线 | **已落地**,hybrid fixtures + baseline 绿 |
|
||||
| L2 在线生成 | **已接线**,本机已成功重刷 |
|
||||
| L3 双 mode 对照目录 | **未做**(第二刀) |
|
||||
| L4 诊断 E2E | 独立 harness,非本文 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 实践清单
|
||||
|
||||
1. **日常**:只跑离线 `eval_rag_retrieval.py`。
|
||||
2. **改检索 / 索引 / mode / 闸门**:seed → hybrid 生成 → 离线 → 看 diff 再更新 baseline。
|
||||
3. **对照 dense**:`-SearchMode dense` 指到另一 fixtures 目录(第二刀可产品化报表)。
|
||||
4. **Golden** 锁业务真值;**score / 完整 trace** 默认不锁。
|
||||
5. **relevance_level** 先观察,慎作硬门禁。
|
||||
6. **排序听 RRF**;**retry/level 听绝对 quality(dense L2)**。
|
||||
7. 评测语料固定 `kb_scope=rag-eval`,勿绑生产杂库。
|
||||
|
||||
---
|
||||
|
||||
## 14. 结语
|
||||
|
||||
离线评测不是「再造一个复杂平台」,而是:
|
||||
|
||||
```text
|
||||
固定问题(Golden)
|
||||
× 冻结答卷(Fixture)
|
||||
× 关键字段裁判
|
||||
→ 可 diff 的报告
|
||||
```
|
||||
|
||||
项目原本骨架正确;本轮补上了 **hybrid 时代的生成接线、fixture meta、baseline 重刷**,并在真实跑通时修正了 **「序数 quality 杀死 filter fallback」** 的闸门设计。
|
||||
|
||||
下一有价值的增量是 **dense/hybrid 同期对照表(第二刀)** 与 **level 分布标定**,而不是换评测框架。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:目录与命令速查
|
||||
|
||||
| 路径 | 作用 |
|
||||
|------|------|
|
||||
| `eval/rag-retrieval/cases/golden-cases.json` | Golden |
|
||||
| `eval/rag-retrieval/fixtures/*.json` | Fixtures(含 searchMode) |
|
||||
| `eval/rag-retrieval/seed-docs/` | 评测语料 |
|
||||
| `eval/rag-retrieval/reports/baseline.*` | 离线基线报告 |
|
||||
| `scripts/eval_rag_retrieval.py` | 离线裁判 |
|
||||
| `scripts/generate_rag_lookup_snapshots.ps1` | 在线生成 fixture |
|
||||
| `scripts/prepare_rag_eval_seed.ps1` | 导入 seed |
|
||||
|
||||
```powershell
|
||||
# 离线
|
||||
python scripts\eval_rag_retrieval.py
|
||||
|
||||
# 完整刷新(需 embedding + Milvus 等)
|
||||
.\scripts\prepare_rag_eval_seed.ps1
|
||||
.\scripts\generate_rag_lookup_snapshots.ps1 -SearchMode hybrid
|
||||
```
|
||||
|
||||
## 附录 B:相关文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| `eval/rag-retrieval/README.md` | 操作说明(以仓库为准) |
|
||||
| `docs/RAG-Hybrid质量分与后处理.md` | quality / 后处理 |
|
||||
| `docs/RAG-Agent如何读relevance_level.md` | Agent 如何读 level |
|
||||
| `docs/RAG排序-多路召回与RRF.md` | 多路与 RRF |
|
||||
| `devflow/projects/2026-07-28-rag-eval-hybrid-baseline/` | 本 change 档案 |
|
||||
| `openspec/specs/rag-eval-offline-baseline/spec.md` | 主规格 |
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user