制造回归验证 gate 会红
Day 67 设计了 gate 的决策逻辑,Day 68 把它接进 CI。但一道从没红过的门禁是不可信的——你不知道它到底会不会拦。今天 Day 69 做「testing the test」:故意制造一个真的坏改动让 pass-rate 跌破阈值,确认 eval-gate job 真的失败(红)。如果故意搞坏后 gate 仍绿,说明阈值或对比逻辑失效,门禁形同虚设。在 B1→B18 曲线上,这是「
阶段: B7 · OAuth 2.1 + MCP 安全 + CI gate(Day 61-70) 标签: #testing-the-test #regression #statistical-power #fail-closed
今日导引(由浅入深)
Day 67 设计了 gate 的决策逻辑,Day 68 把它接进 CI。但一道从没红过的门禁是不可信的——你不知道它到底会不会拦。今天 Day 69 做「testing the test」:故意制造一个真的坏改动让 pass-rate 跌破阈值,确认 eval-gate job 真的失败(红)。如果故意搞坏后 gate 仍绿,说明阈值或对比逻辑失效,门禁形同虚设。在 B1→B18 曲线上,这是「有门禁」升级到「门禁被证明有效」的关键一格——也是评测严谨度(eval rigor)的标志动作。明天 Day 70 会 revert 回归使 gate 转绿,完成「坏→红、好→绿」双向闭环。最小可判定产出:一个回归 PR 上 eval-gate ❌ 失败 的日志(含 Δpass-rate 数值;真实 PR 实跑标注为待跑,需 key + baseline 就位)。
由浅入深三层
- 浅:gate 一直绿,看起来很安心。
- 中:安心是假象——从没红过的门禁可能阈值/逻辑早就坏了,绿只是「没被考过」。
- 深:唯一能确证 gate 有效的办法,是故意制造一个真坏改动让它红;红测通过,绿才有意义。这就是「监控也要被监控」。
1. 机理精读
「红→绿」契约 = 监控本身的监控(testing the test)。 一个监控系统的可信度,不来自它「一直绿」,而来自它「能在该红的时候红」。gate 也是监控:它的可信度来自「先拦住一个真的坏改动」。方法是注入一个已知会让指标退步的改动(ground-truth 的负样本),观察 gate 是否触发。这是质量工程里的 mutation testing 思想在 eval 层的应用——故意引入「变异」(坏改动),看测试/门禁能否捕获。
为什么这一步不能省。 没经过红测的门禁有两类隐藏失效:① 阈值失效——阈值设得太松,pass-rate 掉很多都不触发;② 对比逻辑失效——current/baseline 取错字段、比较方向写反、fail-open 默认放行。这些在「一直绿」的日常里完全不可见,只有故意搞坏才暴露。fail-closed 的 gate.ts 在设计上已堵住 fail-open,但「阈值是否合适」「runner 是否正确读到当前报告」只有红测能验。
红测能暴露的三类隐藏失效(逐条)。
- 阈值太松:搞坏 K 条任务、completion 明显下降,gate 却仍绿 →
maxCompletionDrop设得过宽,需收紧。 - 对比方向反了:实现里若把
drop = current − baseline(写反),退步时 drop 为负、永不触发 → 红测立刻暴露。 - 取错字段 / fail-open:runner 读报告时字段名取错(如把
codePassRate当completionRate),或缺值时默认放行 → 红测暴露。gate.ts的 fail-closed 设计(非有限即 violation)和drop = baseline − current(gate.ts:40,方向正确)在静态上已规避后两类,但「阈值是否合适」「runner 链路是否对」仍须红测动态验证。
关键约束:阈值必须建在足够统计 power 的样本上。 这是本日和 B17 实测教训的硬连接:V4-Pro vs V4-Flash 的 Δ+10.3pp,在 N=29 时 95% CI[0,20.7] 跨 0、不显著,需 ~70 任务才有足够 power 把这个差异从噪声里分出来。含义:
- 若在 N 过小时设过紧阈值(如 maxCompletionDrop=0.02),正常的运行间噪声就可能跌破阈值,制造 false alarm(误拦)——团队会学会无视红灯;
- 反之阈值太松,真退步被噪声掩盖、漏拦。
- 所以红测不只验「会不会红」,也验「红得是否是因为真退步而非噪声」——Δpass-rate 要明显大于运行间噪声带,红才有说服力。
边界:制造的回归要「可控、可逆、ground-truth 明确」。 改的是 tasks.ts 里若干任务使其必失败(人为负样本),不是改 gate 逻辑本身——验的是「真坏改动能否被现有 gate 捕获」,而非「改 gate 让它报错」。改完要能干净 revert(Day 70 收口)。
手算 Δpass-rate(确认会跌破 0.05 阈值)。 基线 completionRate ≈ 0.897(29 任务约 26 条 pass)。若故意让 K=3 条任务必失败,新 pass 数 ≈ 23,新 completionRate ≈ 23/29 ≈ 0.793。则 drop = 0.897 − 0.793 = 0.104(10.4pp)> maxCompletionDrop = 0.05 → gate 触发,job 红。要点:K=3 的人为回归制造约 10pp 跌幅,远超 5pp 阈值,红得干净利落;若只搞坏 K=1(drop≈3.4pp < 5pp),反而不会红——这恰好暴露「阈值边界」,是红测顺带验证阈值灵敏度的副产品。注意这里的 0.793 与真实 codePassRate 79.3% 数值相近纯属巧合,前者是「人为搞坏后的 completion」、后者是「真实 run 的 code-pass」,不可混为一谈。
2. 代码走读(src/agent/eval/tasks.ts + gate.ts 触发路径)
- 回归注入点 =
tasks.ts的 codeCheck。EVAL_TASKS(tasks.ts:353)汇总AML_DETECT + AML_RESTRAINT + AML_COMPLIANCE + AGENT_CORE(~29 任务)。每条任务的codeCheck返回{ pass, detail? }。制造回归的最干净方式:把若干任务的codeCheck改成必失败(例如aml-ctr-threshold的has(/10[,.]?000.../)改成永不匹配的正则),使这些任务 pass=false,整体 completionRate 显著下降。 - 为什么改 codeCheck 而非 prompt:codeCheck 是确定性启发式(cheap heuristic first pass),改它能精确控制「让 K 个任务失败」,Δpass-rate 可算(K/29);改 prompt 还要重跑模型且结果不确定。
- 退步如何流到 gate:改坏的 tasks →
pnpm eval:agent(需 key)产出agent-evals/reports/*.json,其completionRate下降 →scripts/eval-gate.ts的latestReport()(eval-gate.ts:11)读到这份低分报告 →checkGate比对 baseline。 - gate 触发的精确条件(gate.ts:41):
drop = baseline.completionRate − current.completionRate,drop > maxCompletionDrop(0.05)→ push violation →pass=false。 - job 变红(eval-gate.ts:39-43):
!res.pass→ 逐条console.error→process.exit(1)→ GitHub job fail → PR check ❌。 - 诚实前提:触发依赖 ①
agent-evals/baseline.json已就位(当前不存在,reports/ 仅两份真实 run),② 用 key 重跑改坏后的 tasks 产出低分报告。二者未就位前,红测只能在「构造的合成报告」上验证 gate.ts 逻辑(已 built+tested),真实 PR 上的 Δpass-rate 数值为待跑。 - 可在无 key 下先验 gate.ts 的红:直接对
checkGate({completionRate:0.793}, {completionRate:0.897}, {maxCompletionDrop:0.05})写单测,断言pass===false且 violation 含「dropped 10.4pp > allowed 5.0pp」——这条纯逻辑红测无需 key,已属 built+tested 范畴。它验的是「gate 逻辑会红」;而「真实模型跑出低分报告→CI job 红」才需 key+baseline。两层要分清,不能用前者冒充后者。
红测验证的两层结构(务必分清)。
- 第一层(无 key,已可做):
gate.ts的纯逻辑——喂一对「基线高/当前低」的合成 metrics,断言pass=false。这验「决策逻辑会红」。 - 第二层(需 key + baseline):改坏 tasks → 真跑模型产低分报告 → CI 的 eval-gate job 真红。这验「整条流水线(runner 读报告 + 对比 baseline + exit 1 + GitHub 阻断)端到端会红」。 seed 要的「含 Δpass-rate 数值的真实失败截图」属第二层,故标待跑。
具体改哪几条任务(举例)。 选语义清晰、ground-truth 明确的任务最易控制 Δ:
aml-ctr-threshold:has(/10[,.]?000|\$10k|10k/i)改成has(/__never_match__/)→ 必失败。reasoning-numeric-sum:has(/29[,.]?000|29000/)改成永不匹配 → 必失败。aml-sar-deadline:has(/\b30\b/)改成has(/\b999\b/)→ 必失败。 三条都改坏 → K=3 → 预期 completion 从 ~0.897 掉到 ~0.793,drop≈10.4pp,稳过 5pp 阈值。选这三条是因为它们的 codeCheck 是简单has(re),改动可逆且 Δ 可算。
3. 今日实战
- 在一个回归分支上,改
src/agent/eval/tasks.ts:把约 K=3 个任务的codeCheck改成必失败(人为负样本),预期 Δpass-rate ≈ K/29 ≈ 10.4pp。 - 开一个回归 PR,触发 CI 的
eval-gatejob。 - (需 key + baseline 就位)
pnpm eval:agent产低分报告 →pnpm eval:gate比对 baseline → 观察process.exit(1)使 job ❌。 - 截图/存日志:记录 violation 行(含
completionRate dropped X pp > allowed 5.0pp的真实 Δ 数值)。 - 确认 Δpass-rate 明显大于运行间噪声带(否则红得不可信)——回扣 N=29 时 Δ+10.3pp 仍不显著的教训。
- (无 key 可立刻做)补一条 gate.ts 单测:
checkGate({completionRate:0.793},{completionRate:0.897},{maxCompletionDrop:0.05}).pass === false,验决策逻辑会红。 - 记录红测结果到
docs/aipa/day69-regression-redtest.md:含改了哪几条 task、预期 Δ、gate 是否如期红。 - 确认回归改动只在临时分支,绝不合进 main;Day 70 revert。
4. 今日实测 / 产出
- 回归 PR 的
eval-gate ❌ 失败截图/日志(含 Δpass-rate 数值)。 - gate fail-closed 逻辑:已 built+tested。
- 在真实 PR 上跑出 Δpass-rate 数值:待跑(需 key 跑 + baseline 就位)。
- 真实锚点(用于设阈值参照,不臆造):reports/ 现有 run
n=29/completionRate≈0.8965(89.7%)/codePassRate≈0.7931(79.3%)/partialCreditMean=0.900/judgeHumanKappa=null;B17 实测 V4-Pro vs Flash Δ+10.3pp 95% CI[0,20.7](N=29 不显著,需 ~70 任务)。
5. 常见误区 / 陷阱
- 没做红测就信门禁:从没红过的 gate 可能阈值/对比逻辑早已失效而无人知。
- N 过小设过紧阈值:N=29 时正常噪声就能跌破紧阈值 → false alarm → 团队无视红灯。Δ+10.3pp 在 N=29 都不显著,可见小样本噪声之大。
- 改 gate 逻辑而非改任务:那是「让 gate 报错」不是「验真坏改动能否被捕获」,验错了对象。
- 回归不可逆:制造的回归要能干净 revert(Day 70 收口靠它转绿),否则污染主线。
- 把红测的红当真退步:故意搞坏的红是预期内的,别和真实模型退步混记。
- 只搞坏 1 条任务:drop≈3.4pp < 5pp 阈值,gate 不红——会误以为「gate 失效」,其实是 Δ 没过阈值;红测要让 Δ 明显超阈值。
- 回归 PR 直接合进 main:故意搞坏的改动只用于验证,绝不能合并;用临时分支并在 Day 70 revert。
附:概念辨析(易混淆)
- testing the test vs 普通测试:普通测试验「代码对不对」;testing the test 验「门禁/监控本身有没有失效」——靠故意制造坏样本看它能否捕获。
- mutation(变异)vs 真退步:本日的变异是人为注入的负样本(预期内的红);真退步是模型自己掉分(计划外的红)。别混记。
- gate 逻辑红 vs 流水线端到端红:前者无 key 即可(gate.ts 单测);后者需 key+baseline(真跑模型→CI job 红)。seed 要的截图属后者。
- Δ 过阈值 vs Δ 未过阈值:K=3 → Δ≈10.4pp 过 5pp 阈值会红;K=1 → Δ≈3.4pp 不过阈值不红。红测要让 Δ 明显超阈值。
- 0.793(人为搞坏的 completion)vs 79.3%(真实 codePassRate):数值相近纯属巧合,含义完全不同,不可混为一谈。
附:本日「最小可判定产出」判据
- 第一层(无 key):gate.ts 单测断言「基线高/当前低 → pass=false」绿 → 决策逻辑会红,已可达成。
- 第二层(需 key + baseline):回归 PR 的 eval-gate job 出现 ❌ + violation 行含真实
dropped X pp > allowed 5.0pp→ 端到端会红,待跑。 两层都达成才算「gate 被证明能拦坏改动」;当前可交付第一层,第二层标待跑。
6. 学习资源(每条带 YYYY-MM)
- DeMillo/Lipton/Sayward, Hints on Test Data Selection(mutation testing 思想,经典 1978)— 故意注入变异验测试有效性。
- Google Testing Blog, Testing on the Toilet: mutation testing(系列,长期更新)— 工程化「监控自身有效性」的实务视角。
- Jia & Harman, An Analysis and Survey of the Development of Mutation Testing(IEEE TSE,2011,经典综述)— 变异测试方法论全景。
- Anthropic, Demystifying evals(2026-01)— eval rigor / 基线与回归。
- 本仓
src/agent/eval/tasks.ts+src/agent/eval/gate.ts(2026-06)— 回归注入点(codeCheck)与触发路径(checkGate→exit 1)。 - 本仓
scripts/eval-gate.ts(2026-06)—latestReport()读报告、process.exit(1)阻断。 - 本仓
agent-evals/reports/run-2026-06-22T17-34-34-717Z.json(2026-06)— 基线参照(n=29, completion 0.8965)。 - 本仓 B17 A/B 实测记录(2026-06)— Δ+10.3pp 95% CI[0,20.7]、N=29 不显著、~70 任务的 power 估计。
SOTA检查 (2026-06 更新)
- 当前主流:故意回归验证闸门是 2026 eval-rigor 标准实践,仍 SOTA。
- 统计纪律:阈值必须建在足够 power 的样本上——呼应 B17 实测(Δ+10.3pp CI[0,20.7] 在 N=29 不显著,需 ~70 任务)。
- 过时黑名单:N 过小时设过紧阈值(噪声误拦);从不做红测的「假门禁」;fail-open 默认放行。
- 统计补充:要把 Δ+10.3pp 这种量级判为「显著」,paired bootstrap / McNemar 在 N≈70 才有足够 power;N=29 的 CI[0,20.7] 跨 0 是样本不足的直接证据,阈值与显著性判定都须等样本扩容。
- 下次复查点:任务集扩到 ~70 后重设 maxCompletionDrop 并重做红测;baseline.json 就位后补真实 Δpass-rate;季度复查 mutation testing 是否仍是 eval-rigor 推荐实践。
自测题(讲得出才算掌握)
- 为什么从没红过的门禁不可信?(答:阈值/对比逻辑可能早已失效,绿只是「没被考过」。)
- K=3 条任务搞坏,Δpass-rate 约多少?会触发 5pp 阈值吗?(答:≈10.4pp,会触发。)
- K=1 时为什么可能不红?(答:Δ≈3.4pp < 5pp 阈值;这恰好验证了阈值灵敏度。)
- 红测的两层结构各需要什么?(答:第一层 gate.ts 单测无需 key;第二层端到端需 key+baseline。)
- 为什么改 tasks 的 codeCheck 而非改 prompt?(答:codeCheck 改动确定、Δ 可算、可逆;改 prompt 还要重跑且不确定。)
衔接
- 昨天:Day 68 — GitHub Actions 接 eval 阻断(job 待加,blocking required check)。
- 今天:制造回归验证 gate 会红 = testing the test,确认坏改动真触发 exit 1。
- 明天:Day 70 — 修复回归 + 双 transcript 收口:revert 使 gate 转绿,归档 OAuth 成功/被拒双 transcript,B7 复盘。