返回 AICAP-180
B7 · Day 69OAuth 2.1 + MCP 安全 + CI gate

制造回归验证 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 读报告时字段名取错(如把 codePassRatecompletionRate),或缺值时默认放行 → 红测暴露。 gate.ts 的 fail-closed 设计(非有限即 violation)和 drop = baseline − current(gate.ts:40,方向正确)在静态上已规避后两类,但「阈值是否合适」「runner 链路是否对」仍须红测动态验证。

关键约束:阈值必须建在足够统计 power 的样本上。 这是本日和 B17 实测教训的硬连接:V4-Pro vs V4-Flash 的 Δ+10.3pp,在 N=2995% 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 触发路径)

  1. 回归注入点 = tasks.ts 的 codeCheckEVAL_TASKS(tasks.ts:353)汇总 AML_DETECT + AML_RESTRAINT + AML_COMPLIANCE + AGENT_CORE(~29 任务)。每条任务的 codeCheck 返回 { pass, detail? }。制造回归的最干净方式:把若干任务的 codeCheck 改成必失败(例如 aml-ctr-thresholdhas(/10[,.]?000.../) 改成永不匹配的正则),使这些任务 pass=false,整体 completionRate 显著下降
  2. 为什么改 codeCheck 而非 prompt:codeCheck 是确定性启发式(cheap heuristic first pass),改它能精确控制「让 K 个任务失败」,Δpass-rate 可算(K/29);改 prompt 还要重跑模型且结果不确定。
  3. 退步如何流到 gate:改坏的 tasks → pnpm eval:agent(需 key)产出 agent-evals/reports/*.json,其 completionRate 下降 → scripts/eval-gate.tslatestReport()(eval-gate.ts:11)读到这份低分报告 → checkGate 比对 baseline。
  4. gate 触发的精确条件(gate.ts:41):drop = baseline.completionRate − current.completionRatedrop > maxCompletionDrop(0.05) → push violation → pass=false
  5. job 变红(eval-gate.ts:39-43):!res.pass → 逐条 console.errorprocess.exit(1) → GitHub job fail → PR check ❌。
  6. 诚实前提:触发依赖 ① agent-evals/baseline.json 已就位(当前不存在,reports/ 仅两份真实 run),② 用 key 重跑改坏后的 tasks 产出低分报告。二者未就位前,红测只能在「构造的合成报告」上验证 gate.ts 逻辑(已 built+tested),真实 PR 上的 Δpass-rate 数值为待跑
  7. 可在无 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-thresholdhas(/10[,.]?000|\$10k|10k/i) 改成 has(/__never_match__/) → 必失败。
  • reasoning-numeric-sumhas(/29[,.]?000|29000/) 改成永不匹配 → 必失败。
  • aml-sar-deadlinehas(/\b30\b/) 改成 has(/\b999\b/) → 必失败。 三条都改坏 → K=3 → 预期 completion 从 ~0.897 掉到 ~0.793,drop≈10.4pp,稳过 5pp 阈值。选这三条是因为它们的 codeCheck 是简单 has(re),改动可逆且 Δ 可算。

3. 今日实战

  1. 在一个回归分支上,改 src/agent/eval/tasks.ts:把约 K=3 个任务的 codeCheck 改成必失败(人为负样本),预期 Δpass-rate ≈ K/29 ≈ 10.4pp。
  2. 开一个回归 PR,触发 CI 的 eval-gate job。
  3. (需 key + baseline 就位)pnpm eval:agent 产低分报告 → pnpm eval:gate 比对 baseline → 观察 process.exit(1) 使 job ❌。
  4. 截图/存日志:记录 violation 行(含 completionRate dropped X pp > allowed 5.0pp 的真实 Δ 数值)。
  5. 确认 Δpass-rate 明显大于运行间噪声带(否则红得不可信)——回扣 N=29 时 Δ+10.3pp 仍不显著的教训。
  6. (无 key 可立刻做)补一条 gate.ts 单测:checkGate({completionRate:0.793},{completionRate:0.897},{maxCompletionDrop:0.05}).pass === false,验决策逻辑会红。
  7. 记录红测结果到 docs/aipa/day69-regression-redtest.md:含改了哪几条 task、预期 Δ、gate 是否如期红。
  8. 确认回归改动只在临时分支,绝不合进 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 推荐实践。

自测题(讲得出才算掌握)

  1. 为什么从没红过的门禁不可信?(答:阈值/对比逻辑可能早已失效,绿只是「没被考过」。)
  2. K=3 条任务搞坏,Δpass-rate 约多少?会触发 5pp 阈值吗?(答:≈10.4pp,会触发。)
  3. K=1 时为什么可能不红?(答:Δ≈3.4pp < 5pp 阈值;这恰好验证了阈值灵敏度。)
  4. 红测的两层结构各需要什么?(答:第一层 gate.ts 单测无需 key;第二层端到端需 key+baseline。)
  5. 为什么改 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 复盘。