Files
SuperBizAgent-java/mvp/engineering/rag/RAG离线评测-基线设计.md
zhuyongxin 473bb5d004 docs(mvp): update RAG docs for py-rag extraction
- rewrite RAG architecture doc for py-rag service contract mapping, ingest and rebuild ops
- refresh observability/trace doc for post-L0 single-attempt semantics
- add py-rag chain exploration note; mark superseded Milvus/L0 notes with status banners
- close ISS-017 (superseded by L0 sinking); mark knowledge_domain table orphaned
- refresh architecture/mvp/engineering indexes
2026-09-30 17:03:50 +08:00

17 KiB
Raw Permalink Blame History

RAG 离线评测:讨论、设计与落地

需重新校准(2026-09-29):RAG 模块抽离后,L0 hint / categoryFilter 已下沉 py-rag、 质量分改为 RERANK 直传、attempt 只剩 UNFILTERED_VECTOR;本文设计的 fixture/baseline 基于旧 L0/scoreLabel 语义构建,回归评测需按新语义重新生成 fixture 并校准闸门。 当前架构见 ../../architecture/RAG知识检索架构.md。

日期: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
离线 不再访问检索栈;用事先冻住的结果快照,和标准答案比对
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[改代码还是改期望]

工程上的「得出结果」=:

  1. 门禁:must-pass 是否全绿
  2. 诊断:谁红、红在哪类断言
  3. 趋势:相对旧 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. 实践清单

  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. 结语

离线评测不是「再造一个复杂平台」,而是:

固定问题(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 主规格