Files
SuperBizAgent-java/docs/RAG离线评测-基线设计.md
T
zhuyongxin 7ae9707a3b 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.
2026-07-28 19:43:13 +08:00

584 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` | 主规格 |