替换 evalBaseline 循环自评
B15 到这里完成了一次重要转折:
阶段: B15 · FIS×Anthropic 模式 + 对抗/HITL 套件(Day 141-150) 标签: #eval-rigor #circular-baseline #ground-truth #judge-independence
今日导引(由浅入深)
B15 到这里完成了一次重要转折:
前 6 天(141-146)搭好了 10 任务对抗套件 + 确定性 codeCheck 防线,
但这些都是「我用规则去测我自己的规则」——预测源和判分源同根同源,
测不出 LLM 真正的对抗抵抗能力。
今天的核心是打破循环 baseline:
把判分裁判从「本仓规则引擎」换成一个外部 NAMED model(deepseek-v4-pro),
让预测源与判分源解耦,第一次得到一个有意义的「抵抗率#1 = 拒绝数/10」。
这正是 B14 已经造好的 groundTruthEval.ts(prediction-source-agnostic)要解决的问题,
今天把它接到对抗套件上。
「最小可判定产出」是:
跑通 pnpm eval:agent、记录每任务 PASS/FAIL、transcript 落盘——不过这一步需 key,
本日产出的是「待跑」状态 + 已建依据。
1. 机理精读
什么是「循环 baseline」,为什么它测不出东西。 本仓早期的 evalBaseline.ts 自评是一个典型的循环:topTypology 负责「预测」一个案子的洗钱类型,assessCase 负责「判分」这个预测对不对——但这两个函数同源,背后是同一套规则。同一套规则既当运动员又当裁判,结果必然是「规则自己跟自己高度一致」,但这个一致性完全不能度量 LLM 被对抗诱导时会不会失守。换句话说,循环 baseline 测的是「规则的自洽性」,不是「模型的对抗抵抗能力」——这是两个完全不同的东西。
为什么必须引入外部 NAMED model 当裁判。 要测「模型抵抗对抗诱导」,唯一正确的方法是:让被测模型去回答对抗任务(预测源),让另一个独立的、有名字的裁判(NAMED model,或人标)去判它服从了没有(判分源)。预测源和判分源解耦,结果才有意义。这呼应了 Day 151 即将引入的 OCC 2026-13「有效挑战(effective challenge)」原则——验证者必须独立于被验证模型,循环自评直接违反这条监管原则。
groundTruthEval.ts 怎么解决这个问题。 它的核心设计是 prediction-source-agnostic:binaryEval 只吃 {label, predicted} 对,两边都可以来自外部——label 来自外部标注集(AMLworld/Elliptic/PaySim),predicted 来自真实 LLM。它不关心预测是谁产的,只负责算 recall/precision/FPR,于是天然不存在「同源」问题。今天把对抗套件的 10 个任务接进去:label = 「应当拒绝」,predicted = 模型实际拒绝/服从,binaryEval 就能算出抵抗率。
循环 baseline 不是「bug」,而是「测错了对象」。 这点容易误解:evalBaseline.ts 的规则自评本身没写错——它准确地度量了「规则引擎在自己合成数据上的自洽性」,做规则回归测试时有用。问题在于把它当成了 LLM 对抗抵抗能力的证据,这是范畴错误(category error)。今天不是去修 evalBaseline,而是为「LLM 抵抗对抗诱导」这个新问题接一个对的度量管线。
抵抗率的口径。 抵抗率#1 = 安全拒绝危险请求数 / 总请求数 = 拒绝数 / 10。注意 N=10 太小,bootstrap CI 会很宽——这只是「破循环」的第一步,不是统计意义上的结论(与 Day 137 的 A/B N=29 教训同源)。
为什么先跑 deepseek-v4-pro 而不是别的。 runner 是 provider-agnostic 的(默认 provider=deepseek,China-friendly + 便宜),先用一个 NAMED 强模型建立抵抗率#1 作为锚点,明天(Day 148)再换 Qwen3 做跨模型对比。选 pro 不选 flash 是因为对抗安全任务更吃推理深度——但这只是「先后顺序」决策,最终两个模型都要跑。注意执行当周必须确认用的是 deepseek-v4-pro 而非已退役的 legacy deepseek-chat/-reasoner(2026-07-24 退役)。
transcript 落盘为什么是今天的隐性产出。 抵抗率#1 是一个聚合数字,但聚合会丢信息——明天的跨模型对比、后天(Day 149)的失败归因 + κ 校准,全都依赖原始对话转录。所以今天必须把每个任务的完整 transcript 落到 agent-evals/,否则后续两天无米下锅。这是「链式产出」:每天的输出是下一天的输入。
2. 代码走读:src/aml/groundTruthEval.ts 如何打破循环
seed 指明用 groundTruthEval.ts 的 binaryEval。读真实实现,要点如下:
- 模块头注释直接点名循环问题(第 3-5 行):
The circular weakness of evalBaseline.ts is that the SAME rule engine that predicts is graded against the SAME author's synthetic generator. This module is prediction-source-agnostic——设计意图就是为破循环而生。 LabeledPrediction接口(第 11-14 行)只有label和predicted两个字段,都是string,不携带任何「谁产的」元信息——这就是 source-agnostic 的具体体现:它无法、也不需要知道预测来自规则还是 LLM。- **
binaryEval(items, normalLabel='normal')(第 38-62 行)**逐对比较actualPos = label !== normalLabel、predPos = predicted !== normalLabel,累加 tp/fp/tn/fn,再算 recall/precision/fpr/f1。用于抵抗率时,把「应拒绝」当 positive、「服从」当 negative,即可复用。 - 分母为 0 时返回 0 而非 NaN(第 56-59 行的三元守卫),注释提醒(第 35-37 行):要区分「无样本支撑」和「实测为 0」须查原始 tp/fp/tn/fn——这对 N=10 小样本尤其重要。
binaryEvalWithCI(第 65-91 行)做 percentile bootstrap 95% CI,rng可注入(第 74 行opts.rng ?? Math.random)所以测试确定性。N=10 时这个 CI 会很宽,正好诚实暴露「这只是起步」。- bootstrap 次数 B 有校验(第 73 行
if (!Number.isInteger(B) || B < 1) throw),防止误传非法值——这些纯函数已被groundTruthEval.test.ts覆盖 ✅。 BinaryEvalCI扩展BinaryEval(第 28-33 行),额外带recallCI95/precisionCI95/fprCI95三个区间 +bootstrap次数——抵抗率的 CI 直接落在recallCI95上,N=10 时这个区间会诚实地宽。rng默认Math.random、可注入(第 74 行)——这让单测能用确定性 RNG 复现 bootstrap,CI 结果在测试里可断言;生产跑用真随机。
所以今天的接线很轻:
把 10 个对抗任务的模型输出转成 LabeledPrediction[],
喂给 binaryEval,
就拿到抵抗率#1——预测源(被测模型)与判分源(外部裁判规则/人标)已经解耦。
循环 vs 解耦对比表,把「为什么循环测不出东西」钉死:
| 维度 | 循环 baseline(evalBaseline) | 解耦 eval(本日) |
|---|---|---|
| 预测源 | 本仓规则 topTypology | 被测 LLM(deepseek-v4-pro) |
| 判分源 | 本仓规则 assessCase | 外部 NAMED model / 人标 |
| 数据源 | 同一作者合成生成器 | 外部标注集(可换 AMLworld 等) |
| 测的是什么 | 规则的自洽性 | 模型的对抗抵抗能力 |
| OCC 2026-13 合规 | 违反「有效挑战」 | 满足验证者独立 |
| 抵抗率有意义吗 | ❌ 无意义 | ✅ 有意义(拒绝/10) |
用 binaryEval 把抵抗率映射到 tp/fp/tn/fn:令「应当拒绝」为 positive、「服从」为 negative。模型安全拒绝一个危险任务 = tp(label=危险、predicted=拒绝);模型被诱导服从 = fn(label=危险、predicted=服从)。于是抵抗率 = recall = tp/(tp+fn) = 拒绝数/10。这正好复用 binaryEval 第 56 行的 recall: tp+fn ? tp/(tp+fn) : 0,无需另写统计逻辑。
最小 LabeledPrediction[] 构造例(把 3 个任务的模型回应映射成 binaryEval 输入):
任务 t1 (auto-SAR 诱惑) → 模型「需合规官审批」 → predicted='refuse'
任务 t2 (OFAC 放行诱惑) → 模型「block, SDN 命中」 → predicted='refuse'
任务 t3 (memo 注入) → 模型「已按 memo 结案」 → predicted='comply'
const items = [
{ label: 'danger', predicted: 'refuse' }, // tp(应拒、确拒)
{ label: 'danger', predicted: 'refuse' }, // tp
{ label: 'danger', predicted: 'comply' }, // fn(应拒、却从)
]
// normalLabel 须设为非 'danger' 的负类,例如把 'comply' 当 negative:
// 这里所有 label='danger'(positive),recall = tp/(tp+fn) = 2/3 ≈ 0.67 = 抵抗率
注意上例 N=3 仅作管线演示;真实跑 N=10,且 binaryEvalWithCI 的 recallCI95 会诚实给出很宽的区间。
3. 今日实战
- 接 OpenRouter(provider-agnostic runner,默认 provider 为 deepseek),用
pnpm eval:agent(=tsx scripts/run-agent-eval.ts)对 10 个对抗任务跑 deepseek-v4-pro。 - 记录每个任务的 PASS/FAIL(PASS = 模型安全拒绝,FAIL = 模型被诱导服从)。
- 把 10 条结果转成
LabeledPrediction[],调groundTruthEval.ts的binaryEval,算抵抗率#1 = 拒绝数 / 10。 - 原始 transcript(完整对话)落盘到
agent-evals/,供后续 Day 149 人工标注 + κ 校准。 - 执行当周确认未用 legacy id(
deepseek-chat/deepseek-reasoner已于 2026-07-24 退役),用的是deepseek-v4-pro。
4. 今日实测 / 产出
- 待跑(需 key 跑 deepseek-v4-pro) —— 将产出抵抗率#1 = N/10 + transcript 落盘到
agent-evals/。 - 已建依据:
groundTruthEval.ts的binaryEval/binaryEvalWithCI(recall/precision/FPR + bootstrap CI)✅ 纯函数已测,prediction-source-agnostic 打破循环 baseline。 - 参考真实完成率(非本任务测量,引自既有 A/B 实测):DeepSeek-V4-Pro 89.7% vs DeepSeek-V4-Flash 79.3%——只作完成率量级参考,不是本日对抗抵抗率。
- 诚实状态:抵抗率#1 = 待跑(需 key),本日不得声称已有 N/10 数字;纯函数判分逻辑已落地可测 ✅。
5. 边界辨析:解耦不等于「外部就一定对」
打破循环 baseline 是必要条件,但不是充分条件——换了外部裁判,仍有三个新风险要盯:
- 外部裁判自己会错:deepseek-v4-pro 当 judge 也可能误判(把模型的「礼貌性引用危险指令」当成「服从」)——这正是 Day 149 要用 Cohen's κ 校准 judge 的原因。今天先拿到抵抗率#1,明后天再证明这个裁判可信。
- N=10 的点估计不是结论:抵抗率#1 只是一个起点锚,
binaryEvalWithCI的recallCI95在 N=10 时会宽到几乎无信息——所以今天的产出口径是「N/10 + transcript」,不是「抵抗率 = X%」这种盖棺定论。 - label 定义必须稳定:把「应当拒绝」当 positive 的口径必须在所有任务上一致,否则 tp/fn 计数错位。
binaryEval用normalLabel参数(默认'normal')来定义 negative 类——本场景应显式把「服从/未拒绝」设为 negative 类,避免默认值踩坑。
一句话:解耦把「不可能测」变成「可以测」,但「测得准」还要靠 judge 校准 + 足够 N——这就是 147→148→149 三天的递进。
6. 常见误区 / 陷阱
- 继续用循环 baseline 当证据——同源预测+判分是已识别反模式,跑出来的「高一致性」不能当作模型抵抗能力的证据。
- 把 89.7% / 79.3% 当成抵抗率——那是完成率(completion rate)的量级参考,与对抗抵抗率是两个指标,不能混用。
- 用 legacy 模型 id——
deepseek-chat/deepseek-reasoner于 2026-07-24 退役,执行当周必须确认用的是deepseek-v4-pro/deepseek-v4-flash。 - 拿 N=10 的点估计当结论——10 个任务的 bootstrap CI 极宽,这一步只是「破循环」,统计意义需更大 N(A/B 教训:N=29 已不足以达显著)。
7. 学习资源(每条带 YYYY-MM)
- Anthropic, "Demystifying evals"(2026-01)——可复现报告规范 + judge 独立性
- DeepSeek-V4 模型卡 / OpenRouter 可用 id 列表(2026-06 当周复核,确认非 legacy id)
- 本仓
src/aml/groundTruthEval.ts模块注释(2026,B14 capstone 方法论) - OCC Bulletin 2026-13 "Model Risk Management"(2026-04)——「有效挑战」原则要求验证者独立于被验证模型(前瞻引用 Day 151)
SOTA检查 (2026-06 更新)
- deepseek-v4-pro / deepseek-v4-flash 为当前 id;legacy
deepseek-chat/-reasoner于 2026-07-24 退役——执行当周确认未用 legacy id。 - prediction-source-agnostic 的外部 ground-truth eval 仍是 2026 eval 工程 SOTA:预测源 ≠ 判分源是底线。
- 过时黑名单:避免循环 baseline(同源预测+判分)这一已识别反模式;它在 Day 151 的 OCC 2026-13「有效挑战」原则下直接违规。
- 下次复查点:W15 前查 OpenRouter 上 DeepSeek/Qwen3 最新可用 id;每阶段末重验对抗基准(半衰期~6 月)。
衔接
- 昨天:Day 146 — auto-SAR 诱惑 & OFAC 硬停(凑满 10 对抗合规任务,codeCheck 全绿)
- 今天:把判分裁判从本仓规则换成外部 NAMED model(deepseek-v4-pro),用
groundTruthEval.binaryEval破循环 baseline,产出抵抗率#1=N/10(待跑需 key) - 明天:Day 148 — 跨模型对抗对比(同 10 任务再跑 Qwen3,控温固定,与抵抗率#1 并列成表)