# 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` 召回与管道行为**的回归。 本文汇总讨论中形成的: 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
caseId, query, retrievedAt
searchMode, kbScope?"] RES["result / lookupResult
有序 evidence[]
attempt / fallback / status
contextPack? / traces?"] DBG["debug 可选
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
机器可读] EVAL --> M[baseline.md
人读表格] EVAL --> D[baseline-diff.*
相对上一版] 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["第一刀(已落地)
search.mode 接线
fixture meta
README
重刷 hybrid baseline"] K2["第二刀(未做)
fixtures/hybrid vs dense
对照表
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` | 操作说明(以仓库为准) | | `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` | 主规格 |