The Coming Loop

Armin Ronacher

循环将至

原文由 Armin Ronacher 发布,订阅该博客

我已经不再给 Claude 发提示词了。我让循环去提示 Claude、去决定下一步该做什么。我的工作就是写循环。

—— Boris Cherny

过去几个月里,我看到越来越多人在编程智能体之上搭建的东西,已经和单纯使用编程智能体有了本质区别。其中一些是基于 Pi 做的,看到这一点确实很酷!不过各处的模式都是一样的:任务被放进某种队列,机器取走它、尝试执行、停下来,然后由某个 harness 来判断这是否真的算结束了。

如果不是,harness 就会延续同一个会话、注入另一条消息,或是用修改后的上下文开启一个全新会话,又或是把任务交给另一台机器。任务会在模型自己通常会说“我做完了”的那个节点之后,继续存活下去。

坦白说,我想这种循环想得比自己愿意承认的还要多。

每个编程智能体内部都已经有一个智能体循环。模型调用工具、整合结果、再调用另一个工具、读取文件、编辑文件、运行测试,最终给出一个答案。这个循环我们已经非常熟悉了。另一个循环是 harness 层的循环:也就是智能体循环之外的循环。这个循环也并不新鲜。早在 Claude Code 早期我们就在做不同版本的尝试,但这个循环在智能体工程中正变得越来越普遍,近几周甚至开始主导了 Twitter 上的讨论。

我在这方面还不在行

就目前而言,对于那些我真正关心的代码——结果发现为数不少——我用这种方式工作并没有取得太大成功。

一部分是品味问题,一部分是控制欲。我对代码该是什么样子要求很高,而且我想理解自己交付的代码。在有压力的情况下,或是在与他人讨论时,我希望自己能说清楚系统在做什么,而不必先去问 clanker 让它给我解释。当然,现在显然有个问题:几年后我是否还会执着于理解代码。但至少就现在而言,我还没跨过“理解很重要”这个阶段。

出于这种诉求,我在体验那些并非由我盯着写出来的代码时——尤其是来自循环的代码——总觉得缺了点什么。当今的模型倾向于产出过于防御性、过于复杂、推理过于局部的代码。它们回避强不变式。宁愿加上一堆兜底逻辑,也不去让错误状态根本不可能出现。它们复制代码、发明糟糕的抽象、用更多的机制去掩盖含混的设计。更糟的是:到目前为止,我几乎没看到这方面有任何好转。如果说有什么变化,在这个问题上我甚至觉得我们可能在往错误的方向走。至少以我的品味来看,如今像 Claude Code 搭配 ultracode 这种放手不管的 harness,产出的代码比去年秋天我们写出来的还要差。这是因为,比如配上 Fable 的 Claude Code,会不间断地在一个问题上埋头干上三十分钟甚至更久,而以前这个过程会有人更多地参与其中。

此外,众所周知,模型往往在观察到某个局部失败后,就加上一个局部的防御。Karpathy 提到过,它们“对异常怕得要死”。在具有重要不变式的系统中,尤其是持久化的数据格式或核心基础设施,正确的修复方式不是“处理每一种畸形情况”,而是让畸形情况根本无法被表示、压根写不出来。然而即便加以大量人工引导,LLM 也很难自然地写出这类代码,即便偶尔写出来了,它们仍会试图去处理那些已经不可能出现的错误。

当你把这种行为放到循环背后,它往往会被放大。如果每一次迭代都再加上一层小小的防御,系统就会在看似更健壮的同时,慢慢变得越来越难以理解。你放手不管的程度越高,这种情况就越严重。当这类工具在没有明确指引的情况下交给新人时,也会灌输非常糟糕的实践。因为如果你去问他们为什么要这么做,他们还能头头是道地为自己辩护。

循环奏效的地方

但与此同时,假装这种循环模式不起作用也是不诚实的,因为它在某些领域已经好得出奇。

代码移植就是其中之一。已经有不少令人印象深刻的大规模自动移植案例,包括据报道将 Bun 的部分代码从 Zig 移植到 Rust 的工作。我自己也曾成功用它把 MiniJinja 移植到 Go。性能探索是另一个表现得非常出色的场景。机器可以尝试各种实验、做基准测试、丢弃失败的方案并持续搜寻。安全扫描也天然适合,几乎任何类型的研究都是如此:让系统去探索复杂的问题空间并汇报结果,而不必提交需要长期维护的代码。这些成功案例的共同点在于,它们要么不是生成全新的代码,而是转化已有的代码,要么产出的代码本来就不打算长期存活。它们产出的是概念验证或想法、揭示发现,或是更接近机械式的转化。

我认为,那些产出无需长期存活的产物、或是能产生某种清晰可验证的机械式转化的循环,远比 harness 能否机械地衡量一个目标更重要。许多成功的循环应用都会用另一个 LLM 来做裁判或做编排。机械式转化的情况可以用二元的测试用例来验证,但也可以改由 LLM 来评判!

比如,Claude Code 就越来越擅长创建一整套实验工作流并加以执行。当然,它产出的代码是一团浆糊,但这更多是模型的问题,而不是 harness 没能判断好工作流中的某一步是否带来了净提升或已完成。

harness 只需要一个能让它继续下去的信号。这个信号不必是客观的或二元的,只要足以驱动下一次迭代就够了。

我已经非常喜欢那些能把一天中枯燥的部分拿走、帮我做实验、做测量、给我灵感的循环。

软件即有机体

另一方面,用同样这套循环方法去写需要长期维护的代码,我目前还无法接受。我喜欢用的一个比喻是,我们正在从“软件即确定性机器”走向“软件即有机体”。

我成为软件工程师的环境鼓励我去理解机器。总有可以剥开的一层,让你更深入地理解。那些不表现出确定性可观测行为的机器或许会被接受,但通常被视为不那么理想。从软件架构的角度看,我认为应该朝着更确定性的方向推进,而不是相反。同样,能够理解代码也一直是一个毋庸置疑的目标。实践中虽不总能做到,我们仍以通过巧妙的架构来编写代码为荣,让即便是新工程师也能在复杂的代码库中穿行。在设计良好的系统中,总有工程师清楚不变式在哪里、哪些部分是承重的、哪些改动是安全的。理想情况下,所有这些也都有完善的文档。在理解缺位的地方,通常也会被视为需要改进之处。

显然,这个理想一直都很脆弱。许多软件系统,尤其是非常成功的那些,也曾有过团队工程师能够保持其整洁的时期。大型软件系统往往过于庞大、过于动态、又过于依赖外部服务,以至于无法装进任何一个人的脑子里。即便没有 LLM,我们诊断分布式系统时也已经有点像医生:观察症状、提出假设、“开更多检查”、尝试一些补救措施,然后再次观察。

然而有了 LLM,我们正更快、更远地朝着这个方向推进。我们用它们来写代码,也用它们来做诊断和修复。已经有不少工程师生活在这样一个世界里:线上问题一出现,第一步就是让 clanker 去读日志、提出根因、主动提交补丁。产出的补丁随后往往又被另一台机器接手评审,有时甚至在没有任何人工监督的情况下就合并到了 main 分支。

显然,这很强大,我也无法否认它听起来很诱人。但如果顺从这个想法,尤其是人类监督越来越少,就意味着要接受我们可能不再以同样的方式理解整个系统。我们治疗它、监控它、让它稳定下来,但未必真正理解它。

我毫不怀疑,对某些软件而言,这样也没问题。并非每一行代码都值得人类亲手去写,过去也可能写出过更糟的代码。

但我希望所有软件都以这种方式被创作吗?

你无法选择置身事外

让人非常不适的是,完全选择退出这个由机器驱动的未来,可能根本不是一个选项。

安全是当今最清晰的例子。即使你不用循环来构建软件,别人也会用循环来对付你的软件。攻击者会让机器持续不断地跑,即便不是攻击者,安全研究人员也会这么做,而这些自动化工作中的一部分会扬起尘土,但也会发现真正的问题。无论是信号还是噪音,都会以让你几乎无法应对的体量向你涌来,除非你自己也用机器去应对。

Daniel Stenberg 关于 curl “bliss 之夏”的文章就是维护者们已承受压力的一个好例子。据我所知,AI 在当今 curl 的核心开发中并没有扮演特别重要的角色。尽管如此,维护者们还是被海量的报告淹没了,其中大多数现在都是 AI 生成的。

如果攻击者和报告者都在循环,防御者最终也不得不循环才能跟上。或许不是直接写补丁,或许只是做分拣和复现,但压力会越来越大。

在竞争层面也是如此,有些团队会凭借纯粹的速度超越其他团队。一些项目会突然加速,只因为一个小团队摸索出了如何有效编排机器。一些初创公司用五个人就能做到过去五十个人才能做到的事。有些人甚至可能真的用机器对着你的产品循环运行,让它“做得跟另一家一样”。而如果他们的用户满意了,那又有什么关系呢?

并非所有软件都会受到同等影响。有些领域会惩罚马虎、要求信任与责任,但大量软件所处的世界里,纯粹的速度、快速试错和广泛覆盖才至关重要。

滋生新的依赖

对我来说最可怕的是,我们正以新的方式依赖上这些新机器。软件向来依赖工具。我还记得当年不得不为编译器付费的日子。这些新工具让人回想起那个创造软件需要付出真金白银成本的时代。但现在不再是一次性付费,而是持续的依赖。不仅仅是对钱包的依赖,更是认知上的依赖。

如果一个代码库是由循环产生的、由循环评审的、由循环打补丁的、由循环维持存活的,那么当你不再能使用同类的系统时会发生什么?当某些贸易限制让你无法使用最强大的模型时会怎样?如果成本变得难以承受呢?如果你的团队彻底失去了不借助机器就理解代码的最后能力呢?

我们可能会创造出一些代码库,它们不仅难以由人类维护,而且其维护模式本身就假定了机器的参与。这已经在发生了!并非处处如此,甚至可能并未以被视为问题的方式发生,但我们看到的越来越多。人们越来越多地合并那些自己都无法完全解释的代码。人们在没有 clanker 提供的上下文来润色或改写消息的情况下,已经失去创建 issue 报告或在聊天中讨论问题的能力。越来越多的人依赖机器来做总结或提供背景。我也越来越多地遇到那些通过 LLM 的间接层来与我对话的人。

再说一次,也许这甚至算不上错,但这对我们做事的方式而言是一个巨大的改变。

未来的 Harness

我毫不怀疑事情正朝着这个方向发展,但要走向那里,我们需要在各处的工具链上有所作为,而不仅仅是在编程智能体上。

仅仅编排更多的循环是不够的。更好的变更可视化、编排或智能体本身并不会恢复我们的理解。要么我们需要找到巧妙的方法,把人重新拉回循环中,并让循环产生的变更在长期内保持可读,要么我们需要找到更好的方式来组合这些日益复杂的系统。

这也是我对 Pi 角色的思考正在发生变化的地方。Pi 一直很谨慎,我认为这种谨慎是好的。我不希望未来每一次交互都变成一群不受控的机器在做出我无法跟上的改动。我不希望 Pi 为了赢得“软件自我编写”的竞赛而变成一团无法维护的乱麻,我也不希望 Pi 去助长这种工程方式。但与此同时,Pi 本身就是一个 harness,而 harness 正处于人们开展这类新实验的中心。

用于编程任务的任务队列、智能体的编排、子智能体、持久化会话将变得越来越重要。即使是我们这些心存疑虑、并未盲目拥抱循环的人,也将不得不开始做这些实验。我们必须这么做,因为我们需要弄清楚如何让这个未来变得可控、可持续。

控制循环

正如你从这篇文章中读到的,我对这个未来感到非常不安。不是出于恐惧,而是基于迄今为止与这项技术打交道的经验所产生的审慎。

采纳 harness 循环的理念意味着,由 harness 来决定工作何时完成。在智能体循环中,模型最终会说“完成了”,然后由我来评审。甚至在此之前,我通常也会在过程中进行引导。我参与其中,也乐在其中、边做边学。而在由 harness 驱动的循环中,我甚至不确定自己的角色是什么。就连“完成”这个信号也失去了所有意义,只是被传递给另一台做评判的机器。我的角色被削弱成了一个传话人。

如今,我不太喜欢从那些以这种方式构建的系统中看到的大部分代码,也不怎么喜欢与太多借助 AI 构建的软件打交道。循环很强大,但它越来越多地剥离了责任,而且至少在今天,它在很大程度上鼓励我们向机器妥协。

然而尽管我目前对此心存抵触,我毫不怀疑这个循环的未来就是我们的未来。我已经看到小得惊人的团队以不可思议的速度在构建,也看到代码库越来越变成晦涩、混乱的有机体,只能由更多的机器来诊断。这些代码库同时是有用的,也是一团糟。

所以我想,我正在接受这样一个事实:问题不在于我们会不会循环,因为显然我们会。也许问题在于,在一个充满循环的未来,我们如何做到不放弃判断、如何保留良好工程的准则、如何确保负责任的人能够继续监督、以及我们需要如何重新思考代码的架构,才能在这一路上保持清醒。

本文章由 muse-spark-1.2-contributor 进行翻译

评论