Flask 创始人为 Agent 设计编程语言:语法、工具链与测试的八条设计法则

星海拾光AI 前沿📡 觉醒AI2026-09-21732 阅读💛 184 收藏

去年我开始思考,在 Agent(智能体)工程日益兴起的当下,编程语言的未来会是什么样子。一开始我觉得,海量的现有代码会让已有的语言牢牢占据主导地位,但现在我的想法正好相反。在这篇文章里,我想谈谈为什么我们会看到更多新编程语言的出现,以及为什么这个领域有相当大的创新空间。万一有人想动手造一门新语言,这里也有一些我觉得值得追求的方向。

为什么新语言能行(Why New Languages Work)

Agent 在训练数据中见过的语言上表现更好,这不是废话吗?当然是的。但还有一些不那么显而易见的因素影响着 Agent 使用某种语言的能力:围绕该语言的工具链好不好,以及这门语言变化快不快。

Zig 在模型权重中的表示似乎不足(至少在我用过的模型里是这样),而且它还在快速迭代。这个组合并不理想,但也还过得去:只要给 Agent 指向正确的文档,即使是即将发布的 Zig 新版本也能写。只是体验不太好。

另一方面,有些语言在权重中的表示很充分,但 Agent 的表现依然不尽如人意,原因在于工具链。Swift 就是个好例子:以我的经验来看,围绕 Mac 或 iOS 应用构建的工具链实在太折磨人了,Agent 很难搞定。同样不太好。

所以,一门语言存在并不意味着 Agent 就能用好它,一门语言是新的也不意味着 Agent 就一定会踩坑。我相信,只要你不想一次性推翻一切,完全可以逐步建立起对一门新语言的支持。

新语言可能行得通,最大的原因是编写代码的成本正在大幅下降。结果就是,生态系统的广度变得没那么重要了。我现在经常在以前会用 Python 的地方改用 JavaScript,不是因为我喜欢它或它的生态更好,而是因为 Agent 在 TypeScript 上表现好得多。

换个角度想:如果我选的语言缺少某个重要功能,我只要把 Agent 指向另一门语言的某个库,让它移植过来就行了。举个具体的例子,我最近用 JavaScript 写了一个以太网驱动来实现我们沙箱的主机控制器。Rust、C 和 Go 里都有现成的实现,但我想要一个在 JavaScript 里可插拔、可定制的版本。让 Agent 重新实现一遍,比搞定原生绑定的构建系统和分发流程要容易得多。

新语言只要价值主张足够强,并且在演进过程中考虑到 LLM(大语言模型)的训练方式,人们就会采用它们——哪怕它们在权重中的表示还不充分。而且,如果它们本身就为与 Agent 协作而设计,那完全可以采用已被证明好用的熟悉语法。

为什么需要新语言(Why A New Language?)

那我们到底为什么需要一门新语言?这个问题之所以值得思考,是因为今天的很多语言在设计时都有一个假设:敲键盘是苦力活,所以我们用简洁性来换取某些东西。举个例子,很多语言——尤其是现代语言——大量依赖类型推断(type inference),这样你就不用手写类型了。但代价是,你现在需要 LSP(语言服务器协议)或编译器的错误信息才能搞清楚某个表达式的类型。Agent 在这一点上同样很挣扎,在代码评审中这也很让人头疼——复杂的操作会让人很难弄清楚实际的类型是什么。完全动态的语言在这方面更糟。

编写代码的成本在下降,但由于我们也在生产更多代码,理解代码在做什么变得越来越重要。我们可能确实需要写更多的代码,如果这意味着在做代码评审时歧义更少的话。

我还想指出,我们正在走向一个这样的世界:有些代码永远不会被人类看到,只会被机器消费。即便如此,我们仍然希望给用户——可能是个非程序员——一些关于代码在做什么的提示。我们希望能向用户解释代码将会做什么,而不必深入讲解具体怎么做。

所以,新语言的理由归结为:鉴于谁在编程以及代码成本发生了根本性的变化,我们至少应该考虑一下。

Agent 想要什么(What Agents Want)

要说 Agent 想要什么其实挺棘手的,因为 Agent 会对你撒谎,而且它们受到所有见过的代码的影响。但有一种方式可以估算它们的表现:看它们需要对文件做多少次修改,以及完成常见任务需要多少轮迭代。

以下是我发现的一些我认为在相当长一段时间内都会成立的规律。

不依赖 LSP 的上下文(Context Without LSP)

LSP(语言服务器协议)让 IDE 能基于对代码库的语义理解来推断光标下的信息或提供自动补全。这是一个很棒的系统,但它有一个对 Agent 来说很棘手的代价:LSP 必须在运行。

有些时候 Agent 就是不会启动 LSP——不是因为技术限制,而是因为它也很”懒”,能跳过就跳过。如果你给它一段文档里的示例,没法轻松启动 LSP,因为那可能只是个不完整的代码片段。如果你把它指向一个 GitHub 仓库,它拉下来个别文件后就直接看代码,不会为了类型信息去搭建一个 LSP。

一门不会分裂成两种体验(有 LSP vs. 无 LSP)的语言对 Agent 是有益的,因为它在更多场景下提供了统一的工作方式。

花括号、方括号和圆括号(Braces, Brackets, and Parentheses)

作为一个 Python 开发者,说这话让我很痛苦,但基于空白缩进的语法确实是个问题。在 token 层面把空白处理对是很棘手的,一门依赖显著空白的语言对 LLM 来说更难处理。如果你让 LLM 在没有辅助工具的情况下做精确修改,这一点尤为明显。它们经常会故意忽略空白,添加标记来启用或禁用代码,然后依赖代码格式化工具来善后。

另一方面,没有空白分隔的花括号也会造成问题。取决于分词器(tokenizer)的实现,连续的右括号可能会以出乎意料的方式被切分成 token(有点像”strawberry 里有几个 r”的计数问题),LLM 很容易在 Lisp 或 Scheme 上犯错,因为它记不清自己已经输出了多少个右括号。未来的 LLM 能解决吗?当然,但这对人类来说也一样,没有工具辅助也很难搞对。

流式上下文,但要显式(Flow Context But Explicit)

读过我博客的读者可能知道,我非常推崇 async locals(异步本地变量)和流式执行上下文——也就是在调用链中携带数据的能力,这些数据可能只在很深的调用层才会用到。在一家可观测性公司工作让我对此体会尤深。

挑战在于,任何隐式流动的东西都可能没有被配置。拿当前时间来说,你可能想把一个计时器隐式传给所有函数。但如果计时器没有配置,突然冒出一个新的依赖怎么办?把所有东西都显式传递对人类和 Agent 来说都很繁琐,偷懒的做法在所难免。

我实验过的一种方式是在函数上添加 effect markers(副作用标记),通过代码格式化步骤自动添加。函数可以声明它需要当前时间或数据库,但如果它没有显式标注,本质上就是一个 lint 警告,自动格式化会修复它。LLM 可以在函数中直接使用当前时间之类的东西,任何现有的调用者会收到警告,格式化工具会传播这个标注。

这很好,因为当 LLM 写测试的时候,它可以精确地 mock 掉这些副作用——它从错误信息中就能理解自己需要提供什么。

例如:

fn issue(sub: UserId, scopes: []Scope) -> Token needs { time, rng } { return Token{ sub, exp: time.now().add(24h), scopes, } }

test “issue creates exp in the future” { using time = time.fixed(“2026-02-06T23:00:00Z”); using rng = rng.deterministic(seed: 1);

let t = issue(user(“u1”), [“read”]); assert(t.exp > time.now()); }

结果类型优于异常(Results over Exceptions)

Agent 对异常很恐惧。我不确定这在多大程度上可以通过 RL(强化学习)来解决,但目前 Agent 会试图捕获一切能捕获的东西,记录日志,然后做出很糟糕的恢复。考虑到关于错误路径的信息实在太少,这种行为也可以理解。受检异常(checked exceptions)是一种方案,但它们会一路传播到调用链顶端,并不能显著改善局面。即使它们变成了 linter 跟踪哪些错误可能抛出的提示,仍然有很多调用点需要调整。而且,就像前面提到的上下文数据自动传播一样,这可能也不是正确的解决方案。

也许正确的方向是更多地使用类型化的结果(typed results),但在没有相应的类型系统和对象系统支撑的情况下,组合性仍然是个棘手的问题。

最小化 diff 和按行读取(Minimal Diffs and Line Reading)

目前 Agent 读取文件到内存的通用方式是按行读取,这意味着它们经常会选到跨越多行字符串的代码块。一个很容易看到翻车的场景:让 Agent 处理一个 2000 行的文件,里面还嵌入了长长的代码字符串——基本上就是个代码生成器。Agent 有时会在多行字符串内部做编辑,以为那是真正的代码,但其实只是嵌在多行字符串里的代码文本。对于多行字符串,我知道的唯一有好解决方案的语言是 Zig,但它基于前缀的语法对大多数人来说很陌生。

重新格式化也经常导致代码结构移到不同的行。在很多语言中,列表的尾逗号(trailing commas)要么不支持(JSON),要么不常用。如果你想要 diff 稳定性,就应该追求一种需要更少重新格式化、尽量避免多行结构的语法。

让代码可以 grep(Make It Greppable)

Go 的一个很棒的特性是,你基本上没法把另一个包的符号直接导入当前作用域——每次使用都要带上包名前缀。比如 context.Context 而不是 Context。虽然有逃生通道(import 别名和 dot-import),但它们很少见,通常也不被推荐。

这极大地帮助 Agent 理解它在看什么。总的来说,让代码通过最基本的工具就能被找到是非常好的——它适用于没有被索引的外部文件,也意味着在运行时生成代码驱动的大规模自动化中(比如 sed、perl 调用)有更少的误报。

局部推理(Local Reasoning)

我说的很多东西归结为一点:Agent 非常喜欢局部推理。它们希望能分块工作,因为它们通常只在上下文中加载了几个文件,对整个代码库没有太多空间感知。它们依赖 grep 之类的外部工具来查找东西,任何难以 grep 或把信息藏在别处的做法都很棘手。

感知依赖的构建系统(Dependency Aware Builds)

决定 Agent 在一门语言上成败的关键往往就是构建工具好不好用。很多语言由于交叉引用太多,很难确定实际需要重新构建或重新测试什么。Go 在这方面做得很好:它禁止包之间的循环依赖(import cycles),包的布局清晰,测试结果可以缓存。

Agent 讨厌什么(What Agents Hate)宏(Macros)

Agent 经常被宏搞得很痛苦。人类在宏上也很挣扎,这早就很明显了,但支持宏的理由主要是代码生成可以减少需要手写的代码量。既然这不再是那么大的问题了,我们应该追求对宏依赖更少的语言。

泛型(generics)和编译时计算(comptime)是另一个问题。我觉得它们表现要好一些,因为它们基本上是用不同的占位符生成相同的结构,Agent 理解起来容易得多。

重导出和 Barrel 文件(Re-Exports and Barrel Files)

这和可 grep 性相关:Agent 经常搞不懂 barrel files(桶文件,即集中重导出的索引文件),而且它们不喜欢这种模式。没法快速弄清楚一个类或函数从哪里来,会导致从错误的地方导入,或者完全遗漏,浪费上下文去读太多文件。声明位置和导入来源一一对应是最好的。

这也不需要太严格。Go 大致走的就是这条路,但没有走极端。同一个目录下的任何文件都可以定义函数,这不是最优解,但查找起来够快,不用搜太远。之所以行得通,是因为包被强制保持足够小,用 grep 就能找到一切。

最糟糕的情况是到处都是自由的重导出,完全割裂了实现和磁盘上任何可以简单推断的位置。更糟糕的是:起别名(aliasing)。

别名(Aliasing)

Agent 非常讨厌别名。事实上,如果你让它们重构一段大量使用别名的代码,你甚至能在它们的思考过程中看到它们在抱怨。理想情况下,一门语言应该鼓励好的命名,从而在导入时减少对别名的需求。

不稳定的测试和开发环境偏差(Flaky Tests and Dev Env Divergence)

没人喜欢不稳定的测试(flaky tests),但 Agent 更不喜欢。讽刺的是,Agent 恰恰特别擅长制造不稳定的测试。这是因为 Agent 目前很喜欢 mock,而大多数语言对 mock 的支持并不好。所以很多测试最终要么不是并发安全的,要么依赖于开发环境的状态,而这些状态在 CI 或生产环境中就不一样了。

大多数编程语言和框架让写不稳定的测试比写稳定的测试容易得多。因为它们到处鼓励非确定性(indeterminism)。

多重失败条件(Multiple Failure Conditions)

理想情况下,Agent 只需要一个命令来完成 lint、编译,并告知是否一切正常。也许再一个命令来运行所有需要运行的测试。但实际上大多数环境不是这样的。比如在 TypeScript 中,你经常可以在类型检查失败的情况下运行代码。这会误导 Agent。同样,不同的打包器(bundler)配置可能导致一个环境成功了,换到 CI 里一个略有不同的配置就失败了。工具链越统一越好。

理想状态是:要么能运行,要么不能运行;尽可能多的 lint 问题都能机械化修复,这样 Agent 就不用手动处理了。

我们会看到新语言吗?(Will We See New Languages?)

我觉得会。我们现在写的软件比以往任何时候都多——更多网站、更多开源项目、更多一切。即使新语言的比例不变,绝对数量也会增长。但我也真心相信,会有更多人愿意重新思考软件工程的基础和我们使用的语言。因为虽然这些年来大家一直觉得一门语言要火起来需要构建大量基础设施,但现在你可以瞄准一个相当窄的用例:先让 Agent 满意,再从那里扩展到人类。

我希望能看到两件事。第一,一些”素人艺术”:从没造过语言的人来试试手,给我们展示一些新东西。第二,更加刻意地从第一性原理出发,记录什么有效、什么无效。我们其实已经积累了很多关于什么造就好语言、如何将软件工程扩展到大团队的知识。然而,把这些东西写下来,作为一份关于好的和坏的语言设计的可消化概述,是非常难找到的。太多东西被毫无意义的观点之争所左右,而不是基于事实。

不过现在,我们正在慢慢走向一个事实更重要的阶段,因为你可以通过观察 Agent 的表现来衡量什么真正有效。没有人类愿意被当作调查对象,但 Agent 不在乎。我们可以看到它们在哪里成功、在哪里挣扎。

原文信息

作者:@fkysly(中转),原作者 Armin Ronacher(Flask/Sentry 创始人) 原文地址:

文章评论(0

暂无评论,快来抢沙发~