我用 Claude Code 构建了一门编程语言
一位前端工程师使用 AI 代理在四周内完成一门编程语言开发的实验
在一月至二月的四周时间里,我用 Claude Code 构建了一门新的编程语言。我给它起名叫 Cutlet,以我的猫命名。完全合法这么做。你可以在 GitHub 上找到源代码,以及构建说明和示例程序。
从 2021 年 GitHub Copilot 最初发布以来,我一直在使用大语言模型辅助编程,但到目前为止,我限制自己只使用大语言模型来生成样板代码和对项目进行特定的定向修改。然而,在开发 Cutlet 时,我允许 Claude 生成每一行代码。我甚至没有阅读任何代码。相反,我设置了护栏来确保它能正常工作。
我对这个实验的结果感到惊讶。Cutlet 如今确实存在。它可以在 macOS 和 Linux 上构建和运行。它能够执行真正的程序。它的内部可能隐藏着一些 bug,但这些 bug 可能不会比世界上任何其他只有四周历史的编程语言更糟。
我对此以及它对我的职业意味着什么有很多感受,但在登上我的 Soapbox(译者注:此处指发表观点的高地)之前,我想先带你们看看这门语言。
Cutlet 概览
如果你想跟着做,可以从源码构建 Cutlet 解释器并进入 REPL。完整文档请参阅 Git 仓库中的 TUTORIAL.md。
数组和字符串的工作方式与任何动态语言一样。变量使用 my 关键字声明。
变量名可以包含连字符。语法规则与 Raku 相同。目前唯一的数据类型是双精度浮点数(double)。
这里有个很酷的功能:@ 元操作符可以将任何普通的二元操作符转换为数组上的向量化操作。在下一行代码中,我们把 temps-c 中的每个元素乘以 1.8,然后把 32 加到结果数组的每个元素上。
@: 操作符是一个 zip 操作。它将两个数组合并成一个映射(map)。
使用内置的 say 函数输出文本。这个函数返回 nothing,这是 Cutlet 版本的 null。
@ 元操作符也可以用于比较运算。
还有另一个很酷的功能:你可以使用布尔数组来索引数组。这是一个过滤操作。它选取对应于 true 的元素索引,丢弃对应于 false 的索引。
这是一种更简洁的写法。
让我们用一条友好的消息把它打印出来。++ 操作符用于连接字符串和数组。str 内置函数将数据转换为字符串。
@ 元操作符作为前缀使用时,可以执行归约操作。
让我们计算平均温度。@+ 将所有温度相加,len() 内置函数求数组长度。
也让我们把它很好地打印出来。
函数使用 fn 声明。Cutlet 中的一切都是表达式,包括函数和条件表达式。函数中表达式产生的最后一个值成为它的返回值。
你自己的函数也可以使用 @。让我们用我们的 max 函数对温度进行归约,找出最高温度。
Cutlet 还能做更多。它拥有你期望动态语言具备的所有常规功能:循环、对象、原型继承、混入、标记清除垃圾回收器,以及一个友好的交互式解释器(REPL)。我们还没有文件 I/O,一些基本构造(如错误处理)仍然缺失,但我们正在努力!
为什么要构建这门语言?
我是一名前端工程师和(偶尔的)设计师。我尝试过使用大语言模型构建 Web 应用程序,但总是遇到限制。
以我的经验,Claude 和它的朋友们在编写复杂业务逻辑方面非常出色,但在任何需要视觉设计技能的任务上表现都很差。
事实证明,用英语描述响应式布局和动画并不容易。再多的截图和线框图也无法向大语言模型传达流畅的布局和动画。我浪费了数小时与 Claude 争论它声称已经修复的布局问题,但我用我那不争气的人类眼睛仍然可以清楚地看到这些问题。
我还发现,这些工具在生成它们以前在公开仓库中见过的千篇一律的界面方面表现出色,但当我想要做一些新颖的东西时就不行了。我经常为客户构建小众领域的复杂数据可视化,在这些项目上,大语言模型完全无法产生有用的输出。
另一方面,在过去几个月里,我看到人们使用大语言模型取得了令人难以置信的成就,我想自己复制这些实验。但我以前使用大语言模型的经验表明,我必须谨慎选择项目。
我不想解决特别新颖的问题,但我希望能够不时地将大语言模型引向有趣的方向。
我不想手动验证大语言模型生成的代码。我希望给大语言模型提供规范、测试用例、文档和示例输出,让它完成所有困难的工作,来判断自己是否在做正确的事情。
我希望给代理一个强大的反馈循环,让它能够自主运行。
我不喜欢 MCP。我不想处理它们。因此,任何需要连接浏览器、截图或通过网络与 API 交互的东西都自动被淘汰。
我想使用一种尽可能少依赖外部依赖的无聊语言。
一门小型动态编程语言满足所有这些要求。
大语言模型知道如何构建语言实现,因为它们的训练数据包含数千个现有的实现、论文和计算机科学书籍。我对创建一个"混搭"语言的想法很感兴趣——从我喜欢的各种现有语言中挑选和选择功能。
我可以编写一堆小型确定性程序及其预期输出来测试实现。我甚至可以让 Claude 为我编写它们,给我无限多的测试用例来验证语言是否正常工作。
语言实现可以通过命令行进行测试,输入输出都是纯文本。不需要截图、视频或设置脆弱的 MCP。对于代理来说,没有比"运行 make test 和 make check 直到没有更多错误"更好的反馈循环了。
C 语言无聊透顶,但用 C 构建的语言实现有很多。
最后,这也是一个实验,看看我能在多大程度上推动代理工程。我能把六个月的工作压缩到几周吗?我能构建超出我自己能力范围的东西吗?如果我全身心投入大语言模型驱动的编程,我的日常工作生活会是什么样子?我想回答所有这些问题。
我带着一些怀疑开始这个实验。我以前完全使用 Claude Code 构建东西的尝试并不顺利。但这次尝试不仅成功了,而且产生了超出我想象的结果。我不认为未来所有软件都将由大语言模型编写。但我确实相信有很大一部分软件可以部分或大部分外包给这些新工具。
构建 Cutlet 教会了我一些重要的事情:使用大语言模型生成代码并不意味着忘记你所学过的关于构建软件的一切。代理工程就像生成式 AI 之前任何值得构建的软件一样,需要仔细的规划、技能、工艺和纪律。与编码代理合作的技能可能看起来与逐行将代码输入编辑器不同,但它们仍然是我们整个职业生涯中一直在磨练的相同工程技能。
代理工程的四项技能
从大语言模型获得良好输出需要大量工作。代理工程并不意味着向聊天框中倾倒模糊的指令,然后收获输出的代码。
我相信有四项主要技能是今天有效与编码代理合作必须学习的:
了解哪些问题可以有效地使用大语言模型解决,哪些需要人在循环中,哪些应该完全由人处理。
清晰地传达意图并定义成功标准。
创造一个能让大语言模型发挥最佳工作的环境。
监控和优化代理循环,使代理能够高效工作。
了解哪些问题可以有效地使用大语言模型解决
模型和框架变化很快,所以弄清楚哪些问题大语言模型擅长解决需要培养你的直觉,与同行交流,并保持关注。
然而,如果你不想跟上快速变化的领域——我不会评判你,那里很疯狂——这里有两个问题可以问自己来判断你的问题是否适合大语言模型:
对于你想解决的问题,是否可能以自动化方式定义和验证成功标准?
是否有其他人以前解决过这个问题或类似的问题?换句话说,你的问题是否很可能在大语言模型的训练数据中?
如果这两个问题中任何一个的答案是"否",那么用人工智能解决这个问题不太可能产生好的结果。如果两个问题的答案都是"是",那么你可能会在代理工程中获得成功。
好消息是,找出这些的成本只是一个 Claude Code 订阅费加上团队中愿意花一个月时间在你的代码库上尝试的"祭品"。
传达意图
大语言模型使用自然语言工作,所以学会用文字表达你的想法变得至关重要。如果你不能向同事书面解释你的想法,你就不能有效地与编码代理合作。
你可以用简单、模糊、过于笼统的提示从 Claude Code 中获得很多。但这样做时,你把大量的思考和决策外包给了机器人。这对于一次性项目来说没问题,但当你构建要投入生产并维护多年的东西时,你可能需要更小心。
你想给编码代理精确编写的规范,尽可能多地捕获你的问题空间。在开发 Cutlet 时,我大部分时间都在编写、生成、阅读和修正规范文档。
这对我来说是新体验。我主要与早期初创公司合作,所以在我职业生涯的大部分时间里,我把代码当作规范。编写正式规范是一种陌生的体验。
谢天谢地,我可以依靠 Claude 帮助我编写大部分规范。我能够这样做是因为 Cutlet 是一个实验。在我想要赌上声誉的项目上,我可能会完全排除代理,自己编写规范。
这是我对 Cutlet 做任何修改时的通用工作流程:
首先,我会向大语言模型展示一个新功能(例如循环)或重构(例如从树遍历解释器迁移到字节码虚拟机)。然后我会与它讨论这个变化在 Cutlet 背景下如何工作,其他语言如何实现它,设计考虑,我们可以从有趣/小众语言中借鉴什么想法等等。只是一来一回的随意聊天,就像你和同事交谈一样。
当我很好地掌握了功能或更改的样子后,我会要求大语言模型给我一个分解为小步骤的实施计划。
我会审查计划,与大语言模型来回讨论以完善它。我们会探索各种角落情况、陷阱、注意事项、缺失的部分和改进。
当我对计划满意时,我会要求大语言模型将其写入一个放在 plans/doing/ 目录的文件中。有时一个功能会有 3-4 个计划文件。这是故意的。我需要计划是人类可读的,我需要每个计划是一个原子单元,如果事情不顺利可以回滚。它们还作为项目演进的历史。
我会阅读和审查生成的计划文件,再次与大语言模型来回讨论以对其进行修改,当一切看起来不错时提交它。
最后,我会启动一个 Docker 容器,以所有权限(包括 sudo 访问)运行 Claude,并要求它实施我的计划。
这种工作流程将进行任何语言更改的认知工作前置。所有思考都在写任何代码之前发生,这是我几乎从不做的事情。对我来说,编程涉及在工作中自然地发现问题的形状。然而,我发现与大语言模型那样工作很困难。它们擅长对你的代码库进行大规模更改,但在快速、迭代、有机开发工作流程方面表现很差。
也许随着推理速度变快和模型变得更好,我的工作流程会演变,但在那之前,这种瀑布式模型效果最好。
为代理创造最佳工作环境
我发现这是与编码代理一起工作最有趣和最令人兴奋的部分。这是一个全新的问题类别!
核心原则是:编码代理是计算机程序,因此它们对所处世界的看法有限。它们进入你试图解决问题的唯一窗口是它们可以访问的代码目录。这不足以让它们有足够的自主权或信息来做好工作。所以,为了帮助它们茁壮成长,你必须以它们可以用来接触更广阔世界的工具形式赋予它们那种自主权和信息。
在实践中这意味着什么?不同的项目看起来不同,但这是我为 Cutlet 做的:
全面的测试套件。我的项目指示告诉 Claude 编写测试,并在编写任何新代码之前确保它们失败。除此之外,我还要求它在进行重大代码更改或合并任何分支后运行测试。凭借不断增长的测试套件,Claude 能够快速识别并修复它引入代码库的任何回归。测试还作为文档和规范。
示例输入和输出。这些是我的集成测试。我在 Cutlet 仓库中添加了许多示例程序——其中大部分是 Claude 自己写的——它们不仅为人类提供文档,还是端到端测试套件。项目指示告诉 Claude 在每次代码更改后运行它们并验证它们的输出。
Linter、格式化工具和静态分析工具。Cutlet 使用 clang-tidy 和 clang-format 来确保代码质量的基础水平。就像测试一样,项目指示要求大语言模型在每次重大代码更改后运行这些工具。我注意到 clang-tidy 经常产生诊断结果,迫使 Claude 重写部分代码。如果我能访问一些更昂贵的静态分析工具(如 Coverity),我也会将它们添加到我的开发过程中。
内存安全工具。我要求 Claude 创建一个 make test-sanitize 目标,用 ASan 和 UBSan 启用(LSan 通过 ASan 一起运行)重新构建整个项目和测试套件,然后在检测构建下运行每个测试。项目指示包括在实施计划结束时运行此检查。这捕获了测试和 linter 都找不到的内存错误——使用后释放、缓冲区溢出、未定义行为。运行这些测试很耗时,并大大减慢了代理的速度,但它们比 clang-tidy 捕获了更多问题。
符号索引。代理可以使用 ctags 和 cscope 来导航源代码。我不知道这有多大用,因为我很少看到它使用它们。大多数时候它只是 grep 代码中的符号。我以后可能会删除这个。
运行时自省工具。在项目早期,我要求 Claude 赋予 Cutlet 在执行任何代码之前将令牌流、AST 和字节码转储到标准输出的能力。这允许代理快速确定它是否在执行管道的任何部分引入了错误,而无需导航源代码或进入调试器。
管道追踪。我要求 Claude 编写一个 Python 脚本,通过带有调试标志的解释器运行 Cutlet 程序,以捕获完整的编译管道:令牌流、AST 和字节码反汇编。然后它将每个令牌类型、AST 节点和操作码映射回解析器、编译器和虚拟机中处理它们的精确源代码位置。当代理需要添加新的语言功能时,它可以在类似现有功能的示例上运行追踪器,以精确查看需要修改哪些文件和函数。我为这个机制感到非常自豪,但我也从未看到 Claude 充分利用它。
以尽可能多的权限运行。我希望代理自主工作,并可以使用任何可能的调试工具。为此,我在 Docker 容器中运行它,启用 --dangerously-skip-permissions 并拥有完全 sudo 访问。我相信这是在大型项目上使用编码代理的唯一实际方式。当五个代理并行工作时,回答权限提示在认知上很累,限制它们做任何想做的事情的能力会使它们的工作效率降低。当我们给大语言模型完全控制系统能力时,我们需要弄清楚出现的各种安全问题,但在这个项目中,我愿意接受 YOLO 模式带来的风险。
所有这些工具和能力确保了对代码的任何更新至少产生一个可以编译和执行的项目。但更重要的是,它们增加了 Claude 可以访问的信息和自主权,使它能够更有效地发现和调试问题,而无需我的干预。如果我继续在这个项目上工作,我的重点将是赋予我的代理更多对它们正在构建的工件的洞察力、更多调试工具、更多自由和更多有用信息。
你可能想要想出适合你特定项目的自己的工具。如果你正在构建 Django 应用程序,你可能希望代理可以访问一个暂存数据库。如果你正在构建 React 应用程序,你可能希望它可以访问无头浏览器。没有一种答案适用于每个项目,我打赌人们会想出一些非常有趣的工具,让大语言模型能够观察它们在现实世界中工作的结果。
优化代理循环
编码代理有时在使用你给它们的工具方面效率不高。
例如,在这个项目上,有时 Claude 会运行一个命令,判断其输出太长无法放入上下文窗口,然后用管道将输出传送到 head -n 10 再次运行。有时它会运行 make check,忘记 grep 输出中的错误,然后第二次运行以捕获输出。这导致在单个编辑过程中多次运行相同的昂贵检查。这些错误显著减慢了代理循环。
我可以通过编辑 CLAUDE.md 或更改自定义脚本的输出来修复一些性能瓶颈。但有一些问题需要更多努力去发现和修复。
我很快养成了观察代理工作的习惯,注意到代理一遍又一遍重复的命令序列,并将它们转化为代理可以调用的脚本。Cutlet 的 scripts 目录中的许多脚本就是这样产生的。
这是非常繁琐、毫无乐趣的工作。我希望随着时间的推移这会变得更加自动化。也许未来版本的 Claude Code 可以审查自己的工具调用输出并建议你可以为它编写的脚本?
当然,最有成效的优化是在 Docker 中使用 --dangerously-skip-permissions 和 sudo 访问运行 Claude。通过这样做,我把自己从代理循环中移除。一旦计划文件产生,我不想留在那里照看代理,每次它们想运行 ls 时说"是"。
随着 Cutlet 的发展,我为 Claude 构建的基础设施也在发展。最终,我将 Claude 自然遵循的许多工作流程捕获为脚本、斜杠命令或 CLAUDE.md 中的指令。我还了解到代理最容易在哪里跌倒,并通过给它更好的指令或要运行的脚本来预防这些错误。
我为 Claude 构建的基础设施对我这个在项目上工作的人也有价值。帮助 Claude 自动化工作的脚本也帮助我快速完成常见任务。
随着项目的增长,这套基础设施将随之不断进化。模型一直在变化。项目需求和工作流程也是。我将所有这些项目基础设施视为有机的东西,只要项目活跃,它就会不断变化。
软件工程要凉了吗?
现在个人开发者能在如此短的时间内完成这么多工作,作为职业的软件工程要凉了吗?
我对这个问题的答案是完全不。软件工程技能今天和语言模型变好之前一样有价值。如果我没有在大学选修编译原理课程并学习《Crafting Interpreters》,我就无法构建 Cutlet。我仍然必须做出只有因为我拥有(一些)领域知识和经验才能做出的技术决定。
此外,为了在 Cutlet 上有效工作,我必须学习一堆新技能。这些新技能也需要技术知识。一类奇怪、全新、不同类的技术知识,但仍然是技术知识。
在从事这个项目之前,我担心五年后我是否还能找到工作。但今天我确信,世界未来将继续需要软件工程师。我们的工作将发生变化——有些人可能不再喜欢新工作——但我们仍然有大量工作要做。也许我们将比以往有更多工作要做,因为大语言模型使我们能够更快地构建更多软件。
对于那些永远不想接触大语言模型的人来说,有些领域大语言模型永远无法进入。我那些从事底层多媒体系统的朋友发现使用大语言模型的成功率比那些构建 Web 应用的朋友要低。这种情况可能会持续多年。最终,那些工作也会改变,但转变会慢得多。
抢占 Claude 的功劳公平吗?
说我构建了 Cutlet 公平吗?毕竟,Claude 做了大部分工作。除了写提示之外,我的贡献是什么?
此外,这个实验之所以有效,是因为 Claude 的训练数据中有多种语言运行环境和计算机科学书籍。没有数百位程序员、学者和作家免费将他们的工作奉献给公众,这个项目就不可能实现。那么到底是谁构建了 Cutlet?
我没有一个很好的答案。对于在生成令牌时维护和培育编码代理,我感到心安理得,但对于代码本身没有所有权感。
我不认为这是"我的"工作。感觉不对。也许我的感觉将来会改变,但我不太明白怎么会。
由于我对这段代码真正属于谁有所保留,我没有在 Cutlet 的 GitHub 仓库中添加许可证。Cutlet 属于所有在互联网上发布过工作的编程语言设计者、实现者和教育者的集体智慧。
此外,值得注意的是,Cutlet 几乎肯定包含了 Lua 和 Python 解释器的代码。当我们讨论语言特性时,它经常提到这些语言。我还亲眼看到大量来自《Crafting Interpreters》的代码进入了代码库。
这对我的心理健康不利
如果我不在这篇已经很长的博客文章中加上心理健康方面的说明,那就太失职了。
很容易对代理工程工具上瘾。在做这个项目时,我经常发现自己午夜时分还在电脑前"再提示一次",就像在玩世界上最冷门的《文明》游戏。我不好意思承认,当客人来我家时,当我走进浴室时,或者当我出去吃午饭时,我经常让 Claude Code 在后台运转。很少时间完成这么多事情带来了一种令人陶醉的感觉。
比这更令人上瘾的是这些工具固有的不可预测性和随机性。如果你向 Claude 抛出一个问题,你永远不知道它会想出什么。它可能一举解决你被困数周的难题,或者可能搞砸一切。就像老虎机一样,你永远不知道会发生什么。这产生了一种强烈的冲动,想把它用于所有事情。就像老虎机一样,庄家总是赢。
这些天,我限制了允许使用 Claude 的时间和频率。随着大语言模型广泛普及,作为一个社会,我们将不得不找出在不破坏心理健康的情况下使用它们的最佳方式。
这是我不太乐观的部分。我们已经完全未能监管或限制社交媒体的使用,我打赌我们会在大语言模型身上重蹈覆辙。
我们如何处理这些新超能力?
既然我们现在可以非常快速地生产大量代码,我们现在能做什么以前不能做的事情?
这是另一个我目前没有能力完整回答的问题。
不过,就我而言,我能立即看到大语言模型有用的一个领域是快速实验的能力。在 Cutlet 中尝试十个不同的功能对我来说是轻而易举的事,因为我只需要规范它们,然后离开电脑。失败的实验几乎没有任何成本。即使我不能使用 Claude 生成的代码,拥有工作的原型也能帮助我快速验证想法并及早丢弃坏想法。
我还能够大幅减少我对 JavaScript 和 Python 项目中第三方库的依赖。我经常使用大语言模型生成小型实用函数,这些函数以前需要从 NPM 或 PyPI 引入依赖。
但说实话,这些变化微不足道。我无法预测 AI 代理将带来的更大社会变革。我只能说,编程在 2030 年看起来将与 2026 年截然不同。
Cutlet 的未来?
这个项目是一个概念验证,看看我能将 Claude Code 推到什么程度。我目前正在寻找作为前端工程师的新合同,所以可能没有时间继续做 Cutlet。我还有一些推动代理编程进一步发展的想法,所以可能优先考虑那些而不是继续做 Cutlet。
当心血来潮时,我可能还是会偶尔为这门语言添加一些小功能。现在我已经退出了开发循环,不需要太多时间和精力。我甚至可能在 12 月用 Cutlet 参加编程挑战赛!
当然,如果你在 Anthropic 工作并想给我钱让我继续运行这个实验,我在接下来 8 个月可以接受合同工作。
现在,我要结束 Cutlet 的篇章,继续其他项目(和猫)。
感谢 Shruti Sunderraman 校对此文。还要感谢 Cutlet 那只猫,今天三次横穿键盘并删除我所有工作。