一般

智能体验证

10
分类:技术博客
作者:Addy Osmani
来源:跳转
发表时间:

内容

如果你给 AI 智能体一种独立验证其工作的方式,就能大幅提升输出质量。智能体写出的代码“在规模上要好得多”。这里的“在规模上”来自于验证。智能体让检查变得便宜。

过去“好代码”成本高昂的部分(测试、基准测试、规格说明、真正的端到端运行)现在变成了你可以向智能体索取的廉价资源。你可以改进检查的方式。

所以我们的工作不再是“逐行审查”,而是“构建智能体可以自行运行的循环”。给智能体一种验证工作的方式。这个循环就是质量产生的地方。

还要让品味产生复利效应。当智能体出错时,把它记下来(共享配置、钩子、测试),这样就不会再犯同样的错误。人类仍然拥有最终决定权,但现在他们在审查证据,而不仅仅是代码差异。

要让你的智能体产出高质量输出,就要给它配备检查工作的方式。测试是其中的重要部分。

以下是一些我认为值得投入的测试类型。

  • 编写端到端测试,模拟真实用户流程,作为事实标准
  • 使用属性测试来明确永远不会发生的事情,并生成数千个试图打破这些约束的测试用例
  • 如果是替换系统,用随机选择的输入比较新旧版本
  • 要求测试快速且确定性运行,这样当循环缓慢或不可靠时,会强化智能体重试(智能体可能学会重试,而不是修复问题)

示例测试说明应该发生什么;属性测试说明什么必须不发生。

验证是瓶颈

如今的模型已经足够好,主要瓶颈不再是写代码,而是验证所写代码是否正确的能力。

如果你的智能体无法验证其输出,它本质上就是在猜测,而错误的猜测最终会由你的测试套件来验证。也就是说,你会陷入一个反馈循环:反复来回,直到你能阅读代码差异、运行测试、检查应用、发现 bug 并报告回去。这是最慢、成本最高的反馈循环,对于超过一个人来说不可扩展。

但如果你的智能体能测试自己的输出,它会写一个改动、运行检查、读取输出、不断尝试,直到能通过检查。这发生在任何人类看到任何内容之前。自我验证的智能体与人类主导的反馈循环之间的质量提升怎么强调都不为过。我始终更偏好一个更好的反馈循环,而不是一个稍微更好的模型。

这是对测试的新定义。多年以来,测试是人类识别 bug 和可靠重构的主要工具。这些仍然是编写测试的好理由,但测试也是你的智能体感知世界、知道自己何时完成的方式。

并非每个测试对这一目的都有同等的价值。

测试是一个完整的谱系

端到端测试

如果只能选择一种测试,那就是端到端测试。它们描述真实用户的操作。

端到端测试是人类会做的事情:注册、添加商品到购物车、结账、看到确认信息。它忽略代码的内部结构,只看用户能看到的东西。

这是你希望智能体拥有的那种神谕。机器经常会写出通过检查但忽略大局的代码。基于 mock 的单元测试可能通过,但功能可能是坏的,因为智能体和你的测试有共同的理解错误。另一方面,端到端测试很难被欺骗。要么整个流程正常工作,要么不工作。

是什么让端到端测试对智能体特别有价值,而不仅仅是用于 CI/CD 管道?

  • 它们只覆盖重要的、有意义的用户流程,而不是所有可能的路径。
  • 它们告诉你哪里出了问题:截图、追踪以及对哪个步骤失败的解释,告诉智能体哪里出错了,而不是让它在黑暗中摸索。“30秒后超时”什么信息都没有。
  • 它们既可以在智能体的本地配置上运行,也可以在部署环境中运行。
  • 它们定义“完成”:“实现这个功能”太模糊。“让这个端到端测试通过”是智能体可以自己追逐的目标。

重要的是要说明,端到端测试有其真实的权衡:它们比传统单元测试更慢、更不稳定。选择的不是消除它们,而是偏好一小套高价值测试。

注意:端到端测试很好,但 Lauren(@poteto)强调让智能体在真实运行的系统上拥有手和眼是值得的:一个维护良好的技能/CLI,知道如何启动、驱动和观察应用(CDP、模拟器、API 工具等),外加一张功能地图。功能地图是用户视角的清单:存在哪些功能、真实用户如何到达它们、键盘快捷键/选择器、预期结果和注意事项。

属性测试:不会打破的约束

许多测试关注的是人类认为可能出错的地方。属性测试关注的是人类没有考虑到的地方:智能体出错的情况。

人类在测试中写的很多内容都是关于示例的:“如果这是输入,那么输出应该是这个。”这些测试擅长确保某事应该工作,但不擅长确保某事永远不会失败:人类想不出所有会失败的输入。

属性测试反转了前提:你创建一条应该成立的规则,然后一个工具会生成数百或数千个输入来打破它。像 JS/TS 的 fast-check、Python 的 Hypothesis、Rust 的 proptest 这样的工具在这方面做得很好。当发现失败时,输入会被缩减为打破测试的最小用例:它不会因为随机数据块而爆炸。

以下是几乎每个软件项目中常见的属性:

  • 往返编码:我编码然后解码的值与我开始时的值相同。解析和序列化一个值会产生我开始时的值。
  • 不变式:排序不会改变元素数量或集合内容。余额永远不会变成负数。进账等于出账。
  • 幂等性:迁移、规范化或重试同一件事两次与执行一次有相同的效果。
  • 等价性:优化后的函数产生与简单正确函数相同的结果。

当你使用智能体时这很关键,因为智能体会根据其内部逻辑编造自己的成功测试示例。属性测试迫使智能体处理任何生成的输入,而不仅仅是它选择的输入。

注意:属性测试和差分检查覆盖了很多,但 Claude Code 的 Boris Cherny 一直在最难的状态机、并发和数据流部分使用模型(Lean、TLA+)。简短的提示可以通过构建模型、找到反例、复现问题并修复来暴露竞态条件和微妙的 bug。

属性是表达需求的好方法。如果我说购物车总价必须始终等于其行项的总和,这是一个很好的规格说明。智能体可以用这个规格说明来构建自己的代码,而生成器可以寻找差异。

如何开始而不必大费周章:

  • 从一个小型的、本质上纯粹的函数开始,比如解析器、格式化器、价格计算。
  • 在用代码实现之前,用简单的英语写出属性。
  • 当测试库发现问题时,保存最小化的测试用例,以便用于回归测试。
  • 当编写新代码时,要求智能体提供属性而不是测试示例。一旦你告诉它这样做,它就很擅长这个。
  • 当用新系统替换旧系统时,在它们之间生成差异。
  • 当进行重写或迁移时,将旧系统视为神谕。

重写的典型失败模式之一是它能够通过新的测试套件,但与旧系统在许多小的、未被记录的方式上存在分歧。这是官方规格说明与实际行为之间的差异。用户依赖的是实际工作的东西,而不是规格说明说应该工作的东西。

差分测试:旧系统是神谕

差分测试弥合了这一差距。不依赖新的测试套件,而是用相同的输入并行运行新旧系统。如果输出有差异,你要么在新系统中引入了 bug,要么你故意做了一个应该记录为破坏性变更的更改。

这就是选择输入重要的地方。如果你使用手动选择的测试用例,你会错过罕见的边缘情况。随机生成输入、重放实时流量样本,或两者结合。这相当于属性测试,属性是你的新系统输出与旧系统输出一致。

差分测试对于智能体辅助的大规模迁移非常出色。你在新系统调整、运行差分测试、审查不匹配、修复之间循环,然后重新开始循环。因为旧系统是神谕,你不需要向智能体解释每个边缘情况。两个版本都做过并运行差异的团队告诉我,这是一种从修复一个问题又产生另一个问题,到仔细而有条理地逐步进行对比的转变。

一些实际注意事项:

  • 规范化所有不重要的东西和没有语义权重的排序。在比较之前去除这些差异,确保你永远不会得到无穷无尽的无意义差异。
  • 在已批准变更列表中跟踪预期变更:你想做的每个变更都要附上原因。这防止“不同”成为一种可接受的状态。
  • 自信的秘诀就是把所有东西都迁移过去:差异工具给你信心来清除旧系统。让两个系统同时运行比理解其中一个系统要多一倍的工作,而且会引入两种不同的迁移可能出错的方式。

让循环快速且确定性

测试必须快速且确定性运行,否则智能体会学到错误的教训。

这不仅仅是浪费时间的问题——这是一次训练练习。如果测试缓慢或不稳定,智能体会学会有时候重试测试比尝试修复问题更好。

人类对糟糕的测试基础设施更宽容。我们知道哪些测试是不稳定的,我们知道在失败时重新运行 CI,我们愿意等待 20 分钟来构建一个可能不稳定的东西。然而,智能体从循环的激励中学习。如果重试失败的测试有时能成功,它会把重试作为首选策略。如果运行测试需要 15 分钟,它会减少运行频率,在每次运行之间批量更多的变更,并失去精确指出哪个变更导致失败的能力。

这两种失败模式都会让智能体变得更差,但没有明显的失败迹象,所以你产出的输出质量对人类来说会更低。

好的状态是什么样的

以下是我认为“良好”可能是什么样的一些想法——智能体应该从循环的激励中学习。

  • 测试套件运行得足够快,可以在每个周期中运行。对于核心循环,亚秒级就很好了。写一个智能体可以轻松找到和运行的命令,带上任何适用的静默标志。
  • 测试套件每次产生相同的结果。使用种子随机数、冻结时钟、受控网络和隔离状态。如果失败,系统就是坏的。
  • 不稳定的测试与主套件隔离开来,并立即移除。主套件中的不稳定测试比没有测试更糟糕,因为它破坏了对其他所有结果的信任。
  • 输出简短且直接。具体说明什么失败了的消息比大量的通过日志好得多。
  • 不想要的输出会在每个回合消耗智能体的注意力和心智带宽。
  • 智能体的快速第一循环,在集成变更之前运行繁重的端到端和差分测试。
  • 把测试基础设施视为智能体依赖的组件之一。改进它对所有任务都有复合的积极影响。

注意:一些额外的实际习惯包括编码前明确重述目标(“重述你认为的成功标准是什么”)、用新鲜的上下文或单独的审查智能体进行验证,以及把生成的测试本身视为需要审查的产物(这个测试真的能区分正确和看似正确但错误的实现吗?)。

先写检查再写代码

在告诉你的智能体去做什么之前,你必须知道它应该成功完成什么。这可能是一个端到端工作流、一个特定属性,或者与先前系统的比较。无论是什么,确保它是快速且确定性的,你可以让智能体在一个解决方案上迭代。

你的工作不需要阅读每一行输出,但你需要定义成功是什么样的。你从判断中获得了更好的价值,而且它比你自己阅读代码差异的扩展性更好。

当你在测试你的智能体时,你需要有应该发生什么的例子,以及不应该发生什么的属性。要想赢,你的智能体的测试过程需要快速且准确。

评论

(0)
未配置登录方式
暂无评论