eval CI gate 设计 (M2)
B7 前半周(Day 61-66)建的是安全支柱(OAuth 2.1 RS + 审计)。从今天起,B7 后半周(Day 67-70)转入评测支柱:把「模型能力是否退步」从主观印象变成可阻断合并的门禁。在 B1→B18 能力曲线上,这是「有 eval」升级成「eval 进 CI 当 gate」的一格——也是 AISA 作品集里 hiring manager 四问之一「你怎么知道它没退步」的答案。今天
阶段: B7 · OAuth 2.1 + MCP 安全 + CI gate(Day 61-70) 标签: #eval-gate #fail-closed #cohen-kappa #regression-guard
今日导引(由浅入深)
B7 前半周(Day 61-66)建的是安全支柱(OAuth 2.1 RS + 审计)。从今天起,B7 后半周(Day 67-70)转入评测支柱:把「模型能力是否退步」从主观印象变成可阻断合并的门禁。在 B1→B18 能力曲线上,这是「有 eval」升级成「eval 进 CI 当 gate」的一格——也是 AISA 作品集里 hiring manager 四问之一「你怎么知道它没退步」的答案。今天 Day 67 设计 gate 的决策逻辑(纯函数),明天 Day 68 把它接进 GitHub Actions,Day 69 故意制造回归验证它真会红。最小可判定产出:gate.ts 的 fail-closed 决策逻辑(已 built+tested,无 key),以及一份待选定的 baseline.json(待跑,需 key)。
由浅入深三层
- 浅:跑 eval 得到一个 pass-rate,看着像分数。
- 中:单次分数无意义——「89.7% 是好是坏」依赖任务难度。有意义的是「相对可信基线掉了多少」。
- 深:把基线冻成快照、fail-closed 比对(缺值/损坏默认判失败),并用 judge-human κ 给评测器本身上保险——只盯 completion 会被「judge 漂了分没掉」骗过。gate = completion-drop ∧ κ 双条件。
1. 机理精读
CI gate 的本质 = fail-closed 回归闸。 一次「可信基线」(baseline)被冻成快照,后续每次 eval 跑完都和基线比;关键指标跌破阈值即 exit 1 阻断合并。「fail-closed」是核心安全姿态:当指标缺失或损坏(NaN/Infinity)时,默认判失败而非放行——宁可误拦也不放过一个静默退步。对照「fail-open」(出错就放行):在评测门禁里 fail-open 等于没门禁,因为最危险的退步往往伴随 eval 本身出错。
两个把关指标:completion-rate 与 judge-human Cohen's κ。
- completion-rate(任务完成率):衡量「模型把任务做对了多少」。gate 看的是相对基线的跌幅(
baseline − current > maxCompletionDrop即违规),不是绝对值——因为不同任务集绝对值不可比,但「同一套任务相对上次掉了多少」是有意义的回归信号。 - judge-human Cohen's κ:衡量「LLM judge 给的分」和「人工标注」的一致性。κ 修正了「碰巧一致」的概率,是评测自身可信度的指标。Anthropic Demystifying evals(2026-01)强调:eval 必须可重复、有基线、含人类校准——一个没和人工对齐过的 LLM judge,它的分数本身就不可信,gate 就没意义。所以 gate 公式是双条件:completion drop ≤ maxCompletionDrop 且 κ ≥ minKappa。
为什么这样设计:监控也要被监控。 gate 同时盯「能力(completion)」和「评测器可信度(κ)」——只盯前者会被「judge 漂了但分数没掉」骗过。κ 这条相当于「给评测器本身上保险」。
关键权衡:阈值松紧 vs 样本量。 阈值设太紧、样本太小,噪声会频繁误拦(false alarm);设太松则放过真退步。这与 B17 的实测教训直接相关:V4-Pro vs V4-Flash 的 Δ+10.3pp 在 N=29 时 95% CI[0,20.7]跨 0、不显著,需 ~70 任务才有足够 power。所以阈值必须建在足够样本之上,否则 gate 在统计上不可信。
边界:gate 不是 schema 校验,也不是单测。 schema 校验(toolRegistry 的 -32602 INVALID_PARAMS)防畸形输入;单测防代码 bug;eval gate 防的是模型行为质量回归——三者层级不同,不可互相替代。一个 PR 可以单测全绿、schema 校验全过,却让模型行为质量退步(例如换了 prompt 模板后 AML 检出率掉了 8pp)——这种退步只有 eval gate 抓得到。
手算一遍 Cohen's κ(理解 minKappa 为何是 0.6)。 κ = (p_o − p_e) / (1 − p_e),p_o 是观测一致率,p_e 是偶然一致率。设 50 条标注里 LLM judge 与人工在 42 条上一致 → p_o = 0.84。若两者都倾向判 pass(各约 80% 判 pass),偶然一致率 p_e ≈ 0.8×0.8 + 0.2×0.2 = 0.68。则 κ = (0.84 − 0.68) / (1 − 0.68) = 0.16 / 0.32 = 0.50——低于阈值 0.6,gate 该判 judge 不够可信。要点:p_o 看着很高(84%)但扣掉偶然一致后 κ 只有 0.50,正说明为何不能只看「judge 和人工一致率高」就信它——κ 才是去偏后的真信号。minKappa=0.6 落在「中等到较好一致」区间(Landis-Koch 标度 0.61-0.80 为 substantial),是评测器可信度的合理底线。
为什么看跌幅而非绝对值(再强调)。 同一套 29 任务,今天 89.7% 明天 84% 是有意义的回归信号(掉 5.7pp);但「89.7% 是好是坏」依赖任务难度,跨任务集不可比。gate 只问「相对自己的上一版掉了多少」,所以基线必须是「同一套任务、同一评分口径」的快照——换了任务集就得重设基线。
2. 代码走读(src/agent/eval/gate.ts + scripts/eval-gate.ts)
gate.ts 是纯决策逻辑,scripts/eval-gate.ts 包 fs + exit。真实符号走读:
checkGate(current, baseline, thresholds)(gate.ts:31)——返回{ pass, violations }。三个把关维度:completionRate/judgeHumanKappa/unknownRate,对应阈值maxCompletionDrop(默认0.05)/minKappa/maxUnknownRate。- completion 检查仅在有基线时进行(gate.ts:36):
if (Number.isFinite(baseline.completionRate))——首次跑无基线,跳过(无可比对象)。有基线时算drop = baseline − current,drop > maxDrop则记 violation,detail 写明「dropped X pp > allowed Y pp」。 - fail-closed 的精确实现(gate.ts:37-38):配了阈值但
current.completionRate是 absent / NaN / Infinity → 直接记 violation'current completionRate absent or non-finite',绝不静默 green。κ(gate.ts:46-51)和 unknownRate(gate.ts:53-59)同样:阈值已配但当前值非有限 → violation。这是「损坏的 eval 默认判失败」的代码落点。 - κ 阈值(gate.ts:49):
current.judgeHumanKappa < thresholds.minKappa记 violation,detailkappa X < min Y。 pass = violations.length === 0(gate.ts:60)——任一维度违规即整体失败。- runner 侧
latestReport()(eval-gate.ts:11)——读agent-evals/reports/*.json里最新一份,抽出completionRate / unknownRate / judgeHumanKappa?.kappa。阈值在 eval-gate.ts:24 硬编码:{ maxCompletionDrop: 0.05, minKappa: 0.6, maxUnknownRate: 0.25 }。 - 基线缺失时的行为(eval-gate.ts:32):
agent-evals/baseline.json不存在则baseline = {},于是 completion 检查因「baseline 无 finite 值」被跳过——首次运行不会误拦,但也意味着在 baseline.json 就位前,gate 对 completion 的回归是无感的(仅 κ/unknown 按阈值生效)。这点决定了「选定基线」是让 gate 真正生效的前置动作。 judgeHumanKappa的解包(eval-gate.ts:21):报告里的形状是{ kappa: number } | null,runner 取r.judgeHumanKappa?.kappa。当前两份真实 run 里该字段是null(无人工标注),解包得undefined→Number.isFinite(undefined)为 false → 若minKappa已配则记 violation(fail-closed)。所以在 ≥50 标注就位前,若把 minKappa 真的接上,gate 会因 κ 缺失而恒红——这正是为什么 κ 校准(待数据)是让 minKappa 生效的前置,不能提前宣称 κ 这条已在把关。
为什么基线选「哪一份 run」很关键。 reports/ 现有两份 run,选作 baseline 的那份会成为「可信参照系」。若选了一份本身就异常偏低/偏高的 run 当基线,后续比对全部失真。选基线的纪律:挑一份「配置稳定、样本完整、人工抽检过」的 run——这也是为什么 baseline.json 要进 git 版本化(Day 68),让「当前用哪份当真值」可审计。
gate 决策表(穷举三阈值 × 当前值,便于核对实现)。 设 maxCompletionDrop=0.05, minKappa=0.6, maxUnknownRate=0.25:
| current vs baseline | completion 判定 | κ 判定 | unknown 判定 | 整体 |
|---|---|---|---|---|
| comp 0.897→0.870(drop 2.7pp), κ=0.7, unk=0.0 | 通过 | 通过 | 通过 | ✅ pass |
| comp 0.897→0.820(drop 7.7pp), κ=0.7, unk=0.0 | 违规 | 通过 | 通过 | ❌ fail |
| comp 0.897→0.880, κ=0.5, unk=0.0 | 通过 | 违规(<0.6) | 通过 | ❌ fail |
| comp 0.897→0.880, κ=null, unk=0.0 | 通过 | 违规(非有限,fail-closed) | 通过 | ❌ fail |
| comp 0.897→0.880, κ=0.7, unk=0.30 | 通过 | 通过 | 违规(>0.25) | ❌ fail |
| comp 无基线, κ=0.7, unk=0.0 | 跳过(无基线) | 通过 | 通过 | ✅ pass |
第 4 行正是当前现实:真实 run 的 judgeHumanKappa=null,一旦 minKappa 接上就会恒红——这是 κ 标注(待数据)成为前置的根因。
3. 今日实战
- 确认
gate.ts纯决策逻辑已就位(GateMetrics/GateThresholds/checkGate,fail-closed)。 - 确认
scripts/eval-gate.ts(pnpm eval:gate)包好 fs +process.exit,读agent-evals/reports/最新报告对比agent-evals/baseline.json。 - 跑 29-task 集(
pnpm eval:agent,需 key)产出一份基线报告,从agent-evals/reports/选定一份冻成agent-evals/baseline.json(当前该文件尚不存在,reports/ 已有两份真实 run)。 - 选基线纪律:挑「配置稳定 + 样本完整 + 抽检过」的 run,记录所选 run 的 commit(如
2ac7478)与 provider/model(deepseek-v4-pro)。 - 暂以
minKappa不接(或设为可选)跑通门禁的 completion+unknown 两条,待 ≥50 标注就位再接 κ——避免恒红。 - 提交
docs/aipa/day67-eval-gate.md记录 gate 公式、阈值选取理由、所选基线 run。 - 为
checkGate补/确认覆盖测试:三阈值各一条「过/不过」+ 一条「无基线跳过」+ 一条「κ=null fail-closed」,固化决策契约。
4. 今日实测 / 产出
gate.ts+eval-gate.ts:已 built+tested(纯逻辑无 key)。agent-evals/baseline.json:待跑(需 key 跑) —— gated on 选定一个基线 run;将含 pass-rate(参考实测 V4-Flash 79.3% / V4-Pro 89.7%,partial-credit 0.900)与 κ 数值。- judge-human κ:另需 ≥50 手工标注(待数据);当前真实 run 里
judgeHumanKappa = null(未校准),故minKappa=0.6这条目前无法生效,须等标注就位。 - 真实锚点(agent-evals/reports/ 已落两份 run):
n=29、completionRate=0.8965…(≈89.7% V4-Pro)、codePassRate=0.7931…(≈79.3%)、partialCreditMean=0.900、unknownRate=0、judgeHumanKappa=null。不得臆造新数字。
5. 常见误区 / 陷阱
- circular baseline(循环基线):用模型自身输出当真值来评自己——B14 的
groundTruthEval.ts已修此问题;基线必须是独立可信的真值,不能自证。 - fail-open 默认放行:eval 出错(NaN/缺失)时若静默判过,最危险的退步会从门缝溜走——gate.ts 已用「非有限即 violation」堵死。
- 小样本设紧阈值:N=29 时 Δ+10.3pp 都不显著(CI[0,20.7] 跨 0),此时设过紧阈值=制造噪声误拦;阈值要配足够 power 的样本(~70 任务)。
- 把 κ 当成已就位:当前
judgeHumanKappa=null,minKappa 这条实质未生效——别在笔记里假装 κ 已校准。 - 只看 completion 不看 κ:judge 漂了但分数没掉时,只盯 completion 会被骗——κ 是给评测器本身上的保险。
- 绝对值当阈值:「completion 必须 ≥ 0.85」这种绝对阈值跨任务集不可比,且会因换任务集而误判;gate 看的是相对基线的跌幅。
- 混淆 code-pass 与 completion:
codePassRate(cheap heuristic,79.3%)≠completionRate(judge 判,89.7%);gate 把关的是后者(judge + partial credit),别拿前者当门禁指标。
附:概念辨析(易混淆)
- gate vs 单测 vs schema 校验:单测查代码 bug;schema 校验(toolRegistry
-32602)查畸形输入;eval gate 查模型行为质量回归。三层正交,缺一不可互替。 - completionRate vs codePassRate vs partialCreditMean:completionRate=judge 判过率(89.7%);codePassRate=cheap heuristic 过率(79.3%);partialCreditMean=judge 的连续部分分均值(0.900)。gate 把关 completionRate。
- κ vs 一致率:一致率 p_o 含偶然成分(看着高);κ 扣掉偶然一致后才是真信号——p_o=0.84 可能 κ 只 0.50。
- fail-closed vs fail-open:前者「损坏/缺值默认判失败」,后者「出错默认放行」。eval 门禁必须 fail-closed。
- 基线快照 vs 实时重跑:gate 比对的是冻结的基线文件(确定、可复现),不是每次实时重跑模型(不确定、烧钱)。
附:本日实现的诚实边界
- 已建:
checkGate三维 fail-closed 决策(completion-drop / κ / unknown)+eval-gate.ts的 fs/exit 包装,纯逻辑无 key、已 tested。 - 待跑:
baseline.json选定(需 key 跑出基线 run);含真数值的 completion 比对。 - 待数据:judge-human κ 需 ≥50 手工标注(当前
judgeHumanKappa=null,minKappa 未实质生效)。 - 不可宣称:κ 已校准、baseline 已就位、completion 门禁已在把关——三者均未完成,只有决策逻辑就绪。
6. 学习资源(每条带 YYYY-MM)
- Anthropic, Demystifying evals(2026-01)— eval 可重复 / 有基线 / 含人类校准(κ)方法论。
- Cohen, J., A Coefficient of Agreement for Nominal Scales(1960,经典)— κ 定义,修正偶然一致。
- Landis & Koch, The Measurement of Observer Agreement for Categorical Data(Biometrics, 1977,经典)— κ 解读标度(0.61-0.80 substantial)。
- 本仓
src/agent/eval/gate.ts/scripts/eval-gate.ts(2026-06)— fail-closed 决策逻辑与 runner。 - 本仓
agent-evals/reports/run-2026-06-22T17-34-34-717Z.json(2026-06)— 真实 V4-Pro/Flash 基线数据来源(commit 2ac7478, n=29)。
SOTA检查 (2026-06 更新)
- 当前主流:Demystifying evals(2026-01)为现行方法论;fail-closed gate 是 2026 共识,仍 SOTA。
- 过时黑名单:circular baseline(模型自身输出当真值)——已被 B14
groundTruthEval.ts修复,禁止回退;non-blocking warning gate(失去门禁意义)。 - 待复查:κ 校准依赖 ≥50 人工标注(待数据);样本量未达 ~70 前,阈值须保守以免噪声误拦。
- judge 模型时效:当前 judgeModel=
deepseek-v4-flash(legacydeepseek-chat/-reasoner2026-07-24 退役,禁用旧 id);judge 模型换代时须重做 κ 校准,因为换 judge 等于换评分器。 - 下次复查点:标注集到 50 条后复核 minKappa 是否生效;任务集扩到 ~70 后重设 maxCompletionDrop;judge 模型升级时重校 κ。
自测题(讲得出才算掌握)
- 为什么 gate 看跌幅而非绝对值?(答:绝对值跨任务集不可比;跌幅是相对同一基线的回归信号。)
- fail-closed 在代码里怎么体现?(答:阈值已配但当前值非有限/缺失 → 直接记 violation,绝不静默 green。)
- p_o=0.84 时 κ 可能只有 0.50,说明什么?(答:一致率含偶然成分,κ 扣掉偶然后才是真信号;不能只看一致率信 judge。)
- 当前为什么不能直接接上 minKappa?(答:真实 run κ=null,接上会因 κ 缺失 fail-closed 恒红;需 ≥50 标注先校准。)
衔接
- 昨天:Day 66 — 成功路径端到端(scope 收敛 + sub/aud/jti 审计行)。
- 今天:eval CI gate 设计 = fail-closed + completion-drop ∧ κ 双条件,纯决策逻辑已 built+tested。
- 明天:Day 68 — GitHub Actions 接 eval 阻断,把
pnpm eval:gate变成 PR 合并门禁。