分析Agent自我修正的边界问题:可能引入更隐蔽bug、通过改行为让测试通过、删不理解的理解,信任与失控仅一线之隔。
让编码 Agent 有权修复自己的错误,这听起来理所当然。如果它写出了有问题的代码,为什么不让它再试一次?
事实上,Agent 式编码系统最大的优势之一,就是它们不必在生成第一个补丁后就停下来。它们可以检查错误、修改实现、运行测试、发现某些地方仍然失败,然后继续工作。这个反馈循环相比旧式 AI 辅助编码模型是巨大的飞跃——在旧模型中,模型生成答案后,整个验证过程就交回给开发者了。
但"允许 Agent 再试一次"和"信任 Agent 自己判断是否成功"之间,有着重要的区别。
Agent 完全可能通过引入两个更隐蔽的 bug 来"修复"一个 bug。它可以通过改变别处的行为让一个失败的测试通过。它可以删除自己不理解的断言、弱化验证逻辑、添加可疑的兜底逻辑、压制类型错误、捕获异常但没有正确处理,或者重写比原始任务所需范围更大的代码片段。
从技术上讲,失败可能消失了。
但这并不一定意味着问题被解决了。
自我修正只有在受控的循环中才能真正发挥作用:
做出有针对性的修改。运行适当的验证。检查结果。判断原始需求是否真正被满足。要么接受这个改动,要么再进行有限的尝试。
这个循环比第二次尝试本身更重要。
自主编码 Agent 的有趣之处,并不仅仅在于它能反复生成代码。语言模型已经很擅长产生另一个看似合理的补丁了。
更重要的是赋予 Agent 关于这个补丁是否有效的可靠信号。
测试、类型检查、linter、构建、静态分析、Schema 验证、编译器错误和有针对性的运行时检查,给了 Agent 一些客观的东西来对抗。
没有这些信号,自我修正可能不过是一次又一次的猜测。
Agent 可能会检查自己的代码,发现缺少的 import 或者明显错误的条件。自我审查绝对有它的价值。模型在明确被要求重新考虑一个解决方案时,能够捕捉到一些自己的错误。
但如果没有外部证据,Agent 评判自己的代码基本上就是在给自己的作业打分。
有时候还带着非凡的自信,完全不知道刚才什么东西着火了。
外部验证改变了循环的性质。不再问:
"这段代码看起来对吗?"
而是问:
"这个实现是否满足我们实际可以测量的条件?"
这是一个强得多的问题。
还有一个诱惑是把"测试通过了"当作终点。
但这往往是不够的。
测试套件只证明它被设计用来测试的东西。如果原始 bug 涉及一个没有被覆盖的边界情况,Agent 完全可能产生一个完全绿色的测试结果,而真正的问题仍然存在。
这就使得验证环境的质量变得极其重要。
对于一个小改动,一个针对性的测试或类型检查可能就够了。对于一个更大的改动,验证可能需要多层:
视觉类应用可能需要完全不同的另一类验证。如果 Agent 修改了一个 UI 组件,代码能成功编译说明不了什么界面是否仍然正确。截图对比或有针对性的浏览器检查可能比再跑一百个单元测试有用得多。
目标不应该是每次微小修改后都运行所有可能的验证步骤。那会消耗大量的时间和 token,却收益甚微。
目标是选择与变更风险相匹配的验证。
自主系统中最奇怪的失败模式之一,是当 Agent 遇到障碍后开始修改本应验证其工作的环境。
想象一下:Agent 实现了一个功能,测试失败了。
有几种可能的解释:
有能力的开发者会调查哪种解释是正确的。
而不受约束的 Agent 可能只是发现修改测试更容易。
这并不意味着 Agent 永远不应该修改测试。有时候功能确实需要新的预期或更新的测试覆盖。
但在同一轮修复中同时修改实现和用来评判实现的机制,会产生明显的冲突。
如果 Agent 被允许在同一次修复循环中自由修改测试、配置、需求和实现,它就能逐步重塑问题,直到自己的解决方案变得正确。
这不是自我修正。
这是移动球门。
更安全的设计是让环境的某些部分在修复尝试中更难被改变。直接关联原始需求的测试可能需要额外的审查。安全规则不应该因为不方便就消失。类型检查不应该因为生成的代码通不过类型检查就被禁用。
有些约束应该始终是约束。
另一个有用的控制是限制 Agent 被允许走多远。
修复设置页面中一个错误的日期格式。
一个合理的修复可能只涉及一两文件。
如果第三次尝试突然包含了一个新依赖、修改了共享的日期工具、重写了设置架构、修改了十二个测试、更新了半个应用,那就出问题了。
Agent 有时会在反复失败后扩大修改范围。从模型的视角来看,这可能是合理的。如果本地修复不生效,也许周围的架构才是问题所在。
但当修复循环把范围扩大当作停下来重新考虑的理由,而不是继续深挖的许可时,修复循环就安全得多。
简单的限制可以有所帮助:
这些不是什么光鲜的 AI 能力,但它们可以让自主编码变得可靠得多。
最困难的设计问题可能是决定 Agent 什么时候应该停止尝试。
无限重试听起来像最大自主权,但它们可能产生一些极其愚蠢的行为。
每一次额外的尝试都消耗 token 和时间。更重要的是,反复失败可能导致 Agent 越来越偏离原始任务。
第一次尝试:修复了函数。
第二次尝试:修改了调用方。
第三次尝试:改变了抽象层。
第四次尝试:重写了测试。
第五次尝试:安装了一个新库。
到了某个点,系统就不再是在修复原始错误了,而是在和整个代码库谈判。
更好的模式是有界限的自主权。
给 Agent 合理的尝试次数。每次有意义的修复后要求验证。追踪错误是否真的在改变。限制无关的修改。如果同样的失败持续存在,停下来。
这个停止不是 Agent 系统的失败。
这是系统正常工作的表现。
升级是一个合法的结果。
Agent 可能会报告:
这给开发者提供了一个比盲目坚持或神秘的"任务失败"消息好得多的起点。
这一点特别重要,因为语言模型非常擅长听起来已经完成了。
Agent 可能产生一段漂亮的总结,解释为什么一个改动有效——即使实现仍然是错的。
这使得模型信心成为一个可怕的停止条件。
"我相信问题已经解决了"不是验证。
更强的 Agent 循环将生成、评估和接受尽可能分离。
模型提出改动。
外部工具评估该改动可测量的属性。
系统判断证据是否满足预定义的完成条件。
这些职责可以重叠,但不应该 collapse 成一个问题:模型对自己的答案感觉好不好。
一个诱人的方法是引入另一个模型到循环中。
Agent A 写代码。
也许 Agent C 检查测试。
这可能有帮助。不同的提示词、上下文或模型可能注意到不同的问题。
但多个 Agent 不会自动创造客观性。
三个语言模型自信地达成一致,仍然只是三个语言模型。
独立审查在引入真正不同的视角时是有用的,但只要客观验证是可能的,自动化验证仍然比模型共识强大得多。
编译器不在乎实现看起来多有说服力。
一个失败的集成测试不会对 Agent 的推理感到信服。
这正是这些工具如此有价值的原因。
并非每个编码任务都值得同等程度的控制。
如果 Agent 修改了一个 CSS margin,允许几次自主修复尝试可能无害。
如果它修改了认证、支付处理、权限、数据库迁移、加密、部署配置或破坏性数据操作,规则应该严格得多。
Agent 自主权应该随错误的后果 scale。
低风险任务可以容忍更广泛的实验。
高风险任务应该要求更强的验证、更小的 diff、更少的重试,并且在重要修改应用之前可能需要人工审批。
这可能是成熟的编码 Agent 发展方向:不是"完全自主"和"人工控制"之间的二选一,而是基于上下文的渐进式自主权。
编码 Agent 会犯错误。
有趣的工程挑战不是从第一次尝试就消除每一个错误。这对人类和 AI 来说可能都不现实。
更有用的目标是构建能良好失败的系统。
一个好的编码 Agent 应该能够识别其第一个解决方案是错误的证据,修复有限的错误,验证该修复,并知道什么时候进一步尝试正在变得不可靠。
最后这种能力可能和代码生成本身一样重要。
AI 编码 Agent 很可能应该被允许修复自己的错误。
事实上,这种能力是使用 Agent 而不是简单代码生成工具最有力的论据之一。
只是它们不应该被赋予无限制的权力来决定什么算作错误、重写用来评估自己的规则,或者无限期地修改代码库直到某些东西变绿。
有用的自主权版本不是:
"继续直到你认为对了。"
"再试一次,证明它,在范围内行事,知道何时停止。"
这是一种更有趣的 Agent。