本文翻译自 I built a programming language using Claude Code,原载于 Hacker News。
在一月到二月的四周时间里,我用 Claude Code 构建了一门新的编程语言。我用我的猫给它起名叫 Cutlet。这在法律上完全没问题。你可以在 GitHub 上找到源代码、构建说明和示例程序。
我从 2021 年 GitHub Copilot 发布以来就开始使用 LLM 辅助编程,但之前一直局限于生成样板代码和做些有针对性的小改动。但在开发 Cutlet 的过程中,我把每一行代码都交给 Claude 生成。我甚至没有读这些代码。相反,我建立了一套护栏来确保它正常工作(后面会细说)。
这个实验的结果让我惊讶。Cutlet 真的诞生了。它在 macOS 和 Linux 上都能构建和运行。它能执行真正的程序。可能深处藏着一些 bug,但应该不比任何其他四周大的编程语言更糟糕。
我对这一切有很多感受,也思考了它对我这个职业意味着什么。但在站上肥皂盒之前,先带大家参观一下这门语言。
Cutlet 语言巡礼
如果你想跟着操作,可以从源码构建 Cutlet 解释器,然后用 /path/to/cutlet repl 进入 REPL。
数组和字符串的工作方式跟任何动态语言一样。变量用 my 关键字声明:
cutlet> my cities = ["Tokyo", "Paris", "New York", "London", "Sydney"]
=> [Tokyo, Paris, New York, London, Sydney]
变量名可以包含连字符。这跟 Raku 的语法规则一样。目前唯一支持的数字类型是 double:
cutlet> my temps-c = [28, 22, 31, 18, 15]
=> [28, 22, 31, 18, 15]
这里有个很酷的东西:@ 元操作符可以把任何普通二元操作符转换成数组上的向量化操作。下一行代码中,我们将 temps-c 的每个元素乘以 1.8,然后给结果数组的每个元素加上 32:
cutlet> my temps-f = (temps-c @* 1.8) @+ 32
=> [82.4, 71.6, 87.8, 64.4, 59]
@: 操作符是一个 zip 操作,它把两个数组合并成一个 map:
cutlet> my cities-to-temps = cities @: temps-f
=> {Tokyo: 82.4, Paris: 71.6, New York: 87.8, London: 64.4, Sydney: 59}
用内置的 say 函数输出文本。这个函数返回 nothing,就是 Cutlet 版的 null:
cutlet> say(cities-to-temps)
{Tokyo: 82.4, Paris: 71.6, New York: 87.8, London: 64.4, Sydney: 59}
=> nothing
@ 元操作符也可以用于比较:
cutlet> my greater-than-seventy-five = temps-f @> 75
=> [true, false, true, false, false]
还有一个有趣的特性:你可以用布尔数组来索引数组。这是一个过滤操作,它会选出对应 true 的索引位置的元素,丢弃对应 false 的:
cutlet> cities[greater-than-seventy-five]
=> [Tokyo, New York]
更简洁的写法:
cutlet> cities[temps-f @> 75]
=> [Tokyo, New York]
来打印一个用户友好的消息。++ 操作符可以拼接字符串和数组。str 内置函数把东西转成字符串:
cutlet> say("Pack light for: " ++ str(cities[temps-f @> 75]))
Pack light for: [Tokyo, New York]
=> nothing
前缀位置的 @ 元操作符充当 reduce 操作:
cutlet> my total-temp = @+ temps-c
=> 114
来计算平均温度。@+ 把所有温度加起来,len() 内置函数获取数组长度:
cutlet> (@+ temps-c) / len(temps-c)
=> 22.8
再漂亮地打印出来:
cutlet> say("Average: " ++ str((@+ temps-c) / len(temps-c)) ++ "°C")
Average: 22.8°C
=> nothing
函数用 fn 声明。在 Cutlet 里一切都是表达式,包括函数和条件语句。函数中表达式的最后一个值会成为它的返回值:
cutlet> fn max(a, b) is
... if a > b then a else b
... end
=>
你自己定义的函数也可以和 @ 一起工作。用我们的 max 函数来 reduce 温度数组,找出最高温度:
cutlet> my hottest = @max temps-c
=> 31
Cutlet 还能做更多。它有你期望的动态语言的所有常见特性:循环、对象、原型继承、mixin、标记-清除垃圾回收器,以及友好的 REPL。我们还没有文件 I/O,一些基础构造如错误处理也还缺失,但我们在稳步前进!
完整的文档见 git 仓库中的 TUTORIAL.md。
为什么要做这个?
我是一名前端工程师和(偶尔的)设计师。我试过用 LLM 构建 Web 应用,但总是遇到瓶颈。
根据我的经验,Claude 之流在编写复杂业务逻辑方面厉害得吓人,但在任何需要视觉设计技能的任务上表现糟糕。
事实证明,用英语描述响应式布局和动画并不容易。再多的截图和线框图也无法向 LLM 传达流动的布局和动画。我浪费了好几个小时跟 Claude 争论它发誓已经修复的布局问题,但我这双漏水的人类眼睛还是能清楚地看到。
我还发现这些工具非常擅长生成它们在公开仓库中见过的千篇一律的界面,但当我想做点新颖的东西时就掉链子了。我经常为利基领域构建复杂数据可视化的客户工作,LLM 在这些项目上彻底失败,产生不了有用的输出。
另一方面,我在过去几个月看到人们用 LLM 完成了不可思议的事情,我想自己复制这些实验。但我之前使用 LLM 的经验表明,我必须谨慎选择项目。
- 我不想解决特别新颖的问题,但我希望能有时把 LLM 引导到有趣的方向
- 我不想手动验证 LLM 生成的代码。我想给 LLM 规格、测试用例、文档和示例输出,让它自己完成所有判断它是否做对的艰苦工作
- 我想给 agent 一个强反馈循环,让它能自主运行
- 我不喜欢 MCP。我不想处理它们。所以任何需要连接浏览器、截图或通过网络与 API 通信的事情都自动排除
- 我想用一个无聊的语言,外部依赖尽可能少
一个小型的动态编程语言满足了我所有的要求。
- LLM 知道如何构建语言实现,因为它们的训练数据包含数千个现有实现、论文和计算机科学书籍。我对创建一门”混搭”语言很感兴趣,从我喜欢的各种现有语言中挑选特性
- 我可以写一堆小程序连同它们的预期输出来测试实现。我甚至可以让 Claude 帮我写,给我潜在无限多的测试用例来验证语言工作正常
- 语言实现可以从命令行测试,只有纯文本输入和输出。不需要截图、录屏或设置脆弱的 MCP。对于 agent 来说没有比”运行
make test和make check直到没有错误”更好的反馈循环了 - C 语言无聊到极致,而且有大量用 C 构建的语言实现
最后,这也是一个实验,弄清楚我能把 agent 工程推多远。我能把六个月的工作压缩到几周吗?我能构建超出我自己能力的东西吗?如果我全力投入 LLM 驱动的编程,我的日常工作会是什么样子?我想回答所有这些问题。
我带着一些怀疑进入这个实验。我之前尝试完全用 Claude Code 构建东西都没成功。但这次不仅成功了,而且产生的结果超出了我的想象。我不认为未来所有的软件都会由 LLM 编写。但我确实相信有相当大一部分可以部分或大部分外包给这些新工具。
构建 Cutlet 教会了我重要的一点:用 LLM 生成代码并不意味着你忘记了构建软件的一切知识。Agent 工程需要仔细的规划、技能、工艺和纪律,就像任何值得构建的软件一样。与编码 agent 合作所需的技能可能看起来与在编辑器中逐行输入代码不同,但它们仍然是我们整个职业生涯一直在磨练的同样的工程技能。
Agent 工程的四项技能
从 LLM 获得好输出需要做很多工作。Agent 工程并不意味着把模糊的指令倒进聊天框然后收割吐出来的代码。
我认为今天要有效地与编码 agent 合作,你需要学习四项主要技能:
- 理解哪些问题可以有效地用 LLM 解决,哪些需要人类参与,哪些应该完全由人类处理
- 清楚地传达你的意图并定义成功标准
- 创建一个让 LLM 能发挥最佳工作的环境
- 监控和优化 agent 循环,让 agent 能高效工作
理解哪些问题可以有效地用 LLM 解决
模型和工具在快速变化,所以弄清楚 LLM 擅长解决哪些问题需要发展你的直觉、与同行交流、保持信息灵通。
不过,如果你不想跟上这个快速变化的领域——我不会评判你,外面太疯狂了——你可以问自己两个问题来判断你的问题是否是 LLM 形状的:
- 对于你想解决的问题,是否可以以自动化方式定义和验证成功标准?
- 其他人以前解决过这个问题吗——或者类似的问题?换句话说,你的问题可能在 LLM 的训练数据中吗?
如果这两个问题中任何一个的答案是”否”,往问题上扔 AI 不太可能产生好的结果。如果两个答案都是”是”,那么你可能会在 agent 工程上找到成功。
好消息是,弄清楚这一点的成本只是 Claude Code 订阅费和团队中一只愿意花一个月在代码库上尝试的替罪羊。
传达意图
LLM 使用自然语言,所以学习用文字传达你的想法变得至关重要。如果你不能在书面上向同事解释你的想法,你就无法有效地与编码 agent 合作。
你可以用简单、模糊、过于通用的提示从 Claude Code 获得很多。但这样做时,你把很多思考和决策外包给了机器人。这对于一次性项目来说没问题,但当你构建要投入生产并维护多年的东西时,你可能要更小心。
你想给编码 agent 精确编写的规格说明,尽可能多地捕获你的问题空间。在 Cutlet 工作期间,我大部分时间都在编写、生成、阅读和纠正规格文档。
对我来说,这是一个新体验。我主要与早期创业公司合作,所以在职业生涯的大部分时间里,我把代码当作规格。编写正式规格是一种陌生的体验。
幸运的是,我可以依靠 Claude 帮我写大部分规格。我这样做感到舒适只是因为 Cutlet 是一个实验。对于我想押上声誉的项目,我可能会把 agent 从等式中完全去掉,自己写规格。
这是我在对 Cutlet 做任何更改时的一般工作流程:
- 首先,我向 LLM 展示一个新功能(如循环)或重构(如从树遍历解释器迁移到字节码 VM)。然后我会和它讨论这个更改在 Cutlet 的上下文中如何工作,其他语言如何实现它,设计考虑,我们可以从有趣/利基语言中窃取的想法等。只是随意的来回,就像你和同事交谈一样
- 在我对功能或更改的样子有了很好的把握后,我会让 LLM 给我一个分解成小步骤的实现计划
- 我会审查计划,与 LLM 来回完善它。我们会探索各种边缘情况、脚枪、陷阱、缺失的部分和改进
- 当我对计划满意时,我会让 LLM 把它写到一个文件里,放到
plans/doing/目录。有时一个功能会有 3-4 个计划文件。这是故意的。我需要计划是人类可读的,我需要每个计划是一个原子单元,如果事情不顺利可以回滚。它们也作为项目演变的历史。你可以在 Cutlet 仓库中找到所有的历史计划文件 - 我会阅读和审查生成的计划文件,再次与 LLM 来回做修改,当一切看起来不错时提交它
- 最后,我会启动一个 Docker 容器,运行带有所有权限——包括
sudo访问——的 Claude,让它实现我的计划
这个工作流程前置了任何对语言更改的认知努力。所有的思考都发生在写一行代码之前,这几乎是我从未做过的事情。对我来说,编程涉及在工作过程中有机地发现问题的形状。然而,我发现用 LLM 这样工作很困难。它们非常擅长对你的代码库进行大规模更改,但不擅长快速、迭代、有机的开发工作流程。
也许随着推理变得更快、模型变得更好,我的工作流程会演变,但在此之前,这种瀑布式模型最有效。
创建让 agent 发挥最佳工作的环境
我发现这是与编码 agent 合作中最有趣和最有趣的部分。这是一整类新问题要解决!
核心原则是:编码 agent 是计算机程序,因此它们对所存在的世界的视野有限。它们通向你试图解决的问题的唯一窗口是它们可以访问的代码目录。这没有给它们足够的代理或信息来做好工作。所以,为了帮助它们茁壮成长,你必须以它们可以用来接触更广阔世界的工具的形式给它们那种代理和信息。
这在实践中意味着什么?对于不同的项目看起来不同,但这正是我为 Cutlet 做的:
- 全面的测试套件。我的项目指令告诉 Claude 编写测试并确保它们在编写任何新代码之前失败。同时,我要求它在进行重大代码更改或合并任何分支后运行测试。有了不断增长的测试套件,Claude 能够快速识别和修复它引入代码库的任何回归。测试还作为文档和规格说明
- 示例输入和输出。这些是我的集成测试。我在 Cutlet 仓库中添加了许多示例程序——大多数是 Claude 自己编写的——不仅作为人类的文档,还作为端到端测试套件。项目指令告诉 Claude 在每次代码更改后运行所有这些程序并验证它们的输出
- Linter、格式化器和静态分析工具。Cutlet 使用
clang-tidy和clang-format确保基准代码质量。就像测试一样,项目指令要求 LLM 在每次重大代码更改后运行这些工具。我注意到clang-tidy经常产生诊断,迫使 Claude 重写部分代码。如果我能使用一些更昂贵的静态分析工具(如 Coverity),我也会把它们添加到我的开发过程中 - 内存安全工具。我让 Claude 创建一个
make test-sanitize目标,用 ASan 和 UBSan 重新构建整个项目和测试套件(LSan 通过 ASan 一起运行),然后在插桩构建下运行每个测试。项目指令包括在实现计划结束时运行这个检查。这捕获了测试和 linter 都无法发现的内存错误——use-after-free、缓冲区溢出、未定义行为。运行这些测试需要时间,大大减慢了 agent,但它们捕获了比clang-tidy更多的问题 - 符号索引。agent 可以访问
ctags和cscope来导航源代码。我不知道这有多大用处,因为我很少看到它使用它们。大多数时候它只是grep代码中的符号。我将来可能会删除这个 - 运行时自省工具。在项目早期,我让 Claude 给 Cutlet 添加了在执行任何代码之前将标记流、AST 和字节码转储到标准输出的能力。这允许 agent 快速弄清楚它是否在执行管道的任何部分引入了错误,而无需导航源代码或进入调试器
- 管道追踪。我让 Claude 编写一个 Python 脚本,将 Cutlet 程序通过带有调试标志的解释器运行,以捕获完整的编译管道:标记流、AST 和字节码反汇编。然后它将每个标记类型、AST 节点和操作码映射回解析器、编译器和 VM 中处理它们的确切源位置。当 agent 需要添加新的语言功能时,它可以在类似现有功能的示例上运行追踪器,以精确查看要触及哪些文件和函数。我对这个机制非常自豪,但我也从未看到 Claude 充分利用它
- 以所有可能的权限运行。我想让 agent 自主工作,并访问它可能想使用的任何调试工具。为此,我在启用了
--dangerously-skip-permissions和完整sudo访问的 Docker 容器中运行它。我认为这是在大型项目上使用编码 agent 的唯一实际方法。当你有五个 agent 并行工作时,回答权限提示在认知上是负担沉重的,限制它们做任何想做的事情的能力会使它们在工作上效率降低。我们需要解决当给 LLM 完全控制系统时产生的各种安全问题,但在这个项目上,我愿意接受 YOLO 模式带来的风险
所有这些工具和能力保证了代码的任何更新都会产生至少编译和执行的项目。但更重要的是,它们增加了 Claude 可以访问的信息和代理,使其更有效地发现和调试问题而无需我的干预。如果我继续这个项目,我的主要重点是给我的 agent 更多的洞察力来了解它们正在构建的工件,更多的调试工具,更多的自由,以及更多获得有用信息的机会。
你会想出适用于你特定项目的工具。如果你在构建 Django 应用,你可能想给 agent 访问暂存数据库。如果你在构建 React 应用,你可能想给它访问无头浏览器。没有适用于每个项目的单一答案,我敢打赌人们会想出一些非常有趣的工具,让 LLM 观察它们在现实世界中的工作结果。
优化 agent 循环
编码 agent 在使用你给它们的工具方面有时可能效率低下。
例如,在处理这个项目时,有时 Claude 会运行一个命令,决定其输出太长无法放入上下文窗口,然后用输出管道到 head -n 10 再次运行它。其他时候它会运行 make check,忘记 grep 输出中的错误,然后再次运行以捕获输出。这会导致在进行一次编辑的过程中运行多次相同的昂贵检查。这些错误显著减慢了 agent 循环。
我可以通过编辑 CLAUDE.md 或更改自定义脚本的输出来修复一些这些性能瓶颈。但有些问题需要更多的努力来发现和修复。
我很快养成了观察 agent 工作的习惯,注意到 agent 一遍又一遍重复的命令序列,并把它们变成脚本供 agent 调用。Cutlet 的 scripts 目录中的许多脚本就是这样产生的。
这是非常手动的、非常无聊的工作。我希望随着时间推移这会变得更加自动化。也许未来版本的 Claude Code 可以审查自己的工具调用输出并建议你可以为它编写的脚本?
当然,最富有成效的优化是在 Docker 中以 --dangerously-skip-permissions 和 sudo 访问运行 Claude。通过这样做,我把我自己从 agent 循环中拿掉了。在计划文件产生后,我不想在旁边照看 agent,每次它们想运行 ls 时说 Yes。
随着 Cutlet 的演变,我为 Claude 构建的基础设施也在演变。最终,我把 Claude 自然遵循的许多工作流捕获为脚本、斜杠命令或 CLAUDE.md 中的指令。我还了解了 agent 在哪里最容易出错,并通过给它更好的指令或脚本来运行来预先防止这些错误。
我为 Claude 构建的基础设施对作为项目上工作的人类我也有价值。帮助 Claude 自动化其工作的相同脚本也帮助我快速完成常见任务。
随着项目的增长,这个基础设施将随之演变。模型一直在变化。项目需求和工作流程也是如此。我把所有这些项目基础设施看作一个有机的东西,只要项目活跃就会持续变化。
软件工程真的死了吗?
既然个人开发者现在可以在这么短的时间内完成这么多,软件工程作为一个职业死了吗?
我对这个问题的回答是 不,完全没有。软件工程技能在今天和语言模型变好之前一样有价值。如果我在大学没有上过编译器课程,没有读过《Crafting Interpreters》,我就无法构建 Cutlet。我仍然必须做出技术决策,这些决策只有在具备(一些)领域知识和经验的情况下才能做出。
此外,为了有效地在 Cutlet 上工作,我必须学习一堆新技能。这些新技能也需要技术知识。一种奇怪的、新的、不同类型的技术知识,但仍然是技术知识。
在处理这个项目之前,我担心五年后我是否还有工作。但今天我相信世界将来仍然需要软件工程师。我们的工作会转变——有些人可能不再喜欢新的工作——但仍然会有很多工作等我们做。也许我们会有更多的工作要做,因为 LLM 让我们能更快地构建更多的软件。
对于那些永远不想碰 LLM 的人,会存在 LLM 永远无法突破的领域。我的从事低级多媒体系统工作的朋友与构建 Web 应用的人相比,使用 LLM 取得的成功较少。这种情况可能会持续很多年。最终,这些工作也会转变,但转变会慢得多。
抢占 Claude 的功劳公平吗?
说我构建了 Cutlet 公平吗?毕竟,Claude 做了大部分工作。除了写提示,我的贡献是什么?
此外,这个实验之所以成功,只是因为 Claude 在其训练数据中可以访问多种语言运行时和计算机科学书籍。如果没有数百名程序员、学者和作家自由捐赠他们的工作给公众,这个项目就不可能实现。那么到底是谁构建了 Cutlet?
我没有好的答案。我对 coding agent 生成标记时的照料和喂养感到舒适地承担责任,但我对代码本身没有所有权感。
我不认为这是”我的”工作。感觉不对。也许我的感受将来会改变,但我不太明白怎么会。
出于我对这些代码真正属于谁的保留,我没有在 Cutlet 的 GitHub 仓库中添加许可证。Cutlet 属于每一位在互联网上发布其作品的编程语言设计师、实现者和教育者的集体意识。
(另外,值得注意的是,Cutlet 几乎肯定包含来自 Lua 和 Python 解释器的代码。当我们讨论语言功能时,它一直参考那些语言。我还亲眼看到大量来自《Crafting Interpreters》的代码进入了代码库。)
这对我的心理健康不好
如果我不在这篇已经超长的博文中包含关于心理健康的注释,那就是失职了。
很容易对 agent 工程工具上瘾。在处理这个项目时,我经常在午夜坐在电脑前说”再来一个提示”,仿佛我在玩世界上最冷门的《文明》游戏。我很尴尬地承认,当客人来我家时、当我进淋浴间时、当我出去吃午饭时,我经常让 Claude Code 在后台运转。在这么短的时间内完成这么多带来的陶醉感很强烈。
更令人上瘾的是这些工具固有的不可预测性和随机性。如果你把一个问题扔给 Claude,你永远不知道它会想出什么。它可能一次性解决你卡了几周的难题,也可能搞出一团糟。就像老虎机一样,你永远不知道会发生什么。这创造了一种强烈的一直使用的冲动。就像老虎机一样,庄家总是赢。
这些天,我给自己使用 Claude 的时间和频率设定了限制。随着 LLM 变得广泛可用,我们作为一个社会必须弄清楚如何最好地使用它们而不破坏我们的心理健康。
这是我不太乐观的部分。我们已经全面失败于监管或限制社交媒体的使用,我敢打赌 LLM 会重演那个场景。
我们用这些新超能力做什么?
既然我们现在可以非常快速地产生大量代码,我们能做什么以前做不到的事情?
这是我现在没有能力完全回答的另一个问题。
也就是说,我可以看到 LLM 立即对我有用的一个领域是能够非常快速地实验。在 Cutlet 中尝试十个不同的功能对我来说非常容易,因为我只需要把它们规格化然后离开电脑。失败的实验几乎没有成本。即使我不能使用 Claude 生成的代码,拥有工作的原型帮助我快速验证想法并早期丢弃坏的。
我还能够大幅减少我在 JavaScript 和 Python 项目中对第三方库的依赖。我经常使用 LLM 生成以前需要从 NPM 或 PyPI 拉入依赖的小工具函数。
但老实说,这些变化只是小意思。我无法预测因为 AI agent 会产生的更大的社会变化。我只能说编程在 2030 年看起来会与 2026 年截然不同。
Cutlet 的下一步是什么?
这个项目是一个概念验证,看看我能把 Claude Code 推多远。我目前正在找前端工程师的新合同,所以我可能没有时间继续在 Cutlet 上工作。我还有几个进一步推动 agent 编程的想法,所以我可能会优先考虑那些而不是继续 Cutlet 的工作。
心情来了的时候,我可能仍会给语言添加一些小功能。既然我已经把自己从开发循环中移除,这不需要很多时间和精力。我甚至可能在十二月用 Cutlet 做 Advent of Code!
当然,如果你在 Anthropic 工作想给我钱让我继续运行这个实验,我在未来 8 个月可以接合同工作 :)
目前,我正在结束 Cutlet 这一章,继续其他项目(和猫)。
关键收获
-
选对问题是关键:LLM 擅长解决有自动化验证标准、且在训练数据中可能存在的问题。编程语言实现正好符合这两个条件。
-
规格先于代码:与 agent 合作需要前置思考,把所有设计决策都写清楚,这与传统的边写边探索的工作方式很不同。
-
构建基础设施:测试套件、linter、内存检查、自省工具——这些不仅帮助 agent,也帮助人类更好地理解和维护项目。
-
工程技能依然重要:用 LLM 不意味着放弃专业技能。相反,你需要新的技能来指导、验证和优化 agent 的输出。
-
警惕上瘾:这些工具的不可预测性和高效产出具有很强的成瘾性,需要主动设限保护心理健康。
Cutlet 项目是一个有趣的实验,它展示了当开发者从”写代码的人”转变为”指导和验证 AI 的人”时会发生什么。这可能是未来编程工作的一个缩影。