AI任务harness中retry机制存在根本局限:当任务定义本身有误时,重试只会让模型用同样的错误输入再失败一次。max_attempts=3的约束本意是应对随机性,而非修复任务设计缺陷。
这是「The Factory」系列的第二篇。上一篇:The Factory That Merged 37 Tasks。Harness 仓库在 github.com/frozer/factory。
我的任务 harness 的公开说明以一个论断结尾:一个对世界判断错误的包,在每次重试时都会以完全相同的方式失败。
这句话花了我四个废弃任务和九次提交——全部用于修复任务定义,而非写代码。它读起来像是你经过思考后会得出的结论。我是通过盯着同一个失败连续滚过三遍才悟到这一点的。
max_attempts = 3
感觉像是不言自明的卫生习惯。模型是随机的。有时候一次运行莫名其妙地跑偏——一个糟糕的转折、截断的响应、被拒绝的工具调用。重试一次就好了。这是真实存在的,重试预算处理得很好。
优势恰恰在于这种约束本身。重试预算假设下一次尝试会与上一次不同。它从分布中给你第二次采样。
但重试并不会给模型一个全新的局面。它把同一个包又塞了回去。同样的文件、同样的声明、同样的指令。如果包说一个文件存在于某个路径,而那个路径下没有文件,第三次尝试就会在第一次失败的地方精确地失败——三次尝试买到的不过是三倍账单。
没有方差的失败不是抖动。它是穿着可靠性外衣的规格缺陷。
以下是在实践中看到的样子。
B03 是一个全国人口普查数据集的加载器。三次尝试全部烧光,全部折戟于一个根本不存在的文件形态:目录错误、文件名错误,还有一个在真实数据树中无处可寻的 Data/Valor JSON 包装。每一次尝试都为一份不存在的文档生成了解析器。
缺陷在包里,不在模型里。没有人在真正打开实际文件之前就把它发出去了。
根据真实 JSON 重写后,真相是一个扁平的 metadata/data 包装、四个文件——其中一个根本不应该被加载——以及一个包含真实缺失行的高维度透视表。这些从设计文档里完全推不出来。只有打开文件才能知道。
同样的检查应用到两个相邻的包上,发现了一个 schema 不匹配、一个遗漏的透视表、以及一个真实的设计冲突:两个源文件共享了六个类别标签中的五个完全相同,但没有一列可以区分它们。按照包的指示加载两者,会悄无声息地覆盖数据。此外,包中关于西班牙小数逗号的自信声明对该数据来说根本是假的——所有文件的所有总计列中零个逗号。
A11 是更干净的案例,因为它把模型从论证中完全剔除了。它的不变量要求两个字段——一个源 SRID 和一个归属字符串——通过类型化配置路由。那些字段在该类型上根本不存在。而且定义该类型的文件不在 A11 的可编辑集合中。
因此实现者能够产生的任何 diff 都无法满足那个不变量。不是困难任务。是三次完整价格的不可能任务。
C09 是昂贵的那种。它的步骤读起来是 build → validate → write,这是个合理的顺序,看起来也没问题。但 build 调用在返回之前就在内部持久了——所以 build 实际上已经写入了。三次尝试都在写入之后做验证,并尽职地记录了那个不变量本应防止的失败。
工作树中没有任何内容能告诉模型这些。它失败了,被塞回同一个包,然后以同样的方式失败。
在你提高 max_attempts 之前,把三次失败并排摆在一起,问自己一个问题:它们有区别吗?
如果第三次尝试的失败方式与第一次不同,重试正在做它的工作。如果三次失败完全相同,你看到的不是一个不可靠的模型。你看到的是一份无法被执行的指令,而你正在付全价让人反复确认这件事。
重试循环是最后修复它的地方,却也是所有人第一眼看向的地方。
修复方案不是一个更好的实现者提示词。而是承认:捕获缺陷包最便宜的地方,是在任何模型产生任何成本之前。
所以规划步骤有了自己的两个 agent。一个从规格书中裁剪包。第二个——没有编辑任何东西的能力,因为一个能够编辑它正在评判的内容的运行实例,最终会悄悄地修复缺陷而不是报告它——评估每个包并对其中每个可检查的声明做出裁决:成立、违反、或不可验证,每条都附上 path:line 或命令的输出。队列现在拒绝任何没有通过评估(且评估被钉在该包的确切哈希上)的包。
裁剪器从一个地面真相阶梯工作:磁盘上的文件优先,然后是任务将调用的代码,再然后是先前包产生的内容,然后是工具链——规格书排在所有这些之下,作为意图的证据而非事实的证据。
这个排序就是全部的教训。规格书告诉你某人想要什么。只有文件告诉你那里有什么。
我用那九次修复提交作为标签集,对评估器做了回测。所有五个已知缺陷的包都返回了重新裁剪的版本,每个都指出了人类修复者发现过的同样缺陷。那两个我已经修好的返回了就绪状态。
三个已经发版的包返回了重新裁剪——理由是真实的。
而评估 A11 暴露了合并代码中的一个活 Bug:一个加载器在一个目录路径上调用 snapshot_id 属性,而那个属性读取的是单个文件的字节。那段代码通过了 linter、通过了测试、通过了 review、合并了。
没有任何测试能捕获它。数据集不在这个 checkout 中,所以离线套件永远无法到达那个调用。
绿色测试套件告诉我那段代码没问题。一个模型把包和代码放在一起读,在一次通读中就发现了它。