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

M6 A/B 实验设计

Day 161-166 把 outcome 指标定义、聚合、可视化做完了——但这些指标都是「单个系统的体检」。

阶段: B17 · outcome 指标仪表盘 + A/B(Day 161-170) 标签: #ab-testing #paired-design #model-selection #deepseek-v4

今日导引(由浅入深)

Day 161-166 把 outcome 指标定义、聚合、可视化做完了——但这些指标都是「单个系统的体检」。 今天进入 B17 的第二条主线:A/B,也就是把两个模型变体放在同一任务集上对照,回答「该选哪个模型」。 这是 model-strategy PM 最核心的可交付物,也是 AISA 作品集里「为什么选这个模型」那一问的硬证据。 在 B1→B18 能力曲线上,这一步从「观测单系统」跨到「比较两系统」,是 eval 严谨度的一次跃迁——B16 解决「做出来 + 能用」,A/B 解决「两个候选选哪个」,它要求你在受控对照下回答「换 model 值不值」,这正是 AISA 在 build-vs-buy / 模型选型上的核心论证能力。 今天先做实验设计(配对、指标、样本量),明天(Day 168)做配对显著性检验,后天(Day 169)把结论落进仪表盘。 本仓的真实 A/B 已经落地——V4-Pro vs V4-Flash,今天的最小可判定产出是:双模型各跑一遍,落两组 per-task 分数 transcript(JSON)。

1. 机理精读

A/B 的第一性原理是「控制变量」。 你想测「模型 A 比模型 B 强多少」,但每个任务本身有难度方差——有的题谁都做对,有的题谁都做错。 如果模型 A 跑任务集 1、模型 B 跑任务集 2,你测到的差异里混进了「任务集难度差」,无法归因到模型。 配对设计(paired design) 的做法是:让 A 和 B 跑同一批任务,对每个任务 i 看 A 和 B 的成对结果,再对成对差值求平均。 同任务相减,任务难度这个混杂因子被直接消掉了——这就是配对能把 delta 的置信区间收窄的根本原因(stats.ts 注释原话:「Pairing on the same tasks removes task-difficulty variance, so the CI on the delta is tight」)。

指标选 SAR 质量(或完成率)这类 outcome 指标,而非 token 跑分。 B17 整个 block 的立场就是「outcome 而非 benchmark 分数」。 A/B 比的是两模型在真实任务上的产出质量差,不是某个公开榜单的分。 本仓实测用的判定是 judge=pass(LLM-judge 给出的「过/不过」),辅以 partial-credit(部分给分)。

样本量决定「能不能检出真实差异」,这是今天最容易被忽略的设计变量。 即使 A 真的比 B 好,如果任务数 N 太小,配对差值的置信区间会很宽、跨过 0,你就测不出显著。 小 N 只能给「方向性」(A 看起来更好),给不了「统计显著」。 本仓真实 A/B 是 N=29,结果就是「方向性正但不显著」——这正是 Day 168 的核心教训。 设计阶段就要预估:要检出 Δ 这么大的差异、给定配对差值的 SD,需要多少 N 才有 80% power(stats.tsrequiredNForDelta 就是算这个)。

这套范式与 GRPO(arXiv:2402.03300)的「组内相对评测」同源。 GRPO 不依赖绝对的价值函数,而是在同一个 prompt 的一组采样里做相对比较——「同 prompt 内对照消除 prompt 难度」与 A/B 的「同任务内对照消除任务难度」是同一个降方差思路。 理解这个类比,能把 RL 后训练和 eval 方法论打通。

判定器(judge)必须对 A 和 B 完全一致,否则对照失效。 A/B 比的是模型,不是 judge。 如果给 A 用一个 prompt 的 judge、给 B 用另一个,差异里就混进了「judge 差」。 本仓的 judge.label(pass/fail)来自同一套 LLM-judge,对 A、B 一视同仁; 理想情况下 judge 还应对「哪个是 A、哪个是 B」盲(blind),避免 judge 偏向某个模型名。 这与 Day 163 的 SAR 质量 rubric、Day 162 的独立金标是同一条「评测者独立于被评测者」的主线。

A/B 的产出形态是「一句可引用的话」,这是 model-strategy PM 的交付单位。 ab-compare.ts 末尾的 QUOTABLE 行(见下文走读)就是把整个实验压成一句「A vs B on N=…: Δ = X pp [95% CI …]」。 这句话能直接进作品集/选型评审纪要,而不需要读者翻原始 transcript——这是把「工程动作」翻成「决策语言」的关键一步。

A/B 的「同任务集」还隐含一条数据卫生要求:任务集必须在两次跑之间冻结。 如果在跑完 A 之后、跑 B 之前往 tasks.ts 里加了题或改了 reference,两份报告的 taskId 交集就会变化,abCompare 对齐的就不是同一批题,配对的前提(同任务消除难度方差)被破坏。 所以严谨的 A/B 流程是:先冻结任务子集 → 跑 A → 跑 B(不动任务)→ 对齐 → 出数。 这与 Day 162-163 的「金标/rubric 冻结后再评测」是同一条原则:评测对象(任务)和评测标准(judge/金标)在一轮实验内不可变,否则数字不可比。

边界澄清:A/B 是「选模型/选变体」的工具,不是「证明系统达标」的工具。 它给的是相对差(A 比 B 好 X pp),不是绝对值(A 达到了 Y% 的合规标准)。 后者要靠 Day 161-164 的 outcome 绝对指标 + 独立金标。 换句话说:A/B 回答「换 V4-Pro 比 V4-Flash 值不值」,绝对 outcome 指标回答「V4-Pro 的 FPR 够不够低、SAR 质量够不够高」——选型和达标是两个正交问题,别用 A/B 的相对差去冒充合规达标的绝对证据。

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

今天 seed 引用 src/agent/eval/abCompare.ts + scripts/ab-compare.ts。 走读真实函数与行为:

  • abCompare(a, b, opts) 是纯函数,入参是两份 AbReportLike(每份含 model?perTask: Array<{ taskId, judge: { label } }>),出参是 AbResult
  • 对齐逻辑(配对的实现):passMap(r) 把每份报告转成 Map<taskId, 0|1>judge.label === 'pass' ? 1 : 0)。然后 const ids = [...ma.keys()].filter((id) => mb.has(id)).sort() ——只取两份报告共有的 taskId,这就是「同任务对齐」。若无交集,抛 'abCompare: no common taskIds between the two reports'
  • 配对差值与 CI:const pr = pairedBootstrap(av, bv, opts)——把 A、B 在对齐任务上的 0/1 向量交给 stats.tspairedBootstrap,得 deltaMean(= mean(A−B))与 ci95
  • win/loss/tie:对每个对齐任务算 d = av[i] - bv[i]d>0 → wins(A 过 B 没过)、d<0 → losses、d===0 → ties。
  • 返回字段:{ modelA, modelB, nAligned, completionA, completionB, deltaMean, ci95, wins, losses, ties }completionA = mean(av) 即 A 在对齐任务上的通过率。

scripts/ab-compare.ts(命令 pnpm eval:ab)的真实行为:

  • 默认读 agent-evals/reports 目录下最新两个 run-*.jsonfiles[len-1] = A 当 newest,files[len-2] = B);也可显式传 pnpm eval:ab <reportA.json> <reportB.json>
  • 报告少于 2 个时打印 '[eval-ab] need >=2 reports — run \pnpm eval:agent` with two different --model values.'并退出——所以**真跑 A/B 需要先用两个不同--model各跑一遍pnpm eval:agent`(需 key)**。
  • abCompare(A, B),打印 completion A/B、paired Δ(A-B)95% CI、per-task wins/losses/ties,并写 agent-evals/ab.json
  • 末尾输出一行 QUOTABLE: "<A> vs <B> on N=...: Δ completion = X pp [95% CI ...]"——这就是给简历/作品集直接引用的句子。
  • verdict 行 r.ci95[0] > 0 ? 'A significantly better' : r.ci95[1] < 0 ? 'B significantly better' : 'not significant (CI crosses 0)'——A/B 脚本层面就用「CI 跨 0」判显著,与 Day 168 的统计判据一致,本仓真实落第三分支。

手算配对例子(理解 deltaMean 与 win/loss/tie):设 5 个对齐任务,A 的 pass 向量 = [1,1,0,1,1],B = [1,0,0,1,1]。

  • 成对差 = A − B = [0,1,0,0,0]。
  • deltaMean = mean(diffs) = 1/5 = 0.20 = +20pp
  • 逐任务符号:task2 是 d=1>0 → win;其余 d=0 → tie。故 wins=1、losses=0、ties=4。
  • completionA = mean([1,1,0,1,1]) = 0.8completionB = mean([1,0,0,1,1]) = 0.6,朴素差 0.2 与配对 deltaMean 一致(二值 + 全对齐时两者相等)。

这与本仓真实结果的形态高度一致:多数 tie、少量 win、零 loss(本仓 3 wins / 0 losses / 26 ties)。 零 loss 说明 V4-Pro 没在任何任务上输给 V4-Flash,但 26 个 tie 意味着区分信号稀薄——这正埋下了 Day 168「不显著」的伏笔。

3. 今日实战

  1. 准备两个不同模型的 eval 报告:pnpm eval:agent --model deepseek-v4-propnpm eval:agent --model deepseek-v4-flash,各产一份 agent-evals/reports/run-*.json(需 key)。
  2. 任务集取 src/agent/eval/tasks.ts 的子集(FATF/BSA AML typologies + LLM-agent failure modes 那套专家撰写题)。
  3. pnpm eval:ab,让 scripts/ab-compare.ts 自动取最新两份报告,调 abCompareagent-evals/ab.json
  4. 保留两组 per-task 分数 transcript(每个任务在 A 和 B 下的 judge.label),作为 A/B 的可追溯底稿。

两份报告是硬前置ab-compare.tsagent-evals/reports 里少于 2 个 run-*.json 时直接打印提示并退出(见走读),所以「跑一次就想 A/B」是不行的——必须用两个不同 --model 各跑一遍。 可复现性:A/B 的统计层(pairedBootstrap)RNG 可注入,单测里固定 opts.rng 就能复现同一 CI;但真模型回合本身有采样温度,两次跑同一模型的 per-task 结果可能微变。 所以 transcript 要连同 model id、任务子集、判定器版本一起存档,否则「89.7% vs 79.3%」这个数后续无法复算。 这与 Day 162/163 的「评测可追溯」是同一条纪律:数字必须带产生它的上下文。

4. 今日实测 / 产出

  • abCompare.ts + ab-compare.ts 已 built
  • 真实 A/B 已落:V4-Pro 完成率 89.7% vs V4-Flash 79.3%(judge=pass),partial-credit 0.900N=293 wins / 0 losses / 26 ties。两组 per-task 分数 transcript 为真实产出
  • 更大任务集(~70 task 达 power)= 待跑(需 key + 更多任务)

5. 常见误区 / 陷阱

  • 非配对设计:A 和 B 跑不同任务集,差异里混进任务难度方差,无法归因到模型——白跑。
  • N 太小却宣称显著:N=29、26 个 tie,方向性正不等于显著(明天 CI 会触 0)。设计阶段没估 power,结论就站不住。
  • 比公开榜单分而非 outcome:A/B 应比真实任务的产出质量(SAR 质量/完成率),不是某 benchmark 跑分。
  • taskId 不对齐:两份报告若 taskId 命名不一致,abCompare 取不到交集会直接抛错——对齐键必须稳定。
  • judge 不盲、A/B 用不同 judge:判定器对两模型必须一致且最好对模型名盲,否则差异里混进 judge 偏置。
  • 跑完才发现只有一份报告pnpm eval:ab 需 ≥2 份 run-*.json,必须用两个不同 --model 各跑一遍,单跑无法 A/B。
  • 改了任务又跑 B:实验中途动 tasks.ts 会让两份报告的 taskId 交集漂移,破坏配对前提;任务集一轮内必须冻结。

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

  • DeepSeekMath / GRPO 论文,arXiv:2402.03300(2024-02)——组内相对评测范式,与 A/B 配对降方差同源。
  • 本仓 src/agent/eval/abCompare.ts + scripts/ab-compare.ts 源码(AICAP-180 B12/B17,Pillar P5)——配对对齐 + bootstrap CI + win/loss/tie 实现(2026-06)。
  • 本仓 src/agent/eval/tasks.ts 源码(AICAP-180 P1)——专家从 FATF/BSA AML typologies + LLM-agent 失败模式撰写的任务集,A/B 的对齐任务来源(2026-06)。
  • Anthropic, "Demystifying evals / A statistical approach to model evals"(2026-01)——配对评测与置信区间在模型对照中的方法论。
  • OpenRouter / DeepSeek 模型页(2026-06)——deepseek-v4-pro / deepseek-v4-flash 模型 id 与定价,执行当周复验。
  • 交叉引用 Day 162-164 笔记(本仓 docs/notes/aicap/)——独立金标 / SAR rubric / cost-p95,A/B 的指标与「评测者独立于被评测者」原则同源(2026-06)。

SOTA检查 (2026-06 更新)

  • 当前主流:A/B 配对评测是稳定方法论,仍 SOTA。同任务对齐 + 配对 bootstrap 是降方差的标准做法。
  • 模型 id:用 deepseek-v4-pro / deepseek-v4-flash。CLAUDE.md 课程行写「V3 vs Qwen3」为示例,本仓实际跑的是 V4-Pro vs V4-Flash——以实测为准
  • 过时黑名单:legacy deepseek-chat / deepseek-reasoner2026-07-24 退役,A/B 与成本模型禁止再引旧 id。
  • 下次复查:2026-07-24 前确认报告/脚本未硬编旧 model id;OpenRouter 单价按执行当周复验(价格波动快)。

衔接

  • 昨天:Day 166 — 仪表盘可视化(snapshot → outcome 指标卡,极性着色)。
  • 今天:A/B 实验设计——配对同任务消除难度方差,V4-Pro vs V4-Flash 各跑一遍落两组 per-task transcript(真实 N=29,89.7% vs 79.3%)。
  • 明天:Day 168 — M6 配对显著性检验(对两组算 Δ均值 + 95% CI,得出「方向性好 ≠ 显著」的核心教训)。