返回 AICAP-180
B5 · Day 46tool/context engineering + 指标树

A/B eval 设计

在 B1→B18 这条能力曲线上,B5 的前半段(Day 41-45)做的是「把工具集当作有限 token 预算来经营」——审计 token、画指标树、重设计工具(v1→v2)。

阶段: B5 · tool/context engineering + 指标树(Day 41-50) 标签: #evals #ab-test #controlled-experiment #attribution

今日导引(由浅入深)

在 B1→B18 这条能力曲线上,B5 的前半段(Day 41-45)做的是「把工具集当作有限 token 预算来经营」——审计 token、画指标树、重设计工具(v1→v2)。

但重设计完一句「v2 更好」只是断言,不是证据。今天(Day 46)是 B5 后半段的开篇:用受控 A/B 实验把「v2 更好」变成一个带数字、可归因的命题。

昨天我们造出了 v2 工具集,今天就把 v1 和 v2 放进同一个 eval harness 对打;明天(Day 47)则去校准这个对打用的「裁判」是否可信。

今天的最小可判定产出:一对 transcript,记录 v1=A/M、v2=B/M 两个通过数(M=工具子集任务数)。

1. 机理精读

evals-as-experiment 的核心是受控变量法。 A/B eval 不是「跑两次看哪次分高」,而是把工具改进当成一个实验假设 H1: v2 工具集的 tool-use 通过率 > v1,然后设计一个只让工具版本这一个变量变化、其余全部固定的实验。 固定的有三样:同一任务集(same task set)、同一模型(same model)、同一判分器(same judge / same grader)。 只有当这三样都钉死,两组通过率的差异 Δ 才能被归因到工具设计本身,而不是任务难度、模型随机性或判分口径漂移。 这正是 Anthropic《Demystifying evals》(2026-01) 反复强调的:eval 的价值在于它是一个可复现的受控实验,而非一次性打分。

为什么不能「换工具同时换别的」。 假设你把 v1 换成 v2 的同时,顺手把模型从 DeepSeek-V3 换成更强的 Qwen3,那么即使 Δ 为正,你也无法回答「这 +X pp 里有多少来自工具、多少来自模型」——这就是混杂变量(confounding)。 受控 A/B 的纪律就是:一次只动一个旋钮。今天我们动的旋钮是工具版本;模型、任务、judge 全部冻结。

为什么是「同 judge」而不仅是「同任务」。 即便任务集和模型都固定,如果 v1 跑的时候用 judge-α、v2 跑的时候用 judge-β,两个裁判的宽严不同,Δ 就被裁判差异污染了。 所以 same judge 是和 same task set 同等重要的约束——这也是为什么 Day 47 紧接着要做判分一致性校准:A/B 的结论强度,上限被裁判的可信度卡死。

Δ 的统计表达。 因为 v1/v2 跑在同一批任务上,每个任务都有一对结果(v1 是否通过、v2 是否通过),这是配对数据(paired)。 配对结构让我们能做配对自助法(paired bootstrap),得到比独立两样本更紧的置信区间。 本仓的 src/agent/eval/abCompare.ts 就是干这件事的:按 taskId 对齐两份报告,对配对的通过/失败差做 bootstrap,输出 deltaMeanci95,并统计 wins/losses/ties。 今天这条 A/B 链路的「数学骨架」已经是单测绿的纯函数,缺的只是真实模型跑批喂进来的两份报告。

边界:A/B ≠ 因果证明的终点。 单模型上的一次 A/B 只证明「在 DeepSeek-V3 上、在这批任务上、用这个 judge,v2 比 v1 高了 Δ」。 它不证明「v2 对所有模型都更好」——那需要 Day 48 的跨模型复核;也不证明「judge 本身判得对」——那需要 Day 47 的 κ 校准。 今天只负责把单模型的 Δ 这块基石浇筑出来。

与产品视角的连接(AISA 叙事落点)。 为什么一个求 AI Solutions Architect 岗的作品集要花一个 block 在 A/B eval 上? 因为 hiring manager 的四问之一就是「你怎么证明你的改动有效」。 「我重设计了工具」是工程师答案;「我用受控 A/B 测出 Δ=X pp、配 CI、跨模型复核、judge 校准 κ≥0.6」是架构师答案。 A/B eval 把「主观判断的工具改进」升级成「可量化、可复现、可归因的工程证据」——这正是 B5 这十天在能力曲线上想沉淀的核心肌肉,也是后续 B6(真 MCP server)能被严肃评测的前提。

2. 代码走读:abCompare.ts + run-agent-eval.ts

A/B 实测要落地,靠两段真实代码协作:一段产报告(scripts/run-agent-eval.ts),一段比报告(src/agent/eval/abCompare.ts)。

  • abCompare(a, b, opts)src/agent/eval/abCompare.ts):入参是两份 AbReportLike,每份含 perTask: Array<{ taskId, judge: { label } }>。它先用内部 passMap() 把每份报告映射成 taskId -> (label==='pass' ? 1 : 0)
  • 对齐逻辑const ids = [...ma.keys()].filter(id => mb.has(id)).sort()——只保留两份报告共有的 taskId,并排序保证确定性。若交集为空直接 throw new Error('abCompare: no common taskIds...'),这是防呆:A/B 不允许在不同任务集上比。
  • 配对差:把对齐后的两个 0/1 向量 avbv 喂给 pairedBootstrap(av, bv, opts)(来自 ./stats),拿到 deltaMean(= mean(A_pass − B_pass),即配对通过率差)和 ci95
  • 胜负统计:循环里 const d = av[i]! - bv[i]!d>0 计 wins(A 过 B 没过)、d<0 计 losses、d===0 计 ties——给出比单一 Δ 更细的任务级画像。
  • 产报告侧scripts/run-agent-eval.ts):main() 里读 EVAL_TASKS(来自 src/agent/eval/tasks.ts),用 buildModel(...) 起一个真实模型,runTaskEval(...) 跑批,把含 perTask 的 JSON 写到 agent-evals/reports/run-<stamp>.json
  • 无 key 兜底:runner 在 key 未设时打印 [agent-eval] ${envVar} not set — dry run (no model called).return——这就是 seed 说「待跑(需 key)」的代码依据:harness 在,数字要 key 才有。
  • 判分一致:runner 里 judge 默认 judgeId === modelId ? model : buildModel(...),A/B 两组要复用同一个 judge 配置,这是 same-judge 约束在代码层的落点。

手算一次配对差(为什么配对比独立紧)

设工具子集 M=6 个任务,v1/v2 各跑一次,按 taskId 对齐后 0/1 通过向量:

taskIdt1t2t3t4t5t6
v1(av)101010
v2(bv)111110
差 a−b0−10−100
  • completionA = mean(av) = 3/6 = 0.5completionB = mean(bv) = 5/6 ≈ 0.833
  • 配对差均值 deltaMean = mean(av−bv) = (0−1+0−1+0+0)/6 = −2/6 ≈ −0.333,即 v2 比 v1 高约 +33 pp(注意 abCompare 定义 deltaMean=A−B,B=v2 更高时取负,解读方向要对齐)。
  • wins/losses/ties:循环里 d=av[i]−bv[i]:t2、t4 处 d<0 → losses=2(B 即 v2 赢);其余 ties=4;wins=0。
  • 为什么配对更紧:t1、t3、t5、t6 在两版结果相同(配对内消去),只有 t2、t4 真正贡献方差。独立两样本会把这 4 个「平局」也算进方差,CI 被人为撑宽。pairedBootstrap 只重抽配对差,CI 自然更窄——这就是同任务集设计的统计红利。

3. 今日实战

  1. 确认 v2 工具集已在 src/agent/mcp/toolRegistry.ts 注册(Day 45 产物),且 src/agent/eval/tasks.ts 里 tool-use 子集任务 id 已确定。
  2. v1 工具版本跑:pnpm eval:agent --provider openrouter --model deepseek/deepseek-chat,transcript/报告落 agent-evals/reports/
  3. 切到 v2 工具版本,保持同一 provider/model/judge,重跑一次,得到第二份报告。
  4. scripts/ab-compare.ts(或直接调 abCompare())对齐两份报告,输出 deltaMean + ci95 + wins/losses/ties。
  5. 无 key 时:先用离线 fixture 报告跑通 abCompare(),确认对齐、bootstrap、胜负统计逻辑全通,得到 completion% 占位并 commit;待 key 后替换为真实两份报告。

4. 今日实测 / 产出

  • harness 与判分逻辑离线可验abCompare.ts 是纯函数 + 单测绿;run-agent-eval.ts dry-run 路径可跑)。
  • 真实 A/B 两数字 「待跑(需 OPENROUTER_API_KEY);可先用离线 fixture 跑通 harness 出 completion%+commit」
  • 产出 transcript:v1=A/M、v2=B/M 两数字(M=工具子集任务数)。
  • 可引的既有真实基线(非本日新测):仓库 tokenizer 已绿,3536 chars→1760 tokens,≈2.01 c/t,vocab=456;整体 378 tests passing、tsc clean
  • 本日不臆造任何 v1/v2 通过数。

5. 常见误区 / 陷阱

  • 换工具同时换模型或判分:最经典的混杂错误,Δ 立刻失去归因力。一次只动一个旋钮。
  • v1/v2 用了不完全相同的 taskId 交集abCompare 会用交集,但若交集太小 CI 会很宽——要保证两份报告覆盖同一子集。
  • 用独立两样本统计代替配对:明明是同任务配对数据却按独立样本算 CI,丢掉配对结构会高估方差、CI 虚宽。
  • 无 key 时把 fixture 占位数字当真实结果:fixture 只验逻辑,绝不能写进里程碑当实测。
  • 任务集太小导致 Δ 的 CI 跨 0:M 只有 5-6 个时,单次随机就能淹没真实差异;要么扩大 tool-use 子集,要么明确写「不显著」而非硬下结论。
  • 混淆 deltaMean 的符号方向abCompare 定义 deltaMean = A − B,引用时务必和「v2 更好」的口径对齐,别把负号读反。

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

  • Anthropic, Demystifying evals — 2026-01(eval 即受控实验、字段定义、judge 校准主线)。
  • Anthropic, Writing effective tools for agents — 2025-09(v1→v2 工具重设计的依据)。
  • Anthropic, Building effective agents — 2024-12(agent 评测与迭代的工程框架背景)。
  • 本仓代码:src/agent/eval/abCompare.tsscripts/run-agent-eval.tssrc/agent/eval/tasks.ts(tool-use 子集)、src/agent/eval/stats.tspairedBootstrap)— 2026-06。
  • 经典统计:Efron & Tibshirani, An Introduction to the Bootstrap — 经典(paired bootstrap / 配对设计的重抽样推断方法,无过时风险)。

SOTA检查 (2026-06 更新)

  • 当前主流:受控 A/B(固定任务/模型/judge,只切工具版本)+ 配对 bootstrap CI 仍是 eval 改进归因的金标准,2026-06 未被取代。
  • 是否仍 SOTA:是。Anthropic《Demystifying evals》(2026-01) 为当前主线方法论。
  • 过时黑名单:换工具同时换模型/判分(混杂变量);只跑一次不报 CI 就下结论;用「分高就是好」代替受控实验。
  • 下次复查点:执行当周用 npm/OpenRouter 重验 DeepSeek-V3 在 OpenRouter 的 id 与价格(模型 id 漂移会导致 A/B 两组实际跑在不同后端)。

衔接

  • 昨天:Day 45 — tool 重设计(合并+收窄),产出待对比的 v2 工具集。
  • 今天:用同任务/同模型/同 judge 的受控 A/B,把「v2 更好」变成带 Δ + CI 的可归因数字。
  • 明天:Day 47 — 判分一致性校准,用 Cohen's κ 验证 A/B 所依赖的 judge 是否可信。