即将到来的循环时代:当 AI 接管编程工作流

我再也不会主动“催促”克劳德了。我早已编写好了循环,不断向克劳德提出问题,并在其中摸索该如何应对。我的工作,就是编写这些循环。

——鲍里斯·切尔尼

在过去几个月里,我目睹越来越多的人在编码代理的基础上构建出一些全新的成果, 这些成果与单纯使用编码代理相比,有着截然不同的意义和价值。 其中一些成果甚至建立在Pi 之上——看到这样的进展,无疑令人倍感欣喜! 不过,无论在何处,其运作模式都大同小异:工作被放入一个类似队列的结构中, 由一台机器将其取出、尝试执行、暂停,然后由另一台机器负责判断,究竟这是否是最终的处理结果。

若未达成目标,这台机器便会继续维持同一会话,注入新的消息,以修改后的上下文重新开启一次会话, 或者将任务发送给另一台机器。而任务的生命周期,往往远超模型本应自行判定的那一点: “我已完成任务。”

每每回想起这种循环机制,我的内心总有些难以言喻的复杂情绪。

事实上,每一种编码代理内部,其实早已存在一套独立的循环机制。 模型调用某个工具,将结果整合进系统,再调用另一个工具,读取文件、编辑文件、 运行测试,最终生成某种答案。这套循环机制,我们已经非常熟悉了,而且这一套循环机制早已存在了很长一段时间。 而另一种循环,则是位于代理循环之外的“Harness 层次”的循环:它并非什么新鲜事物,早在Ralph 时代起,我们就一直在实践这种模式。从早期的 Claude Code 时代起,我们便不断迭代着类似的方案,但如今,这种循环机制在代理式工程中愈发凸显, 尤其是在最近几周,它更是逐渐主导了 Twitter 上的讨论。

我还没真正擅长这项工作

目前我的状态是:对于那些我深以为然、却往往需要投入大量精力去完成的代码,我至今尚未取得太多成功。

这其中一部分源于个人品味,另一部分则源于对控制力的追求。 我总是试图为代码的外观设定更高的标准,并且希望彻底理解自己所交付的代码。 当面临压力,或是在与他人交流时,我渴望能够在不先请另一位开发者替我解释的情况下, 清晰地阐述出系统的运作逻辑。当然,我们也可以问:这种想要深入理解代码的欲望, 真的会在几年后依然存在吗?至少目前,我还没有超越“理解代码对我来说仍然至关重要”的阶段。

然而,正是出于这种渴望,我在那些无需我亲自干预、尤其是通过循环来实现的代码开发经验中, 仍有所欠缺。如今的模型往往倾向于生成过于防御性、过于复杂、过于局限于局部推理的代码; 它们回避强健的不变式,反而选择添加备选方案,而非从根本上杜绝不良状态的发生; 它们重复代码、生造糟糕的抽象概念,用更多的机械手段来掩盖设计上的模糊之处。 更糟的是,迄今为止,我们在这方面几乎看不到任何实质性的进步。 相反,从某些方面来看,我甚至觉得,我们或许正朝着错误的方向迈进。 至少就我个人的审美而言,像 Claude Code 这样的“放手式”工具,即便配备了超能力代码,其产出的代码反而比去年秋天我们所创作的还要糟糕。 原因在于,比如使用 Fable,Claude Code 可以不间断地运行三十分钟甚至更久,而此前,这个过程在循环中往往要更加依赖人类的参与。

此外,人们也深知,模型往往会观察到局部故障,并在此基础上加入局部防御措施。Karpathy 曾提到:“模型对异常情况感到极度恐惧。 ”而在那些拥有重要不变式的系统中——尤其是那些用于持久化数据格式或核心基础设施的系统——正确的解决之道并不是“应对每一个畸形输入”。 真正的解决方案,是让这些畸形输入变得无法被表示,甚至根本不可能被写入。 然而,即使我们进行了大量的手动引导,这类代码依然难以自然地从大型语言模型中诞生; 即便代码能够自然地生成,它们也依旧会试图去处理那些原本根本不可能发生的错误。

当你把这种行为置于循环之中时,往往会进一步放大其影响。 如果每一次迭代都增加一层小小的防御措施,系统就会在看似更“稳健”的表象之下, 逐渐变得越来越难以理解。你越是“放手”,这种现象就越容易发生。 同时,这也常常会教导我们养成一些极其糟糕的习惯:当像这样的工具被交予初级开发者时, 如果没有明确的指导,他们往往只会用各种理由为自己辩解。

循环的适用场景

与此同时,若我们假装循环模式并不奏效,那也是不诚实的——因为这种模式在某些领域中,其实已经取得了惊人的成效。

例如,代码迁移就是一个很好的例子。已经有令人印象深刻的大规模自动化迁移项目, 其中包括那些将Bun 从 Zig 迁移至 Rust 的案例。我自己也曾成功地利用该工具,将 MiniJinja 迁移到 Go 中。性能优化的探索,同样是一个非常适合运用这种模式的场景。 机器可以尝试各种实验,对结果进行基准测试,丢弃失败的案例,继续寻找更好的解决方案。 安全扫描也同样自然而然地融入其中,几乎任何类型的科研活动都可以借助这种方式展开: 让系统去探索复杂的潜在问题空间,并在不必要地编写长期代码的前提下,将结果反馈回来。 这些工具的共同点在于:它们要么不会生成全新的代码,而是对现有代码进行改造; 要么生成的代码,刻意避免了长久的使用寿命。 它们要么产出概念验证或创意,要么只是揭示了一些新发现,又或者更接近于一种机械化的转化过程。

我认为,那些在无需长久存活的情况下生成成果的循环,或者那些创造出某种可清晰验证的机械式转换方案的循环, 其价值远胜于单纯的“机械式”目标衡量能力。 许多成功的循环应用,都会借助另一款大型语言模型作为评判者,或是作为编排者。 机械式翻译的案例,可以通过二进制测试用例来验证,但同样也可以由大型语言模型来评判!

以 Claude Code 为例,它越来越擅长创建完整的实验流程,并在之后执行这些流程。 当然,它生成的代码难免存在瑕疵,但这更多是模型本身的问题,而非由于编排者未能准确判断流程中的某一步是否带来了净收益或完成了预期目标。

编排者只需要一个信号,就能让整个流程继续推进。这个信号不必是客观的、非黑即白的,只要足够有用,足以推动下一轮迭代即可。

我特别喜欢那些能将枯燥的工作转化为实验、测量并为我提供灵感的循环。

软件如有机体

另一方面,若我们用同样的循环方法来编写持久性更强的代码,却仍然让我感到不太自在。我最喜欢借用的一个比喻是:从“软件是一种确定性机器”转向“软件是一种有机体”。

我成为一名软件工程师,成长于一个鼓励我深入了解机器的环境。 总有一层可以层层剥开,帮助我深化对机器的理解。 那些并未表现出确定性可观察行为的机器,或许还能被接受,但通常被认为并不完全理想。 从软件架构的角度来看,我始终希望进一步追求更高的确定性,而不是降低确定性。 同样地,理解代码的能力,也一直是不可否认的目标。 尽管在实践中并非总是能够完全实现,但我们依然自豪地书写代码,让新工程师也能凭借巧妙的架构, 轻松驾驭复杂的代码库。在设计精良的系统中,总有一些工程师清楚地知道哪些不变量存在于何处, 哪些部分是承重结构,哪些变更是安全的。 理想情况下,所有这些信息也都得到了充分的记录。 而对于那些缺乏这种理解的地方,人们往往认为,这些地方还有待改进。

显然,这样的理想从未真正实现过。许多软件系统,尤其是那些极为成功的系统, 曾经历过一段时期,在此期间,团队中的工程师们能够将系统维护得井然有序。 大型软件系统往往规模庞大、动态变化频繁,且高度依赖外部服务,以至于很难被任何人完全掌握。 即便没有大型语言模型,我们早已像医生一样,通过观察症状、提出假设、“增加更多检测项”、 尝试各种补救措施,再反复观察,来诊断分布式系统的问题。

然而,借助大型语言模型,我们正以更快的速度、更深远的视角迈向这一方向。 我们不仅用它们来编写代码,还用它们来进行诊断与修复。 许多工程师早已生活在一个这样的世界里:每当生产问题出现时,第一步往往是让一位开发者阅读日志, 提出根本原因,并主动提交修复补丁。随后,这些补丁往往会被另一台机器接手, 经过审查,有时甚至直接被合并到主分支,而无需人工干预。

这种做法确实强大,我也无法否认它的吸引力。 然而,若我们一味迎合这种想法——尤其是在人类监督越来越少的情况下——就意味着我们不得不接受这样一个事实: 也许我们再也无法以同样的方式理解整个系统。 我们对待它、监控它、稳定它,却未必真正理解它。

我毫不怀疑,对于某些软件来说,这样做的确无可厚非。并非每一行代码都值得由人类亲笔撰写,甚至过去可能还写出了更糟糕的代码。

但,我真希望所有的软件都以这种方式被编写吗?

你根本无法完全退出

最让人不安的是:要完全摆脱这种完全由机器驱动的未来,似乎并非一个可行的选择。

如今,安全性是最明显的例子。即便你不使用循环来构建自己的软件,其他人也会利用循环来攻击你的软件。 攻击者会持续运行机器,即便不是攻击者,安全研究人员也会如此。 这些自动化的工作虽然会带来一些杂音,但也往往能发现真正的漏洞。 无论是信号还是噪音,都会以一种近乎难以应对的规模涌向你——除非你自己亲自动手, 将机器投入到问题当中。

Daniel Stenberg 关于 curl 的一篇帖子,标题为《curl 的“幸福之夏”》,很好地展现了维护者们已然承受的压力。 据我所知,如今 AI 并未在 curl 的核心开发中扮演举足轻重的角色。然而,尽管如此,维护者们仍然被各类报告所淹没, 而这些报告大多已由 AI 自动生成。

如果攻击者和报告者都依赖循环,那么最终,防御者也需要通过循环来跟上步伐。或许不必直接编写补丁,或许只需进行分类、复现,压力便会随之加剧。

竞争也是如此:有些团队会凭借原始速度超越其他团队。 有些项目会因为一小群人找到了高效协调机器的方法,而突然间加速发展。 有些初创公司仅用五个人,就能完成过去需要五十个人才能完成的任务。 甚至有人会直接将机器“嵌入”你的产品中,让它“模仿其他机器”。 而如果用户的体验很愉快,这又真的重要吗?

并非所有软件都会受到同样的影响。有些领域会严惩粗心大意的行为,要求人们承担信任与责任; 但也有许多软件生活在这样一个世界里:原始速度、快速实验以及广泛的覆盖范围, 才是至关重要的因素。

构建新的依赖关系

对我来说,最令人担忧的是:我们正以全新的方式,开始依赖这些新机器。 软件一直以来都依赖于各种工具。我记得曾经为了使用编译器而付费的日子。 这些新工具,仿佛将我们带回到软件开发成本高昂的时代。 然而如今,这不再是一次性的支付,而是一种持续的依赖。 这种依赖不仅仅是对钱包的依赖,更是对认知层面的依赖。

如果一个代码库由循环生成、由循环审核、由循环修补,并由循环持续维护,那么当您不再能接触到同一类系统时, 会发生什么呢?当某些贸易限制剥夺了您获取最强大模型的权限时,又会怎样? 如果成本变得难以承受,又该怎么办?如果连您和您的团队,都失去了唯一剩下的、 无需借助机器就能理解代码的能力,又会怎样?

我们或许会创造出一些代码库,它们不仅难以被人类维护,甚至会将机器的参与视为其维护模式的一部分。 这样的事情已经在发生了!当然,这并非普遍现象,甚至可能并没有以我们认为有问题的方式发生, 但我们却越来越常看到这样的场景。越来越多的人会合并那些自己无法完全解释的代码。 人们渐渐失去了撰写问题报告或在聊天中讨论问题的能力,而无需借助编排者来补充或重新表述自己的信息。 越来越多的人开始依赖机器来总结或梳理内容。 我遇到的越来越多的人,会通过大型语言模型的间接对话与我交流。

当然,也许这样做并不会有什么问题,但这也意味着我们做事方式的彻底改变。

未来的编排者

我毫不怀疑,未来的发展趋势正是如此——然而,要走向那个未来,我们需要在各个领域,而不仅仅是在编码代理中,对我们的工具体系做出相应的调整。

仅仅通过编排更多的循环,显然是不够的。 更优质的变更可视化、更高效的编排方式,或更智能的编排工具,都无法恢复我们的理解。 要么我们需要找到巧妙的方法,让人类重新回到循环中,让循环的变更在长期内变得清晰可见; 要么我们需要找到更好的方式,来构建这些日益复杂的系统。

这也是我思考 Pi 角色转变的起点。Pi 一直保持着谨慎的态度,而我认为这种谨慎是件好事。 我不希望未来每一次交互,都演变成一群不受控制的机器,不断做出我无法追踪的改变。 我也不希望 Pi 在努力争取“自我编写软件”的过程中,沦为一团难以维护的烂摊子; 我也绝不想让 Pi 推广这种类型的工程实践。与此同时,Pi 本身就是一种编排工具,而编排工具正是推动人们开展这些新型实验的核心力量。

针对编码任务的作业队列、编排代理、子代理,以及持久的会话,都将变得愈发重要。 即便是那些心存疑虑、并未盲目拥抱循环的人,也必须开始尝试这些实验。 我们必须这么做,因为我们需要了解如何让这个未来既有限制,又能持续生存下去。

掌控循环

正如这篇帖子所揭示的那样,我对这样的未来感到十分不安。这并非出于恐惧,而是源于我们对这项技术迄今所积累的经验所带来的谨慎态度。

采用“编排式循环”的理念,意味着编排者将决定何时完成工作。 在代理循环中,模型最终会说“完成”,而我会进行复核。 甚至在那之前,我通常也会在途中进行引导。 我积极参与其中,并乐在其中,不断学习。 而在编排者主导的循环中,我甚至不确定自己的角色究竟是什么。 就连“完成”的信号,也失去了原有的意义,仅仅被传递给另一台机器进行判断。 我的角色,也只剩下了一名信使。

如今,我并不太喜欢从那些以这种方式构建的系统中看到的代码,也并不享受与那些由人工智能辅助生成的软件打交道。 循环的力量固然强大,但它却在不断削弱我们的责任感,至少在今天,它更鼓励我们向机器妥协。

然而,我毫不怀疑,尽管我对此心怀抵触,但这个“循环驱动的未来”终究会成为我们的未来。 我已经看到,一些团队正以难以想象的速度构建代码,而代码库也正逐渐演变为晦涩难懂、 令人困惑的有机体,只能由更多机器来诊断。 这些代码库既实用,又混乱不堪。

所以,我想,我正在逐渐接受这样一个事实:问题不在于我们是否会循环——因为显然我们会。 也许问题在于,在一个循环驱动的未来,我们该如何不放弃判断力? 我们该如何保留良好工程实践的规则?我们又该如何确保负责任的人类能够继续进行监督? 我们又该如何重新思考代码架构的设计,以便在这一过程中保持理智?

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论