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/ties。significant标志可由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
}
其中 significant 由 ci95[0] > 0(false)推出,与 ab-compare.ts 的 verdict 逻辑一致。
3. 今日实战
- 给
src/agent/eval/dashboard.ts的DashboardSnapshot增experiment?: { winner; deltaMean; ci95; n; significant }字段,并让buildDashboard(或新增重载/参数)能接收一个AbResult并推出significant = ci95[0] > 0 || ci95[1] < 0。 - 让
scripts/ab-compare.ts(或scripts/build-dashboard.ts)读agent-evals/ab.json,把它接进 dashboard snapshot 的 experiment block。 - end-to-end 跑通:
pnpm eval:ab(先有两份run-*.json,需 key)→ 写ab.json→ dashboard 聚合 → 出含 experiment 的完整 snapshot。 - 给 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 block:
winner=V4-Pro (directional)、Δ=+10.3pp、CI**[0.0, 20.7]**、N=29、significant=false。 abCompare与dashboard均已 built;end-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=false、winner 带 (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/modelB用deepseek-v4-pro/-flash,legacydeepseek-chat/-reasoner2026-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)。