返回 AICAP-180
B14 · Day 139外部 ground-truth AML eval

替换循环 baseline

在 B1→B18 的能力曲线上,今天是 B14「外部 ground-truth」段的核心动作——把「规则引擎评测自己造的数据」这个循环亲手杀掉。前面整个 block(Day 131-138)都是为这一刀做准备:列结构映射、去偏 loader、judge 校准、混淆矩阵、CI、交叉验证,全是在搭「用外部真标签评测」的脚手架。今天把 src/aml/evalBaseline.ts 里 assessCa

阶段: B14 · 外部 ground-truth AML eval(Day 131-140) 标签: #external-validity #kill-the-loop #eval-contamination #amlworld

今日导引(由浅入深)

在 B1→B18 的能力曲线上,今天是 B14「外部 ground-truth」段的核心动作——把「规则引擎评测自己造的数据」这个循环亲手杀掉。前面整个 block(Day 131-138)都是为这一刀做准备:列结构映射、去偏 loader、judge 校准、混淆矩阵、CI、交叉验证,全是在搭「用外部真标签评测」的脚手架。今天把 src/aml/evalBaseline.tsassessCase 喂的自造金标换成 AMLworld 真 Is_Laundering 标签,让同一套规则在外部标注集上重测,暴露真实掉分——day131 那个虚高 recall 在外部集上预期会显著下降。今天的最小可判定产出是:改后测试绿 + 规则基线「真标注集」指标。其中代码改动 + fixture 单测可先跑绿,但规则基线在真 AMLworld 上的掉分幅度仍待数据产生,不预填

一句话点题:Day 136-138 换的是预测来源(用 LLM judge 当预测、换裁判),今天换的是金标来源(label 从自造 generator 换成外部 AMLworld)。两者合起来才把「自己造数据、自己评自己」的双重循环彻底拆掉——预测端解耦(任意来源)+ 金标端解耦(外部标注),这才是 prediction-source-agnostic 设计的完整兑现。

1. 机理精读

什么是「循环 baseline」。 evalBaseline.ts 的原始设计:用 assessCasetopTypology 当预测,对本仓 generator.ts 合成的金标算召回率。问题在于——预测和金标出自同一作者的同一套世界观:generator 按某些洗钱模式造数据,typology 规则按同样的模式去抓。规则当然抓得准,因为数据就是照着规则的假设造的。这测的不是「规则在真实世界多有用」,而是「规则和生成器自洽不自洽」。groundTruthEval.ts 的注释把这叫「规则引擎评测自己的数据」的 circularity——recall 虚高是结构性必然,不是规则真的强。

把这个循环画成一句话:generator(造数据的假设)→ c.label(金标);同一套假设 → assessCase(规则)→ predicted。两条路从同一个源头出发,在 binaryEval 里相遇,自然高度一致。今天要做的就是斩断左边那条——让 label 不再来自 generator,而来自外部 AMLworld。右边的 assessCase 一字不改,让它在陌生的金标上裸奔,真实水平才显形。

外部效度(external validity)为什么是评测的命门。 一个评测的价值在于它的结论能不能外推到「没见过的真实数据」。循环 baseline 的内部效度可以很好(规则确实匹配生成器假设),但外部效度为零——换一份真实标注集,结论立刻崩。Anthropic《Demystifying evals》(2026-01)把外部效度列为评测可信的硬要求:评测集必须独立于被评系统的设计来源。今天用 AMLworld(IBM 公开标注,2024-04)这个外部金标重测,正是把内部自洽的虚高数字打回真实档位。

用一个反例把「循环」讲透。 设想极端情形:generator 只造「恰好 3 笔、恰好 $9,000、恰好 10 天内」的结构化案,checkStruct01 也正好按这套阈值写。那么在合成集上 recall 会接近 1.0——但这 1.0 毫无意义,它只证明「规则和生成器是同一个人写的」。把同一套规则放到 AMLworld,真实结构化五花八门(2 笔的、$7,500 的、跨 20 天的),checkStruct01 大面积漏报,recall 可能从 1.0 掉到 0.5 以下。这个掉幅不是规则变差了,而是原来的高分是假的——它从来没测过真实世界。这就是为什么「内部效度高」可以和「外部效度为零」并存,也是循环 baseline 最危险的地方:它给你一个漂亮但骗人的数字。

为什么预期掉分。 真实 AMLworld 的洗钱模式比 generator 造的丰富、噪声更多、贴线规避手法更多样。assessCase 的规则是按本仓设定的阈值(CTR $10,000、贴线带 $8,000、窗口 10/14 天等)写死的——这些阈值在真实分布上不一定对齐,会漏掉 generator 没覆盖的洗钱形态(recall 掉),也可能误报真实分布里的正常大额交易(FPR 升)。掉幅本身就是诊断信息:掉得越多,说明原来的虚高越严重、规则越是「过拟合到自己的生成器」。

掉分是「特征」不是「bug」——它是评测诚实的证据。 一个常见的心理陷阱是把外部集上的掉分当成「我的系统退步了」而想去补救。恰恰相反:掉分意味着评测终于开始反映真实世界,原来的高分才是问题。正确的姿态是把掉分当诊断信号——按规则逐条拆解「哪条规则在外部集上漏得最多」,那就是规则与真实分布最不对齐的地方,是下一步改进的优先项。AISA 视角下,这一步产出的「规则 vs 真实分布的 gap 清单」比那个虚高的 recall 数字有价值得多。

这与 AML 产品决策的直接关联。 如果团队基于循环 baseline 的虚高 recall 决定「规则引擎够用、不上 LLM」,那是在假数字上做架构决策——上线后真实漏报会让合规暴雷。今天把规则的真实档位测出来,才能诚实回答「规则够不够、要不要叠 LLM 兜底」——这正是 Day 140 三方 leaderboard 要支撑的 build-vs-augment 决策。

一句更狠的话总结循环 baseline 的危害。 循环 baseline 不是「没有评测」,它比没有评测更危险——没有评测时你知道自己不知道;有了循环 baseline 你以为自己知道,却握着一个系统性高估的数字去做决策。这种「带着假信心的无知」在合规领域代价最高。今天这一刀的全部意义,就是把「假信心」换成「真档位」,哪怕真档位难看。

「杀死循环」的判据:换标签,不换皮。 真正杀死循环的标志是金标来源换成外部,而不是换个变量名继续用自造数据。seed 的 SOTA 段点破这个反模式:「换皮不换标签」——若仍用自造金标,只是改了文件路径或函数名,循环没死。判据很硬:predicted 仍可来自 assessCase(规则不变),但 label 必须是 AMLworld 的 Is_Laundering,经 binaryEval 统一二元口径计算。

循环 baseline 与「过拟合」的关系,但又不完全是过拟合。 表面看循环 baseline 像规则「过拟合」到生成器,但更准确的描述是评测污染(evaluation contamination):不是模型参数过拟合,而是评测集本身和被评系统共享了设计假设。过拟合是训练问题(模型记住了训练集),评测污染是测量问题(尺子和被测物同源)。两者都会给出虚高数字,但根因不同——前者靠正则化/更多数据解,后者只能靠换一把独立的尺子(外部金标)解。今天做的正是后者:不是改规则,而是换尺子。

为什么这是整个 B14 的「核心动作」。 B14 前面九天(列映射、loader、judge 校准、混淆矩阵、CI、交叉验证)全是方法与脚手架——它们让评测变得严谨,但还没真正动那个最致命的循环。今天才是 payload:把规则引擎的金标来源从「自己人」换成「外部裁判」。groundTruthEval.ts 顶部注释把这件事叫得很直白——「这正是杀死循环 baseline 的动作」。前九天的所有部件,都是为了让今天这一刀砍得干净、砍得可测量。

与相邻概念的边界。 今天 ≠ Day 136-137(那是用 LLM judge 当预测,今天是用规则 assessCase 当预测);今天 ≠ Day 134(那是抽 balanced_300,今天是拿它当外部金标重测规则);今天 ≠ 改规则(今天不调阈值,只换金标来源,看规则在外部集上的真实表现)。今天只回答:同一套规则,喂外部真标签,召回率从虚高掉到多少。

2. 代码走读:evalBaseline.ts 与待接的 loader

Read 了 src/aml/evalBaseline.ts,现状与改动点:

  • 现状evalRuleBaseline(ds: AmlDataset) 遍历 ds.cases,对每条 const predicted = assessCase(c).topTypology ?? 'normal',构 confusion[${c.label}->${predicted}] 多类混淆矩阵,并算 perTypology[t].recall = correct[t]/total[t]normalFalsePositiveRate = normalFp/normalN金标是 c.label——来自传入的 AmlDataset,当前是 generator 造的合成集。这就是循环所在:c.labelassessCase 同源。

  • 改动方向(seed 指定):让 evalBaseline.tsamlworldLoaderbalanced_300,使 assessCasetopTypology 预测在真标签上重算 recall/FPR,经 binaryEval 统一二元口径。保持 assessCase 纯函数与既有测试不破坏,新增「真标注集」分支测试。

  • 诚实标注:amlworldLoader.ts 尚未落地。我在 src/aml/ 目录核查,现有 evalBaseline.tsgroundTruthEval.tstypology.tsconfusionMatrix.tsgenerator.ts 等,但 amlworldLoader.tsamlworldSchema.ts 不存在——与 Day 133-134 标注的「待数据/待建」一致。所以今天的「接 loader」是待建依赖:loader 落盘前,本日只能对合成 CSV fixture 写「真标注集分支」测试跑绿,真实接入需 loader + 数据到位。这是诚实边界,不得写成已接通。

  • 接入后的口径:predicted 取 assessCase(c).topTypology ?? 'normal',label 取 AMLworld Is_Laundering(1→可疑类、0→'normal'),构 LabeledPrediction[]groundTruthEval.tsbinaryEval(items, 'normal') 出统一二元 recall/precision/FPR——这一步复用的是 Day 136 已 built+tested 的函数。

  • assessCase 为什么会在外部集上掉分(看规则即可推断)assessCasetypology.ts)跑 6 条确定性检查——checkStruct01/02checkLayer01/02checkMule01/02——每条都钉死在本仓阈值上:CTR 门槛 CTR_THRESHOLD_CENTS=1_000_000($10,000)、贴线带下限 STRUCT_BAND_MIN_CENTS=800_000($8,000)、STRUCT_01_WINDOW_DAYS=10STRUCT_01_MIN_TXS=3、分层每跳 LAYER_DWELL_DAYS=2 天、LAYER_FORWARD_RATIO=0.8、钱骡 MULE_01_MIN_COUNTERPARTIES=5 等。这些阈值是按本仓 generator.ts 的合成模式调出来的;真实 AMLworld 的洗钱形态(不同币种、不同贴线习惯、更碎的拆分)大量落在这些硬阈值的缝隙里——比如真实结构化只拆 2 笔或贴 $7,500 线,就被 STRUCT_01_MIN_TXS=3STRUCT_BAND_MIN_CENTS 直接漏掉,recall 必掉。

  • topTypology 的阈值逻辑assessCase 把各规则得分按类型学求和 cap 到 1,只有聚合分 ≥ASSESS_THRESHOLD(0.5) 才输出 topTypology,否则 null(→ 'normal')。主规则 SCORE_PRIMARY=0.6 单独命中即过阈、辅规则 SCORE_SECONDARY=0.4 需叠加。外部集上若只触发辅规则(0.4<0.5),整案被判 normal → 对真可疑案就是 FN。

  • 预测映射:接入后 predicted = assessCase(c).topTypology ?? 'normal'(与现有 evalRuleBaseline 第 27 行同款),label 取 AMLworld Is_Laundering,构 LabeledPrediction[]binaryEval(items, 'normal') 出统一二元口径。

  • 列映射前置依赖(Day 133)assessCase 吃的是 AmlCase(含 accounts/transactions 结构),而 AMLworld 行是扁平的 Timestamp/From-Bank+Account/To-Bank+Account/Amount/Currency/Payment Format/Is_Laundering。要让规则在外部集上跑,必须先经 amlworldSchema(Day 133,待建)把行聚合成 AmlCase——这一步本身可能引入信息损失(规则需要的 channel/direction/openedDaysAgo 等字段在 AMLworld 里未必全有),是掉分的另一来源,需在 README 诚实标注「映射近似」。

  • 口径选择:二元而非多类evalRuleBaseline 原本出 perTypology 多类召回,但 AMLworld 只给二元 Is_Laundering(没有 structuring/layering/mule 的细分标签)。所以外部集上只能binaryEval 的二元口径(可疑 vs 正常),不能比「具体类型学」召回——这是数据本身的约束,对照 day131 时必须确认是同口径(都用二元)。

走读结论:assessCasetypology.ts)与 binaryEvalgroundTruthEval.ts已 built+testedevalBaseline.ts 的改动 + 真标注集分支测试可对 fixture 先跑绿;真实接入受阻于尚未落地的 amlworldLoader.ts(待建)+ AMLworld 数据下载(待数据)

补一句关于 assessCase 不破坏的纪律:今天的改动只在 evalBaseline.ts 新增「真标注集」分支,不动 typology.ts 的任何规则与阈值assessCase 必须保持纯函数、既有 evalRuleBaselineaml.test.ts/p1evals.test.ts 全绿——这样才能保证「掉分来自换金标,而非顺手改了规则」。把改动严格隔离在评测层、不碰被评的规则层,是这次去循环实验干净的前提。

3. 今日实战

前置(依赖未就位时先做的):

0a. 在 src/aml/ 确认 amlworldSchema.ts(Day 133)与 amlworldLoader.ts(Day 134)是否已落地——本日核查结果是两者都不存在(待建),所以下面 1-2 的真实接入受阻,只能先写逻辑 + fixture 测试。 0b. 准备一份合成 CSV fixture(模拟 AMLworld 行结构),用于在真实数据下载前验证「行→AmlCase 映射 + 二元重测」的逻辑正确性。

主路径:

  1. src/aml/evalBaseline.ts:新增一条「真标注集」路径——接 amlworldLoader(待建)的 balanced_300,把每条的 assessCase(c).topTypology ?? 'normal' 作 predicted、AMLworld Is_Laundering 作 label。
  2. src/aml/groundTruthEval.tsbinaryEval(items, 'normal') 统一二元口径,算规则基线在真标签上的 recall/precision/FPR(可叠 binaryEvalWithCI)。
  3. 保持 assessCase 纯函数与既有 evalRuleBaseline 测试不破坏;新增「真标注集」分支测试,先对合成 CSV fixture 跑绿。
  4. 与 day131 的虚高 recall 对照,记录掉幅(待真实数据产生具体数字)。
  5. 拉一张「逐规则漏报明细」:在外部集的 FN(真可疑被判 normal)里,统计每条规则(STRUCT/LAYER/MULE)本应触发却没触发的占比——这张表直指规则与真实分布最不对齐处,是后续阈值迭代的优先级清单。
  6. 严守 train/test 隔离的纪律:今天只重测、不调阈值,天然无循环风险;但若后续要拿掉分去调阈值,必须把 AMLworld 切成 train/test 两半,在 train 上调、在 test 上测,否则又制造一个新循环。今天先把这条纪律写进 README。
  7. 把规则基线的二元 recall/precision/FPR(待数据)落盘成与 Day 136-138 LLM 行同结构{label,predicted} 评测单元,确保 Day 140 三方 leaderboard 能把规则行和两 LLM 行放进同一张表、用同一个 binaryEvalWithCI 口径横比。

4. 今日实测 / 产出

  • 改后测试 = 可对 fixture 先跑绿:代码改动 + 「真标注集」分支单测在 423 测试套件框架内可绿(assessCasebinaryEval 均已 built+tested);既有 evalRuleBaseline / aml.test.ts / p1evals.test.ts 不破坏是硬约束(保证掉分来自换金标而非改规则)。
  • 规则基线「真标注集」指标 = 待数据产生真实掉幅:与 day131 虚高对比的掉分幅度需真实 AMLworld 跑出,不预填数字
  • 依赖阻塞(诚实标注)amlworldLoader.ts / amlworldSchema.ts 尚未存在(待建),AMLworld 数据待下载(待数据);真实接入在两者就位前不可完成。我已在 src/aml/ 目录核查——现有 evalBaseline.ts/groundTruthEval.ts/typology.ts/confusionMatrix.ts/generator.ts 等,但 loader/schema 两文件确实不存在,与 Day 133-134 的「待建」标注一致。
  • 可对 fixture 跑绿的部分:「真标注集」分支可对合成 CSV fixture 写测试先验证逻辑正确(口径映射、binaryEval 接线),这部分不依赖真实数据下载即可绿;真实掉幅数字才依赖数据 + loader。
  • 掉分的两类来源要分开记:(1) 规则阈值与真实分布不对齐(真正想暴露的);(2) AMLworld→AmlCase 列映射的信息损失(工程 artifact)。README 必须区分这两类,否则把映射损失误算成规则差,会误导后续阈值迭代方向。
  • 不预填任何虚高对照的具体数字:day131 的虚高 recall 与外部集掉幅的对比,只有真实跑出后才填;现在写「预期显著下降」是定性预期,不是测量结果,不得伪造数字。

5. 常见误区 / 陷阱

本节的坑大多围绕「以为换了金标就万事大吉」与「把诚实的掉分当事故」两类。

  • 换皮不换标签:只改文件名/变量名却仍喂自造金标——循环没死。判据硬:label 必须来自外部 AMLworld,不是 generator。这是本日最致命也最隐蔽的反模式。
  • 顺手改规则阈值去「救」掉分:今天目的是暴露规则在外部集上的真实表现,不是调参把数字拉回来。改阈值是另一天的事,今天动了就污染了诊断。
  • 拿多类 recall 与二元 recall 混比evalRuleBaseline 出的是 perTypology 多类召回,binaryEval 出的是二元(可疑 vs 正常)召回,口径不同。对照 day131 时要确认是同口径。AMLworld 只给二元标签,外部集上只能用二元口径。
  • 以为「换金标」就够、忘了换预测端也是去循环的一半:完整去循环 = 预测端解耦(Day 136-138 换裁判)+ 金标端解耦(今天换外部标签)。只做一半(比如仍用自造 label、只换 LLM 预测),循环没真正断。
  • 把待建 loader 当已接通amlworldLoader.ts 还不存在,本日真实接入受阻;写报告时不得把「待建依赖」升级成「已完成」。
  • 把掉分当「规则坏了」而恐慌:外部集上掉分是预期且健康的——它说明评测终于诚实了。该做的是分析掉在哪类样本(哪条规则漏得多),而非把数字掉了当事故。
  • 用 AMLworld 训练阈值再回测 AMLworld:若拿 AMLworld 一部分调阈值、另一部分测,要严格切分 train/test,否则又制造了新的循环(在调过参的数据上测)。今天只重测、不调参,天然无此风险,但后续若调阈值务必守 train/test 隔离。
  • 忽略列映射的信息损失:AMLworld 是扁平交易行,assessCase 要的 channel/direction/openedDaysAgo 等字段未必齐全,映射成 AmlCase 时的近似会额外拉低 recall。把这部分掉分误算到「规则差」头上是错的——要在 README 区分「规则不对齐」与「映射信息损失」两类掉分来源。
  • 拿合成 AMLworld 当「真实世界」:AMLworld 本身由 IBM AMLSim 生成,也带模拟器假设。它比自造金标强一个量级(独立设计来源),但不是终极真值;想彻底摆脱模拟器假设需再叠一份真实交易标注集。

6. 学习资源(每条带 YYYY-MM)

  • Anthropic — Demystifying evals(2026-01):外部效度要求、评测集需独立于被评系统设计来源。
  • IBM AMLworld dataset card(2024-04):外部公开洗钱标注集,Is_Laundering 真标签。
  • 外部效度 / 数据集漂移(dataset shift)议题:统计与 ML 的经典命题(held-out / out-of-distribution 评测),作方法论打底;最新分布漂移评测进展见 2025-2026 文献,执行当周复查。
  • IBM AMLSim / AMLworld 生成方法说明(2024-04):理解 AMLworld 是合成标注、自带模拟器假设,为「需再叠真实集」的判断提供依据。
  • 评测污染(evaluation contamination)相关讨论(2024-2026,benchmark 泄漏/同源污染是 LLM 评测热点):与本日「尺子与被测物同源」同构,执行当周复查最新案例。
  • 本仓代码(待建依赖):src/aml/amlworldSchema.ts(Day 133)、src/aml/amlworldLoader.ts(Day 134)——本日核查均不存在(待建),是真实接入的前置阻塞。
  • 本仓测试(不可破坏):src/aml/__tests__/aml.test.tsp1evals.test.tsp3aml.test.ts——改 evalBaseline.ts 时必须保持这些与 groundTruthEval.test.ts 全绿。
  • 本仓代码:src/aml/evalBaseline.tsevalRuleBaseline,待改)、src/aml/typology.tsassessCase + 6 条 checkStruct/Layer/Mule 规则 + 阈值常量,已 built+tested)、src/aml/groundTruthEval.tsbinaryEval,已 built+tested,注释明述「杀死循环」动机)。

SOTA检查 (2026-06 更新)

  • 现役主线:用外部金标重测内部规则是 eval 去循环的标准做法,无替代;AMLworld(2024-04)仍当前可用的公开洗钱标注集。「评测集独立于被评系统设计来源」是 2026 eval-rigor 共识的硬条款。
  • 评测污染是当前热点:LLM benchmark 泄漏 / 同源污染(contamination)是 2024-2026 评测领域反复被点名的问题,本日的「规则评测自己造的数据」是其在规则系统上的同构形态——去循环方法论跨范式通用。
  • 外部集补充候选:AMLworld 是合成标注(IBM AMLSim 生成),本身也带「模拟器假设」,理想上应再叠一份真实交易标注集交叉验证;Elliptic / Elliptic2(比特币交易图标注)是常见补充候选,可作下一份外部集。单一外部集仍比循环 baseline 强一个量级,但不是终点。
  • 避免/禁用:避免「换皮不换标签」(仍用自造金标则未真正杀死循环);避免靠改阈值掩盖外部集上的真实掉分;避免把待建的 amlworldLoader 当已接通;避免在调过参的 AMLworld 子集上测(新循环)。
  • 下次复查点:执行当周复查是否有比 AMLworld 更新、更贴近真实分布的公开 AML 标注集(如 Elliptic2 等)可作交叉外部集;确认 AMLworld 列结构自 2024-04 未变;评估是否需要补一份真实(非合成)外部集以彻底摆脱「模拟器假设」。

衔接

  • 昨天:Day 138 — 第二模型交叉验证(Qwen3 复跑 balanced_300,比两套指标 delta + per-case agreement %,换的是预测端裁判)。
  • 今天:把 evalBaseline.ts 的自造金标换成 AMLworld 真标签,规则 assessCase 在外部集上重测、暴露虚高 recall 的真实掉分(换的是金标端来源;代码可跑绿,掉幅待数据;loader 待建)。
  • 明天:Day 140 — 三方榜单与归档(规则基线 vs deepseek-v4-flash vs Qwen3 在同一真标注集上排 leaderboard + CI + 可复现 README,收口 B14)。
  • 一句话记忆点:杀死循环 = 换尺子(外部金标),不是改规则;掉分是诚实的证据,不是事故。
  • 收口提醒:今天的规则基线二元指标(待数据)与 Day 136-138 的两 LLM 行同结构落盘,是 Day 140 三方 leaderboard 可横比的直接前提。