用 Claude 构建个人软件
原文由 Nelson Elhage 于 发布,订阅该博客
本月早些时候,我用 Claude 把一个 Emacs 插件的(部分)逻辑移植到了 Rust,执行时间缩短了 1000 倍以上(举一个具体的例子:从 90 秒缩短到了约 15毫秒)。
这类为了完成一件事而不得不先去折腾另一件事的连环折腾,无论是在工作中还是为了打理自己的个人计算环境,我平时都经常会做。不过这一次,在我的监督下,Claude 几乎独立完成了整个项目,我几乎一行代码都没写,与亲手去做相比,项目进度大大加快。
着手这个项目时,我原本只指望 Claude 能提供一点小帮助——无非是“更好用的文档搜索引擎,再加上更好用的 Stack Overflow”。尽管我在 Anthropic 工作,也时不时会读读 Simon Willison 的文章,但我的预期显然已经严重过时了。
这次经历改变了我对大语言模型在软件工程以及我个人工作中所扮演角色的许多想法。这些想法仍在逐步形成中,本文算是对这次经历的记录,也是我在重新思考该如何更新自己的行为、认知与预期时的自言自语。
问题
在过去一年左右的时间里,我成了 Obsidian.md 的重度用户,用它取代了之前 Emacs org-mode 文件与 Workflowy 的组合。关于 Obsidian,我喜欢的地方有很多,但对本文而言关键的一点是:(a)它直接把数据以原生 Markdown 文件的形式保存在本地磁盘上,(b)有一个还不错的 Emacs 模式可以与这些文件交互。即使在笔记本电脑上,我主要还是使用 Obsidian 应用,但能够随时切到 Emacs 里做复杂的文本处理,或者在写代码时直接在编辑器里管理待办清单,对我来说非常有价值。
然而,随着知识库越来越大,我遇到了一个问题:obsidian.el 变得慢到无法使用。哪怕只是打开一篇笔记,Emacs 都会卡住,起初是几秒钟,后来甚至长达一分多钟。问题出在 obsidian-update 函数上:obsidian.el 会定期重新遍历整个知识库,扫描每一篇笔记的标签和元数据,以更新其内部索引,而这个完全用 elisp 编写的扫描过程最终变得极其缓慢。
我的计划
我的 elisp 水平还算过得去,但我完全没有做过性能分析或优化,而且 GitHub issue 里已经有几个人尝试过了。我想换个思路。
我打算用 Rust 写一个小程序来扫描并盘点整个知识库,并以 JSON 的形式输出最精简的元数据,然后修改 obsidian.el 来消费这些输出。我对 Rust 更熟悉,也知道它通常性能很好,而且有非常出色的性能分析工具。
这样的方案可能不太容易被上游接受,但我相当确信,这对我个人而言是个相对小巧的项目,而且肯定能解决我自己的问题。
使用 Claude
一时兴起,我决定看看 Claude 能帮我解决多少问题。我本来没抱太大期望,但还是想试试——也许 Claude 会给我惊喜。我决定从一个目标相对明确、定义相对清晰的请求开始,但要求它一口气完成大部分工作:
[已上传文件:
obsidian.el]
obsidian-update函数慢得令人痛苦。我想通过把相关逻辑移植到一个 Rust 程序来加速它,该程序会以 JSON 形式输出相关信息。你能为这样一个 Rust 程序写出第一个版本吗?它应该接受 vault 目录作为命令行参数,其它所有值都可以假定为默认值。
简而言之:这招奏效了。这是初版。
仅用一个提示词,且没有任何思维链或其他显式推理,Claude 就读完了约 1000 行 Emacs Lisp,找出了其中与 obsidian-update 相关的约 200 行,识别出这段代码与文件其余部分之间的数据边界,设计了一种用于这些数据的 JSON 格式,然后把相关逻辑移植成了约 150 行 Rust 代码。
生成的代码一次编译通过,第一次运行就正常工作,Claude 甚至还输出了一份可用的 Cargo.toml,里面包含了所需的依赖。
我又进一步尝试——它能不能也把 Emacs 这边的工作做了?我决定大胆一点,让它使用 Emacs advice 来为 obsidian.el 打补丁,目标是做出一个无需 fork 或修改上游项目、只需放进我自己配置里就能用的东西。提示词如下:
[已上传文件:
obsidian.el]
[已上传文件:obsidian-scan/src/main.rs]现在,请写一段 elisp 代码来为
obsidian.el打补丁,使其使用obsidian-scan。请不要直接编辑原文件,而是使用 elisp 的“advice”功能,这样我就可以在加载 obsidian.el 之后,把你的代码片段放进我自己的配置里。
这一段也基本一次性就成功了。就我所知,只有一个 bug:Emacs 端会在标签前加一个 #,而 Rust 端已经带上了 #,导致标签变成了 ##writing 这样的形式。我让 Claude 修复它:
obsidian--tags-list里出现了带双##的条目,比如##writing。你觉得我们应该修 Rust 代码还是 elisp?选一个并更新它。
Claude 选择修改 elisp,重写了文件并完成了一个简单的 bug 修复。这也成了整个项目中其余所有交互的典型模式——我提出一个改进或修复需求,Claude 就能执行,最多只有一些小磕绊。
我又用这种方式来回了几次。值得一提的是,我让 Claude 输出(文件 -> 标签)的映射,并让 obsidian-tag-find 在 Emacs 中使用它。我现在已经把 Rust 程序和 elisp 补丁都加入了我的个人系统配置,目前用得非常满意!
整个项目大概只花了一个下午。
反思
Claude 写代码的能力远比我想象的要强
我最初向 Claude 发出那个提示时,基本没抱什么期望。但我听说 Claude 3.5 Sonnet 在写代码方面有了很大提升,而且我一般会定期试用一下大语言模型,即使预期会失败——因为我发现只有这样才能真正校准我对它们能力的判断。
首先,初次重写就让我印象深刻:我原以为 Claude 在给定清晰规格的情况下写个约 100 行代码还算在行,但要从 1000 行 elisp 中提炼出隐含的规格并据此实现,还是出乎我的意料。更让我印象深刻的是后续迭代整个系统的过程;Claude 持续展现出从上下文窗口中读取适量代码,并以相当老练的方式进行更新或回应的能力。基本上我尝试的每一件事都“直接奏效”了。
理智上,我完全明白这些模型正在以指数级的速度进步,6 个月或 12 个月前形成的印象几乎肯定已经严重过时。但这种抽象的认知,并不足以让人对新的现状形成准确的判断!据我所知,唯一的办法就是不断使用这些模型、与它们互动(并阅读其他人的使用报告)。正因如此,尽管预期会失败,我还是尝试了这个提示,也正因如此,我才写下这篇反思。
这真的值得惊叹吗?
在写这篇笔记的过程中,我对自己面对 Claude 时的反应和想法感到着迷。
如上所述,我最初的反应是“惊讶且印象深刻”,Claude 超出了我的预期。原始的 obsidian.el 大约有 10000 个 token;就在 2023 年,Anthropic 才首次发布了能够完全处理如此长文本的公开模型。而在我的任务中,Claude 不仅要从输入中提取单一事实或大致梗概,而是要准确理解和解析分散在文件各处的大量细节。直到最近,模型还做不到这一点!而且它是在单个提示词内、没有任何思维链或显式推理的情况下完成的!所以,是的,我当时很惊艳。
与此同时,就在我起草这篇文章的过程中,我就注意到自己脑中一种熟悉的目标位移!我发现自己正从惊讶与赞叹,滑向一种近乎见怪不怪的轻描淡写,至少有两种角度:
- 好吧,但我难道不该预料到这个吗?我已经见过许多工程师惊叹于 Claude 3.5 Sonnet 的编码能力。我自己也见过它解决 Anthropic 的面试题;这次的练习涉及的代码量虽然多一点,但我也见过它阅读和回答关于大得多的代码库的问题。我的惊讶,更多是我的预期出了问题,而不是模型有多么了不起!
- 这个任务真的那么了不起吗?这个项目“只”涉及约 1000 行代码;我平时处理的许多项目都涉及数十万甚至数百万行代码!把 elisp 重写成 Rust 听起来可能很厉害,但它“本质上”也只是个翻译问题:根子上规格相当明确,而语言模型在那些“看起来像翻译”的任务上通常都很强。
我认为这两种视角——敬畏,以及随之而来的淡化——在不同意义上都是成立的!以下这些说法都是事实:
- 大语言模型在编码方面的能力比一年前强得多
- 更不用说,它们现在的能力哪怕放在 2 到 3 年前,也绝对像是天方夜谭
- 但同时,当前的模型或多或少也“只是同一趋势的延续”;它们令人印象深刻,但与一年前机器学习圈内人士和领袖们预测的水平相比,似乎也没那么出人意料
- 而且,尽管令人印象深刻,当前的模型仍然远未达到人类专家的水平。在合适的场景下它们很有用,但(目前!)还无法“替代我的工作”
我想,这基本上就是当下机器学习和大语言模型的常态。指数级的性能提升意味着我们永远处在敬畏、惊叹、科幻感,与“哦,这不过是老样子,有什么大不了的?”之间的中间地带。
我怀着审慎的兴奋
坦白说,这个项目让我第一次真切地感受到了一种用 Claude / 其他大语言模型辅助来构建软件的个人兴奋与热情,这是我之前从未有过的。
具体来说,我感受到了那种为“挠自己的痒处”而写软件的快乐与兴奋——为了解决自己遇到的问题、打造自己想要的工具——这种感觉我已经多年未曾体会。年轻时,尤其是在本科阶段,我身边充斥着我自己、我的朋友或像我们这样的人为了满足自身需求、赋予自己新能力而写的各种软件片段。我们曾感受到巨大的力量感、乐观与可能性,真切地相信自己可以塑造自己的计算环境,打造自己的工具,创造自己想要的体验。这种精神体现在各种尺度上,既包括只有几个朋友在使用的小脚本,也包括当年许多规模最大、最成功的开源项目。
这些年来,世界变了,我也变了。我们的数据越来越被隔离在各个由或冷漠或恶意的垄断企业拥有和运营的云服务孤岛中。一切都变得更加复杂,层层叠叠地隐藏在高耸的抽象与复杂性背后,还有令人费解的 OAuth。我也把职业生涯花在了那些大型机构里,学会了本能地把所有软件都当作要大规模部署、长期运行来编写。我甚至喜欢这种工作方式,喜欢稳健构建、寻找坚实基础的挑战,但每当我不得不去搞清楚如何向某个 Google API 做认证,或迁移到某个 JavaScript 框架的新版本时,那种彻底的绝望感,让我基本上不再为自己和朋友构建那些小巧的软件了。
而现在,久违地,我第一次又感受到了一丝当年的兴奋!Claude 在处理繁琐的认证系统、了解最新版 React 或其他类似事情上,远比我强得多。感觉我们正生活在一个——现在,或至少很快——只需准确描述出我希望存在的工具或脚本,Claude 就能完成那些苦活的世界。如果时过境迁,API 被弃用,需要迁移到某个框架的第 27 版,Claude 很可能也能胜任这些工作。甚至,我们或许正生活在一个每隔几年就可以把所有代码直接扔掉,在新的一轮对话中用 Claude 重新开始,用同样的问题描述加上最新的文档,从零开始的世界里。我感到一丝希望与兴奋:我能否重新学会为自己、为我的社群构建小巧的软件,让 Claude 来处理过去二十年里软件工程中滋生的无数繁琐杂事,也让我得以停留在高层的设计与构思层面,避免因过度追求完美而陷在细节里?
我是在与工具较劲,而非与之协作
Claude.app / claude.ai 在这个问题上感觉设计得并不好用。我花了大量时间在界面与本地磁盘文件之间来回复制粘贴,又把错误或其他输出再粘贴回给 Claude。
我发现很难琢磨该如何为对话维持合适的上下文。最后我手头有了三个文件——原始的 obsidian.el、Rust 移植版 main.rs 以及 elisp 适配代码 obsidian-scan.el。我不清楚究竟该一直用一个长对话,还是该把它们都放进一个项目(Project)里并定期创建新对话(又该何时创建呢?)。在单个长对话中工作很直接,但随着上下文窗口变长,速度会变慢,而且我也发现 Claude 最终会被历史记录中出现的各种版本的文件搞糊涂。我尝试过用“添加到项目”按钮把 Claude 创建的 artifacts 复制回项目,但遇到了两个主要问题:
- 由于我与 Claude 一起快速迭代生成的 artifacts,项目里的版本很快就过时了
- Claude 不让我重命名/修改 artifacts 的标题,所以我最终得到的是一个名为“Obsidian.el Performance Enhancement with Rust Scanner”的文件,这让我在提问或要求修改某个具体文件时很难指代。
这个领域目前有大量的活跃开发;也许使用合适的 MCP 服务器或众多新兴的 AI 原生 IDE 之一会让事情更简单。
不过总的来说,这次经历强化了我的一个看法:围绕大语言模型的工具和界面设计,远远落后于模型本身的能力,除了未来的模型进步之外,这也是一个正在积极探索和发展的领域。
在清晰定义的接口之间工作
在与 Claude 协作时,我发现自己本能地会把问题拆解成具有相对明确且可测试接口的子问题。例如,我不会在一个请求中同时要求它修改 Rust 和 elisp 代码,而是会先让它给 Rust 端添加一个功能,然后通过检查输出的 JSON 来快速验证,再让它做对应的 elisp 修改。
我不确定这在多大程度上是必需的,但我认为这样做很大程度上是为了我自己,而不仅仅是为了 Claude;基于接口边界和可测试性来做拆分,有助于我感觉自己理解并能验证正在发生的事情,也让我知道如果出现混乱该如何介入或调试。这在某种程度上就像与更初级的工程师合作——我自己来做更多的系统设计或任务分解,然后把定义明确的组件委派出去。
这无疑也有助于正确性和测试。我并没有费心去验证 Claude 的 Rust 代码与原始 elisp 的匹配程度有多高,因为在我注意到 bug 之前,我其实并不在乎;但“给定这个文件系统树,你应该找到这些笔记,其中包含这些标签和其他元数据”是一个非常容易测试的问题,如果我更在意,或者发现了 bug,我可以通过把它们编码成测试用例并让 Claude 去完善代码来解决,而不必亲自去深挖内部实现。
我预计——至少在可预见的未来——这仍将是一个强大的模式:由人类定义接口边界或系统分解,让 Claude 在“划定的范围之内”工作。可以想见,随着时间的推移,Claude 将能够越来越自主地处理更大、更复杂的子系统,而我们也将逐渐建立起信心,并形成处理那些规格更模糊的子系统的方法。
代码比以往任何时候都更廉价
有一段时间以来,我越来越持有这样一种观点:代码行数本身是相对廉价的;软件开发中真正有价值的资源,更像是对于代码的理解——它在做什么、为什么这么做——以及知道该写什么样的代码1。
大语言模型似乎很可能极大地加剧这一趋势,至少在短期内是如此。现在你只需花费几分钱就能生成数千行代码;但没有人能真正理解这些代码,而且至少就目前而言,大语言模型在调试、重构、设计和维护这些代码方面的能力,还远不如它们生成代码的能力。因此,代码比以往任何时候都更廉价,但我猜想,洞见和良好的架构设计以及理解,至少在目前,会变得比以往任何时候都更有价值。
我猜想这种变化也会影响哪些架构模式会变得合理。也许编写易于删除而非易于扩展的代码会变得比以往任何时候都更加重要,这样你就可以直接丢弃整个模块,让下一代 Claude 或 GPT 从头开始。也许构建“多语言”系统的门槛会大大降低,如果你能够收获不同语言生态或性能特性的好处,而不必太担心招聘那些精通四种冷门语言的工程师的话。这将会很有趣。
我一直想写点关于 Naur 的 Programming as Theory-Building 的东西,这是我最喜欢的论文之一,也是我所知道的最早捕捉到这一洞见的论文之一。↩︎
随机一篇博客
评论
登录后参与讨论