The Coming Loop

[!abstract] 摘要
一篇关于在编程 Agent 之上构建自动化外壳循环(Loop)模式的深度译文:作者探讨这种外部循环的兴起、它在代码移植与性能探索等领域的出色表现、它对人机控制力与代码理解力的冲击,以及面对不可逆趋势时如何守住判断力与工程设计原则。

我不再直接给 Claude 写提示词(Prompt)了。我让循环(Loops)自动运行,由它们去给 Claude 发送提示并决定下一步该做什么。我的工作就是编写这些循环。

—— Boris Cherny

在过去的几个月里,我看到越来越多的人在编程智能体(Coding Agents)之上构建一些东西,这些东西给人的感觉与单纯使用 Agent 有着本质的区别。其中一些是基于 Pi 构建的,这确实很酷!不过,无论在哪里,其模式都是相同的:任务被塞进某种队列,机器接起任务,尝试运行,停下,接着由某种**外壳程序(Harness,或称调度外壳)**来判定这是否已是终局。

若非终局,外壳程序便会延续当前的会话,注入下一条消息,或是启动一个调整了上下文的全新会话,抑或将任务分派给另一台机器。任务因此在模型本该说出“我完工了”之后,依然长久地活跃着。

我对这种“循环(Loop)”模式的思考,多得连我自己都不愿承认。

其实,在每个编程 Agent 内部已经存在一个“Agent 循环”(Agent Loop)了。模型调用工具、整合结果、调用另一个工具、读取文件、编辑文件、运行测试,并最终产生某种答案。这种内部循环我们早就非常熟悉了。而另一种循环则是外壳层循环(Harness-level Loop):即运行在 Agent 循环之外的外部循环。这种循环也并非新鲜事。早在 Claude Code 诞生之初,我们就已经在使用它的各种变体了。但在如今的 Agent 化工程中,这种外部循环的存在感越来越强,近几周它甚至开始主导 Twitter 上的技术讨论。


1. 我还没能适应这种方式

于我而言,在那些我深切关切的代码上(事实证明,这类代码几乎占了我所写代码的绝大多数),这种工作方式至今未能带给我令人满意的成功。

这既关乎“代码品味”,也关乎“掌控力”。我习惯于为代码质量设定极高的标准,并且我希望能彻底理解自己交付的代码。在承受压力或与他人讨论时,我希望能够解释系统是如何运作的,而不是必须先去求助一个“铁皮人(Clanker,即 AI)”来给我解释。当然,这种对“理解代码”的渴望在几年后是否还会存在,显然是个疑问。但至少就目前而言,我依然认为“理解”至关重要。

正是这种追求透彻理解的执念,让我对那些在我浑然不觉中写就的代码——尤其是出自自动化循环的代码——产生了一种隔膜与缺憾感。当下的模型编写代码时,往往表现得过于防御性、过于臃肿,且逻辑推理极其局限于局部。它们避开强烈的不变量(Invariants);它们倾向于添加容错回退(Fallbacks),而不是从根本上让“糟糕状态”(Bad States)变得不可能发生。它们复制重复代码、创造糟糕的抽象,并用更多的机械结构来掩盖不清晰的设计。更糟糕的是:目前我几乎看不到这方面有任何改善的迹象。如果有的话,我甚至觉得我们在这个方向上正在倒退。

至少在我的审美里,目前像 Claude Code 搭配 ultracode 这种完全免干涉的“放手掌柜式”外壳程序,产出的代码甚至还不如去年秋天我们的成果。原因在于,如今搭载了 Fable 模型的 Claude Code 会在一个问题上心无旁骛、连续折腾 30 分钟乃至更久,而在以前,这一过程会有更高频的“人机协同(Human in the loop)”。

如 Karpathy 曾指出的,AI 模型对“抛出异常(Exceptions)”怀有致命的恐惧。然而,在有着严格不变量的系统中,特别是在持久化数据格式或核心基础设施里,正确的修复策略绝非“去捕获并容忍每一种残缺情况”。最根本的解决之道,是通过优良的设计让这些异常状态**“根本无法被表示”(Make malformed case unrepresentable)**,或者让它们在写阶段就直接失效。然而,即使人类进行了大量的肉身干预,大模型(LLM)也几乎无法自发编写出如此优雅且具备强不变量的代码;即便写出了这样的设计,它们转过头来又会去画蛇添足地处理那些早已不再可能发生的错误。

当把这种退化倾向丢进自动运行的“外部循环”中时,其缺陷便会被成倍放大。每一次迭代在局部新增一层微不足道的“防御”,系统就在看似愈发强韧的同时,逐步滑向不可理解的深渊。越是彻底放手,这一过程就发生得越快。若在缺乏明确工程指引的情况下将这类工具交给初级开发者,极易催生极其糟糕的编程习惯。因为如果你追问他们为何堆砌这些代码,他们能凭借直觉,给出逻辑完全自洽且极具欺骗性的“防御性编程”辩词。


2. “循环”在何处大显身手

然而,如果假装“循环”模式没有效果,那将是不诚实的,因为在某些特定的领域,它已经运转得惊人地出色。

**代码移植(Porting code)**就是其中之一。目前已经有许多令人瞩目的大规模自动移植案例,例如据报道,将 Bun 的部分代码从 Zig 移植到 Rust 的工作。我自己也曾成功利用这一模式将 MiniJinja 移植到了 Go 语言。

**性能极限的探索(Performance explorations)**则是另一个让循环大放异彩的温床:机器可以不知疲倦地提出实验方案、运行基准测试(Benchmark)、无情抛弃失败的尝试,并在这个无尽的搜索空间中寻找更优解。

**安全扫描(Security scanning)也与此天然契合,几乎所有的研究工作(Research)**也是如此:让系统去探索一个复杂的、未知的工程空间并生成报告,而不一定需要立即提交能长久存活的代码。

这些成功案例都有一个共同点:它们要么不产生全新的代码,而是对已有代码进行转换,要么生成的代码本身就不需要太长的生命周期。它们产出的是概念验证(PoC)、点子和发现,或者更类似于某种“机械式的转换(Mechanical translation)”。

相较于外壳程序机械式地去衡量某个目标,我认为那些**“无需长久存在”的临时产物**,或者具有明确验证机制的机械式翻译,才是发挥循环威力的核心所在。许多成功的循环应用会使用另一个 LLM 作为“裁判(Judge)”或“编排器(Orchestrator)”。机械式的翻译可以通过二进制测试用例(如编译器、单测)来验证,但也完全可以由另一个 LLM 来评判!

以 Claude Code 为例,它越来越擅长自主创建并执行一整套实验性工作流。诚然,它写出的代码可能有些粗制滥造(Slop),但这主要是模型的局限,而不能怪罪于外壳程序无法准确判断某一步工作流是否带来了净改进或任务完成。

外壳程序只需要某种**信号(Signal)**来让其得以继续运行。这个信号不必绝对客观,也不必是黑白分明的二元信号,它只需要足够有效,能够推动下一次迭代即可。

我已经对这种能帮我把一天中枯燥乏味的实验、测量和构思工作接管过去的“循环”爱不释手了。


3. 软件即有机体

然而,使用同样这种“循环”方法来编写需要长期维护的代码,我目前依然感到难以接受。我喜欢用一个隐喻来描述这种转变:我们正在从“将软件视为确定性的机器”,转向“将软件视为一种有机的生命体(Organism)”。

在我步入软件工程界的那个时代,整个行业都鼓励我们去“看透机器”。软件是一层可以不断向下剥离的洋葱,每一次剥离都在加深对底层机制的理解。一个无法表现出“确定性且可预测行为”的系统,或许能勉强服役,但绝不会被视为正统。在架构层面,我始终认为应当向着更强的确定性——而非混沌——去迈进。把“透彻理解代码”视为不可动摇的黄金标杆。尽管在现实中这无法时刻百分之百实现,但我们曾由衷地以高超的架构设计为傲,以此来确保即使是新加入的工程师也能迅速理清复杂的代码脉络。在优良的系统里,总会有工程师清楚地掌握每一处核心承重墙(Load-bearing codes)的位置,洞悉“不变量”的宿所,并能笃定哪些改动是绝对安全的。在最理想的情况下,这一切都有据可查。任何一处失控的理解盲区,都会被视为必须被铲除的工程债务。

当然,这种理想状态总是面临挑战。许多软件系统(尤其是非常成功的系统)也只有在某些特定阶段,团队里的工程师才能勉强保持其干净整洁。大型软件系统往往过于庞大、过于动态,且极度依赖外部服务,以至于无法塞进任何一个人的脑袋里。即使没有 LLM,我们在诊断分布式系统时也已经有点像医生了:观察症状、提出假设、“开具更多检测单(增加日志)”、尝试一些疗法,然后继续观察。

但有了大语言模型,我们正在以更快的速度在这个方向上走得更远。我们不仅用它们来写代码,还用它们来诊断和修复。已经有相当一部分工程师习惯了这样的世界:生产环境一旦发生故障,第一步就是让 AI 自动去读取日志、分析根本原因并主动提交补丁。随后,这个补丁会被另一台机器自动进行 Code Review,有时甚至在没有任何人工干预的情况下直接合入 main 主分支。

这固然极其强大,我也不否认它听起来非常吸引人。然而,一旦完全接受这种模式(尤其是当人工监管越来越少时),就意味着我们必须承认:我们可能再也无法以同样的方式去理解整个系统了。我们只能治疗它、监控它、维持它的稳定,但并不一定能真正“参透”它。

毫无疑问,对于某些软件而言,这完全没问题。毕竟,并不是每一行代码都值得人类亲自去撰写,甚至过去人类自己写出的代码可能比这还要糟糕。

但是,我们真的希望所有的软件都以这种方式被创作出来吗?


4. 你其实无法置身事外

让人感到非常不安的是:彻底在这个纯机器驱动的未来中“抽身而去”,或许根本不是一个可行的选项。

安全领域就是当下最明显的例证。即使你不用自动循环来编写软件,其他人也会用循环来攻击你的软件。黑客会不间断地运行他们的机器;就算不是恶意攻击者,安全研究人员也会这么做,这些自动化工具会卷起满天沙尘(产生大量噪音),但同时也会找出真实的漏洞。届时,随之而来的信号和噪音将达到惊人的体量,如果你不亲自部署一台机器来应对,你将完全无法招架。

Daniel Stenberg 撰写的关于 curl 遭遇“夏日狂澜” (Summer of bliss) 的博文,生动揭示了开源维护者如今正承受着怎样的万钧重压。据我所知,AI 至今在 curl 的核心开发中几乎没有存在感。然而,即便在这方人类坚守的阵地里,维护者们也彻底被铺天盖地的漏洞报告所淹没——其中绝大多数,皆是 AI 自动化循环不知疲倦地制造出的“杰作”。

如果攻击者和漏洞汇报者都在使用“循环(Loop)”,那么防守方为了跟上步伐,最终也必须部署自己的“循环”。哪怕不是直接让 AI 写补丁,至少也需要用它来进行初步筛选、复现和分流,这种来自外部的压力只会持续增加。

在商业竞争中也是同理。一些团队会通过极致的速度把对手远远甩在身后。一些项目会因为一小撮人学会了如何高效编排机器,而突然实现跨越式的发展。一些初创公司可以用 5 个人完成过去需要 50 个人才能做到的事情。甚至有人会直接把你的产品扔进一个循环里,对机器下达指令:“把它做成和那个一模一样的产品。”如果他们的用户对此感到满意,这背后的代码细节真的还重要吗?

并不是所有的软件都会受到相同程度的波及。有些领域依然会严厉惩罚粗制滥造,并极其看重信任与责任;但大量软件所生存的世界里,极快的速度、快速的实验迭代以及大面积的功能覆盖,才是压倒一切的硬道理。


5. 建立新的依赖

对我而言,最可怕的部分在于我们正在以全新的方式对这些新机器产生依赖。软件开发向来依赖工具,我至今还记得当年必须要自掏腰包购买编译器的日子。如今这些新工具,仿佛让我们穿越回了那个“开发软件需要支付真实物理成本”的年代。但现在它不再是一次性的买单,而是一个持续不断的依赖项。这不仅仅是对钱包厚度的依赖,更是一种认知上的依赖(Cognitive dependency)。

当一个代码库,其生成、审查、修补乃至长期生命力的维持,都完全依托于循环自动化进行时,一旦你失去对这一级别系统的访问权限,后果将不堪设想。若地缘政治带来的贸易壁垒切断了你获取最强模型的通道,该如何自处?若模型的使用开销昂贵到无力承担,又当如何?更可怕的是,若你和你的团队在彻底失去机器的扶持后,连理解最基础代码的能力也已退化殆尽,那该如何是好?

我们正在亲手创建一些代码库,它们不仅仅是对人类而言难以维护,而且其维护模型本身就默认了必须有机器的参与。这已经在发生了!虽然它还没有蔓延到每一个角落,可能甚至还没有以一种公认的灾难性方式显现出来,但它的迹象已经越来越明显。越来越多的人开始合并那些他们自己都无法完全合理解释的代码。越来越多的人在没有 AI(Clanker)帮他们润色、改写或者提供上下文的前提下,已经失去了写一份清晰 Issue 报告或在聊天中讨论技术问题的能力。太多的人正变得越来越依赖机器来对信息进行总结或提炼。我甚至在日常交流中,也越来越频繁地遇到必须通过 LLM 的间接转译来与我对话的人。

同样,也许在未来这并不算是件错事,但这确实对我们过去习以为常的软件开发方式带来了颠覆性的改变。


6. 未来的外壳程序

我毫不怀疑这就是未来的发展方向。但在通往那里的路上,我们需要彻底革新现有的工具链,而不单单是升级那些编程 Agent。

仅仅编排更多的循环是远远不够的。仅仅在界面上提供更好的代码变更可视化、或者是更好的 Agent 编排,也无法重新帮我们夺回对代码的理解。我们要么需要找到聪明的办法,把人类重新拉回循环之中(Human back into the loop),并让这些由循环自动产生的修改在长期尺度上保持清晰可读;要么,我们就需要寻找更好的方法来组合和构建这些日趋复杂的系统。

这也是我对 Pi(pi.dev)所扮演的角色产生观念转变的地方。Pi 一直保持着克制与谨慎,我认为这种谨慎是非常正确的。我绝不希望看到这样一个未来:每一次小小的人机交互,都会引发一大群不受控制的机器蜂拥而上,做出一大堆我根本无法理解和追踪的代码修改。我不想看到 Pi 为了去赢得这场“软件自动编写自己”的竞速游戏,而把自己变成一个不可维护的烂摊子,我也不希望 Pi 去倡导这种激进的工程方式。但与此同时,Pi 本身就是一个外壳程序(Harness),而外壳程序恰恰处于人们开展这类全新实验的核心阵地。

用于编程任务的任务队列、Agent 的编排调度、子智能体(Subagents)以及持久化会话,这些概念正变得越来越重要。即便我们当中那些持保留态度、不愿盲目拥抱“循环”的人,也不得不开始着手进行这些实验。我们必须这么做,因为我们需要懂得如何让这个注定到来的未来变得可控、界限明确,且能够让我们体面地生存下去。


7. 掌控循环

正如你从这篇博文中所读到的,我对这个未来感到非常不安。这并非出于对未知的恐惧,而是基于目前我对这门技术的使用经验所产生的警惕。

采用“外壳程序循环”的思想,意味着最终由外壳调度程序来决定工作何时结束。在常规的 Agent 内部循环中,模型最终会说“搞定了”,然后由我来审查。甚至在结束之前,我通常也会在一旁指引方向。我参与其中,并享受在这一过程中的学习乐趣。然而,在完全由外壳程序操纵的外部循环中,我甚至不确定我自己的角色到底是什么。甚至连“搞定了”这个信号也失去了全部意义,因为它只是被传达给了另一台用于判定结果的机器。我的角色被降级为了一个传话筒。

如今,我并不喜欢我所看到的、通过那种方式构建的系统里的绝大部分代码,也并不享受与太多由 AI 辅助构建的软件打交道。自动循环诚然强大,但它在不断剥离人的责任感。至少在今天,它极大地鼓励了我们向机器妥协。

然而,毫无疑问,尽管我目前对此心存抗拒,这个被“循环”支配的未来依然会是属于我们的未来。我亲眼目睹了小得令人难以置信的团队以不可思议的速度进行开发,我也看到代码库正越来越快地演变成晦涩、令人困惑的有机生命体,只能被更多的机器去诊断。这些代码库虽然实用,但确实一团混乱。

所以,我想我正在逐渐接受这个现实:问题不在于我们是否要进入“循环”时代,因为我们显然别无选择。也许真正的问题在于,在“循环”的未来中,我们如何不放弃评判能力,如何坚守优秀工程设计的原则,如何确保有责任感的人类能够继续进行监管,以及我们该如何重新思考代码的架构方式,从而在前进的道路上保持理智与清醒。