返回 AICAP-180
B17 · Day 169outcome 指标仪表盘 + A/B

A/B 落仪表盘

Day 167 设计了 A/B、Day 168 算出了 Δ + CI 并判了「不显著」。

阶段: B17 · outcome 指标仪表盘 + A/B(Day 161-170) 标签: #eval-as-ci #experiment-tracking #observability #honest-reporting

今日导引(由浅入深)

Day 167 设计了 A/B、Day 168 算出了 Δ + CI 并判了「不显著」。 但到此为止,这个结论还只是一次脚本运行的控制台输出——跑完就散了。 今天把它接进仪表盘 snapshot 的 experiment 字段,让实验成为可追溯的 outcome 记录,而非一次性脚本产物。 在 B1→B18 能力曲线上,这一步把「单次 A/B」升级为「实验可观测、可回放、可对比历史」——这正是 2026 eval-as-CI 趋势的落点:实验结果像 CI 状态一样被持久化、被追踪。 今天的最小可判定产出是:scripts/ab-compare.ts 的输出接进 dashboard.ts snapshot 的 experiment block,含赢家 / Δ / CI / N / significant 标志,end-to-end 跑通。

1. 机理精读

A/B 结论应该是「一等公民 outcome」,不是临时控制台打印。 Day 167-168 跑出来的 Δ +10.3pp / CI[0,20.7] / N=29 / not-sig 如果只停在 stdout 或一个临时 agent-evals/ab.json,下次有人问「上次 A/B 到底比的什么、什么时候、结论是啥」就答不上来。 把它写进 dashboard snapshot 的 experiment 字段,等于给实验上了版本——它和 FPR / SAR 质量 / cost / p95 这些 outcome 指标平起平坐,被同一套趋势/追溯机制管理。

experiment block 的字段集要完整到「能独立复盘」:赢家 + Δ + CI + N + significant 标志。 缺一不可:

  • winner:哪个模型胜(注意要标「directional/方向性」还是「显著」——本仓是 directional)。
  • deltaMean:效应量,多大。
  • ci95:估计有多准、是否跨 0。
  • N:样本量,决定结论的 power。
  • significant:布尔显著标志,由「CI 是否跨 0」推出。

只有这五样齐了,读者不用回看原始脚本也能判断这个结论该信几分。

诚实记录「不显著」与记录「赢家」同等重要——这是本日的伦理内核。 很多团队的实验追踪只记「赢家」,把不显著的实验悄悄丢掉(发表偏倚的工程版)。 但在 outcome 仪表盘里,significant=false 是一条有信息量的记录:它告诉未来的自己「这个方向看着对,但 N=29 撑不住,别急着切模型,先扩任务集」。 把「不显著」如实写进 snapshot,和写「赢家」一样是产出,绝不能为了好看而隐藏。 本仓真实 experiment block 就是 winner=V4-Pro (directional)、significant=false——方向性赢、统计不显著,两条都记。

这符合 2026 的 eval-as-CI 趋势。 评测不再是「上线前跑一次」的离散动作,而是像单元测试/CI 一样持续运行、结果持久化、回归可比。 把 A/B 落进可观测 snapshot,就是让「模型选型实验」进入这套 CI 化的 outcome 流水线。

significant 标志必须是「派生字段」而非「手填字段」。 它应由 ci95 是否跨 0 这一条规则自动推出(significant = ci95[0] > 0 || ci95[1] < 0),而不是让人手敲一个 true/false。 理由有二:一是避免人为粉饰(手填容易把不显著写成显著); 二是保证 snapshot 自洽——CI 和 significant 永远不会矛盾。 ab-compare.ts 的 verdict 逻辑已经是这条规则,接线时复用它即可,不要另起一套判据。

winner 字段要带「资格修饰」:directional(方向性)vs significant(显著)。 本仓 winner=V4-Pro,但它是「方向性赢」(CI 跨 0),不是「显著赢」。 snapshot 里若只写 winner: 'V4-Pro' 而不带修饰,读者会误以为是显著结论。 正确写法是 winner: 'deepseek-v4-pro (directional)' 或单独一个 winnerQualified: 'directional' 字段,让「赢得有多硬」一眼可见。

experiment 字段也要带「实验上下文指纹」才算可追溯。 光有 winner/Δ/CI/N/significant 还不够,回看时还需知道「比的是哪两个 model、跑的哪个任务子集、什么时候跑的」。 所以 experiment block 实际应再带 modelA/modelB(abCompare 已输出)、任务集标识、时间戳。 否则三个月后看到 snapshot 里「Δ+10.3pp not-sig」,却说不清是哪两个模型在哪批题上的结论——这条记录就退化成了一个孤立数字,失去 eval-as-CI 的「可回归」价值。 AbResult 已含 modelA/modelB/nAligned,接线时一并带上即可,零额外成本。

边界澄清:今天不重新算统计——Δ/CI/N/significant 都是 Day 168 已得的真值,今天只做「接线 + 持久化」。 统计结论本身是已测真值; 把它写进 snapshot 的 end-to-end 接线与真值填充才是今天 gated 的部分。

为什么「持久化」本身是一项工程能力而非顺手为之。 一次性脚本输出(stdout)的半衰期是「关掉终端」; 落进 snapshot 的实验记录半衰期是「项目生命周期」。 前者只能回答「现在哪个模型好」,后者能回答「过去三个月模型选型怎么演化、每次换 model 的依据是什么」——后者才是架构师对系统负责的证据链。 把 A/B 从「跑一次看一眼」升级成「进可观测流水线」,正是 B17 区别于「随手 benchmark」的地方。

2. 推导 / 手算 / 代码走读

今天 seed 说让 scripts/ab-compare.ts 的输出接进 dashboard.ts snapshot 的 experiment block。 走读两端的真实现状,明确「已 built」与「待接线」的边界:

  • scripts/ab-compare.ts(已 built)当前的真实落盘行为是 writeFileSync(resolve(process.cwd(), 'agent-evals/ab.json'), JSON.stringify(r, null, 2))——它把 abCompare 的完整 AbResult 写到独立文件 agent-evals/ab.json并未写进 dashboard snapshot。它末尾还打 QUOTABLE 句。脚本里的 verdict 用 r.ci95[0] > 0 ? ... : r.ci95[1] < 0 ? ... : 'not significant (CI crosses 0)' 推显著。
  • AbResult(abCompare.ts)已含今天需要的全部字段:modelA/modelB(→ winner 来源)、deltaMean(→ Δ)、ci95(→ CI)、nAligned(→ N)、wins/losses/tiessignificant 标志可由 ci95 跨 0 与否在接线时推出(脚本 verdict 已是同一逻辑)。
  • dashboard.ts(已 built)的 DashboardSnapshot 当前真实字段是 { runs, totalTasks, nsm, leaves, note? }——当前没有 experiment 字段buildDashboard 是纯函数,只从 EvalReportLike[] 聚合 NSM + leaves。
  • dashboard.ts 头注释明确「the fs reader lives in scripts/build-dashboard.ts so this stays bundler-safe. No API key.」——这条约束决定了 experiment 字段必须作为入参喂进来,不能让 dashboard.ts 自己读 ab.json(见今日实战的单向依赖纪律)。
  • 诚实校准:把 A/B 结论接进 snapshot 需要两处改动尚未落地——(1) 给 DashboardSnapshot/buildDashboard 增一个 experiment? block(含 winner/deltaMean/ci95/N/significant);(2) 让 ab-compare.ts(或 build-dashboard.ts)把 ab.json 读进来填到 snapshot。这两步是今天「待跑(需 key 重跑出完整 snapshot)」里的接线部分,不得声称已完成。统计结论本身(Δ/CI/N/significant)是 Day 168 的已测真值。

把真值代入 experiment block 的形状(接线后应长这样):

experiment: {
  winner: 'deepseek-v4-pro (directional)',
  deltaMean: 0.103,        // +10.3pp
  ci95: [0.0, 0.207],      // [0.0, 20.7]pp
  n: 29,
  significant: false       // CI 下界触 0
}

其中 significantci95[0] > 0(false)推出,与 ab-compare.ts 的 verdict 逻辑一致。

3. 今日实战

  1. src/agent/eval/dashboard.tsDashboardSnapshotexperiment?: { winner; deltaMean; ci95; n; significant } 字段,并让 buildDashboard(或新增重载/参数)能接收一个 AbResult 并推出 significant = ci95[0] > 0 || ci95[1] < 0
  2. scripts/ab-compare.ts(或 scripts/build-dashboard.ts)读 agent-evals/ab.json,把它接进 dashboard snapshot 的 experiment block。
  3. end-to-end 跑通:pnpm eval:ab(先有两份 run-*.json,需 key)→ 写 ab.json → dashboard 聚合 → 出含 experiment 的完整 snapshot。
  4. 给 experiment 接线加单测:用已知 AbResult 断言 snapshot 里 winner/deltaMean/ci95/n/significant 字段齐全且 significant 推导正确(CI 跨 0 → false)。

接线时的「单向依赖」纪律dashboard.ts 注释明确它要保持 bundler-safe(无 fs),所以 experiment 字段应作为入参喂给 buildDashboard,而不是让 dashboard.ts 自己去 readFileSync('agent-evals/ab.json')。 读文件这步留在 scripts/(如 build-dashboard.ts / ab-compare.ts)里,纯函数层只接收已解析的 AbResult。 这保持了 Day 165 立下的分层:数据聚合(纯、可测、前端可 import)与 IO(脚本侧)分离。 违反它(在 dashboard.ts 里读 fs)会让前端 import 这个模块时炸在 Node-only API 上——这是接线最容易踩的架构坑。

4. 今日实测 / 产出

  • 真实 experiment blockwinner=V4-Pro (directional)、Δ=+10.3pp、CI**[0.0, 20.7]**、N=29significant=false
  • abComparedashboard已 builtend-to-end 接线 + 真值填充 = 待跑(需 key 重跑出完整 snapshot)
  • 统计结论本身为已测真值(来自 Day 168)。

5. 常见误区 / 陷阱

  • 只记赢家、丢掉不显著的实验:发表偏倚的工程版。significant=false 是有信息量的记录,必须如实写。
  • 把 ab.json 当终点:脚本现在只写独立 ab.json,没进 snapshot——若不接线,实验就不可追溯,违背 eval-as-CI 目标。
  • 谎报接线已完成DashboardSnapshot 当前无 experiment 字段,接线是「待跑」,不能写成已 built。
  • winner 不标方向性/显著:本仓 winner 是 directional(CI 跨 0),若标成「显著赢」就是误报。
  • significant 手填而非派生:手敲 true/false 易粉饰且可能与 CI 矛盾;必须由 ci95[0] > 0 || ci95[1] < 0 自动推出。
  • dashboard.ts 里读 fs 接线:会破坏它的 bundler-safe 约束,前端 import 即炸;读文件留脚本侧,纯函数层只收 AbResult
  • experiment 不带上下文指纹:缺 modelA/modelB/任务集/时间戳,三个月后这条记录退化成孤立数字,失去可回归价值。

收尾再强调一次本日的伦理内核:把「不显著」如实写进可观测 snapshot,和写「赢家」一样是产出。隐藏不显著的实验是发表偏倚的工程版,违背这套笔记的诚信底线。本仓 experiment block 就老老实实写 significant=falsewinner(directional)

6. 学习资源(每条带 YYYY-MM)

  • 本仓 src/agent/eval/abCompare.ts 输出 schema + scripts/ab-compare.ts + src/agent/eval/dashboard.ts(AICAP-180 B12/B17)——experiment 接线两端现状(2026-06)。
  • Anthropic, "Demystifying evals"(2026-01)——评测结果持久化/追溯与显著性记录的方法论。
  • OpenTelemetry GenAI Semantic Conventions(2026-01)——snapshot 指标/字段命名约定,experiment 字段命名避免漂移。
  • "Evals as CI/CD for LLM apps" 行业实践综述(2026 趋势,按执行当周复验最新文章)——eval-as-CI 趋势背景。
  • 交叉引用 Day 165 / Day 168 笔记(本仓 docs/notes/aicap/)——snapshot 聚合(含 delta 规划)与配对显著性,本日 experiment 接线的上下游(2026-06)。

SOTA检查 (2026-06 更新)

  • 当前主流:把实验落进可观测 snapshot 符合 2026 eval-as-CI 趋势,仍 SOTA。评测结果像 CI 状态一样持久化、可回归,是近 12 个月 LLM-app 工程的主线方向,未被取代。
  • 需复查点dashboard.ts 的 snapshot schema 与 abCompare 输出字段是否对齐——避免 winner / significant 字段语义漂移(如 winner 该标 directional 却被读成显著)。
  • 模型 id 复查:experiment block 里的 modelA/modelBdeepseek-v4-pro/-flash,legacy deepseek-chat/-reasoner 2026-07-24 退役后历史 snapshot 的旧 id 需在长文标注为历史值。
  • 避免:只记赢家不记不显著;experiment 字段命名自造(沿用 OTel gen_ai.* 风格 + 既有 AbResult 字段名)。
  • 下次复查:B18 全局 SOTA 复核时核对 dashboard ↔ abCompare 字段对齐;experiment 接线落地后补 end-to-end 单测。

衔接

  • 昨天:Day 168 — M6 配对显著性检验(bootstrap CI 判「CI 跨 0 = 不显著」,Δ+10.3pp CI[0,20.7] N=29 不显著)。
  • 今天:把 A/B 结论接进 dashboard snapshot 的 experiment block(winner/Δ/CI/N/significant),诚实记录「不显著」,end-to-end 接线待跑(需 key)。
  • 明天:Day 170 — Block 收口 + SOTA 检查(全量跑 161-169 测试,复盘 outcome 指标 vs 旧循环自评,给出下步扩任务集到 ~70 取 power)。