每日 evals 防退化固化 — daily runner、退化告警与 CI gate
每日 evals 防退化固化 — daily runner、退化告警与 CI gate
日期: 2026-08-29 阶段: Phase 3 - AML 调查 Copilot 标签: #daily-evals #regression-gate #ci #drift-alert
核心问题
W11 到这里,证据/类型学/SAR 三组 evaluator 都已经写好(P1 沉淀的 evalChecks 代码型检查 + P3 的 judge),judge 也用 Day 17 的 Cohen's κ 校准过。但还剩一个最容易被忽略、却最致命的缺口:这些 eval 只在我手动跑 npx vitest 时才跑。
这意味着退化在两种时机会悄无声息地溜进来:
- 代码变更:改了
sarDraft.ts的段落拼装、动了typology.ts的阈值——本地忘跑全套 eval 就 merge。 - 模型漂移:供应商静默换了权重(Day 17 SOTA 检查里已警示的「κ 漂移」同源威胁),我这边一行代码没动,SAR 质量却跌了——没有定时跑,根本看不见。
今天回答三个问题:(A) 三组 evals 怎么固化成每日自动跑的 daily runner,PR 跑什么、夜间跑什么、为什么分层;(B) 退化怎么从「人眼看趋势」变成可阻断 merge 的硬告警——给出退化判定的统计方法与阈值表;(C) 这套 daily runner 怎么和 P1 已有的 evalChecks.ts + CI 衔接,不另造一套。
贯穿全篇一个判断:eval 不是「写出来」就完事,是要「定时自动跑 + 退化即阻断」才算落地。 一个躺在仓库里、只在我记得时才跑的 eval suite,对抗模型漂移的能力等于零。
关键内容
A. 三层分跑:PR 快检 / 夜间全扫 / canary——为什么不能只跑一层
2026 的工程共识(FutureAGI CI/CD eval 工作流,2026-03-31 发布 / 2026-05-20 更新)把 eval 按成本-频率-覆盖切成三层,绝不在每个 PR 上跑全量 judge:
┌─ 每个 PR(path-scoped 快检) ──────────────────┐
│ · 确定性代码检查 + 分类器级联(无 LLM 或廉价 LLM)│
│ · 仅跑受改动影响的路由:100-200 例 │
│ · 硬约束:<5 分钟、~$0.20/PR │
│ · 失败即 hard-fail PR(exit 2) │
└────────────────────────────────────────────────┘
│ merge 后
▼
┌─ 夜间(scheduled cron,全量 judge 扫) ─────────┐
│ · 完整语料 LLM-judge sweep:500-2000 例/路由 │
│ · 产出滚动 7 天基线 JSON │
│ · 失败 → Slack webhook 告警(最低限度) │
└────────────────────────────────────────────────┘
│ 部署后
▼
┌─ canary(1-5% 生产流量) ───────────────────────┐
│ · 同一 rubric 库给 candidate 与 baseline 双打分 │
│ · 持续漂移 >2-3 个百分点 / 15-60 分钟 → 自动回滚 │
└────────────────────────────────────────────────┘
为什么必须分层,FutureAGI 给了一句话定律:
反直觉洞察①(CI eval 三角不可能,硬选俩):「CI eval on every PR has to be cheap, fast, and statistically significant. Pick any two and the gate is theater.」(FutureAGI, 2026-03)——廉价 + 快 + 统计显著,三者只能取其二。想在每个 PR 上跑全量 judge 求统计显著,就既不廉价也不快,开发者会嫌慢而绕过 gate,gate 沦为摆设(theater)。正解不是「三个都要」,而是把它们拆到不同层:PR 层要快和廉价(确定性检查,放弃单 PR 的统计显著性),夜间层要统计显著(全量、慢、贵都能接受,因为不阻塞人)。把这个三角硬塞进一层,是 eval 落地最常见的死法。
对本项目(静态站、无生产流量)的裁剪:canary 层 P3 无法落(没有 1-5% 真实流量可分流),但 PR 快检 + 夜间全扫这两层完全可落——PR 层就是 P1 已有的 codeCheckPassRate(纯确定性、毫秒级、零 LLM 成本),夜间层是新加的 daily runner,把三组 evaluator 在全量金标(66 案 + P3 扩集)上跑一遍并落基线。
三组 evaluator 与三层的映射:
| evaluator 组 | 判定方式 | PR 快检 | 夜间全扫 | 退化关注的核心指标 |
|---|---|---|---|---|
| 证据组 | 确定性(cited_tx_exist / cited_tx_nonempty) | ✅ | ✅ | 引用幻觉率、证据非空率 |
| 类型学组 | 确定性(top_typology_consistent + 金标 recall/FPR) | ✅ | ✅ | per-typology recall、normal FPR |
| SAR 组 | LLM-judge(语义:faithfulness/coverage) | ❌ 太贵 | ✅ | judge 通过率、judge×人工 κ |
证据组与类型学组是确定性的,两层都跑;SAR 组是 LLM-judge,只在夜间全扫跑——这正是 A 节三角定律的直接应用:judge 既不廉价也不快,放进 PR 层会让每次 commit 都慢且烧 token,所以下放到夜间。
B. 退化判定:从「看趋势」到「可阻断 merge 的硬告警」
退化告警最业余的做法是「画个折线图,人眼看着跌了就警觉」。问题是 eval 分数本身有噪声——judge 重跑、采样抖动都会让分数上下飘几个点,把噪声当退化会让 gate 频繁误报、最终被无视。必须用统计方法把「真退化」和「噪声抖动」分开。
FutureAGI 的退化判定逻辑(2026-03)是一个双条件合取,缺一不可:
function isRegression(candidate, baseline, rubric):
delta = baseline.score - candidate.score # 跌了多少
# 条件①:统计显著(连续指标用 Welch's t-test,二元用 z-test)
p = welch_t_test(candidate.samples, baseline.samples)
# 条件②:效应量超过该 rubric 的噪声地板(~0.03 on 0-1 scale)
significant = (p < 0.05) AND (delta > rubric.noise_floor) # ≈ 0.03
return significant
两个条件都要满足才算退化:p < 0.05(不是随机抖动)且 delta > 0.03(跌幅超过该 rubric 的噪声地板)。只满足 p 值不够——大样本下哪怕 0.005 的微跌也能统计显著,但那在实践上无意义;只满足跌幅不够——一次抖动也可能跌 0.04 但不显著。合取才能既不漏真退化、又不被噪声刷屏。
配套两个进阶技巧(FutureAGI):
- 配对自助法(bootstrap CI on paired deltas):candidate 和 baseline 跑同一批样本,对配对差值做自助置信区间——比独立双样本检验方差更紧,更早抓到真退化。本项目金标固定 66 案,天然适合配对。
- 尾部用 p95 而非均值:对「长尾失败」型 rubric(少数案件烂得离谱、均值被多数好案件撑住),gate 用
p95_score而非 mean——均值会掩盖尾部塌陷。AML 的幻觉就是这种尾部分布:99% 的 SAR 不幻觉、1% 凭空捏造交易,均值看不出,p95 一抓一个准。
退化告警阈值表(本项目设计口径,绝对地板对齐 P1 的 CI 门槛,相对退化对齐 FutureAGI):
| 指标 | 绝对地板(跌破即 hard-fail) | 相对退化(vs 7 日基线) | 噪声地板 | 触发动作 |
|---|---|---|---|---|
| structuring recall | ≥ 0.85(P1 既定) | Δ < −0.03 且 p<0.05 | ~0.03 | 阻断 merge(exit 2) |
| layering / mule recall | ≥ 0.80 | Δ < −0.03 且 p<0.05 | ~0.03 | 阻断 merge |
| normal FPR | ≤ 0.15 | Δ > +0.05 且 p<0.05 | ~0.03 | 阻断 merge |
| 证据引用幻觉率(p95) | = 0(任一幻觉即红) | 任一新增即报 | 0 | hard-fail(critical) |
| SAR judge 通过率 | ≥ 基线 | Δ < −0.03 且 p<0.05 | ~0.03 | Slack 告警(夜间) |
| judge×人工 κ | ≥ 0.6(Day 17) | 跌破 0.6 | — | judge 降级回人工 |
退化的退出码契约(直接复用 FutureAGI 的 exit code 设计,落进 CI):
| exit code | 含义 | CI 策略 |
|---|---|---|
| 0 | 全过 | merge 放行 |
| 2 | 断言失败(跌破绝对地板 / 出现幻觉) | hard-fail PR |
| 3 | 警告(--strict 下的相对退化) | Slack 通知、不阻断 |
| 6 | judge API 错误 | 重试,持续则报错 |
| 7 | 超时 | 提高 timeout 或分片 |
关键设计:幻觉是 exit 2(hard-fail),SAR judge 退化是 exit 3(告警不阻断)。理由——幻觉是 critical(凭空捏造交易,AML 绝对红线,见 failureTaxonomy 的 hallucination severity=critical),任何新增即阻断;而 judge 语义评分有主观性、可能是 judge 自身漂移而非产物退化,先告警人工看一眼,不直接卡死 merge。
反直觉洞察②(aggregate 涨了,仍可能必须阻断 merge):最隐蔽的退化是总分涨、关键 cohort 跌。FutureAGI 给的真实例子:「aggregate pass rate improves from 0.91 to 0.93, but the refund cohort drops from 0.94 to 0.83 on ToolSelectionAccuracy」——总通过率从 0.91 涨到 0.93,看着是改进,但退款 cohort 在工具选择上从 0.94 崩到 0.83。映射到 AML:整体 SAR 通过率涨了,但 structuring 这一类的 recall 从 1.0 跌到 0.88——总分被 layering/mule/normal 的好表现撑住,structuring 的塌陷被平均掉了。所以 gate 绝不能只看 aggregate,必须先暴露 per-evaluator / per-cohort delta,再做聚合 pass/fail 决策(FutureAGI:「expose per-evaluator deltas before any aggregate pass/fail decision」「keep the baseline run immutable」)。一个能改进聚合却让某个 release-critical cohort 退化的 candidate,必须被拦下——哪怕总分更好看。
C. 与 P1 evalChecks + CI 的衔接:不另造一套
daily runner 不是新发明一套 eval,而是把 P1 已沉淀的资产调度起来 + 加退化对比。三处衔接:
衔接①——复用 codeCheckPassRate 作 PR 快检层。evalChecks.ts 的 codeCheckPassRate(dataset, assess, draft) 已经在全 66 案上跑五类确定性检查并按 checkId/failuresByClass 聚合——这就是 A 节的 PR 快检层,毫秒级、零 LLM 成本、已进测试。daily runner 的证据组与类型学组直接调它,不重写。
衔接②——evalBaseline 产出即「不可变基线」。evalRuleBaseline(ds) 已产出 per-typology recall + normalFPR + confusion——这正是 B 节退化对比要的「上一次通过的基线」。daily runner 把每晚的 BaselineEval 落盘成带日期的 JSON(evalBaseline-YYYY-MM-DD.json),形成 FutureAGI 说的「rolling 7-day baseline」;对比时基线行只增不改(immutable baseline——呼应 Day 75 审计 trail 的 append-only 同源思想:基线被悄悄改写 = 退化检测失效)。
衔接③——退化报告挂回 failureTaxonomy 归桶。退化不只报「分数跌了」,还要报「跌在哪类失败」——daily runner 把跌破的检查按 relatedFailure 字段(evalChecks 里每条 CheckResult 已带)归到 6 类 taxonomy,输出「本次退化新增 N 例 hallucination / M 例 typology_misjudge」,直接对接 Day 17 的分歧驱动迭代闭环(知道跌在哪类,才知道改 rubric 的哪一段)。
daily runner 的执行步骤(伪代码,全确定性可单测,judge 段在无 key 时诚实降级):
function runDailyEvals(dataset, prevBaseline):
# 1) 确定性层(证据 + 类型学)——复用 P1
code = codeCheckPassRate(dataset, assessCase, draftSar)
base = evalRuleBaseline(dataset)
# 2) judge 层(SAR 语义)——无 key 时跳过并标记 skipped,不伪造分数
sar = hasJudgeKey() ? runSarJudge(dataset) : { skipped: true }
# 3) 退化对比(B 节双条件)
regs = []
for metric in [recall_struct, recall_layer, recall_mule, fpr_normal, halluc_p95]:
if isRegression(code/base[metric], prevBaseline[metric], NOISE_FLOOR):
regs.push({ metric, delta, severity, relatedFailure })
# 4) 落基线 + 定退出码
writeBaseline(`evalBaseline-${today}.json`, { code, base, sar })
return exitCode(regs) # 有 critical → 2;仅 warning → 3;全过 → 0
设计要点/决策表
| 要点 | 决策 | 理由 |
|---|---|---|
| 分层 | PR 快检(确定性)/ 夜间全扫(judge)/ canary(P3 无流量,不落) | 三角不可能,硬选俩(洞察①) |
| PR 层跑什么 | 仅 codeCheckPassRate(毫秒级、零 LLM) | 快+廉价,放弃单 PR 统计显著性 |
| 夜间层跑什么 | 全量金标 + SAR judge + 落 7 日基线 | 慢/贵可接受,换统计显著 |
| 退化判定 | p<0.05 且 Δ>噪声地板(~0.03) 双条件合取 | 缺一则误报或漏报 |
| 配对检验 | bootstrap CI on paired deltas(同 66 案) | 方差更紧,更早抓真退化 |
| 尾部指标 | 幻觉率用 p95 非 mean | 均值掩盖尾部塌陷 |
| 聚合纪律 | 先暴露 per-cohort delta,再聚合判定 | aggregate 涨可掩盖 cohort 跌(洞察②) |
| 退出码 | 幻觉=exit2 阻断;judge 退化=exit3 告警 | 幻觉 critical 必拦,judge 主观先告警 |
| 基线 | append-only、带日期落盘、只增不改 | 基线被改 = 退化检测失效(呼应 Day 75) |
| 不另造 | 复用 codeCheckPassRate/evalBaseline/failureTaxonomy | daily runner 是调度器不是新 eval |
对本项目的落地
- 新建
src/aml/dailyEvalRunner.ts:导出runDailyEvals(dataset, prevBaseline) → { code, base, sar, regressions, exitCode }(实现 C 节伪代码)。证据组/类型学组直接调codeCheckPassRate(evalChecks.ts)与evalRuleBaseline(evalBaseline.ts),不重写检查逻辑;SAR judge 段在无 API key 时返回{ skipped: true }并不伪造分数(沿用evalChecks.ts头注的「无 key 诚实降级」纪律)。 - 退化判定
detectRegression(candidate, baseline, noiseFloor=0.03):实现 B 节双条件合取。统计检验 v1 用保守占位——固定 66 案样本量小,先用「Δ > 噪声地板 + 方向判定」做确定性判定,Welch's t-test / bootstrap CI 标注为「P3 扩集(≥100 案)后接入」,不谎称已做统计显著性检验(小样本下 t-test 本就不可靠,诚实标注比假装严谨更重要)。 - 基线落盘:
writeBaseline()把每次BaselineEval+ 代码检查汇总 + judge 结果序列化为evalBaseline-YYYY-MM-DD.json(结构对齐evalBaseline.ts的BaselineEval),形成滚动 7 日窗口;基线文件只增不改,与 Day 75 审计 trail 的 append-only 同源——基线可被悄改,退化检测就失去意义。 - 退化报告归桶:退化项按
evalChecks已带的relatedFailure字段归到failureTaxonomy.ts的 6 类,输出「本次新增 N 例 hallucination / M 例 typology_misjudge」,喂给 Day 17 分歧驱动闭环(定位改 rubric 哪一段)。 - CI 接线:在测试里断言
runDailyEvals(getGoldenDataset(), lastBaseline).exitCode !== 2(无 critical 退化)才允许 merge;退出码契约对齐 B 节表(幻觉=2 阻断、judge 退化=3 告警)。诚实标注:cron 夜间调度 + Slack webhook 为 P3 上线后的运营动作(当前静态站无后端调度),W11 仅落「可被 CI/手动触发的 runner 函数 + 退化对比 + 基线落盘」纯 TS 实现,不谎称已有夜间定时任务在跑。
参考资料
- FutureAGI — CI/CD LLM Eval with GitHub Actions: 2026 Workflow(PR 快检 100-200 例 <5min ~$0.20、夜间全扫 500-2000 例/路由出 7 日基线、canary 1-5% 流量漂移 >2-3pp/15-60min 自动回滚;退化判定
p<0.05且效应量 > 噪声地板 ~0.03;Welch's t-test 连续/z-test 二元;p95 尾部指标;退出码契约 0/2/3/6/7;「cheap, fast, statistically significant — pick any two and the gate is theater」)(2026-03-31 发布 / 2026-05-20 更新) - FutureAGI — LLM Regression Testing Guide(回归=受控对比「same rows, same evaluators, same metric thresholds, new candidate」;immutable baseline;「expose per-evaluator deltas before any aggregate pass/fail decision」;真实反例「aggregate 0.91→0.93 但 refund cohort 0.94→0.83 on ToolSelectionAccuracy」;控制重跑 ±0.03 噪声;供应商静默更新 2-4pp 漂移;versioned dataset 行只增不改)(2026)
- testquality.com — LLM Regression Testing Pipeline for QA Engineers: RAG Triad & Gold Sets in 2026(每 PR 跑 Gold Set + LLM-judge、对比 main 分支出回归报告)(2026)
- EY — Rethinking transaction monitoring for AML(告警量激增、95%+ FP、投资者复核能力受限的结构性压力;持续控制监控含场景自动化测试 + 漂移检测 + controls-health 看板)(2026)
- 本仓库
src/aml/evalChecks.ts(codeCheckPassRate/relatedFailure归桶)、src/aml/evalBaseline.ts(evalRuleBaseline产 per-typology recall/FPR/confusion)、src/aml/failureTaxonomy.ts(6 类 + severity)、docs/aipa/day17-judge-calibration.md(κ 漂移监控)、docs/aipa/day75-immutable-trail.md(append-only 基线同源)(2026-06~08)
SOTA 检查 (2026-08-29 / 复核基线 2026-06-11)
- 三层分跑(PR 快检 / 夜间全扫 / canary)是 2026 现行主流:FutureAGI(2026-03/05)、testquality(2026)、Adaline 完整指南口径一致——「确定性检查上 PR、全量 judge 上夜间、统计门控 + 自动回滚上 canary」已是 table-stakes,未见替代架构。本项目无生产流量,canary 层不落,PR + 夜间两层可落。
- 退化判定「p<0.05 且 Δ>噪声地板」双条件为现行做法:单看 p 值(大样本微跌也显著)或单看跌幅(一次抖动也可能超阈)都会误判,合取是共识。本项目诚实标注:固定 66 案样本量小,v1 用「Δ>噪声地板 + 方向」确定性判定,t-test/bootstrap 留待 P3 扩集(≥100 案)接入——小样本硬套 t-test 是伪严谨。
- 「先 per-cohort delta 再聚合」是本篇最易被忽略的活踩坑点:FutureAGI 的 0.91→0.93 / cohort 0.94→0.83 反例(洞察②)说明只看 aggregate 的 gate 会放过 release-critical cohort 退化;映射 AML 即「总通过率涨、structuring recall 跌」。任何只断言总分的 CI gate 都不合格。
- 过时认知警示:①「eval 写出来就算落地」过时——不定时自动跑就抓不住模型漂移;②「每个 PR 跑全量 judge」过时——三角不可能,judge 下放夜间;③「看折线图判退化」过时——噪声会刷屏,须统计门控;④「基线可滚动覆盖」过时——基线须 append-only,被改即检测失效。
- 待跟踪:P3 扩集到 ≥100 案后,评估 Welch's t-test / 配对 bootstrap CI 是否接入(样本量是否足够支撑统计显著性判定);canary 层在「若 P3 上线后有真实流量」时是否补;judge 漂移(Day 17 κ 重算节奏)与本日 daily runner 的调度合并为同一夜间任务。