构建能证明智能体错了的软件
Opening|12 分钟阅读
最近我在搭一套工作流:让编码智能体(coding agent)把一项任务从头走完——分诊、规划、实现、验证,最后开一个 PR。随着工作流和模型越来越好,让我意外的反而是:实现已经不再是我最担心的环节了。
智能体改代码快得惊人。真正难的是让它验证这些改动,并判断什么样的证据才算够,能认定任务完成。
工作流里当然可以加一个验证环节:智能体在浏览器里跑起应用、复现行为、执行测试、返回截图或其他证据……但这就引出一个我最初设计工作流时没考虑的问题:
智能体到底能发现哪些类型的错误?
拿支付结账的改动来说。智能体加了一条新的支付路径,跑起应用,走完结账流程,点击「支付」,然后看到一条提示:「支付成功」。
这是成功证据,但很弱。界面能显示「成功」,后台却可能落了两笔订单。重试逻辑可能扣两次钱。事件可能被发了两遍。浏览器验证只证明了一件事:正常路径看起来没问题。它并没有证明那些对我们真正要紧的失败不存在。
一旦开始用这个视角看智能体工作流,我就不再只问:
智能体能不能用这个产品?
我开始问:
这个产品究竟能把哪些错误暴露给智能体?
这个问题不再像个工作流设计问题,而越来越像个应用设计问题:产品本身在决定智能体能验证什么。
§ 1 有些系统更容易被证伪
为了方便理解,设想同一个结账功能放在两个不同的项目里。
第一个项目里,智能体能打开应用、点完整个流程,但要想知道底层发生了什么,得有人手动查数据库、翻日志、重建状态,或者解释重试应该是什么行为。
在第二种情况下,智能体可以把应用重置到确定状态,执行支付,检查结构化的订单状态,断言余额变化,重试操作,再查询链路追踪。
用户看到的体验可能一模一样。模型可能也是同一个。但你能安全交给智能体的自主程度,完全不同。
我们的第一反应是把这种差别全归到智能体的外围工具链上:模型周围的工具、上下文、约束、评估器和反馈。工具链工程已经成为思考编程智能体的一个有用框架:工具可以包含应用界面、日志、指标、链路追踪和隔离的运行时环境,尤其是当人工测试成为瓶颈时。
但工具链拿不到它需要的所有信息,最终会撞上应用的边界——由应用本身决定什么可以被观测和监控。应用决定了一个重要状态是否可观测,操作能否通过脚本完成,某些具体场景是否可复现,以及失败和错误里是否包含足够的数据让智能体真正采取行动。
被构建的系统(应用)为构建它的系统(工具链)提供了一部分传感器。
这正是我觉得有意思的地方。
§ 2 开发开始看起来像一个反馈循环
传感器这一层之所以重要,是因为一旦智能体能根据应用暴露出来的信息采取行动,工作就开始变得不像一次交接,而更像一个反馈循环。
旧工作流简化来看大概是这样:
有了更强的编程智能体和工作流之后,更多环节可以变成一个循环:
在我的工作流里,这意味着智能体可以改代码、跑产品、验证改动、收集证据、判断看起来对不对,然后重试。
这看起来不太像一次性的代码生成,更像是一个控制问题。而控制系统离不开好用的传感器。
对编程智能体(coding agent)来说,传感器不只是测试;凡是能让它观察到「发生了什么」的东西都算:类型、运行时状态、日志、指标、链路追踪、浏览器输出、测试,以及独立的评估器。它还需要能对系统施加动作的手段(也就是执行器),比如浏览器、命令行、API 和脚本。
但我反复撞上的是一个更具体的问题:验证效果的上限,取决于软件能暴露多少信息。如果智能体看不到某个状态、事件或不变式(invariant),它也就检查不了。
这和 Birgitta Böckeler 提出的 harnessability 有重合之处:一个代码库在多大程度上支持这类由智能体驱动的工作。但我在这里把视角收得更窄:不只看智能体能不能在代码库里干活,还要看这个应用是否暴露出足够的证据,让人能低成本、独立地证明智能体错了。
§3 验证层必须能看见 bug
这话听起来理所当然,但设计智能体工作流时很容易忽略:增加更多验证手段,只有在这类工具能暴露我们真正关心的失败类型时才有用。
比方说,一张截图能告诉智能体 toast 提示渲染正常,却说明不了某个操作是否幂等(idempotent)。浏览器智能体可以顺利完成结算,却没察觉实际落库了两笔订单。如果智能体基于同一套对需求的错误理解,既写了实现又写了测试,那测试全绿也同样能通过。
这种现象不只出现在结算流程里。Achint Mehta 最近在超过一千个由智能体生成的 Web 应用上量化了这种效应:不同的验证机制对应不同的失败模式——启动检查能揪出起不来的应用,截图能暴露肉眼可见的 UI 错误,而性能问题只有在证据本身测量了性能时才会浮现。
从这一点出发,我要问的核心问题并不是「智能体验证过自己的工作了吗?」,而是「如果相关故障真的存在,智能体手上的证据能揭露出它吗?」
这个标准高得多。
§ 4 让错误状态更容易暴露
这个标准改变了我对验证的理解框架:让智能体确认自己的代码改动能用,和让它去找出能证明代码不能用的证据,是两件微妙不同的事。前者鼓励确认,后者逼我们把失败讲清楚。
拿结账这个需求举例:“点击「支付」应当恰好创建一笔订单,并从余额中扣除正确金额。”
较弱的验证回路长这样:
较强的回路则是这样:
第二个回路更强,并不是因为它步骤更多,而是因为这些步骤让实现有更多机会被验证为错误。
这让我开始问自己一个新的问题:
这款软件能否让重要的错误状态被便宜地暴露出来?
当我开始把这个问题套用到智能体回路上,反复浮现的是一组平淡却有用的特性:
- 确定性、可复现的场景很重要,因为智能体可以回到一个已知状态,而不是去调试上一次尝试留下的残局。
- 必须始终成立的显式规则很重要,因为它们给智能体提供了一个具体的检查条件。
- 可查询的运行时状态很重要,因为界面通常只展示最终结果,而看不到状态转换的过程。
- 结构化的失败信息很重要,因为智能体需要一个明确的失败原因,才能选择正确的修法。
- 快速且隔离的环境很重要,因为每一个缓慢的验证步骤,都会变成等待智能体反馈的时间。
需要说明的是,这些都不是新的工程理念,它们和可测试性、测试驱动开发(TDD)、可观测性、可运维性有很多重叠。真正让我改变看法的是它们的经济价值:一旦自主智能体进入反馈循环,这些特性就能降低每次失败尝试的成本,让智能体继续迭代变得更安全。
§ 5 证据获取速度成为系统设计的一部分
我们本来就会为了可测试性、可观测性和可运维性来设计系统,但智能体的反馈循环改变了这些特性的经济价值:它们决定了智能体能多快拿到有用的证据。
追踪记录不再只是生产环境出故障后工程师才打开看的东西,它也可以作为智能体的输入。命令行工具(CLI)也不再只是开发者的专属,智能体同样能用它来设置状态、执行操作、检查执行过程中发生了什么。
Shopify 在移动端智能体方面的工作(shopify.engineering/back-to-native)就是这种设计压力的好例子:一旦实现速度变快,获取反馈的成本就开始成为循环的主要部分。他们描述的问题不是智能体写代码太慢,而是围绕代码的反馈循环太慢:智能体几秒钟就能完成一次改动,然后要花好几分钟等模拟器交互、验证目标是否达成。
他们还提到,把业务逻辑改成可以无头(headless)运行,并通过命令行工具暴露出来,这样智能体就能检查状态、导航、执行操作,不必每次迭代都付出模拟器的代价。
命令行工具本身不是重点,这种东西早就有。有意思的是,获取证据的速度已经重要到足以改变系统的形态。
§ 6 实际中怎么做
说实话,我不觉得这需要发明什么「智能体架构」。实际做法比你想的简单:站在智能体的角度审视系统,看它能不能拿到证据来证明自己的改动是错的。
它能否低成本地创建已知状态?
如果复现一个 bug 需要人工去注册账号、走完引导流程、等待异步任务、还要口头解释发生了什么,那这个反馈循环的成本就太高了。我们应该有确定性的测试夹具、随机种子、快照,以及可重置的环境。这能把成本降下来。
它能检查真正关键的状态吗?
UI 或截图暴露的只是整个状态转换过程结束时的那一小块。如果能结构化地访问特定的状态、事件、追踪和诊断信息,agent 就能看到“变了什么”,而不只是“屏幕上显示了什么”。
重要的业务假设容易测试吗?
“绝对不应该创建两笔订单”这种说法有帮助,但一条自动运行、一旦出现两笔订单就报错的检查,能给 agent 提供证据,而不是让它去猜。
不做多余的 UI 操作也能测试行为吗?
浏览器是一个很有价值的传感器和执行器,尤其是针对用户能直接看到的需求。但有些问题更适合用更确定的方式回答,比如 CLI、API、无头模式,或者领域专用的测试辅助工具。通过浏览器使用产品,和调试系统,是两件不同的事,需要不同的工作方式。
失败信息机器可读吗?
一条“出错了”的错误提示,对人来说是糟糕的反馈,对自主循环来说更是如此。想减少 agent 需要猜测和解释的空间,我们就该保证有结构化的错误信息、追踪记录和明确的失败原因。
循环重启有多快?
构建慢、共享环境、手动认证、不稳定的初始配置、难以重置的状态——这些已经不只是开发者体验的问题了。它们恰恰是真正的摩擦点,决定了 agent 从一次失败尝试中学习、调整计划、执行下一步的速度。
最后:
还有哪些重要结论缺乏可靠的自动化检查?
这些缺口应该被明确指出。这里的目的不是把每一个判断都自动化,而是搞清楚证据的边界在哪里。
如今的不同之处在于,这些做法影响的不只是可维护性和开发体验,还决定了在人类介入之前,智能体到底能安全地完成多少实现工作。
§ 7 更多验证,未必意味着更多信任
有一点很重要:验证更多,并不等于信任更多。这又是一个陷阱。
假设智能体做了这些事:
-
理解需求
-
编写实现
-
编写测试
-
运行这些测试
-
审查代码
表面上看有好几层验证,但它们可能犯的是同一个错误。如果智能体在第一步就误解了需求,它会忠实地把这个误解一路带到实现和测试里。建立在同一个错误假设之上的五道检查,可能还不如一条独立的规则来得可靠。
我依然认为覆盖率很重要,也正因如此,一个有用的验证至少应该有两个维度:
-
覆盖率:这项检查有没有可能捕捉到失败?
-
独立性:这项检查是不是建立在同一个可能出错的假设之上?
独立性说的是检查从哪来:一条一直成立、早就存在的规则,比实现之后才生成的测试更独立。可查询的运行时状态之所以有用,也是同样的道理:它能展示实际发生了什么变化,而不是模型以为自己写出的代码做了什么。
有时候,唯一能回答问题的验证层,就是人。
所以我不认为目标应该是造出某种绝对意义上的“自我验证”智能体,而是给它们更强的能力,在需要人来判断之前先证明自己是错的。
§ 8 人类仍然重要之处
这就给人类留下了一个更窄、但依然重要的角色:判断那些系统无法转化为可靠证据的主张。
有些关于软件的断言,智能体可以清楚地验证:
-
是不是恰好创建了一个订单?
-
改动之后,这条规则是否仍然成立?
-
这次请求有没有超过延迟阈值?
另一些则不能:
-
这个交互体验真的好吗?
-
这是用户想要的吗?
-
这个风险可以接受吗?
-
它让人感觉值得信任吗?
多加一个智能体,并不能让这些问题自动变得确定。
所以关键不在于零人工介入,而是在人类判断能提供智能体无法获取、也不该自行推断的信息时,才引入人工。
智能体实现代码的速度越快,这个区分就越重要。如果智能体生成一段 1200 行改动比资深工程师读一遍还快,那让工程师只看 diff 判断改动对不对,就不是一个能规模化的做法。
随着时间推移,我认为越来越多的评审会从证据开始:
-
智能体测试了什么行为?
-
检查了哪些规则?
-
观察到了哪些状态变化?
-
智能体尝试复现了哪些失败场景?
也许最重要的一点是:
智能体有哪些地方无法验证?
代码依然重要,但评审者还应该看证据,而不只是逐行检查 diff。
§ 9 应用边界很关键
话虽如此,如果评审要从证据而不是 diff 开始,那下一个问题就是:这些证据从哪来。
回顾编码智能体的第一轮改进,会发现它来自更好的模型。而当前第二轮改进,很多来自更好的 harness(模型外层的支撑机制):上下文、工具、指令、评估器、浏览器控制、隔离环境,以及反馈回路。
这些工具很重要,但最终 harness 会碰到应用边界,必须向应用问几个问题:
-
发生了什么?
-
什么状态发生了变化?
-
这条规则仍然成立吗?
-
我能复现这个场景吗?
-
我能执行这个操作,而不用手动点过六个页面吗?
这不意味着每个产品都得为智能体专门做一套 API。系统可观测性更强,也可能带来耦合、安全风险和运维成本。所以只有当它帮助检查的行为足够重要、值回代价时,才值得为智能体专门做一层接口。有时候,正确答案就是已有的测试、追踪、命令行工具,或者人工评审。
但设计压力依然真实存在:越来越多的软件,正被那些靠反馈运作的系统修改。系统越能向智能体暴露自己的真实状态,智能体就变得越强——哪怕模型完全没变。
模型没有升级,升级的是环境。
这种变化把我们引向另一个问题:
软件要想证明智能体出错,究竟有多容易?