我是如何用 Agent 编程的
原文:David Crawshaw - 2025.06.08
这是我持续自学如何将编程经验应用到计算机能“对话”的世界中的第二部分。第一部分,《我是如何用 LLM 编程的》,探讨了如何将 LLM 融入我们现有的工具(基本上就是自动补全),以及如何通过精心设计的提示(prompting)替代传统的网页搜索。现在,我想谈谈更具挑战性但也更有回报的实践:使用 Agent(智能代理)进行编程。
定义 Agent
有必要先定义一下在 LLM 语境下“Agent”这个词的含义。在 Agent 真正成为有用的构建工具之前,围绕这个非常通用的词进行的“AI”炒作周期就已经开始了。因此,这个词本身被包裹在不少营销噱头和神秘色彩之中,需要我们拨开迷雾才能发现其价值所在。对于有工程背景的人来说,现在有一个直白的定义:一个 Agent 就是 9 行代码。也就是说,Agent 是一个包含 LLM 调用的循环(for loop)。这个 LLM 可以在没有人类介入的情况下执行命令并查看其输出结果。
就是这样。就像所有简单的事物一样,你的本能反应很可能是:“那又怎样?”它不过是一个循环。但其结果,相比原始大语言模型的能力而言,却带来了惊人的提升。
白板编程
想象你站在白板前,用马克笔写一个 C 语言函数,用于测试 UTF-8 字符串是否有效。(这确实发生在我身上,是标准的面试技巧。那次面试改变了我的职业生涯,关系重大!)你在这个任务上的表现,取决于你作为程序员的经验,以及你掩饰无法使用外部资源的能力。你需要记住 UTF-8 的编码规则。你需要避免将 C 语言的语法与你职业生涯中使用过的其他类 C 编程语言混淆(是“名后型”还是“型后名”?)。在日常工作中,如果犯了错误,你会得到编译器的反馈;你可以查阅 UTF-8 的规范;最重要的是,你可以编写程序并插入一些printf
语句来找出问题所在。
要求一个没有 Agent 的 LLM 写代码,就相当于要求你在白板上写代码。这是一次挖掘半遗忘记忆的练习,在一个极其低效的基质上运行解析器,试图避免凭空臆造出实际上有用的编程接口。LLM 能够凭空生成程序,这本身是一项令人惊叹的技术成就,我很庆幸自己能活着看到它;但同样不足为奇的是,给虚拟白板挂上一块 GPU,并不能完成多少有用的编程工作。
但是,如果我们给 LLM 的不只是一个虚拟白板呢?如果它能调用编译器,查看编译错误,并在我们看到结果之前有机会修复它们呢?如果它能使用grep
和cat
来读取项目中现有的文件呢?如果它能修改多个现有文件(包括单元测试),并反复运行测试呢?Agent 就是基于反馈驱动的 LLM。
Agent 是能获得环境反馈的 LLM
正如人类在充满反馈的环境中茁壮成长一样,当 LLM 被赋予一套程序员非常熟悉的、出奇精简的核心工具集时,它们就能从漂亮的演示转变为有用的程序员:
bash(cmd)
(执行 Bash 命令)patch(hunks)
(应用补丁)todo(tasks)
(管理待办事项)web_nav(url), web_eval(script), web_logs(), web_screenshot(), etc
(网页导航、执行脚本、查看日志、截图等)keyword_search(keywords)
(关键词搜索)codereview()
(代码审查)
Agent 非常擅长使用 Bash 工具(如find
, cat
, grep -R
)来浏览代码库——这和我们这些在 IDE 出现之前就编程的人惯用的方式一样。我们预先指导它向 git 提交更改,它也确实会使用 Bash 工具运行git add
, git commit
等命令。
与无法使用这些工具的 LLM 生成代码相比,其结果大不相同。值得注意的是:
- API 使用大幅改进,因为 Agent 可以搜索文档,并通过
curl
将文档拉入其上下文窗口。 - 编译器反馈减少了语法错误和臆造的接口。
- 在完整的开发环境中,编译器还能改善依赖管理,帮助 LLM 理解项目所使用的特定依赖版本的特性。(虽然这仍然是 LLM 的一个持续弱点,它们可能会使用更新 API 版本的文档,或者做出仅适用于旧版本依赖的假设。我们计划通过 sketch.dev 来解决这个问题。)
- 测试失败有助于发现生成代码中的错误,并能产生积极强化的效果,促使 LLM 为新代码编写测试。
- LLM 可以处理超出上下文窗口容量的大型代码库,因为它们会选择性读取代码库的特定部分。
- Agent 可以亲自尝试最终产品:运行代码,从浏览器截取页面,将其反馈给模型,并根据端到端的渲染效果不断调整 CSS。当情况变得非常糟糕时,读取服务器日志,找到崩溃点,修复它并添加测试。
Agent 的缺点是时间。一个原本只需生成 200 个 token 响应的简单句子请求,现在可能会产生数万个驱动工具的中间 token,包括一些网页搜索和项目测试套件的多次运行。这需要几分钟时间,而我们一天能健康地冲几杯咖啡也是有限的。
目前它可能看起来成本较高(我上一次由 Agent 驱动的重要提交花了我 1.15 美元的 API 额度!),但随着驱动模型的芯片不断改进,成本将迅速消失。GPU 未来的进步空间远大于 CPU(CPU 在物理层面的改进空间要受限得多,编译器驱动的指令级并行(ILP)作用有限)。我们至今仍称 LLM 芯片为“图形”芯片,这表明软件底层的经济机器在完全聚焦于 LLM 的能效比之前,还有大量的重组工作要做。
最终,Agent 花费 CPU 和 GPU 周期来完成中间工作,从而解放人类。任何时候,只要我能将劳动机械化,我就能完成更多工作,因此 Agent 对我来说是一个巨大的进步。由于一天中的时间有限,我最终只能编写我想写程序中极小的一部分。有了 Agent 的支持,我能在愿望清单上更进一步。愿大家都像我过去一年这样幸运。因此,我深信 Agent 值得投入大量工程精力来解决其局限性。
相对容易看到 Agent 产生有用工作的例子。在你的项目中放一个 Agent,拆分一个小任务输入进去,看看它会做什么。让我给你举两个例子。
示例 1:GitHub App 认证
让我通过一个例子说明如何使用 Agent 在项目中完成大量工作。我使用 sketch.dev 为托管服务实现了 GitHub App 认证的第一版。整个过程我只在点击界面发现错误时给了 3-4 次反馈。这真是了不起的成就。傲慢者很容易将粘合知名 API 的工作贬低为“不是真正的”编程,但在我职业生涯的实际经验中,每做一小时真正有趣的编程,我就不得不花 10 小时或更多的时间在 API、库、有问题的编译器、构建系统或晦涩的包管理器上进行枯燥的工作,才能使那一小时变得有用。拥有一个工具,让我只需写几句精心构思的句子,就能完成 30 分钟的“非真正”编程,并让我在它工作时能去打扫孩子的房间,这保持了工作势头。
但现在问题来了。它实现了我想要的 GitHub App 认证流程。它甚至满足了我提出的严格需求:我问它能否避免为每个用户保存令牌,而是使用应用的全局私钥来驱动一切,以简化数据库。它做到了!但在实现过程中,它写了一些非常糟糕的代码。
首先,是它造成的巨大安全漏洞。因为它允许任何授权了该应用的用户操作任何授权给该应用的仓库,即使他们无权访问该仓库。灾难!幸运的是,这个问题如此严重,如果我们在团队中进行测试,当彼此的私有仓库开始出现在对方的仓库列表中时,我们很快就会发现它。(而且问题如此明显,我甚至在那之前就发现了。) 向 sketch.dev 简单解释了一下问题,它修复了它,实现了用户授权检查并做对了。又一个惊人的成就,我写了一个句子,就得到了一个功能正常、经过修改的提交分支。
下一个问题是性能。虽然新代码能工作,但一旦有多个用户,它将慢得无法使用。它通过以下方式生成了用户有权访问的仓库列表:
|
|
这意味着,每次我想向用户显示他们已授权的仓库列表时,我必须为每个允许使用 Sketch 的 GitHub 组织进行一次 API 调用来获取其仓库列表,然后对每一个仓库再进行一次 API 调用来检查此人是否是用户。这意味着 API 调用次数会随着产品用户总数的增长而增长,这根本行不通。
事实证明,问题出在我最初那个天真的要求上:避免存储来自 GitHub 的每个用户令牌。结果发现,GitHub 在 App 认证层面没有任何高效的 API 调用来确定用户能访问什么。唯一有效的方法是询问认证令牌拥有什么权限,并使用该用户的认证令牌。
意识到这点后,我告诉 sketch 回去移除我最初的要求:保存每个用户的认证令牌并用它们处理一切。它很快提出了高效的 API 调用方案。
讲述这个故事所用的字数比我输入到 Sketch 生成所需 GitHub 认证代码的总字数还多,而写作所花的心思也比发现所有问题的代码审查还多。我需要强调这一点,因为很容易抓住这类轶事的某个片段,用这些工具的局限性和错误来宣称它们“无用”或“危险”,而我的经验完全不是这样。我们今天拥有的工具显然还不能取代我作为程序员的角色,但它确实让我在一天内完成了一项传统上可能需要一周才能艰难完成的枯燥任务。而且我还顺便打扫了孩子的房间。
示例 2:围绕 JSON 的 SQL 约定
这里有一个例子,说明我的 Agent 需要经常做某件事,但在找到帮助它的方法之前,它一直很吃力(我相信这抓住了人们初次尝试使用 LLM 时遇到的典型限制)。
我在 Tailscale(从 Brad 和 Maisem 那里)学到了一种使用 SQL 的奇特方式:让每个表都是一个 JSON 对象。具体来说,只有一个“真实”列,其余列都由 JSON 生成。所以典型的表看起来像这样:
|
|
这有很多优点也有很多缺点。它充当了一种简陋的 ORM,因为每个表都有一个“显而易见”的、与每条记录匹配的数据类型。它使得添加模式变得微不足道。你可以选择ADD COLUMN
,但并非必须。SQL 列约束对 JSON 的质量起到了良好的动态检查作用。它大大增加了每行存储的数据量。你必须围绕 JSON 来构造所有的INSERT
和UPDATE
语句。算是半只脚踏入了文档数据库的世界,但我仍然可以写一个老式的JOIN
。等等。
撇开优缺点不谈(这可以成为未来一篇有趣的博客文章),我们的 Agent 经常在这种风格上栽跟头。在创建新表和列时,它有时(但并非总是)会遵循生成列的模式。当我们第一次添加了一个不使用这种全生成列风格的表时,它变得更加困惑,似乎几乎是在不同风格之间随机选择。
结果修复 Agent 的行为出奇地容易。我在 SQL 模式文件的顶部尝试添加了一个三句话的描述。关键似乎是“每个表只有一个具体的 Data JSON 列,所有其他列都由此生成”这句,然后对那些不遵循此模式的表添加注释说明它们是例外情况,之后行为就显著改善了。
这有点反直觉。我对此类指令的生活经验是,工程师们非常不重视它们。可能是“广告盲区”(ad blindness),可能是保持注释更新的挑战,也可能是因为大多数注释不值得太多关注……相反,行业里传递这类知识的常规做法是,让不了解情况的工程师写一个错误方式的 PR,然后收到评论并被要求做更多工作。LLM 似乎让注释承担了更多工作,希望这是好事。
代码的“资产”与“债务”模型
反对将 LLM 作为代码生成工具的一个论点是,生成代码只占代码总成本的很小一部分。该论点认为,处理现有代码的持续工作才是成本的大头。对于某些代码库来说,这显然是正确的。在用户基础不断增长、不断添加新使用方式的成熟产品中,工程师确实将大部分时间花在理清现有代码中那些未被记录、被误解的相互依赖关系上。对于从事此类工作的人来说,一个能对话、能对“用 fortran 实现冒泡排序”给出可用结果的计算机,其地位介于玩具和麻烦之间。有时人们会尝试将其与“资产”和“债务”等金融概念进行比较,但我将跳过这些,因为它们似乎总是不太贴切。
这种对工程的理解(对某些项目是正确的)是否适用于整个工程领域,是值得怀疑的。很少有程序能达到被广泛使用且长期存在的程度。几乎所有东西都用户稀少,或者生命周期短暂,或者两者兼具。我们不要只从那些只维护大型现有产品的工程师的经验来推断整个行业。
幸运的是,我们不需要回答整个编程领域是什么样子才能判断 Agent 是否有价值,因为即使在维护现有产品方面,Agent 也可能有用。Agent 不仅仅是代码生成。它是一个配备了一系列工具的 LLM,能够读取代码,并通过编辑文件来修改代码。
结果是改变。是的,这种改变是“更多的工作”,因为驱动 Agent 的人必须理解所做的更改。是的,Agent 目前可能还没有足够的能力去修改大型产品。但改变正是驱动该工具的工程师的终极目标,而 Agent 正在展现出能够谨慎编辑中等规模项目的能力。这使它们成为整个编程行业中潜在有用的工具。如果 Agent 目前还不够好(这是个很大的“如果”,非常值得测试),它们也正走在正确的轨道上,并且现在已经具备了达到目标的所有基本要素。
一个相关但更棘手的话题是,围绕更难以使用的编程工具(例如,设施简陋、构建系统复杂的 C 语言)有一个比较隐晦的论点:这些工具充当了项目的守门员,阻止了低质量、平庸的开发。如果没人知道如何添加依赖项,项目就不可能有无节制的依赖。如果你相信这样的论点,那么任何使编写代码更容易的东西:类型安全、垃圾回收、包管理以及 LLM 驱动的 Agent,都会让事情变得更糟。如果你的目标是减速和避免改变,那么 Agent 就没有用处。
为什么我们现在才看到 Agent?
与支撑 LLM 的 Transformer 架构这种有些神秘的概念不同,给 LLM 引入机械反馈似乎“显而易见”。这对于我们这些思考开发工具的人来说很清楚:我在这上面已经工作了一年多,但我们在今年一月发布的 sketch.dev 的第一个版本,尽管将 Go 工具链接入了 LLM,按今天的标准几乎算不上是 Agent。(最初版本的 sketch 与当前开源的sketch项目在实用性上的差异令人惊讶。)反馈的效用对于在 ML 领域工作的每个人来说也很清楚,因为强化学习作为该领域的核心原则之一已有 50 年历史。
答案是,使 Agent 有用的关键部分工作在于底层模型的训练过程。2023 年的 LLM 无法驱动 Agent,而 2025 年的 LLM 已为此优化。 模型必须能够稳健地调用所赋予的工具并有效利用它们。我们现在才开始看到擅长此道的前沿模型(frontier models)。虽然我们的目标是最终完全使用开源模型工作,但在我们的工具调用评估中,开源模型目前落后于前沿模型。我们相信六个月后情况会改变,但就目前而言,有效的重复工具调用对于底层模型来说是一项新特性。
下一步是什么
在一个发展迅速的领域思考下一步是充满挑战的,更何况今天大多数工程师甚至还没开始使用这些工具。但我们这些构建这些工具的人需要思考它。
目前 Agent 的使用大多在 IDE 中,或者在开发机上的代码库中。这是入门的简单方式,安装一个 vscode 分支或命令行工具并运行它很容易。但它有两个显著的限制。
第一个主要限制是,Agent 需要内置大量安全防护措施,以避免它们失控。我的一台机器上藏着生产环境的凭据,我可以用它来做部署。作为运行命令的一部分,Agent 会不会抓取这些凭据并运行我的部署脚本去提交它未提交的更改?如果你的 Agent 直接在真实的电脑上运行,要避免这种情况,就需要程序员大量地照看工具调用,并给 Agent 运行命令的权限。即使这样,危险依然存在。我可以对运行curl
说“全部同意”,却忘记了我正在本地主机(localhost)上开发的网络服务器还没有任何身份验证,并且可以读取磁盘上的任意文件。糟糕,我的生产凭据又暴露了。
第二个主要限制是,要求开发者使用他们自己定制配置的手动开发环境来运行 Agent,实际上意味着我们是在串行执行Agent 的运行。如前所述,Agent 的一个主要弱点是,每轮(turn)生成良好结果需要几分钟时间(并且在可预见的未来很可能仍会如此)。更好地利用我们时间的一种方式是让一个工程师同时驱动多个 Agent,但这种 Agent 部署方式使得这变得不切实际。
我们正在sketch.dev探索使用**容器(containers)**来解决这两个问题。默认情况下,sketch 会在容器中创建一个小的开发环境,里面包含一份源代码副本,运行器(runner)有能力从容器中提取 git 提交。这让你可以同时运行多个 Agent。(其他 Agent 也在探索这个领域,总体来说 Agent 是一个非常活跃的领域!)
给你举个这种并行性在实践中如何运作的例子:当我在处理上面提到的 GitHub 认证时,我正准备在一个群聊里抱怨我做的一个表单有多丑。相反,我打开了第二个 Sketch 窗口,粘贴了表单的截图并写下“这太丑了,请让它好看点。”然后我回去思考认证问题。半小时后想起来时,我查看了结果,认为确实有改进。于是我叫它做 rebase 代码,它解决了合并冲突(这是我最不喜欢的编程任务之一!),然后我推送了更新。它的质量远不及真正的设计师,但肯定比我测试时创建的丑陋无样式表单好多了。在过去,我会在问题跟踪器上创建一个 issue——也就是说,我会在我的个人待办事项.txt 文件里记一笔要创建 issue,因为 issue 对其他程序员可见,需要我打出比粘贴截图并说“这太丑了,请修复”更连贯、更有建设性的内容。与 Agent 对话的一个巨大好处是,即使你只剩一点点脑力,也有很大机会从 30 秒的工作中获得一些有价值的东西。老实说,我过去把待办事项.txt 条目变成真实 issue 的可能性相当低。
因此,我们过去六个月探索 Agent 用户体验(UX)的收获是,我们可能终于为“开发”容器找到了一个好用途。
IDE 会变成什么?
一个我们投入大量时间探索的开放问题是:在这种环境下,IDE 会变成什么?假设我们通过与 Agent 对话开始工作。执行容器可以完全从 GitHub 派生出来,更改显示为差异(diff)并以分支(或 PR)形式推送。这就是实际的工作流程吗?
在实践中,由 sketch 或我们尝试过的任何其他 Agent 生成的提交,许多都需要之后进行一些人工清理。(我们的经验是,程序员初次使用 Agent 时,大多数提交都需要手动干预,但练习写提示(prompt)可以减少必要的干预次数。)可能简单到编辑一个注释或改变一个变量名,也可能更复杂。我们如何让这在一个容器化的世界里运作?
到目前为止,我们有几个非常喜欢的工作流,其他 Agent 尚未探索。一个是让差异视图可编辑。你可以在 Sketch 的差异视图右侧直接输入,修改会进入提交并为你推送。这对单行编辑非常棒。
对于那些你想运行sed
、grep
更改内容,或者以有趣方式运行测试的修复(fixup)工作,我们通过给用户 SSH 访问容器的权限取得了巨大成功。你不仅能 shell 进去(我们的 UI 里也有一个小型 Web 终端),还能轻松将其变成一个vscode://
URL,直接在传统 IDE 中打开,这有时正是我们想要的。
最后,我们让你在 sketch.dev 的差异视图上写“代码审查”风格的评论,并将其作为反馈发回给 Agent。直接在差异的某一行上评论可以大大减少我们需要输入的内容(并且这在我们长期的代码审查实践中非常熟悉)。
总的来说,我们相信容器对编程是有用且必要的。这个想法由来已久,但我个人以前从未想过要在容器里开始编程。但在容器中清理一个 Agent 为我写好的差异,这要有趣得多。
最后的思考
学习和试验 LLM 衍生技术的过程是一次谦逊的练习。总的来说,当编程的艺术发生变化时,我喜欢学习新事物:应对向多核编程的转变;当 SSD 取代 HDD 并消除了寻道延迟时重新思考软件设计;当一切突然可以通过同一个互连网络访问时——这些行业转变是令人愉快的挑战。(不要与无用的表面功夫混淆,比如最新的 JavaScript 框架、最新的云服务商服务或最新的集群编排软件。)这类挑战影响了我程序的工作方式:算法、语言、库等的选择。但 LLM,更具体地说是 Agent,以一种新的、令人困惑的方式影响了编写程序的过程。关于我如何工作的每一个基本假设都必须受到质疑,这波及了我积累的所有经验。有些时候,感觉如果我完全不懂编程,从零开始反而会更好。而且它仍在变化。
今天这一切的运作方式与六个月前大不相同,我不相信我们已经达到了一个稳定的状态。我相信围绕团队互动的许多规范也将发生变化。例如,那个在整个行业被采用、勉强解决问题但基本已失效的、半心半意的代码审查流程,如今连它过去勉强解决的问题也解决不了了。它需要被重新发明。“IDE”从未如其声称的那样“集成”,它需要被拆解并重新定位。行业现在似乎意识到了这一点,但尚未采取“Agent 优先”(agent-first)的方法。还有很多事情要做,我怀疑六个月后情况又会大不相同。好奇心和谦逊将带领我们度过难关,但比以往任何时候都更要建议远离那些人们围绕这项技术无休止空谈的网络论坛。那是 Agent 该干的活。
- 原文链接:https://www.gocode.top/post/2025/06/13/programming-with-agents/
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,转载请注明出处(作者「阿然」,原文链接)。