> 派生产物写回输入的命名空间,这一步就不再幂等——而删掉产物不是修法

Research Tools Intermediate 2026-10-13 20:30 2026-10-13
#idempotence #pipelines #glob #schema #audit #failure-modes

两个脚本读同一批输入:

// TABLE_OF_CONTENTS

结论先行

两个脚本读同一批输入:

for capj in sorted(STATE.glob("capability_*.json")):   # 两个脚本都这么写

而其中一个脚本的输出叫 capability_summary.json,就写在同一个目录里。

>>> fnmatch('capability_summary.json', 'capability_*.json')
True

于是:

  • 汇总脚本吃自己的输出,已有 summary 时再跑一次就不幂等;
  • 审计脚本崩了。summary 的顶层按模型分组,没有 model/seed/checkpoints, 于是 rep['model'] 抛 KeyError: 'model'。

崩在中途这件事比崩本身更糟:那一次审计没有得到完整的 FAIL 总数, 后面的 freshness 检查也从来没跑。一个在中途崩掉的审计, 它已经打印出来的那些 PASS 既不是"全部通过"也不是"全部失败",是一段不知道有多长的前缀。

而最诱人的那个修法是错的,当时就被写进了验收标准里:

不要把删除 summary 当作永久修复。 验收必须包含"已有 summary 时连跑两次"和"全部预期输入缺失时应当失败"。

为什么

幂等不是一个性能性质,是一个正确性性质,而破坏它最常见的方式就是 让一步的产物落进它自己的输入命名空间。一旦落进去,这一步的行为就取决于 "之前跑过没有",而那是最难复现、最容易在别人机器上不一样的状态。

这里的运气在于schema 不同,所以它崩了。 如果 summary 恰好带着 model 和 seed 字段, 它就会被当成第 N+1 个输入静默计进汇总,于是:

症状
schema 不同(实际情况) KeyError,崩在中途 — 吵,但看得见
schema 兼容(很容易发生) 多算一份、或者把上一轮的汇总当这一轮的输入 — 静默错数

崩掉是这个 bug 最好的结局。 所以修法不能只是"让它别崩"。

实际的修补是在两个脚本里各加一道 schema 过滤:

if "checkpoints" not in rep:   # e.g. our own summary output
    continue

这确实恢复了幂等,而且那句注释把问题说得很准("our own summary output")。 但要诚实地看清它修了什么、没修什么:

命名空间冲突还在。 修的是两个消费者,没修那个布局。 同一道四个 token 的过滤现在在两个文件里各有一份, 而第三个消费者忘了写它的代价,就是回到崩溃或者静默错数。

真正的修法有两档,按成本选: 把产物写到另一个目录或换一个不匹配的前缀(summary_capability.json), 这样 glob 在构造上就吃不到它;或者放弃 glob,用一份显式的输入清单 (这也正好修掉"glob 可能一项都不命中"那条,见 0 FAIL 不等于那些条件都成立——一份严格审计里八处用廉价代理冒充实质检查)。

怎么用

  • 产物不要写进自己的输入命名空间。 换目录,或者换一个和输入模式不匹配的名字。 这是零成本的,而它消掉的是一整类 bug。
  • 已经冲突了而暫时不想改布局时,schema 过滤要写在一个共享的加载函数里, 不要在每个消费者里抄一遍。抄一遍就等于把正确性托付给下一个人的记性。
  • 幂等要进验收标准,而且形态是固定的两条: "在已有产物的情况下连跑两次" 和 "预期输入全缺时必须失败"。 前者抓自噬,后者抓空 glob。
  • 一个会中途崩掉的审计,要么把"已检查到哪一步"也打印出来, 要么把总数的计算放在最后并显式声明"不完整"。 一段不知长度的 PASS 前缀会被当成完整报告读。
  • 相关:被引用的产物要有一个任何运行都生成不出来的名字, 见 被引用的产物要有一个"任何一次运行都生成不出来"的文件名; exists() 当成有效性判据的同类坑见 参照语料坏掉不会让检查器报错,会让它放行一切——而 exists() 当缓存判据会把损坏永久化。

出处

自己的 audit_maintrack_artifacts.py 与 capability_summary.py: 两者都用 STATE.glob("capability_*.json"),而后者把 capability_summary.json 写在同一目录。KeyError: 'model' 与"没拿到完整 FAIL 总数"出自当时的复现记录; "不要把删除 summary 当作永久修复"和那两条验收标准是原话。 glob 确实匹配自身输出、以及两处 schema 过滤各有一份,是我这次核的。