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

schema 对齐

Day 132 选定了 IBM AMLworld 并量化了它的极端不平衡,但外部数据要能喂进我们既有的评测代码,必须先做列结构 → 领域模型的映射与运行时校验——这就是今天的 schema 对齐(schema alignment)。

阶段: B14 · 外部 ground-truth AML eval(Day 131-140) 标签: #schema-mapping #zod #runtime-validation #auditability

今日导引(由浅入深)

Day 132 选定了 IBM AMLworld 并量化了它的极端不平衡,但外部数据要能喂进我们既有的评测代码,必须先做列结构 → 领域模型的映射与运行时校验——这就是今天的 schema 对齐(schema alignment)。

它在 B14 的位置是「数据接入的第一道闸门」:没有可靠的解析与校验,后面 Day 134 的 loader、Day 135 的 judge、Day 136 的混淆矩阵都建在流沙上。一行脏数据(缺列、金额非数、标签是 'yes' 而非 1)若静默通过,会一路潜伏到混淆矩阵阶段才爆出 NaN,定位成本极高。

放到 B1→B18 能力曲线上:这是把「外部真值」真正接入系统的工程动作,对应 AI Solutions Architect 的「数据契约 + 防御式接入」能力。最小可判定产出:写 src/aml/amlworldSchema.ts,定义 AmlworldRow 行类型 + 列校验函数,对前 1000 行解析断言 0 错误(真实 1000 行需数据落盘后补测,先对合成 fixture 跑绿)。

1. 机理精读

为什么必须显式映射。 外部数据集的列结构(AMLworld 的银行/账户/金额/时间戳/支付格式/Is_Laundering)和我们的内部领域模型(src/aml/types.tsAmlCase/AmlTransaction)是两套词汇。要复用既有评测代码(binaryEval、混淆矩阵、SAR、审计轨迹),必须建一座确定性、可校验、可审计的桥。

映射不是「随便对齐字段」,而是一份契约(contract):哪些列必填、数值/日期能否解析、Is_Laundering 是否严格 ∈ {0,1}。契约越严,下游越稳。

为什么要保留可审计原始字段。 AML 是强监管域——后续 SAR(可疑活动报告,Suspicious Activity Report)草稿和审计轨迹要能追溯到「这笔可疑结论基于哪几笔原始交易」。所以映射时不能丢弃原始 from/to/amount,要把它们作为可审计字段保留。

src/aml/types.ts 已为此设计好承接点:

  • AmlTransactioncounterpartyAccountId?(数据集内对手)、counterpartyName?(数据集外对手)、amountCentsmemo? 都能承接原始信息;
  • AuditEvent(types.ts 第 113-118 行:at / actor / action / detail?)承接「谁、何时、做了什么」的审计三元组。

关键权衡:运行时校验 vs 编译期类型。 TypeScript 的类型只在编译期存在,CSV 是运行时才读到的外部输入——any 解析会把脏数据静默放进系统,到混淆矩阵阶段才爆出 NaN,难定位。

对策是运行时 schema 校验:解析时即断言「必填列存在、Amount 可解析为数、Timestamp 可解析为日期、Is_Laundering ∈ {0,1}」,校验失败立刻 fail-fast 报错并定位到行号。2026 的 TS 生态推荐用 zod 做这层运行时校验——类型推断 + 运行时校验合一,z.infer 让类型从 schema 自动派生,避免类型和校验逻辑两处维护、漂移不一致。

关键边界:schema 对齐 ≠ 语义建案。 今天只做「行级解析与校验」——确认每行结构合法、字段类型对。把多笔交易聚合成一个 AmlCase(带 subjectPartyIdwindowDaysalertReason、按主体和时间窗分组)是更上层的建案逻辑,不在今天范围。

今天的产出是干净的 AmlworldRow[],它是 Day 134 loader 抽样的输入;建案聚合可以延后,但行级校验不能省——它是后续一切的前提。

2. 推导 / 手算 / 代码走读

注意:src/aml/amlworldSchema.ts 当前尚未创建(本日新建),下面是要建的契约设计,不是既有代码走读;映射目标 src/aml/types.ts 是真实既有代码(已 Read 确认字段)。

要新建的 AmlworldRow + 校验函数设计(落 src/aml/amlworldSchema.ts):

  1. AmlworldRow 行类型:字段对应 AMLworld 列——timestamp: stringfromBank: stringfromAccount: stringtoBank: stringtoAccount: stringamount: numbercurrency: stringpaymentFormat: stringisLaundering: 0 | 1
  2. 必填列校验:解析表头时断言上述列全部存在,缺列 fail-fast(报缺失列名 + 行号)。
  3. 数值可解析Amount 必须 Number.isFinite(parsed) 且非负;映射到 amountCentsMath.round(amount * 100)(沿用 types.ts 的整数分纪律,禁止 float 金额)。
  4. 日期可解析Timestamp 必须能解析为合法日期(用于后续算 dayOffset),非法日期即报错。
  5. 标签域校验Is_Laundering 严格 ∈ {0,1},其它值(如空、2、'yes'、'Y')即报错——这是真标签,不容松散;标签污染会直接扭曲 Day 136 的混淆矩阵。
  6. 行号定位:校验失败的错误信息带行号,便于排查脏数据。
  7. 推荐用 zodz.object({...}) 定义 schema,schema.safeParse(row) 收集 success/errorz.infer<typeof schema> 派生 AmlworldRow 类型,避免手写松散 any 解析。

映射到领域模型(落 Day 134 loader 时用,今天先记下)isLaundering → 二元真标签;fromAccount/toAccountaccountId/counterpartyAccountIdamount×100 取整amountCentspaymentFormatchannel(Cash/Wire/ACH/Cheque…);timestampdayOffset(相对窗口最早一天的天序号)。

最小手算例(合成 fixture 设计):构造 5 行测试数据——

  • 3 行合法(Is_Laundering 分别 0/0/1,Amount 为合法浮点)→ 期望 validateRows 返回 ok: true, errors: []
  • 1 行缺 To Account 列 → 期望报「缺失列 toAccount @ 行 4」;
  • 1 行 Is_Laundering=2 → 期望报「标签越界 @ 行 5」。

这组 fixture 不依赖真实数据,可立即跑绿,验证校验逻辑本身正确。

金额精度的边界推导(为什么必须整数分):AMLworld 的 Amount 是十进制浮点字符串,如 "9812.37"。若直接以 JS number(IEEE 754 双精度)存储并做加减,9812.37 + 0.01 这类运算会引入 ±1e-13 量级的二进制误差,跨上千笔交易累加后可能在「是否略低于 $10,000 申报阈值」的判定边界上翻转结论——而 structuring 判定恰恰盯着这个阈值。对策:解析即 Math.round(9812.37 * 100) = 981237(整数分),所有比较/累加都在整数域做,零精度损失。这与 src/aml/types.ts 顶部注释「金额一律整数分、避免 float 货币运算、避开 bigint 的 JSON/React 序列化问题」完全一致——金额上限远低于 2^53,整数 number 足够安全。

校验严格度的取舍(fail-fast vs 容错跳过):两种策略——

  • fail-fast:遇到第一个脏行即抛错停止。适合「数据应当干净,脏即 bug」的场景,但一行坏数据卡死整批不友好。
  • 收集式(safeParse 累加 errors):跑完全部行、汇总所有错误 + 各自行号,最后一次性返回 {ok: false, errors: [...]}。便于一次性看清数据质量全貌。

本任务取收集式validateRows 跑完前 1000 行、汇总所有错误,断言 errors.length === 0;这样既能定位每个问题行,又能一眼看出「是个别脏行还是系统性列错位」。

3. 今日实战

  1. 新建 src/aml/amlworldSchema.ts:定义 AmlworldRow 类型 + 用 zod 写 amlworldRowSchema + 导出 validateRows(rows): {ok, errors} 列校验函数(必填列、数值/日期可解析、Is_Laundering ∈ {0,1})。
  2. src/aml/__tests__/ 新增对应单测:先用手写合成 fixture(几行合法 + 几行故意脏:缺列、Amount 非数、Is_Laundering=2)断言校验能正确 pass/fail,跑绿。
  3. 对下载的真实 HI-Small 前 1000 行validateRows,断言 0 解析错误——此断言依赖数据落盘,先标注「待数据」。
  4. 保留可审计字段:映射时不丢弃原始 from/to/amount,为后续的 SAR 与审计轨迹留证据链。

4. 今日实测 / 产出

  • 状态待数据(下载公开集)
  • 产出(待数据后):通过的 schema 校验测试(1000 行 0 解析错误)。
  • 可先做的部分:schema/校验代码本身可先写好,并对合成 fixture 跑绿(这部分不 gated,今天即可完成)。
  • 诚实标注:对真实 1000 行的「0-error」断言需数据落盘后补测,标「待数据」。src/aml/amlworldSchema.ts本日新建文件(当前仓库中尚不存在,已确认)。

CSV 解析的常见暗坑(写校验时要防)

  • 带引号的字段含逗号:如 memo "Smith, John",朴素 split(',') 会错位列——用正经 CSV parser(如 csv-parse)而非手写 split。
  • CRLF vs LF 行尾:Windows 导出的 CSV 行尾是 \r\nsplit('\n') 会把 \r 残留进最后一列——解析时 trim。
  • 表头大小写/空格不一Is_Laundering vs Is Laundering vs is_laundering——校验前规范化列名。
  • 科学计数法金额:超大额可能写成 1.2E6parseFloat 能解但要 Number.isFinite 兜底。

这些坑不防,会以「莫名其妙的 0 行解析成功」或「全列错位」形式在 Day 134 抽样时爆出来,比标签污染更难定位。

5. 常见误区 / 陷阱

  • 手写松散 any 解析:类型不安全、脏数据静默通过,到混淆矩阵阶段才爆 NaN——必须运行时校验(推荐 zod)。
  • float 金额Amount 直接当浮点存会引入货币精度误差;沿用整数分(Math.round(amount*100)),与 types.ts 纪律一致。
  • 丢弃原始字段:只留派生字段会断掉审计链,SAR 无法追溯证据交易——保留 from/to/amount。
  • 把「合成 fixture 跑绿」当成「真实 1000 行 0 错误」:两者状态不同,前者已可做、后者待数据,不可混淆升级状态。
  • 用朴素 split(',') 解析含引号的 CSV:列错位且静默,是本日最易踩的工程坑。

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

  • IBM AMLworld dataset card, Kaggle (2024-04)——列定义(Timestamp / From-Bank+Account / To-Bank+Account / Amount / Currency / Payment Format / Is_Laundering)。
  • zod 官方文档(v3/v4,2026 维护中)——运行时 schema 校验与 z.infer 类型推断。
  • 本仓代码:src/aml/types.tsAmlTransaction/AmlCase/AuditEvent 映射目标,已 Read 确认)。
  • Anthropic《Demystifying evals》(2026-01)——可审计、可复现的 eval 数据管线要求。

SOTA检查 (2026-06 更新)

  • 当前主流:AMLworld 列结构自 2024-04 未变,映射契约稳定。
  • 运行时校验:TypeScript 运行时校验推荐 zod(2026 仍主流,类型安全 + 校验合一);arktype/valibot 为可选替代但生态较小。
  • CSV 解析:用成熟 parser(csv-parse/papaparse,2026 仍维护)处理引号/转义/行尾,不手写 split。
  • 是否仍 SOTA:是。schema-first + 运行时校验是外部数据接入的标准做法。
  • 过时黑名单:避免手写松散 any 解析——类型不安全且无法支撑可审计要求;避免用未校验的 CSV 直接喂下游。
  • 下次复查点:Day 134 loader 前,确认 schema 输出的 AmlworldRow[] 字段足以支撑分层抽样(isLaundering 是抽样分层键,必须可靠)。
  • 是否仍 SOTA(CSV 工具链)csv-parse/papaparse 为 2026 主流,无需替换;避免自研 CSV 状态机。

衔接

  • 昨天:Day 132 — AML 公开标注集选型(选定 AMLworld,量化极端不平衡)。
  • 今天:把 AMLworld 行映射到 AmlCase、写 amlworldSchema.ts 做运行时列校验,保留可审计原始字段。
  • 明天:Day 134 — loader 与去偏(流式读 + 按 Is_Laundering 分层平衡抽样,导出 balanced_300.json)。