Relocate RAG and diagnosis decision/E2E writeups from docs/ root into mvp/engineering so architecture, issues, and engineering narrative stay together. Update indexes and cross-links; leave docs/learning as legacy.
17 KiB
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(已归档) - 前置:
RAG-Hybrid质量分与后处理.md、RAG-Agent如何读relevance_level.md
1. 为什么要单独谈评测
hybrid、chunk 去重、qualityScore 统一之后,工程上仍缺一块:
改检索之后,用什么可重复的信号判断「变好了还是变坏了」?
完整诊断 E2E(多工具 + 最终回答)太重、太噪。需要一层只盯 lookup_knowledge 召回与管道行为的回归。
本文汇总讨论中形成的:
- 离线评测是什么、不是什么
- Golden / Fixture 怎么设计、比什么
- 项目现状是否符合定义
- 改造复杂度与 sm-flow 落地(含 apply 中发现的闸门问题)
- 指标、报告、纪律
2. 离线评测:概念边界
2.1 离线 vs 在线
| 说法 | 含义 |
|---|---|
| 在线 | 真跑检索:embedding、Milvus、完整 lookup_knowledge |
| 离线 | 不再访问检索栈;用事先冻住的结果快照,和标准答案比对 |
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
可以记成:
在线:考试现场答题(环境会变)
离线:用标准答卷复印件批改(环境冻结)
2.2 离线适合 / 不适合回答的问题
适合:
- 契约有没有破(结构、关键字段、行为路径)
- 在「同一份检索结果」假设下,期望文档/关键词/attempt 是否仍满足
- 相对上一版 baseline 的回归 diff
不适合单独承担:
- hybrid 是否比 dense 更好 → 需要同一时期在线双跑
- 阈值 0.75 是否合适 → 需要在线统计 level 分布
- 换 embedding 后召回如何 → 必须重刷 fixture 或 live
- Agent 最终诊断对不对 → 诊断 E2E
flowchart LR
Q1[契约 / 期望回归] --> OFF[离线 baseline]
Q2[当前召回是否正确] --> ON[在线生成 fixture 或 live]
Q3[dense vs hybrid 增益] --> CMP[同期双 mode 对照]
Q4[诊断是否正确] --> E2E[诊断 harness]
3. 三块积木
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 |
时间线:
① 设计 Golden
② (改检索 / 换库 / 换 mode 时)在线跑 → 写/更新 Fixtures
③ 日常:Fixtures × Golden → 报告(多数时候只做这一步)
4. 为什么不全量字段对比
讨论中的直觉「分数不固定」是对的,但原因不止这一条:
| 原因 | 说明 |
|---|---|
| 分数不稳定 | L2 / RRF / embedding 会漂 |
| 实现细节会变 | trace 结构、reason 文案、时间戳、tool_call_id |
| 截断与预算会变 | excerpt 长度、packedText |
| 目标是「对不对」 | 不是字节级一致 |
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:一行长什么样(分层)
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:最少保留什么
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 最少:
docId 或可对齐的 source
excerpt / content(关键词断言需要)
顺序 = rank(数组下标即可)
evidenceKey / chunkIndex(多 chunk case 需要)
title / breadcrumb(按需)
故意不锁: score 全文、完整 trace、tool_call_id、packedText 全文(除非单独立项)。
5.3 Golden → Fixture 取值对照
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 两层指标
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 报告长什么样
flowchart LR
EVAL[离线评测] --> J[baseline.json<br/>机器可读]
EVAL --> M[baseline.md<br/>人读表格]
EVAL --> D[baseline-diff.*<br/>相对上一版]
J --> CI[CI / 脚本解析]
M --> HUM[人看失败原因]
D --> REV[改代码还是改期望]
工程上的「得出结果」=:
- 门禁:must-pass 是否全绿
- 诊断:谁红、红在哪类断言
- 趋势:相对旧 baseline 变好还是变差
7. 项目现状审计(改造前)
讨论结论:模型符合定义,内容偏旧(约 70%)。
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 两刀切分
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 日常离线
flowchart LR
GC[golden-cases.json] --> PY[eval_rag_retrieval.py]
FX[fixtures/*.json] --> PY
PY --> BR[reports/baseline.*]
python scripts/eval_rag_retrieval.py
9.2 改检索后的完整环
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 |
.\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永不成立
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」 |
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 纪律
flowchart TB
RED[离线变红] --> Q{实现退步还是预期变了?}
Q -->|退步| CODE[改代码]
Q -->|预期变了| GOLD[改 golden / 重刷 fixture]
GOLD --> NOTE[写清原因 · 更新 baseline]
Q -->|禁止| BLIND[不看 diff 整锅覆盖]
README 原话仍然成立:fixture 对不上,要么修链路,要么改期望——二选一要显式。
12. 与完整评测体系的位置
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. 实践清单
- 日常:只跑离线
eval_rag_retrieval.py。 - 改检索 / 索引 / mode / 闸门:seed → hybrid 生成 → 离线 → 看 diff 再更新 baseline。
- 对照 dense:
-SearchMode dense指到另一 fixtures 目录(第二刀可产品化报表)。 - Golden 锁业务真值;score / 完整 trace 默认不锁。
- relevance_level 先观察,慎作硬门禁。
- 排序听 RRF;retry/level 听绝对 quality(dense L2)。
- 评测语料固定
kb_scope=rag-eval,勿绑生产杂库。
14. 结语
离线评测不是「再造一个复杂平台」,而是:
固定问题(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 |
# 离线
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 |
操作说明(以仓库为准) |
RAG-Hybrid质量分与后处理.md |
quality / 后处理 |
RAG-Agent如何读relevance_level.md |
Agent 如何读 level |
RAG排序-多路召回与RRF.md |
多路与 RRF |
devflow/projects/2026-07-28-rag-eval-hybrid-baseline/ |
本 change 档案 |
openspec/specs/rag-eval-offline-baseline/spec.md |
主规格 |