Agentic Verification

TL;DR · AI 摘要
Agentic Verification - by Addy Osmani - Elevate Agentic Verification The bottleneck has moved from writing code to knowi...
核心要点
- 主题聚焦:Agentic Verification
- 来源:Elevate,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
智能验证 - Addy Osmani - Elevate
智能验证
瓶颈已从编写代码转移到验证代码的正确性
Addy Osmani
2026年10月14日
如果给AI代理一个独立验证其工作成果的机制,可以显著提升输出质量。代理在大规模场景下编写代码的平均水平要高得多,这种"大规模"特性来自于验证机制。验证使检查成本变得低廉。
过去让"高质量代码"昂贵的因素(测试、基准、规格说明、真实端到端运行)现在都变成了可以向代理请求的低成本事项。你可以改进这些检查机制。
因此我们的工作重心不再是"逐行审查",而是"构建代理可以自主运行的验证循环"。给代理提供验证工作的方法。这种验证循环才是质量的来源。
让验证经验产生复利效应。当代理出现错误时,将其记录下来(共享配置、钩子、测试),避免重复发生。人类仍然掌控最终发布的代码,但现在他们审查的是证据而非仅仅是代码差异。
要让代理产出高质量输出,必须为其配备验证工作的方法。测试是其中重要组成部分。以下是我认为值得投入的几种测试类型:
- 编写端到端测试,模拟真实用户流程并作为基准标准
- 使用属性化测试定义绝对不能发生的情况,并生成数千个试图突破这些限制的测试用例
- 在替换系统时,使用随机选择的输入对比新旧版本
- 要求测试快速且确定性执行,这会促使代理在循环变慢或不可靠时重试(代理可能学会重试而非解决问题)
示例测试说明应该发生什么;属性测试说明绝对不能发生什么。
验证成为新的瓶颈
当今模型已经足够成熟,主要瓶颈不再是代码编写,而是验证所写代码正确性的能力。
如果代理无法验证其输出,本质上就是在猜,错误猜测最终会被你的测试套件验证。也就是说,你会陷入一个反馈循环:来回查看差异、运行测试、检查应用、发现漏洞并报告问题。这是最慢、成本最高的反馈循环,且无法扩展到多个人类参与。
但如果代理能自主测试输出,它会编写变更、运行检查、读取输出,并不断重试直到通过验证。这甚至在任何人类看到结果之前就会发生。与人类主导的反馈循环相比,自验证代理在质量提升方面带来的改进难以高估。我宁愿选择一个更优的反馈循环,也不愿使用一个稍优的模型。
这是对测试的新定义。多年来,测试一直是人类识别漏洞和可靠重构的主要工具。这些仍然是编写测试的充分理由,但测试也是代理感知世界、判断任务完成的途径。
并非所有测试对这个目的都有同等价值。
测试存在完整光谱
端到端测试
如果只能选择一种测试,那应该是端到端测试。它们描述了真实用户的行为。
端到端测试就是人类会做的事:注册、将商品加入购物车、结账并查看确认信息。它忽略代码的内部结构,只关注用户可见的内容。
这是你希望为代理选择的类型。机器通常会编写通过检查但忽略整体情况的代码。基于模拟的单元测试可能通过,但功能可能因代理和测试之间存在误解而损坏。另一方面,端到端测试(E2E test)很难被欺骗。要么整个系统都能正常工作,要么完全无法工作。
除了在 CI/CD 管道中的使用,E2E 测试对代理有哪些特别的价值?
- 它们只覆盖重要的、有意义的用户流程,而不是所有可能的路径。
- 它能明确告诉你哪里出错了:截图、追踪和解释哪一步失败,能告诉代理哪里出了问题,而不是让它在黑暗中盲目摸索。“30 秒后超时”没有任何提示。
- 它们在代理的本地配置和部署环境中都能正常工作。
- 它们定义了“完成”的标准:“实现这个功能”太模糊了。“让这个 E2E 测试通过”是一个代理可以自行追求的目标。
需要强调的是,E2E 测试确实存在权衡:它们比传统的单元测试更慢且更不稳定。选择它们并不是要消除它们,而是要优先选择少量高价值的测试。
注意:E2E 测试非常有用,但正如 Lauren(@poteto)所强调的,为代理提供在真实运行系统上的“手”和“眼睛”也是有价值的:一个维护良好的技能/CLI,知道如何启动、驱动和观察应用程序(如 CDP、模拟器、API 工具等),加上一个功能图。功能图是一个用户视角的清单:存在哪些功能、真实用户如何到达这些功能、键盘快捷键/选择器、预期结果和注意事项。
属性测试:无法打破的约束
许多测试关注的是人类认为可能出错的情况。属性测试关注的是人类未考虑的情况:代理出错的案例。
人类在测试中写的大部分内容都是关于示例的:“如果输入是这个,输出应该是这个。”这些测试擅长确保某些功能应该正常工作,但不擅长确保某些功能永远不会失败:人类无法想到所有可能导致失败的输入。
基于属性的测试改变了前提:你创建一个应该成立的规则,工具会生成数百甚至数千个输入来打破它。像 fast-check(用于 JS/TS)、Hypothesis(用于 Python)和 proptest(用于 Rust)这样的工具能很好地完成这项工作。当发现失败时,输入会被简化为导致测试失败的最小情况:它不会因为随机数据块而崩溃。
以下是一些几乎所有软件项目中都常见的属性:
- 往返编码:我编码后解码的值与我最初开始的值相同。解析和序列化一个值会得到我最初开始的值。
- 不变量:排序不会改变元素的数量或集合的内容。余额永远不会变成负数。进来的钱等于出去的钱。
- 幂等性:迁移、规范化或重试相同的操作两次,效果与只做一次相同。
- 等价性:优化后的函数与简单、正确的函数产生相同的结果。
当你与代理一起工作时,这一点至关重要,因为代理会根据自己的内部逻辑生成自己的测试示例并使其通过。属性迫使代理处理任何生成的输入,而不仅仅是它自己选择的输入。
Note: 属性测试和差异检查覆盖了大量内容,但Claude Code的Boris Cherny一直在使用模型(Lean、TLA+)处理最复杂的有限状态机、并发和数据流部分。简短的提示可以通过构建模型、寻找反例、重现问题并修复来发现竞态条件和细微的错误。
属性是规定需求的绝佳方式。如果我说购物车的总金额必须始终等于其商品项的总和,这就是一个优秀的规范。代理可以利用该规范构建自己的代码,生成器可以寻找差异。
#### 如何在不陷入全面战争的情况下开始:
- 从一个小型且本质上是纯函数的组件开始,例如解析器、格式化器或定价计算。
- 在用代码实现之前,先用普通英语写出属性描述。
- 当测试库发现问题时,保存最小化测试用例以便用于回归测试。
- 在编写新代码时,要求代理生成属性而非测试示例。一旦你告诉它,它在这方面非常擅长。
- 在用新系统替换旧系统时,生成它们之间的差异。
- 在进行重写或迁移时,将旧系统视为预言机。
重写的一个经典失败模式是新系统能够通过新的测试套件,但与旧系统在许多未记录的小细节上存在分歧。这是官方规范与实际行为之间的差异。用户最终依赖的是实际有效的内容,而非规范中规定的内容。
差异测试:旧系统即预言机
差异测试弥合了这一差距。与其依赖新的测试套件,不如让新旧系统在相同输入下并行运行。如果输出存在差异,说明要么新系统引入了错误,要么你故意进行了需要记录为重大变更的改动。
此时选择输入的方式至关重要。如果你手动选择测试用例,会错过罕见的边界情况。应随机生成输入、回放部分实时流量或两者结合。这等同于基于属性的测试,属性即新系统的输出与旧系统输出一致。
差异测试对代理协助的大规模迁移非常有效。你可以在调整新系统、运行差异测试、审查不匹配项、修复问题并重启循环之间反复迭代。由于旧系统是预言机,你无需向代理解释每个边界情况。做过两个版本并运行差异测试的团队告诉我,这改变了他们从修复一个错误又引入另一个错误,转变为有条不紊地推进比较。
#### 一些实用建议:
- 正常化所有无关紧要的内容和不携带语义权重的排序。在比较前剥离这些差异,确保不会产生无限的虚假差异流。
- 在已批准的变更列表中跟踪预期变更:每个你想要的变更都需要注明原因。这能防止"不同"变成可接受的状态。
- 增强信心的关键是彻底迁移所有内容:差异测试框架让你有信心彻底删除旧系统。同时运行两个系统是理解任一系统的两倍工作量,且会引入两种不同的迁移失败方式。
让循环快速且确定性
测试必须快速且确定性,否则代理会学到错误的结论。
这不仅仅是一个耗时的问题,它是一次训练练习。如果测试运行缓慢或不稳定,代理会学到有时仅仅重启测试比尝试解决问题更好。
人类对糟糕的测试基础设施更宽容。我们知道哪些测试是不稳定的,知道在失败时重新运行CI,愿意等待20分钟以应对可能不稳定的构建。然而,代理会从循环的激励机制中学习。如果有可能重试失败的测试并偶尔成功,它会将重试作为首选策略。如果测试需要15分钟才能运行完,它会减少运行频率,在每次运行之间累积更多更改,从而失去准确定位导致失败的具体更改的能力。
这两种失败模式都会让代理表现变差,但不会留下任何明显的失败迹象,因此你产出的代码质量会降低,供人类审查时需要付出更多努力。
#### 什么是良好表现
以下是我认为“良好”可能的表现形式——代理应从循环的激励机制中学习。
- 测试套件运行速度足够快,能够在每个循环中执行。对于核心循环,亚秒级表现最佳。编写一个代理可以轻松找到并运行的命令,并附带任何适用的静默标志。
- 测试套件每次运行结果完全一致。使用种子随机数、固定时钟、受控网络和隔离状态。如果测试失败,说明系统本身存在缺陷。
- 不稳定的测试要与主测试套件隔离,并立即移除。主测试套件中的不稳定测试比完全没有测试更糟糕,因为它会破坏所有其他结果的可信度。
- 输出简短且直击要点。关于具体失败的明确信息比关于通过情况的冗长日志更有价值。
- 不必要的输出会占用代理的注意力和认知资源。
- 为代理设计快速的初始循环,并在集成更改前运行耗时的E2E和差异测试。
- 将测试基础设施视为代理依赖的组件之一。改进它会对所有任务产生复合正向影响。
注意:一些额外的实用习惯包括在编码前明确重述目标(“重述你认为的成功标准”),使用全新上下文或独立审查代理进行验证,以及将生成的测试本身视为需要审查的产物(这个测试是否真的能区分正确实现和看似合理但错误的实现?)。
在编写代码前先编写检查
在告诉代理去完成某事之前,你必须明确它需要成功完成的目标。这可能是一个端到端的工作流、一个特定属性,或与先前系统的对比。无论是什么,确保它足够快速且确定性,并允许代理对解决方案进行迭代。
你的工作不需要逐行阅读输出,但需要定义成功的样子。你对判断的投入会获得更好的回报,而且这种模式的扩展性远超你阅读diff的能力。
在测试代理时,你需要有预期发生情况的示例和预期不发生情况的属性。要取得胜利,代理的测试过程必须快速且准确。