智能体用测试与验证技术的水平到底如何?
作者在 Zstd 实现评测上测了 26 个测试/验证提示条件加 4 个 skill:几乎没有一个超额表现,Default 反而高于平均。agent 会把技术用成表面功夫,证明无关性质、拿随机输入撞同一批错误路径,甚至把同一处 bug 写进两份「差分」实现。
中文
复制
我们此前指出过,虽然让编码 agent 使用有效的测试技术比以往任何时候都更容易达到某个质量标准,但软件质量似乎正在变差,这说明开发者正在使用的那些默认做法可能不太管用。这里我们测试的是:简单指示 agent 使用特定技术或库,能否提高实现的正确性。这算是检验 agent 在由一个没有测试专业知识、只是「听说」应该用某些技术或库的人引导时,能有多有效。
我们复用在编程语言对 agent 有效性比较中讨论过的 Zstd 实现评测,只是改为在让 agent 实现 Zstd 的提示词上附加不同的补充说明,比较不同的测试技术与测试库,比如「使用测试驱动开发」「使用 Lean 4」「使用 QuickCheck」「使用基于性质的测试」等等。我也跑了一些其他评测,比如针对 IMAP RFC 的,这些会简要讨论。
所有实现都用 Rust。测试的 26 个提示词条件是 ACL2、Alloy、「审计并模糊测试高风险区域」、「先审计」、Creusot、Default(不加任何额外指示)、差分测试、模糊测试、Hegel、Insta、Judgement(让 agent 使用最好的技术)、Kani、Lean 4、「别犯错误」、变形测试、变异测试、基于性质的测试、Proptest、QuickCheck、rstest、Rust 内置测试框架、SMT 求解器(Z3、cvc5 和 Yices 都可用)、Spin、TDD、TLA+ 和 Verus。此外还测了 4 个 skill:带官方 Hegel skill的 Hegel、ECC 的 Rust 测试 skill(ECC 是一个 skill 合集,有 25 万 GitHub star、3.8 万 fork)、Trail of Bits 的基于性质测试 skill,以及我自己写的一个测试 skill(我是个卢德分子,用提示词而不用 skill,对怎么写好 skill 毫无手感)。除了我自己那个,这些 skill 之所以被选中,是因为它们是让 codex 去找相关 skill 时它给出的最靠前的几个。
预测
我预注册了一些关于各条件表现的猜测:
- TDD 会表现不佳(55% 把握)
我特意加进 TDD,就是因为我以为它会表现不佳
- 我在这里把握很低,因为我不知道被指示做 TDD 时 agent 会做什么;也许 agent 不会做 TDD,而是做某种不会表现不佳的事(又也许我对 TDD 表现不佳的判断是错的)
形式化方法不会超额表现(52% 把握)
- 我的想法是:形式化方法是有效且有用的(现在比以往更是),好的测试方法同样有效且有用,而在简单问题上,如果在相近的熟练水平下使用,形式化方法不该超额表现
- 和上面一样,而且更甚:我把握很低,因为我不知道被指示做任何事时 agent 会做什么,而且形式化方法在 agentic coding 上被炒得比有效的测试技术更响,所以完全有可能是实验室用合成数据的 RL 环境训练过 agent,把它们训练得非常擅长形式化方法,却没有训练 agent 擅长好的测试技术(我本以为后者更容易做,只是因为它相对不时髦而没人做)
「别犯错误」不会比不加指示表现更好(95% 把握)
- 这是个玩笑,而且很多人试过。如果它管用,肯定会有人注意到吧?
ECC 测试 skill(25 万 star、3.8 万 fork)不会超额表现(65% 把握)
- 它有点大,而且里面没有任何我认为会有用的信息。它指示 agent 使用 TDD;在它能促使 agent 使用 TDD 的程度上,我预计这会让情况变糟(而且它比 TDD 那个条件更具指令性,也许更可能成功,虽然据我所知那也可能让它更不容易成功);其余信息看起来没什么用,还有一定成本
- 我对所有 skill 的预测把握都很低,因为我不太用 skill,也不知道怎么真正评估它们。我是这样想的:「如果我把这段文本当提示词传进去、让这个东西在 LLM 的上下文窗口里飘着,能有多有效?」
Hegel 的 skill 不会超额表现(65% 把握)
- 它非常大(SKILL.md 加上链接的 Rust 参考超过 2 万 token),而且读起来更像教程而不是给 agent 的指令
Trail of Bits 的测试 skill 不会超额表现(55% 把握)
- 它里面有一些看起来可能有用的信息,但它也相当大
总体结果
下面这张非常凌乱的图展示了各测试条件的结果(codex 配 GPT-5.6 Sol,medium 与 xhigh 两档 effort)。看数据时,我倾向于比大多数人更喜欢那种更密集、更凌乱的图,比如这里的第一张图。因为大多数人觉得这类图凌乱得读不下去,所以向别人呈现信息时,我倾向于把信息拆成一系列图,每张图展示更少的信息。出于下面讨论的原因,这里我不这么做,就只给出这张极度凌乱的图:横轴是成本,纵轴是 100% 通过(隐藏)测试的运行比例,每个条件和每档 effort 平均 80 次运行(鼠标悬停会显示 bootstrap 协方差、50% 不确定性,并且做了一些尝试让同类的东西颜色相近,比如形式化方法偏蓝、基于性质的测试偏绿,等等):
能看出的一点是,没有什么东西真的超额表现得很厉害。不过 Default(不加额外指示)明显高于平均。看 xhigh 档,平均而言模糊测试和 PBT 相关条件比形式化方法稍好一些,而 medium 档的情况要混乱得多。codex 推荐我们试的那些测试相关 skill 表现不佳,而我们那个临时写的自定义 skill 表现还行(一个重要差别是,我们那个 skill 的设计目的是把 agent 从它们的默认行为推向更有产出的行为,而其他 skill 更像是教程)。TDD 如预测的那样表现不好(有一个 skill 也建议 agent 使用 TDD,而那个 skill 在 agent 试图遵循该指示的那些情况下同样表现糟糕)。
如果我们真的去看 agent 做了什么,很快就会明显看出,总体而言 agent 并不知道怎么很好地使用这些工具或技术。正如我们在这里指出的,也正如我交谈过的每个人都指出的,agent 在测试方面真的很差,似乎并不理解如何「默认」合理地进行测试。比如,这里是 Gary Bernhardt 的一条评论:
AI agent 对待测试的方式,大致是:
- 拿出 15 年前某个反对 mock 的人幻想出来的那些病态案例,而且从没真正用过 mock。对过度 mock 的天真幻想。
- 把这些病态案例变成你测试策略的骨干。
结果发现,如果你让 agent 使用某个特定的测试技术或测试库,这种做法并不会像你希望的那样改变多少。我们会更详细地看各个条件里发生了什么,但总体而言,在测试技术方面,agent 要么只是把它们平时会写的测试写进另一种测试技术的框架里,要么就是表面地用了某项技术,却没有真正做那些能从这项技术中获得价值的事。多数情况下,当某个技术被点名时,它们做的正是 Gary 描述的事,只不过针对那项技术(比如形式化方法,它们大多证明了一些无关的性质;而在基于性质的测试里,agent 会严重依赖完全随机的输入,大量撞到无效/被拒的案例,或者找一条平凡的性质来检查、再拿低价值的随机案例去跑那条平凡性质)。在 IMAP RFC(我对每个条件试了 40 次运行)或其他随机 RFC(各试了几次单独运行)上,结果没有实质差别。总体而言,不管问题是什么类型,是像 Zstd 这样的位操作问题、像 IMAP1 这样的协议,还是别的什么,agent 都没有以有效的方式使用形式化方法、测试库或测试技术。
在 xhigh 档,agent 总体上能让它们写的测试通过,但它们写的是糟糕的测试(比如它们会往一个使用四条比特流的特性的测试里提交四条一模一样的比特流,从而漏掉任何由比特流顺序颠倒引起的 bug)。正如我们此前在 Zstd 评测中关于语言所指出的,在一个朴素的循环里以更低 effort 运行会得到更差的结果(agent 会做更多这种事,并在更低的正确性上卡住)。
我很好奇为什么 AI 实验室没有创建 RL 环境来让 agent 学会如何测试,因为软件不能合理运行似乎对编码 agent 的采用很重要,而且这看起来也是那种适合 RL 的问题。正如我们此前看到的,agent 已经在有界运行时优化问题上变得相当擅长,这很合理,因为那正是你可以廉价地创建大量 RL 环境来训练的东西。也许这属于那种试起来比看起来更难的事,但为有效测试和测试技术创建 RL 环境看起来属于同一类问题。也许限制因素只是有效测试技术的知识不够普及,所以没人想到去试,而人们正让 agent 低效地测试(比如做标准的单元测试)2;又也许这个问题出于某种原因比运行时优化难打包得多?如果 agent 变得足够好、普遍能写出正确代码而无需测试或验证,这件事可能很快就会变得无意义;但至少从公开可用的 agent 诞生至今(2026 年 9 月),如果 agent 能在没有测试专家引导的情况下对如何测试有点概念,似乎会大幅提升 agentic coding 的有效性。
下面我们逐个看各条件下 agent 是怎么做的,按正确性从最差到最好排列,但我要提醒任何人不要从这个排序里得出任何强烈的结论。
这里的很多失败,看起来都类似于我们考察编程语言对 token 用量和正确性的影响时看到的失败:它们往往是特异性的。比如在编程语言上,我们看到 agent 在 Clojure 里把字节转换语义搞错的比例相当高,而在 Java 里则不然,尽管 agent「应该」(而且大概某种程度上确实)知道它们可以用 unchecked-byte 而不是 byte 来得到 Java 的字节转换语义。
虽然人们对为什么某些语言更适合 agent 有各种各样的空泛解释,但当我们看 agent 实际做了什么、失败模式是什么时,我听到的关于为什么某人钟爱的语言适合 agentic coding 的解释,无论是 Elixir、OCaml 还是 J,其实没有一条成立(关于 Rust 内存安全的评论除外,它在我们试过的多语言 pandoc 评测里通过对 agent 写的 C、C++ 和 Rust 之间内存安全问题的比较得到了验证)。我们看到的反而是一堆出于不明原因发生的特异性失败3。在语言这件事上,因为我们可以观察到语言流行度与表现之间存在中等程度的相关(成本更低、正确性更高),所以合理地猜测原因是流行语言有更多训练数据(可能是合成数据,而不只是人写的代码)。而在这里没有清晰的规律,除了这样一点:当 agent 手里只有某个库或技术的名字时,它们大多不太能有效地应用测试或验证技术(后面我们会讨论什么更管用)。如果你不想读每个条件里发生了什么,点这里跳到最后一个条目。
Verus
Verus 使用 SMT 求解器和各种推理来证明代码符合规格。
虽然 Verus 能证明代码符合规格,但 agent 并没有这么做。相反,它们证明了各种与 Zstd 相关的抽象性质。我自己没用过 Verus 这类工具,所以我无法说明一个专家、甚至一个初学者用户通常会做什么,但从读教程来看,我觉得有点奇怪:agent 没有尝试用 Verus 验证任何实际代码,只用它做抽象推理,而这个工具看起来就是为了让「证明实际代码的性质」变得容易而设计的。
此外,如果看它们证明的性质,通常数量很少,而且证明的性质都很无趣。比如 agent 会证明「给定一个有效的 cursor/index/distance,结果操作保持在边界内」这类东西,这本身不算坏,但它并不是 bug 的来源。而且 agent 经常写出空洞的证明,实际上就是 A => A。一个真实的 Verus 证明长这样:
requires
0 < a <= window,
0 < b <= window,
0 < c <= window,
ensures
0 < c <= window,
0 < a <= window,
0 < b <= window,
在 agent 确实证明了某些东西的情况下,它们一般证明的是相对简单的东西,并且避开了那些很可能有 bug 的部分(比如 agent 常常没能在编码和解码时把比特流顺序反过来,而它们写的测试检测不到这一点,因为测试用的是回文输入;也许在这里做某种「反转」的证明能让 agent 以不同的方式「思考」这件事)。
看来在只提供 Verus 和 Verus 文档的情况下,agent 并没有从 Verus 中获得价值。
如果看结果,xhigh 档 Verus 的总体结果还行(正确性略低于平均,但便宜得多)。medium 档结果成本处于平均,正确运行比例最低,平均正确测试数也最低。因为 agent 并没有真正从 Verus 中获得价值,它们在正确性上实际做的事主要还是传统测试(用 Rust 内置的 #[test] 函数做单元测试)。从 medium 到 xhigh,agent 在传统测试上花的力气大得多,在 Verus 上只多花了一点,这才让 xhigh 的结果还行。
看实际的测试,在 Verus 条件明显更差的两个特性之一(四路流跳转表)上,Verus 的 agent 在 160 次里有 89 次为它写了测试,碰巧和 Default 条件的次数完全一样,但 Verus 的 agent 更可能写出糟糕的测试。它们更可能把错误结果编码进测试里,也更可能写那种容易通过、但覆盖不好的测试,比如把四条流做成完全一样。这类东西就是我说的「失败是特异性的」。Verus 本身没有任何东西必然导致人在不用 Verus 时写出糟糕的测试,而且一般而言我们不会预期一个用过 Verus 的人写出糟糕的单元测试,就像我们不会预期一个用 Clojure 的人犯更多字节转换错误一样,但这里出于某种原因发生了(可能是巧合)。
我不知道 AI 实验室内部的人能否拿到更好的信息来解释这些事为什么发生,但在外面,要判断这类事情的原因通常相当困难(即使我们对语言那个问题形成了一个看似合理的假设,也需要对多种语言跑很多样本;而我们看过的研究同一问题的论文并没有观察到语言流行度与 agent 有效性之间的相关性,因为它们要么看的语言太少、无法就这种微弱的相关性做推理,要么看的问题太小太琐碎)。
Alloy
Alloy 常被称为有界模型检查器。对 Alloy 6 来说这也许不太准确,因为它引入了一些额外特性,但这远在我的专业范围之外。我的理解是,用 Alloy 时你通常是证明关于你模型的性质(而不是证明你的代码能工作)。
Alloy 拿到了倒数第二的正确性分数,而且不同寻常的是,它在 medium 和 xhigh 两档都普遍表现不佳。虽然没在图上展示(因为展示它似乎没有增加信息),但总体而言 max 与 xhigh 的结果高度相关,而它们与 medium 的结果相当不同。
和我们在 Verus 上看到的一样,使用 Alloy 的 agent 在正确性上几乎完全依赖标准的 Rust #[test],而用 Alloy 只是瞎折腾。再一次,糟糕地使用形式化工具并没有帮到正确性。
有个别 Alloy 的使用案例接近发现某个问题或风险,但即使如此,数量也很少。有一个案例里,Alloy 找到了一个反例,随后让 agent 在 Rust 实现里为这个潜在 bug 加了缓解措施。不幸的是,那个反例依赖一个 8 位溢出,而这在实践中不可能发生,因为实际实现用的是 64 位 usize,在给定输入下不可能溢出——所以它只是让代码更复杂,并没有防住一个真实的 bug。
另一个案例里,Alloy 规格本身是错的,一个相关的测试失败了。测试失败之后,agent 修好了 Alloy 规格。如果那个规格本来就是对的,也许 agent 不用经历失败就能写出正确的代码。也有一些案例可能是这类好事发生了,但看不出是否真的防住了一个潜在的 bug。
Alloy 的 agent 建模的东西比 Verus 的 agent 更贴近 Zstd 算法(后者大多检查算术之类),但建模仍然是错的。
差分测试
差分测试是一种技术:把同样的输入给多个实现,然后比较结果来发现问题。原则上,这看起来是用 LLM 时值得一试的做法,因为不同的掷骰子常常给出不同结果;而且正如我们在这里指出的,让 agent 在某个实现上迭代更多次(可能只是整个东西的一部分,甚至只是一个函数的一部分)往往比让 agent 从头重来效果更差。
但这给了我们倒数第三的结果。这个条件在 xhigh 上略高于平均,在 medium 上远低于平均。没有任何 agent 创建两个完整实现来做比较。160 次运行里,135 次做了某种可以称为差分测试的事,但和我们已经看到的其他条件一样,这些一般都很平凡、实际上没用。而且在差分测试本可能抓到 bug 的情况下,agent 没有以独立的方式实现,而是把同一件事做了两遍,把同一个 bug 编码进了两个版本。
我有时会让 agent 独立地做事、并让它们用各自独立的上下文启动,但差分测试这里没有有效地做到这一点,agent 一般就是把同一件事写两遍。
Hegel Skill
讨论官方 Hegel skill 如何改变 Hegel 的行为是有意义的,但按正确性从差到好排列时,Hegel Skill 出现在 Hegel 之上,因为它在正确性上结果更差。关于这个 skill 的讨论见下面的 Hegel 一节。
Lean 4
Lean 4 也许可以被描述为一个交互式定理证明器。
虽然我没有预注册关于 Lean 的猜测,但如果我预注册了哪些形式化工具会表现好,我会把 Lean 放进预期表现好的名单,因为它相对热门/时髦,因而由于 RL 环境的合成数据,表现好的可能性相对较大。
Lean 的 agent 确实证明了性质,和 Verus 条件一样,agent 大多做的是算术证明,没有触及那些容易出 bug 或有风险的部分。
和其他形式化条件一样,Lean 的 agent 严重依赖标准的 Rust 测试。和目前为止的形式化条件一样,证明几件无关紧要的事并没有帮到正确性。
QuickCheck
QuickCheck 是一个基于性质的测试库,大概在很长时间里是最有名的这类库,虽然现在这个头衔也许属于 Hypothesis。
不幸的是,agent 使用基于性质的测试的有效程度,和它们使用我们迄今看到的形式化工具差不多。使用 QuickCheck 时,agent 大多写非常简单的「冒烟测试」,什么都检查不了。它们还使用随机输入,而完全随机的输入对测试 Zstd 这类东西相当糟糕(因为随机输入只会走上少数几条失败/被拒的代码路径)。
另外,被检查的性质相对很少。虽然所有 agent 都用了 QuickCheck,但 160 次运行里有 63 次只检查了一条性质。agent 再一次主要依赖传统测试,尽管它们在技术上确实用了 QuickCheck。出于某种原因,agent 实际写的传统测试比 Default 条件或大多数其他条件更多,但做的测试-修复迭代更少(这让这个条件的成本低于平均)。
TDD
TDD 在这里表现不佳,在 IMAP RFC 评测里也一样。
TDD 提示词似乎让 agent 行为发生了很大变化。agent 产生的测试数量翻了一倍,并且以一种迭代性强得多的「测试-代码-测试-代码」工作流工作,虽然 TDD 的拥护者大概会说 agent 并没有真正使用 TDD。只有少数几例 agent 做了某种细粒度的迭代式 TDD。
总体而言,agent 事前写了更多测试;比如,agent 在做实质性(非桩)实现之前就已经有一个或多个失败测试的情况有 67 次(共 160 次),而 Default 条件是 0 次(共 160 次)。
按宽泛的测试类别看,TDD 在每一类上都有更多测试。既有更多小的、平凡的测试,也有更多集成测试和端到端测试。任何那种「agent 在某方面做太多或太少」的明显高层判断,似乎都不符合数据。如果我们看具体的失败以及测试是如何漏掉它们的,可以观察到 TDD 条件有不少这类情况。比如,当有四条 Huffman 流时,Zstd 会用一个叫跳转表的东西。
TDD 的 agent 更可能在这个特性的评测测试上失败,尽管它们写了更多覆盖一般情况的测试。出于某种原因,TDD 的 agent 更可能写出不覆盖难例的测试(比如把四条流做成完全一样,然后还把它们做成平凡的,就像我们在 Verus 那里看到的)。这又是一个我会好奇 AI 实验室里的人有什么样视野的案例,因为从外面看不出为什么用 TDD 给 agent 做前置引导会让它们写出更差的测试和更差的实现。
如果我们只有 TDD 和少数几个测试条件可看,一个假设可能是:TDD 出来的代码常常有很多不太好的小测试,所以用 TDD 引导 agent 也许会让它们写出更多这类无效测试。但不清楚为什么我们在 Verus 上也该看到同样的模式。也许如果我们能确认 Verus(或其他)行为是否有共同原因,就能判断这对 TDD 是否成立——比如在一个开源模型上重跑这个实验,并在某个层面上检查模型内部实际发生了什么?
有两个 skill 也让 agent 以更迭代的方式工作,也许背后的理论是执行得更频繁会得到更好的结果,而这两个 skill 同样表现不佳。总体而言,在所有条件里,agent 都能让它们自己写的测试在 xhigh 和 max 上通过(max 没展示,但它的正确性略好于 xhigh,成本却便宜得多)。更迭代地让自己写的测试通过,往往会让 agent 写出更多错误的测试,从而把错误行为固化下来。
Yossi Kreinin 对 TDD 为什么可能导致更差的测试有过这样的想法:
顺便说,我认为如果你在写代码之前写测试,那么测试难例会更困难,因为你对什么会变难知道得更少;即使你做随机测试(我不认为「tdd」与随机测试有关联),你也更不可能把分布引导到 bug 所在的方向。如果你写了代码,或者至少能看着它,你就知道什么看起来显然正确、什么可能行也可能不行,因为它的行为并不容易理解。换句话说,tdd 把你引向黑盒测试,而对复杂的机械来说,在我看来黑盒测试不如白盒测试有效;我很确定对人来说是这样,对 agent 就不太确定。
我关于 TDD 会表现不佳的猜测对吗?严格就结果而言,答案是对。就我的推理而言(没有明确以书面形式预注册,但我知道我当时在想什么),我认为不清楚。我的想法大致是:正如我们最近在各种语境里讨论过的,让 agent 真正做对的事、而不只是过拟合,是用 agent 取得好表现或正确性的关键一环。就方法论一般而论、而非就这条指示如何改变 agent 行为而论,TDD 似乎天生容易导致过拟合。
agent 确实写了更差的测试,有时用了相对昂贵而无效的迭代工作流,但我不确定我预期的那种「人用 TDD 然后指挥 agent 去实现」的失败模式是不是这里真正的问题,而我的直觉正来自那个问题。我会把这里的推理评为「也许在正确方向附近,也许不在」;我认为要判断这一点需要更多评测和调查,而我猜更多数据的结果会是我的原始推理是错的。
Spin
Spin 是一个模型检查器。
现在我们进入结果离平均不太远的范围了。Spin 在 medium 和 xhigh 两档都略差于平均,成本低于平均。和我们在其他形式化工具上看到的一样,Spin 的使用普遍无效。具体到这个案例,用 Spin 对某一类行为建模,与覆盖该行为的隐藏测试通过或失败毫无相关性。Spin 的使用是表面的,也没有产出。
Hegel
Hegel 是一个基于 Hypothesis 的基于性质的测试库。
到现在我们大概都能预料到,agent 没有有效地使用 Hegel。在它们用它的程度上,用得也很表面,而且通常是在大量依赖普通测试之后才用。因为反复说「agent 没有真正有意义地做这件事」很啰嗦,这些小节我会写短一点,只挑出特别奇怪的地方。
agent 实际使用的工作流一般是
- 读 RFC 和 API/契约
- 实现 Zstd
- 跑普通测试
- 读 Hegel 文档
- 用 Hegel 写 1 到 4 个简单的基于性质的测试
- 继续使用普通的内置 Rust 测试
如前所述,Hegel skill 并没有提高正确性。正确性更差了(虽然差距小到可能只是随机)。更引人注意的是成本高得多(medium 高 26%,xhigh 高 41%),而这些原因看起来是因果性的。
这个 skill 让 agent 生成更多测试。多出来的测试大多是在检查畸形输入不会导致 panic,以及往返(round-trip)测试。前者是所有基于性质和模糊测试条件里 agent 本来就倾向于过度做的事,所以在这上面多花力气没有用。后者本身看起来不算坏主意(我甚至经常明确指示 agent 创建往返测试,它们对检查特定性质看起来是有用的),但它没有做在任何最容易出 bug 的地方。没有额外指示时,agent 倾向于为那些本来就很可能正确的平凡性质写往返测试。
至于成本,有多个原因。其一是这个 skill 相当大(skill 本身 3.4 万字符,还会加载一份 4.5 万字符的 Rust 专用参考,最终超过 2 万 token)。它在运行开始时被加载,并在之后许多次操作里被反复读取。这让平均额外费用在 medium 上增加 16%、xhigh 上增加 18%(按原始 token 计,medium 平均增加 90 万、xhigh 增加 180 万;虽然这些内容的缓存命中率很高——初次读取之后达 99.85%——但被反复读取的次数足够多,仍然占总成本的相当一部分)。
还有一个乘性成本(这个乘数已包含在前面的数字里):这个 skill 还规定了一套结构化操作,导致要多做很多工作。这些工作没有提高正确性,所以只是增加了成本,没有相应收益。
需要注意的一点是,这个 skill「只」在 160 次里的 157 次被用到。和用 LLM 时的一般情况一样,行为和结果是随机的。如果你有一个你认为 agent 在某项任务中应该使用的 skill,它用不用可能取决于一些在 AI 实验室之外的人看来不透明的因素。
ToB skill
这个案例里,160 次运行中只有 108 次真的打开并读了那个 skill。这个 skill 建议在 Rust 里用 proptest,但它也建议添加依赖需要批准,而这些全是单轮自主运行,所以没有做。
和目前为止其他基于性质的测试案例一样,性质测试很初级,也没有以有帮助的方式去做。
Rstest
Rstest 是一个基于夹具的测试库。
agent 实际上没有用 rstest。它们技术上确实用了它,但基本上只是在 rstest 里写标准单元测试,没有按 rstest 的意图使用它,等于废掉了 rstest 的意义。虽然从某个高层意义上说,这一点对目前为止看到的技术都成立,但 agent 至少表面上用了其他一些技术(比如用 Hegel 写了一些低价值的性质测试),而这里 agent 没有使用那个让 Rstest 之所以是 Rstest 的东西(如果换成基于性质的测试库,类似的失败就是只用它们写非基于性质的单元测试)。
Rust test
这里指的是标准的 Rust 内置测试框架,agent 在 Default 条件下用了它,在其他条件下也非常依赖它。
明确要求 agent 使用内置测试框架,导致测试更多(medium 上是正常的两倍,xhigh 上多 25%),但这并没有带来更好的正确性。agent 出错时,往往是因为它们没有测试重要行为,或者实现了错误的测试行为。加更多测试并没有实质性地提高风险行为的覆盖,也没有减少那些测试里编码了错误行为的运行比例。
Yossi Kreinin 补充说:
我认为固定输入/输出式的测试在机器和人身上都会助长这一点。如果你生成输入,你就需要再有代码来判断输出是对是错,而这段代码本身可能有 bug,但它至少比「跑一遍代码、假定它的输出就是正确答案」更能让你去思考什么算正确、以及如何判断某样东西是否正确。而用固定输出,你很可能就是直接把代码的输出编码下来,然后说服自己它说得通。
Creusot
Creusot 处在和 Verus 相同的空间里。
和我们看到的其他形式化条件一样,Creusot 没有被有效使用。
变异测试
变异测试的做法是修改代码来判断测试有多有效,然后补测试以获得好的覆盖。虽然变异测试是一个标准的编程术语,但 agent 一般并没有真正做变异测试,而是做普通测试、外加少量某种算不上变异测试的改动,就像 TDD 那条指示改变了行为、却没能让 agent 做 TDD 一样。
有少数几例确实发生了变异测试,但量很小,而且很罕见。
Judgement
这个条件要求 agent 依据自己的判断自适应地使用适当的测试方法。鉴于我们迄今看到的,不出意外,agent 大多使用标准的 Rust 单元测试。少数 agent 做了一些有限的模糊测试。agent 可以使用其他测试库和形式化库,但没有用。
模糊测试
模糊测试的做法是以某种方式随机化测试输入。
agent 严重依赖往里扔随机字节,而这大多只是走上同样的代码路径(无效输入)。agent 也试过扔进有效输入的随机变体,这大多也只是反复走输入被拒的路径。
在 agent 生成随机结构化输入的那些罕见场合(160 次里有 10 次),有一半发现了真实的 bug,其中一些还是非平凡的案例。160 次里有 5 次比较有效地用了模糊测试,算不上好,但这是目前为止我们看到的技术里比较有效的用法之一。这似乎也说明,agent 可以被训练得更擅长做这件事,也说明不用改变训练就能引导它们做得更好。某种意义上它们知道怎么做;只是在不被推动的情况下,它们通常不会真去做。
Insta
Insta 是一个做快照测试(有时叫 golden testing)的库:你把结果与一份「快照」或「golden file」形式的正确结果做比较。一般说来,快照通常是某种序列化数据,比如某个数据结构的 JSON 对象、CLI 输出的日志,等等。
如你所料,快照测试几乎没被用上,agent 大多依赖传统测试。agent 确实用了 Insta,但往往只是在 Insta 里写普通的单元测试。
SMT
agent 被指示使用 SMT 求解器,Z3、cvc5 和 Yices 都装好了。
agent 大多把 SMT 求解器当作某种草稿纸,用来算 FSE 状态范围、头部算术之类的东西。即使 agent 建了模,它们一般也没有对正确的东西建模,从而避开一个常见错误。
比如有一处计算本应是 byte1 + (byte2 << 8) + 0x7F00。很多 agent 实现成了 byte1 + (byte2 << 8) | 0x7F00。agent 用 SMT 求解器证明了与这处计算相关的性质,然后仍然写错了代码,使得 SMT 的使用看起来并不比 Default(不加指示)更好。
TLA+
TLA+ 是一门用于对行为建模的语言和工具。
我们进入了高于平均的那一组结果(但仍然比 Default 差),不过如前所述,我不会太当真于实际的排序。这不一定有意义,但 TLA+ 在 medium 上略高于平均,在 xhigh 上高于平均更多一些。
160 个 agent 里有 159 个创建了某种 TLA+ 模型,一般是一个 Zstd 的状态机模型。就具体覆盖而言,有 30 个对 Huffman/FSE/熵编码建模(这些是常有 bug 的区域)。和其他形式化案例一样,TLA+ 建模发生在流程相对靠后的位置(在大量标准测试和实现之后)。agent 有时会发现并修掉 TLA+ 模型里的错误,但我没找到任何一例 TLA+ 的问题最终导致 Rust 代码真的被改动。
虽然确实发生了一些看起来像真的 TLA+ 建模,但如果它提高了正确性,那也是以很难观察到的方式小幅提高的。总体而言,TLA+ 建模更精细的运行,正确性并没有更好。
变形测试
用变形测试,我们检查相关的输入产生的输出之间具有预期的关系。比如你可以检查:对一个排序函数,改变不相等输入的顺序不会改变输出的顺序;或者对加法,给一个输入加上某个值,会让输出也加上这个值(模溢出)。
和我们在其他条件上看到的一样,变形测试在正确性方面没有被很有用地执行。确实检查了一些还算合理的性质(比如在帧边界插入一个可跳过帧不应改变输出、合法的块重新划分不应改变输出,等等),但这些没有触及 agent 相对频繁出错的那些区域,所以检查这些性质没有帮助。总体而言,agent 似乎是那个老笑话的粉丝:
一个警察看见一个醉汉在路灯下找东西,问他丢了什么。醉汉说钥匙丢了,于是两人一起在路灯下找。几分钟后警察问他是否确定是在这里丢的,醉汉说不是,是在公园里丢的。警察问他为什么在这里找,醉汉回答:「因为这里才有光。」
有意思的是,变形测试在 xhigh 上用得比 medium 上少。
ECC
ECC 的 Rust 测试 skill表现还行,但主要是因为 skill 里很大一部分被忽略了。agent 一般会打开并读这个 skill(160 次里 153 次读了),而这似乎让它们生成了更多测试。不仅这个条件里 agent 生成的测试更多,如果我们看 agent 是什么时候读这个 skill 的(较早、较晚、从未),测试增加量上存在一个与曝光程度相关的梯度。
虽然 ECC 的分数几乎和 Default 一样好,但根据 agent 更多暴露于该 skill 时的表现,我猜这是随机的。agent 看这个 skill 越早,行为受影响越大,正确性结果越差。
ECC 在原始分数上看起来还行,是因为没读该 skill 的那 7 个 agent 表现异常好、拿到了 100% 正确的结果,而另外 9 个较晚看到 ECC、几乎没受影响的 agent 也表现不错、正确率 100%。这也解释了 ECC 那个不寻常的结果:medium 和 xhigh 分数一样(agent 基本没看 skill 的那些运行几乎都发生在 medium 上)。虽然 skill 是否被调用可能存在某种偏差,但整体模式说明 ECC 是无效的——除非你认为 ECC 起到了护身符的作用,能在 skill 其实没被真正使用时改善结果,而这种情况在较低 effort 档更可能发生。
当然,agent 不应该被一个它们没看过的 skill 影响,我们应该按 skill 确实被使用的情况来打分。如果看 skill 真的影响了 agent 的那些案例,ECC 的分数低于平均(介于 Rust 内置框架和 Creusot 之间),失败模式与 Rust 内置框架非常相似:大量小而没意义的测试。这个 skill 告诉 agent 使用红绿 TDD。agent 的行为大概不是 TDD 实践者会称之为 TDD 的东西,但 agent 确实会在实现功能之前写一个小测试,这导致测试数量很大。如前所述,这不是 agent 有效的开发方式,所以结果比不加指示、不用 skill 更差。
顺便说,正如我们在试用 Caveman 模式时指出的,方差相当大,人们常常被少量几次运行误导,以为某个 skill 有用。这个案例里,我们对一个 skill 试了 160 次运行,这是相当大的数量,比任何理性的人会做的都多。然而表面上,如果我们只看分数,ECC 看起来还行。
要把使用 LLM 时固有的噪声平均掉,我们需要多得多的运行次数。我们可以像这里这样做,检查结果、稍微用一下人的脑子,但在人们谈论公开 LLM 基准时,我很少看到有人这么做,无论是 skill 还是别的什么(我确实试过让 LLM 分析结果,但和往常一样,即使是当前公开的 SOTA 模型,分析也很糟,充满基本的推理错误)。我看到的更多是人们传播那个头条数字,即使它出于无聊的统计原因并没有意义,或者更糟——基准本身有致命缺陷,就像我们在 Senior SWE-Bench 上看到的。
Default
Default 不给 agent 任何测试或验证指示。
鉴于我们迄今看到的,Default 得分高于平均并不意外。当被要求使用特定库或特定测试技术时,agent 做的事一般都没用。不告诉 agent 去做那些会让它们做无用功的事,比告诉它们去做那些会让它们做无用功的事更好,这很合理。
Audit
Audit 要求 agent 在实现之后审计代码。160 次里有 152 次真的这么做了,151 个 agent 声称发现了一个问题,并因这次审计做出了改动。agent 一般会挑合理的区域来审计,但通常没有用全新的上下文做独立审计(我经常要求 agent 这么做),而且往往在审计里犯了和之前已经犯过的同样的错误。
有 42 次用了独立 agent,但这些运行实际上得分更差(这可能不是因果关系,而是 agent 因为处于更糟或更难的处境才决定另起一个独立审计)。Audit 在 xhigh 上拿到了最好的正确性,但在 medium 上低于平均,而所有这些审计大幅增加了成本,尤其在 xhigh 上。平均而言,Audit 的表现和 Default 差不多,不清楚它在 xhigh 上是否真的更好、在 medium 上是否真的更差。这有可能是真的,但我认为我们没有足够证据判断。
Em Chu 有这样的评论:
这里的结果与我的经验一致。审计代码是目前我 token 花得最多的地方,因为我觉得它相当有用。不过我总是给两条指示:
- 不要派生子 agent;你自己去读并理解代码/diff
- 不要执行任何代码 因为我发现,如果你让 LLM 做这两件事中的任何一件,它都会明显变笨(当然我没有量化过……)。它默认真的不读也不推理代码,即使我从没做过大到超过它上下文窗口的改动。 我还通常会加一些废话,比如「要有对抗性」「考虑所有可能的特性组合」「考虑整个输入空间」,但我不太确定这有没有帮助。
试试那个会很有意思,但正如我在最近几篇文章里提到的,我正在尝试在文章里写得更简略,所以也许那会是另一篇文章的主题。
Audit and fuzz risky areas
对 Zstd 来说,当这条指示被遵循时,它让 agent 大量聚焦于 FSE、Huffman、位读取器和状态。总体而言这些正是 agent 常常漏掉问题的区域,所以 agent 认为这些区域有风险是对的。被指定做模糊测试的区域,比单纯 Fuzzing 条件的选择更好。
在 medium 档,agent 大多忽略这条指示、没有执行,但在 xhigh 上它们确实遵循了。虽然这个条件表现不差,但看起来也没有比不加指示更好。
看 agent 实际做了什么,一个问题是 agent 常常只是生成一堆随机输入,而这些输入一般无效、也测不到任何有意思的情况。
人类测试者生成随机测试时,一般会试着让随机化指向那些能产生「有意思」输入的方向,而 agent 没能做到这一点。agent 检查输出也不够有效,很多情况下只是找崩溃。模糊测试常被认为就是只找崩溃、不检查性质,所以这也许不太意外,但如果有人在测试一个 Zstd 实现,这大概不是他想要的。
Make no mistakes
虽然这个条件在技术上得分高于 Default,但行为看起来没有实质差别,分数也相当接近;我猜这是随机波动。在我看结果的每一个层面上,它都与 Default 的随机抽样无法区分。
Kani
Kani 是一个 Rust 的模型检查库。
就「真的在将要执行的代码上使用形式化方法」而言,Kani 的覆盖最好,因为 Kani 确实被用在了 Zstd 代码上。不过这只偶尔发生,大多数使用都很表面。
有一例真实的 Kani 使用抓到了一个非平凡的 bug,并导致 Rust 代码被改动。160 次里 1 次算不上惊人,但它确实说明 agent 有时能凑巧合理地使用 Kani(我猜这意味着,如果放在 RL 环境里,模型可以学会更有效地使用 Kani)。
Kani 的成本明显高于其他条件。这似乎是因为反复读 Kani 的输出很贵,导致输入 token 成本很高。
ACL2
ACL2 是一个定理证明器。这里结果有一点要注意:很多情况下 ACL2 会 OOM(192 GiB 上限)。OOM 的结果没有被计入,这以某种不透明的方式让结果产生了偏差。
虽然 ACL2 得分高于 Default,但如果这是因果性的且显著的,我会觉得意外。和我们看到几乎所有其他形式化方法一样,ACL2 大多被用来证明那些对正确性没有显著影响的东西,所以不清楚它为什么会提高正确性。
在有这么多不同条件的情况下,除非其他条件表现严重退化,我们本来不会预期 Default 或者看起来与它等效的 Make no mistakes 能排在最前面。
Proptest
Proptest 是一个基于性质的测试库。
正如我们在其他随机化测试上看到的,大多数测试没什么意思,而过度依赖随机性导致覆盖很差。
尽管基于性质的测试总体上用得不好,proptest 的收缩(shrink,即找出一个能触发测试失败的更简单输入)有时确实提供了价值,这比我们在大多数其他案例里看到的「几乎没有价值」要好。
基于性质的测试
和其他基于技术的做法一样,agent 拿到一个装好了所有选项的容器。每个 agent 都选择用 proptest,所以这实际上变成了第二个 proptest 条件。
和 proptest 条件一样,测试大多不太好,但它们有时确实发现了 bug,收缩似乎也带来了一些收益。
我觉得有点意思的是,这第二条「意外形成」的 proptest 分支同样得分远高于平均,和 proptest 一样。
Skill
这里的 Skill 指的是我写的那个 skill,用来测试「拥有一个简单的 skill」会怎样(相对于我让 agent 去找相关测试 skill 时找到的那些又大又复杂的 skill)。
也许我该用 skill,但我一般不用,而是依赖提示、看发生了什么、再继续提示。因此,我对什么样的 skill 算好毫无直觉,因为我没有练习过;但 Max Bittker 建议说,用一个试图把我脑子里关于测试的部分知识编码进去的测试 skill 来看看结果会很有意思。看到结果之后,他露出了「我早说过了」的反应。
我没有为这个预注册猜测,但我脑子里的猜测是它不会很管用。从我 2015 年试着把这件事传达给人的经历来看——我会说那次基本失败了——我认为我并不擅长用文字明确地写出一个人应该怎么测试。我坐下来给人演示过该怎么做,这通常能一劳永逸地改变他们,把他们变成远超平均的 bug 猎手;但能通过演示传达一件事,和能通过写下做法来传达它,是两种不同的(也更容易的)技能。
这个案例里,skill 是这样的:
- 在实现之前,思考哪些区域可能有微妙的 bug;对每一处,写出可能的错误和合理的另一种解释,然后想出一个结果会有差异的检查(偏向不对称的/边界两侧的例子)
- 实现之后,对高风险区域,在没有生产代码上下文的情况下独立重新推导结果并比较(全新上下文,不要复用辅助函数)
- 在可行时,用基于性质的测试或随机输入去探索空间,尽量减少在「不 panic」「不崩溃」这类随机化上的投入
- 随机化时,倾向于那些能探索有意思的状态和代码路径的输入(不要天真地随机化那些全都会落进同一条错误路径的输入);这可能需要结构化的随机输入
- 如果你对细节不确定,用独立推理去核对什么是对的(全新上下文,不要复用辅助函数)
这个拿到了最高分,但并没有按预期起作用。它几乎没有真正做过「全新上下文」那件事,所以把它写在里面是没意义的;我们也不知道这是一件有效、只是需要改进以迫使 agent 更频繁去做的事,还是一件应该删掉的事(虽然它在技术上有可能正好以最优频率发生,但我非常怀疑)。
我们在「Audit and fuzz risky」里指出,agent 似乎知道怎么识别风险区域。这里同样如此,但这不一定意味着 agent 做了对的事。比如 agent 识别出 Zstd 里编码与解码的比特流顺序被颠倒是风险点,但 agent 在覆盖这一点的测试上并没有做得更好。如果看具体例子,medium 的第 35 次运行里,一个 agent 把它识别为风险点,做了独立推导和审计,但仍然失败了。它有一个相关的测试,但输入是回文的,所以颠倒顺序会给出同样的结果,从而让一个把顺序搞反的实现通过了测试。
另一个问题——如果我们能称之为问题的话——是所有的模糊测试/基于性质的测试都是「手工」做的。考虑到 agent 似乎还比较会用 proptest,而 proptest 有一些有用的机制可以依靠,这个 skill 大概只要指示 agent 使用 proptest 就能轻易改进。那些意在减少「生成大量没用的、过度随机的测试」这一标准失败模式的指示,方向上是有效的,更多比例的 agent 生成了多少有点意义的测试,但这些测试仍然比我预期一个人(或者一个有人主动指导的 agent)会写的要差。不做迭代的话,我不确定什么样的通用指导会好(相对于花几分钟看一下 Zstd 的结构、给出 Zstd 专用的指导,后者是我在其他问题上用得很顺的一类做法)。
作为一份待迭代的 skill 初稿,我不觉得它很糟,但也不觉得它真的能用。如果拿多得多的例子来试,确保它没有对 RFC 类问题、位操作密集类问题等过拟合,我能想象一个改进版会管用;但由于我平时不做 skill、也从没试过迭代一个 skill,它没能体现我或另一个人在真正驱动 agent 时会怎么做。
因为我习惯了先提示、再看结果(不一定是看代码,但至少看 agent 说它们做了什么、某种对发生了什么的 agent 式总结,某些实验工作还会看部分实际结果),然后据此继续提示,所以我不习惯把信息前置——而前置信息和根据信息作出反应是相当不同的问题。从以前的模糊测试工作里,我见过 agent 常掉进去的失败模式,这个 skill 就是用来防止那些失败模式的;但如果你哪怕偶尔回头看一下,做这件事会比完全前置更容易,而这些前置指示并不足以挡住那些标准失败模式,虽然它们多少缓解了一些。
总体评论
如前所述,我没有试着用漂亮、易读的方式拆解数据。我之所以没这么做,是因为一旦看 agent 实际做了什么,它们似乎大多相当无效,而我认为看「糟糕地使用 Verus 的 agent」比「糟糕地使用 QuickCheck 的 agent」好多少并不特别有意思。我觉得有点意思的一点是,当被要求识别有风险或容易出微妙 bug 的区域时,agent 能做到。
但总体而言,无论建议的是哪个库或技术,agent 都没能使用那项技术。如前所述,只是让 agent「测试」、或者反复让它们多测试,得到的是糟糕的测试。结果发现,让它们使用测试技术(其中一些我个人认为非常有效)同样得到糟糕的测试。我写的那个快速粗糙的 skill 看起来能稍微改善情况,但要真正有用,需要的远不止我花在上面的两分钟。Yossi Kreinin 评论说,软件测试的现状很糟糕,所以可以这么说,如果 agent 退回到它们的训练所教会的东西,我们就该预期糟糕的结果——而我们看到的正是这样。
不知为何,agent 在使用 proptest 上似乎稍好一些,尽管测试水平远低于我预期一个读过 proptest 手册、又得到了一些测试方向的理性的人所能做到的。我很好奇,被给予更多方向的 agent 使用 proptest 是否比其他库更有效;但那是另一篇文章的话题了,因为我一直想半小时内发一篇,而这篇已经接近 9000 字,超过了半小时内能敲完的合理字数。
怎么让 agent 写出好测试?
我的经验是,如果你引导 agent 建立起一套合理的测试与分诊结构,那么在没有大量监督的情况下让 agent 有效地往里面加东西,效果还算凑合。由于我的背景(偏见),我倾向依赖的测试方式是某种随机化测试/模糊测试/基于性质的测试。
我和 Jamie Brandon 聊过这件事,他在快照测试上也有同样的发现。他提到,在某个项目里,当他让 agent(用了多种模型)做快照测试时,它们会说自己在做,然后根本不做(它们会写一个单元测试,然后说自己写了快照测试)。在另一个项目里,他让它们用模拟 IO 写出了合理的端到端测试,但前提是把测试移到一个单独的 crate 里,并在 AGENTS.md 里写明:测试要留在这个 crate 里、不要修改公共接口。
至少到目前为止,我比 Jamie 更依赖让 agent 写测试代码(我的习惯是在 CLI 里对 agent 打字;至少目前,他比我更喜欢手写代码),但只要你建立起某种合理的结构,怎么做似乎并不重要。
和我们此前看过的那个问题类似,看起来只要做了任何勉强合理的事就管用。如果你只是「跟」agent 说几句话、给它一些轻量指示——就像我们在 Zstd 或 IMAP 评测里做的那样——agent 会做得很差。但如果你看看它做了什么、再打几句,你往往很快就能把它带到不错的位置(至少这是我在其他问题上的经验)。我从未试图理解模型为什么会管用,所以我编造的、大概完全错误的猜测是:模型内部某处存在某种关于怎么做好这些事的理解。它不是默认行为,甚至离默认行为不够近——近到点名该用什么技术就能生效的程度——但如果模型被充分引导,关于怎么做好这些事的知识确实会被付诸实践。
我很好奇这能否被有效地打包成 skill,或者 AI 实验室是否会开始训练模型、让它们在测试或形式化方法上更强,又或者他们正在做的那些不直接改善这些能力的事情,会不会间接改善得足够多,以至于 agent 不需要多少监督或结构就能写出像样的测试。
预测准确度
- TDD 会表现不佳(55% 把握)
正确
形式化方法不会超额表现(52% 把握)
- 正确,但不是我预期的原因。agent 根本没能有效地使用它们,所以它们当然不可能超额表现
「别犯错误」不会比不加指示更好(95% 把握)
- 正确;它超过了大多数条件,因为什么都不做比让 agent 去做无效的事要好
ECC skill 不会超额表现
- 正确;我想如果我用 skill 更多,我对这一点的把握会更高,因为该 skill 的大部分文本看起来什么都做不了,而看起来会起作用的那部分文本看起来还会起反作用
Hegel skill 不会超额表现
- 正确;又一个「如果我用 skill 更多把握就会更高」的例子,因为它没起作用的原因正是我猜的那个;我只是对自己的感觉毫无把握
ToB skill 不会超额表现
- 正确
我没有注册、但能看出自己隐含持有的猜测(因为看到结果时我感到意外):
- Lean 在形式化方法里会表现相对较好
错误
我的 skill 会是平庸到糟糕
- 错误。尽管 Max Bittker 正确猜到了会发生什么、事先告诉了我、并给出了一个与实际情况一致的原因;当有人指出了某件事会发生的原因而你不信,然后它真的以他们指出的原因发生时,感觉相当傻
Skill
在不同时期,我都曾觉得自己有一套糟糕/过时/无效的工作流,因为我听说别人在做什么,而我懒得去试。对 skill 我有一阵子有这种感觉,因为我并不怎么用 skill。我取而代之的做法是维护一个很大的草稿本,有时把里面的东西复制粘贴进去当提示词——这有点像为了保存代码块而把它们注释掉、而不是用版本控制。
但后来我看了 Thorsten Ball 的这个演讲,他在里面提到自己并不重度依赖 skill;我又和几个看起来用 LLM 比较有效、也不怎么用 skill 的人聊过,这让我开始想自己是不是并没有错过多少。
然后我做了这个实验。我的感觉是,我看过的那些 skill 不会有帮助,而且很可能实际上有害——把握很低,因为我对 skill 一无所知。这些 skill 的表现基本上就是我以为的那样,所以看起来,我从观察 agent 如何响应事物、以及跑一堆小实验里得到的直觉,对 skill 也还算站得住。我还看过不少号称能改善测试、但没有纳入这个实验的 skill,它们看起来会有和被测 skill 相同的失败模式。
我还在另外两个「官方」skill 上跑了两个实验(细节这里不讨论,也许以后另一篇 1 万字的文章里再说),那些是公司为支持自家产品而提供的 skill。一个来自某家大型 AI 实验室,另一个来自一家「小」公司(几十亿美元规模),但在两个案例里,skill 都让结果变差了,就像我们这里看到的。有意思的是,做完这些实验之后,我其实比之前更看好 skill 在个人使用中的前景,因为失败模式看起来是可预测的,因而可以在不做大量昂贵实验的情况下修掉。要做一个公开发布的 skill、并指望它真的很好、能在不同模型和 harness 上通用,看起来可能很难(claude 和 codex 似乎「想要」不同风格的提示,所以对 skill 当然也该如此),但仅仅解决那些让很多 skill 在个人使用中不如不用 skill 的问题,看起来相当可行?
关于写 skill 的朴素想法
我对 skill 的了解不足以说明怎么写一个好 skill,但就这篇文章里看过的所有 skill(除了我花一两分钟写的那个)以及另外两个实验里的 skill 而言,它们写起来都像是给人看的教程说明——也就是说,skill 的目标似乎是解释怎么做某件事。作为一个只写过一个 skill 的人,我朴素的想法是:当模型本身就该对主题有一定了解时(这里和另一个实验里都是这种情况),这大概不是最优的。模型本来就会有一个默认行为分布,所以我觉得更自然的做法是给出能修改那个行为的陈述,而不是写出那些能让一个人或一个没有相关知识的 agent 从零做起这件事的说明。
一个明显的问题是,不同的 harness、模型和 effort 档会给出不同的默认行为,但往提示词或 skill 里扔一堆文本并不能改变这一点;那只是用更长的方式把 agent 推离默认,而且其中很多文本可能会造成某种无意的推动。如我们这里看到的,大部分文本只是被忽略(而什么被忽略、什么时候被忽略,当然取决于 harness、模型和 effort)。比如 ECC 那个 skill,即使被读了,大部分指示也被忽略;而尽管 TDD 那些指示有影响力(让结果变差),那些要求做 TDD 的指示仍然没有被真正遵循,尽管它们写得很清楚。
由于什么样的提示有效会随模型版本和 effort 档变化得足够多,对于「把代码测试好」这类通用的事,我不清楚这些 skill 该怎样在这么多模型和 effort 档上都管用。仅仅从 GPT-5.5 换到 GPT-5.6 就大幅改变了我的工作方式,因为很多在 GPT-5.5 上相当可靠的东西要么不再管用,要么可靠性差得多(即使总体能力水平似乎更高)。就像我不会用提示 GPT-5.5 的方式来提示 GPT-5.6 一样,我认为我也不会想用同一套 skill。
我不确定除了 AI 实验室里的人,还有谁会真的费劲去对 skill 跑评测、看它对每个模型和 effort 档什么有效,然后做出一套按模型和 effort 区分的 skill 组合;而我也不指望 AI 实验室会去做针对竞争对手 harness 和模型优化的 skill。所以对于通用的「测试」skill 这类东西,我不确定(如前所述,我看过但没在这里测的不少测试 skill,看起来会有和我们测过的现成 skill 完全相同的失败模式);但我能想象拥有几个适用于我自己用例、配合我常使用的特定 harness/模型/effort 的 skill。
仅就测试而言,虽然某些模型在某些 effort 档会掉进某些特定的坑、而我想把它们推离那些坑,但并没有什么通用的测试工作流是我想要交给 agent 的——它与被测试的东西、我想要的质量水平、以及我想要在哪些维度上保证质量都无关。所以我不认为自己会想要一个通用的测试 skill,把 agent 一般应该做的一套测试步骤列出来。我做很多事情时都是这样,我希望它们以任务特定的方式完成,而不是通用方式。我能想象某种 skill 先问我问题、再向 agent 发出正确的指示;但考虑到模型改进得多快,如果我是为自己做东西,我认为花时间把一个这样的 skill 调到有用是不划算的。如果我是在做一个 agentic 产品、想让更多人使用它,那也许是另一回事;但我试过的 skill 都有和我们这里测过的 skill 相同的失败模式,所以要做出一个结果并不太有效的 skill,看起来相当容易。
我能想象 skill 在教 agent 如何执行工作流、或如何与 API/接口交互这类事情上是通用有用的,比如 Sawyer Hood 那个帮 agent 驱动网页浏览器的 skill。因为我没试过那个 skill,所以我不是在推荐它,但从读它来看,它像是那种会很好用、能帮我在想让 agent 驱动网页浏览器时省掉很多麻烦的东西。不过,如果你去读那个 skill 本身(以及脚本),它的风格和我们这里试过的测试 skill 非常不同。
感谢 Max Bittker、Yossi Kreinin、Em Chu、Dennis Snell、@panoramic.blue 和 Jamie Brandon 的评论/更正/讨论。
附言:我在最近几篇文章里都带了一个说明,说我正在做一个实验:尽可能快地把半成品(几乎没修/没审/没清理的)结果写出来,因为 agent 让你能跑实验跑得这么快,否则我根本什么都写不出来。这个实验其实是我在做编程语言 token 成本/正确性实验之后紧接着做的,但我一直没时间写出来,因为我想先写那个用解释器和原生代码编译器做正则引擎的项目、那个跑 ripgrep 分叉版(在 codex 的 ripgrep 查询上用原生代码编译器)的实验,以及其他几件我还没时间写的事;我一直给自己定的目标是每篇半小时写完,但就我跑过的实验与写出来的东西相比,我仍然落后得相当远。
有几年时间,我做这类实验、只告诉几个朋友一些奇怪的结果,然后就继续往前走,从没真正公开谈过这些事。如果你对这些更快(也更低质量)的实验和文章有看法,告诉我你怎么想(X Bsky Mastodon)!
附录:一些回应
遗憾的是,@danluu 是对的。Hegel 那个 skill 目前有点烂。 我认为很多 agent skill 的共同问题是:agent 写 agent skill 很糟,而且每个人(包括我们)都用 agent 来写自己的 skill。
一位共同的朋友提到,MacIver(后来?)建立了一个基准,确认了 Hegel Skill 的缺陷,并且大概正在改进这个 skill、或者调整 Hegel 的注释/文档,让这个 skill 变得不必要。如果这篇文章唯一的产出是 Hegel 得到一个改进的 skill,我认为那已经很棒了。如前所述,我认为测量和基准被低估了,很大程度上是因为仅仅发布一次测量,往往就能指出一个人们不知道存在的问题,并推动一些改变。我有不止几篇文章仅仅通过展示差距在哪里就推动了某种改变。很高兴这篇也是其中之一。
附录:agent 的蠢事
在让一个 agent 为某次分析做一个简单的查找之后,它执行了一个 perl 进程,跑了 2 小时 20 分钟,直到我把它杀掉(我真的需要一个能自动抓这类东西的东西,因为这相当常见)。
一个子 agent 用 perl 在一个相对较小的文件(44kB、1364 行)上做正则搜索,但那个表达式在 PCRE 下是退化的,做了组合爆炸级别的计算量。我试着用我们在这里花几分钟搭的 FRE 正则引擎重跑,匹配在 0.7 秒内完成(Rust regex 是 0.6 秒)。
agent 在外面做事时你永远不知道会发生什么,但这件事本来就不该发生,原因有三个。第一,agent 不该调用一个会带来这种组合爆炸的正则引擎;没有理由不用更安全的正则引擎(比如默认设置下的 ripgrep)。第二,那个表达式是错的;实际那个正则在成功时会返回一个无用的大捕获,并没有做它想做的事。第三,为什么子 agent(或者 harness)没有在子 agent 结束后自动杀掉它?当然,不是子 agent 跑的每样东西都该这样处理,但 agent 常常把这类失控进程就这么扔在那里。
我确实有一个进程专门清理 agent 留下的东西(会泄漏内存的 agent、占空间的临时构建产物,等等),但它并没有在找失控的 perl 进程。这是又一个要加进去的,但肯定我不是唯一遇到这个问题的人。我想我可以把我这个傻工具开源,但等主流 harness 修好这个问题,它就该过时了,所以即使我开源,似乎也没有任何好理由让人去用我的东西。
附录:实验细节
为了快速写完这篇,这部分我就先跳过了(抱歉!)。各条件的分布与我们考察语言如何影响正确性和 token 成本时看到的并没有根本不同。不知怎么的,这篇我本想半小时快速写完的文章已经接近 1 万字,这肯定不止半小时的写作量(半小时写 1 万字意味着每分钟 300 多字)。
我要指出的一点是,就像那篇关于编程语言的文章一样,有几件事看起来是相当有意思/有说服力的结果(至少从只看顶层图、看有没有什么突出的角度是这样),但更仔细看之后,它们是由于给 agent 一个短提示来搭实验所导致的实验错误。修掉那些错误之后,我们得到一个无聊得多的负面结果——除了那部分:codex 建议可能有用的一些 skill 看起来起了反作用。
如果看实际结果,和之前一样,所有方法都不太有效,甚至那些你可能觉得会很合适的方法。比如你可能以为 TLA+ 会表现好,因为它天然适合 IMAP 的状态与并发;原则上它应该很擅长对协议里很多高层部分建模。然而,正如我们在 Zstd 结果里看到的,agent「决定」用 Rust 做完大部分实现,然后才拿出最初提示里让它们用的那个工具(80 个 agent 里有 5 个在写代码之前真的用了 TLA+,但有 75 个是反过来)。不知为何,agent 几乎总是决定用 TLA+ 去建模一个小的邮箱变更。没有 agent 用 TLA+ 去建模多个观察者、事件队列、UIDVALIDITY 与邮箱纪元(epoch)之类的东西——而 agent 在实现这些东西时犯了很多错误。相反,agent 建模的是 CONDSTORE 和 QRESYNC 这类东西,而 Default 条件在这些地方的测试通过率超过 99.6%,TLA+ 的 agent 尽管对这些东西建了模,通过率反而更低。
我为此试过的所有评测都有一个共同点:它们都是 RFC,而 RFC 极不现实;但它们的「极不现实」之处在于,规格比几乎所有程序员在让 agent 实现某样东西时给出的规格都清晰得多、详细得多、歧义少得多。我预计我们在这里看到的失败模式,在大多数现实世界的问题上会一样、或者更糟。
确实有一些与 RL 环境和随机化测试相关的工作,比如这篇论文,但从模型在没有任何具体指导时对 {PBT, fuzzing, randomized testing, etc.} 中任何一项都如此无效来看,这似乎并没有以严肃的方式进入大型 AI 实验室的模型训练。
如果谈的是人类学怎么跟人类打扑克,那么用这类概念大概仍然合理,因为在休闲对局里,人类一般不会想研究求解器线路到足以理解什么接近最优的程度;但如果谈的是编码 agent 擅长什么、不擅长什么,我看不出有什么理由去抛洒鸡尾酒会上的想法——我们本可以跑实验看什么管用,而迄今为止跑过的实验并不支持那些被四处传扬的、关于 X 好或不好的抽象理由。
Footnotes
-
总体而言,我认为 IMAP 的结果没那么有意思。我本以为试它可能更有意思,因为它更像一个「业务逻辑」问题,但不像我们考察语言时看的那个 pandoc 评测那样庞大昂贵——那个评测对一些效果较差的语言每次运行要花超过 1000 美元的 API 成本,而在 Rust 上每次运行也要 700 美元才能达到只有 30% 的测试通过率(注意那个评测用的指标难得多:达到满分的运行比例)。在 IMAP 评测里,只有一次运行拿到了满分(「Make no mistakes」,medium 档)。总体而言,有些逻辑片段显然对 5.6 Sol 来说太难、无法一次做对。我想一种看法是:Make no mistakes 以 medium 档 2.5% 的分数碾压了其他所有条件的 0%。终于,有证据表明「Make no mistakes」管用了! ↩
-
有意思的是,当我让 ChatGPT 核实这一点时,它告诉我这一段是错的,因为有论文表明人们用过 RL 环境训练 agent 测试,然后链接了三篇论文——而那些论文正是通过训练 agent 像大多数程序员那样写单元测试,把 agent 训练成写糟糕测试的。那恰恰就是我会预期导致我们今天看到 LLM 那种糟糕测试的做法:需要有懂得更有效测试技术的人来引导 agent。在多个关心正确性的独立子领域里,人们各自收敛到了几套相关的技术,而这些技术总体上是与写小单元测试相反的。当然,训练 agent 去做这种「与人们认真对待正确性时所做相反」的事,不太可能带来好的正确性。 ↩
-
也许人们为自家语言优越性给出的理由在模型变得好得多之后会成真,但似乎没有任何理由认为事情会那样发展。如果非要我猜,我猜它会更像扑克:人们对什么最有效有过各种想法,而当模拟强大到计算机能在很多情况下战胜人类之后,这些想法就都被推翻了。即使是像「范围优势」(range advantage)这种人们常说与求解器最优打法一致的「现代」概念,仔细看之后其实根本不出自求解器数据。它们只是些很适合在鸡尾酒会上聊的概念,好理解、听起来也有说服力。 ↩
来源: danluu.com← 返回首页