我已经不再直接向 Claude 提示了。我在运行一些循环,让 Claude 接收提示并思考该做什么。我的工作就是写这些循环。
—— Boris Cherny
过去几个月里,我看到越来越多的人在编码智能体之上构建某种东西,这种东西与单纯使用编码智能体相比,确实有着明显不同的感觉。其中一些发生在 Pi 之上,能看到这一点当然很酷!不过模式到处都一样:工作被放入某种队列,机器接手、尝试、停止,然后由某个执行框架决定这是否真的是结束。
如果不是,执行框架就会继续同一个会话,注入另一条消息,基于修改过的上下文开启一个全新会话,或者把任务发送给另一台机器。任务会在模型本来通常会说“我已经完成了”的那个点之后,继续存活下去。
我对这种循环的思考,比我愿意承认的还要多。
每个编码智能体内部本来就已经有一个智能体循环。模型调用工具,吸收结果,再调用另一个工具,读取文件,编辑文件,运行测试,最后产出某个答案。这个循环我们早已相当熟悉。另一个循环则是执行框架层面的循环:也就是智能体循环之外的循环。这个循环也并不新鲜。从早期 Claude Code 的时候起,我们就一直在做这类事情,只是这种循环如今在智能体工程里越来越常见,最近几周甚至开始主导 Twitter 上的讨论。
我目前的状况是:对于那些我非常在意的代码,我还没有在这种工作方式上取得太多成功,而事实证明,我在意的代码相当多。
这部分关乎品味,另一部分关乎控制。我会努力为自己想要的代码设定很高的标准,而且我希望理解自己交付的代码。在压力之下,或者与另一个人讨论时,我希望能够解释系统在做什么,而不是先得叫个“铁皮罐头”来给我解释。现在显然还存在一个问题:几年后我是否仍然会有这种想要理解代码的欲望。至少到目前为止,我还没有跨过“理解对我很重要”这个门槛。
既然我有这种需求,那么对于那些在我没有留意时写出来的代码,尤其是来自循环的代码,我总会缺少某些东西。当前的模型往往会生成过于防御、过于复杂、推理过于局部的代码。它们回避强不变量,不去让坏状态变得不可能,而是添加各种兜底逻辑。它们会重复代码、发明糟糕的抽象,并用更多机制去掩盖不清晰的设计。更糟的是:到目前为止,我几乎看不到这方面的改进。如果有的话,至少在这一点上,我甚至感觉我们可能在朝错误的方向前进。至少按我的口味来看,如今像 Claude Code 搭配 ultracode 这样的“放手式”执行框架,产出的代码比去年秋天我们写出来的还要差。这是因为 Claude Code 比如与 Fable 配合时,会连续不间断地在一个问题上工作三十分钟甚至更久,而在此前,这个过程会更明显地有人类参与其中。
此外,大家都知道模型往往会观察到某个局部失败,然后增加局部防御。Karpathy 提到过,它们“对异常极度恐惧,仿佛那会致命”。在那些具有重要不变量的系统里,尤其是持久化数据格式或核心基础设施,正确的修复方式并不是“处理每一种格式错误的情况”。正确的修复方式是让这种错误状态无法被表示,或者在第一时间就根本不可能写入。然而,即便经过大量人工引导,这类代码也不会从 LLM 中自然地长出来;而且即使代码自然地长成了那样,它们仍然会尝试处理那些现在已经不可能出现的错误。
当你把这种行为放进循环里,就会把它放大。如果每次迭代都增加一点小小的防御,系统就会在看似更健壮的同时,慢慢变得更难理解。你越是放手,这种情况就越容易发生。如果把这种工具交给缺乏明确指导的初级工程师,这还会教会他们非常糟糕的实践。因为如果你问他们为什么要这么做,他们会很有说服力地替自己辩护。
与此同时,如果假装循环模式不起作用,那就不诚实了,因为它在某些领域已经好得惊人。
代码迁移就是其中之一。已经有一些令人印象深刻的大规模自动迁移案例,包括据称把 Bun 从 Zig 迁移到 Rust 的相关工作。我自己也成功用它做过事:把 MiniJinja 迁移到 Go。性能探索是另一个非常适合的场景。机器可以尝试各种实验,对其进行基准测试,丢弃失败项,并继续搜索。安全扫描也天然适配,几乎任何研究工作亦然:让系统探索一个复杂问题空间并返回结果,而不一定要提交长期存在的代码。这些场景有一个共同点:它们要么根本不生成新代码,而是转换已有代码;要么生成的代码本来就不打算长久存在。它们要么产出概念验证或想法,要么浮现发现,或者更接近于机械转换。
我认为,与其说是执行框架机械测量目标的总体能力更重要,不如说是那些产出并不要求长期存在的工件、或者能进行某种清晰可验证机械翻译的循环更重要。许多成功的循环应用都会使用另一个 LLM 作为评判者或编排者。机械翻译这种情况可以通过二元测试用例来验证,但也可以直接由 LLM 来判断!
比如 Claude Code,如今越来越擅长创建整套实验工作流,然后再由它自己执行。没错,它产出的代码很糟糕,但这更多是模型的问题,而不是执行框架在判断工作流中的某一步是否确实带来了净改进或完成时不够好。
执行框架只需要某种信号,能让它继续下去。这个信号不必客观,也不必二元化,只要足够有用,能驱动下一轮迭代就行。
我已经非常喜欢那些能把我一天中枯燥的部分拿走、让我去实验、测量、并给我灵感的循环了。
但另一方面,用同样的循环方法去编写可长期存在的代码,对我来说还不太舒服。我喜欢用的一个比喻,是从“软件作为确定性机器”转向“软件作为有机体”。
我成为软件工程师时,所处的环境鼓励我去理解机器。总有一层可以剥开,进一步加深你的理解。那些没有表现出确定性、可观察行为的机器,或许是被接受的,但总体来说并不被视为最佳选择。在软件架构上,我一直认为应当更进一步推动确定性,而不是更少。同样,理解代码的能力一直是一个无可争议的目标。实践中虽然并不总能做到,但我们仍然以写出这样的代码为荣:即便是新工程师,也能通过巧妙的架构去导航复杂代码库。在设计良好的系统里,总会有工程师知道不变量在哪里,哪些部分是承重的,哪些修改是安全的。理想情况下,所有这些也都有完善文档。若缺乏这种理解,通常会被视作需要改进的问题。
显然,这种理想一直都承受着压力。许多软件系统,尤其是非常成功的系统,都会经历一些时期,团队里的工程师能够把它们维持得很干净。大型软件系统往往太大、太动态,而且对外部服务依赖太强,以至于任何人都难以将其完整装进脑子里。即使没有 LLM,我们也已经有点像医生那样诊断分布式系统:观察症状,提出假设,“下更多测试”,尝试一些疗法,然后再次观察。
而有了 LLM,我们正以更快的速度、朝着这个方向走得更远。我们用它们来写代码,也用它们来做诊断和修复。已经有不少工程师生活在这样一个世界里:一旦出现生产问题,第一步就是让“铁皮罐头”读日志、提出根因并主动打补丁。随后,这个补丁常常又被另一台机器接手审查,甚至有时在没有任何人工监督的情况下直接合入主分支。
显然,这很强大,我也无法否认这听起来很诱人。但一旦接受这种想法,尤其是在人工监督越来越少的情况下,就意味着我们可能不再以同样的方式理解整个系统。我们会去处理它、监控它、稳定它,但未必真正理解它。
我毫不怀疑,对某些软件来说,这样是可以的。不是每一行代码都值得由人类亲自编写,而且过去也确实写过更糟的代码。
但我真的希望所有软件都以这种方式被创作出来吗?
真正令人不安的是,完全退出这种机器主导的未来,可能并不是一个选项。
今天最明显的例子就是安全。即使你自己不使用循环来构建软件,别人也会用循环来对付你的软件。攻击者会持续运行机器;即使不是攻击者,安全研究人员也会这么做,而这些自动化工作中既会产生大量噪声,也会发现真实问题。而信号与噪声都会以一种让你几乎无法应对的规模向你涌来,除非你自己也用一台机器去对抗这个问题。
Daniel Stenberg 关于 curl 的夏日安宁的帖子,就是维护者已经承受巨大压力的一个很好的例子。据我所知,AI 今天并没有在 curl 的核心开发中扮演极其重要的角色。然而即便如此,维护者仍然被大量报告淹没,而其中绝大多数如今都是 AI 生成的。
如果攻击者和报告者都在循环,防御者最终也必须循环起来才能跟上。也许不一定是直接写补丁,也许只是为了分流、复现和压测,但压力只会越来越大。
竞争层面也是如此,因为一些团队会凭借原始速度击败其他团队。某些项目会突然跑得更快,因为一小群人找到了有效编排机器的方法。一些初创公司会用五个人做出过去需要五十个人才能完成的事。有人甚至可能真的把一台机器放到你的产品上跑循环,然后要求它“把它做得像另一个一样”。如果他们的用户满意,那真的重要吗?
并非所有软件都会受到同等影响。有些领域会惩罚粗糙,要求信任与责任,但大量软件所处的世界,原始速度、快速试验和广覆盖都极其重要。
对我来说最可怕的是,我们会以新的方式依赖这些新机器。软件一直以来都依赖工具。我还记得曾经不得不为编译器付费的年代。这些新工具让我想起那些创造软件也伴随着真实成本的时期。但现在,这不再是一次性付费,而是一种持续依赖。不仅依赖于钱包是否充足,也依赖于认知能力。
如果一个代码库是由循环生成、由循环审查、由循环修补、并由循环维持生命,那么当你不再能使用同一类系统时,会发生什么?当某些贸易限制剥夺了你使用最强模型的权限时,会发生什么?如果只是成本变得难以承受呢?如果你和你的团队失去了在不借助机器的情况下理解代码的最后能力呢?
我们可能会创造出这样的代码库:它们不仅对人类来说难以维护,而且在维护模型中本身就假设了机器参与。这件事已经在发生了!并非到处都在发生,也未必以被视为有问题的方式发生,但我们看到的越来越多。人们越来越多地合并那些自己无法完全解释的代码。人们失去了在不借助或不改写聊天消息上下文的情况下创建问题报告或讨论事情的能力,而这些上下文来自某个“铁皮罐头”。越来越多的人依赖机器来总结或提供上下文。越来越多的时候,我遇到的人都通过 LLM 这一层间接地与我交流。
再说一次,也许这甚至不一定是错的,但这对我们过去的做事方式来说,是一次巨大的变化。
我几乎毫不怀疑事情就是朝这个方向发展,但要走到那里,我们需要在各处的工具上做些什么,而不仅仅是在编码智能体里。
仅仅编排更多循环还不够。更好的变更可视化、编排或智能体,也无法恢复我们的理解。我们要么需要找到巧妙的方法,把人重新拉回循环中,让循环中的变化在长期内可读;要么就需要找到更好的方式来组合这些越来越复杂的系统。
这也是我对 Pi 角色的看法正在改变的地方。Pi 一直很谨慎,我认为这种谨慎是好事。我不想要一个未来:每一次交互都变成一群失控机器的蜂拥行动,产生我无法追踪的改动。我也不希望 Pi 为了赢得迈向“自我编写软件”的竞赛,而变成一团不可维护的烂摊子;我同样不希望 Pi 去鼓励这种工程方式。与此同时,Pi 本身就是一个执行框架,而执行框架正处在运行这类新实验的人们的核心位置。
用于编码任务的任务队列、智能体编排、子智能体、持久会话,都只会越来越重要。即便是那些像我一样对此持保留态度、并没有盲目拥抱循环的人,也必须开始做这些实验。我们必须这样做,因为我们需要弄清楚,如何让这个未来保持边界并且可生存。
从这篇文章你也能看出来,我对这个未来非常不安。不是因为恐惧,而是因为基于迄今为止这项技术的使用经验,我保持谨慎。
采纳执行框架循环这个想法,意味着由执行框架来决定工作何时结束。在智能体循环中,模型最终会说“完成了”,然后我来审查。甚至在那之前,我通常也会一路进行引导。我是参与其中的,而且我享受一路学习的过程。而在由执行框架主导的循环里,我甚至不确定自己的角色是什么。就连“完成”的信号也失去了全部意义,只是被传递给另一台进行判断的机器而已。我的角色被缩减成了一个信使。
今天我并不喜欢我在这类系统中看到的大多数代码,我也不太享受与太多由 AI 辅助构建的软件交互。循环很强大,但它越来越削弱责任感,而且至少就今天而言,它非常鼓励我们向机器让步。
然而,我也毫不怀疑,尽管我现在对此感到反感,这个循环的未来终将成为我们的未来。我已经看到了令人震惊的小团队以不可能的速度构建东西,也看到了代码库越来越变成只有更多机器才能诊断的、晦涩而混乱的有机体。这些代码库既有用,又凌乱。
所以我想,我正在接受这样一个事实:问题不再是我们是否会进入循环,因为显然我们会。也许真正的问题是,在循环的未来里,我们如何不放弃判断力,如何保留良好工程的规则,如何确保负责任的人类能够继续监督,以及我们需要如何重新思考代码架构,才能在整个过程中保持理智。