去年,我开始思考随着代理工程成为一种增长趋势,未来的编程语言可能会是什么样子。最初,我觉得庞大的现有代码库会让现有语言根深蒂固,但现在我开始认为相反的情况更为真实。在这里,我想阐述为什么我们将会看到更多新的编程语言,以及为什么存在大量空间进行有趣的创新。为了让想要构建新语言的人参考,以下是我对我们应该追求目标的一些想法!
新语言为何有效
代理在其权重中包含的语言上表现是否会显著更好?显然是。还有一些不那么明显的因素会影响代理在某种语言中编程的能力:周边工具的质量以及该语言的演变频率。
Zig 在权重中似乎被低估(至少在我使用的模型中),且其版本变化迅速。这个组合并非最优,但仍可接受:如果你指向正确的文档,即使是即将发布的 Zig 版本也能进行编程。不过并不理想。
另一方面,一些语言在权重中表现良好,但由于工具选择的原因,代理的成功率仍不高。Swift 是一个很好的例子:根据我的经验,构建 Mac 或 iOS 应用的工具链可能如此痛苦,以至于代理难以驾驭。同样不理想。
因此,仅仅因为存在就不意味着代理一定成功,也仅仅因为它是新语言并不意味着代理会遇到困难。我相信,如果你不想一次性迁移到所有地方,你可以逐步让自己适应新语言。
新语言可能成功的最大原因是编程成本正显著下降。结果是生态系统的广度变得不那么重要。现在我经常在本想用 Python 的地方改用 JavaScript。这并非因为我更喜欢它或生态更好,而是因为代理在 TypeScript 上表现更佳。
思考方式是:如果我选择的语言缺失重要功能,我就把代理指向另一种语言的库,让它构建一个移植版本。举个具体例子,我最近用 JavaScript 写了一个以太网驱动程序,以实现我们沙盒的主机控制器。该功能已有 Rust、C 和 Go 的实现,但我想要一个可插拔、可定制的 JavaScript 版本。让代理重新实现要比让构建系统和发行版支持原生绑定更容易。
如果新语言的价值主张足够强大,并且在设计时结合了 LLM 的训练规律,它们就会奏效。即使在权重中被低估,人们仍会采纳它们。而且,如果它们被设计为能很好地与代理协同,那么它们可能会基于已知有效的熟悉语法进行设计。
为什么需要新的语言?
那么为什么我们还需要新的语言?之所以值得思考,是因为今天许多语言是在键盘输入繁琐的前提下设计的,为了简洁我们做出了某些权衡。例如,许多语言,尤其是现代语言,极度依赖类型推断,使得不必显式写出类型。缺点是,你现在需要 LSP 或编译器产生的错误信息来确定表达式的类型。代理也难以处理这点,在 Pull Request 审查中,复杂操作更是让人难以确定实际类型。完全动态语言在这方面更糟糕。
编写代码的成本在下降,但由于我们生产的代码量也在增加,理解代码行为变得更加重要。如果能通过产生更多代码来减少审查时的歧义,我们可能实际上想要编写更多代码。
我还想指出,我们正走向一个某些代码永远不被人类看到、只被机器消费的世界。即使在这种情况下,我们仍然想为可能不是程序员的用户提供关于发生了什么的提示。我们希望能够向用户解释代码将做什么,而不必深入细节。
因此,推出新语言的理由在于:鉴于谁在编程以及代码成本的根本变化,我们至少应该考虑开发一种新语言。
代理的需求
很难准确说出代理想要什么,因为代理会撒谎,并且受到它们所见代码的影响。但估计代理表现的一种方式是观察它们在文件上需要做多少次修改,以及完成常见任务需要多少次迭代。
我发现有些事情我认为在一段时间内会一直成立。
无 LSP 的上下文
语言服务器协议(LSP)让 IDE 能根据代码库的语义知识推断光标下的内容或应自动补全的内容。这是一个很棒的系统,但它带来一个对代理而言棘手的成本:LSP 必须运行。
有些情况代理根本不会运行 LSP ------ 不是技术限制,而是它懒惰并会在不需要时跳过此步骤。如果你给它文档中的示例,它无法轻易运行 LSP,因为那是可能不完整的代码片段。如果你指向一个 GitHub 仓库并让它拉取单个文件,它只会查看代码,而不会为类型信息设置 LSP。
一种不分两种体验(有 LSP 与无 LSP)的语言将对代理有利,因为它为代理提供了在更多情境下统一的工作方式。
括号、方括号和圆括号
作为 Python 开发者,我说这句话很不舒服,但基于空白的缩进是一个问题。正确处理空白的底层 token 效率很棘手,使用显著空白的语言更难被 LLM 处理。尤其是如果你试图让 LLM 在没有辅助工具的情况下进行细粒度修改时更明显。它们往往会刻意忽略空白,添加标记来开启或禁用代码,然后依赖代码格式化器稍后清理缩进。
另一方面,未被空白分隔的花括号也会引起问题。根据 tokenizer 的不同,一串闭括号可能会被意外拆分成多个 token(有点像 "strawberry" 计数问题),LLM 很容易把 Lisp 或 Scheme 处理错误,因为它失去了已输出或正在查看的闭括号数量。未来的 LLM 能否解决?可以,但这也是人类在没有工具的情况下难以正确处理的事情。
流式上下文但显式
读者可能知道,我非常相信异步局部变量和流执行上下文------基本上是将数据贯穿每一次调用的能力,甚至可能在调用链深层才需要。 在一家可观测性公司的工作深刻强化了这点的重要性。
挑战在于任何隐式流动的东西都可能没有配置。例如当前时间,你可能想把计时器隐式传递给所有函数。但如果计时器未配置,突然出现新的依赖怎么办?显式传递所有内容对人类和代理都很繁琐,且会出现不良捷径。
我曾尝试过在函数上添加效果标记,通过代码格式化步骤完成。一个函数可以声明它需要当前时间或数据库,但如果没有显式标记,这相当于 lint 警告,自动格式化会修复。LLM 可以在函数中开始使用当前时间,任何现有调用者都会收到警告;格式化会传播该注解。
这很好,因为当 LLM 构建测试时,它可以精确地模拟这些副作用------它能从错误信息中了解需要提供什么。
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());
}
最小差异与行读取
目前代理读取文件到内存的通用方法是按行读取,这意味着它们经常选择跨多行字符串的块。观察其失效的一种简单方式是让代理在一个 2000 行的文件中工作,同时该文件包含长的嵌入代码字符串------基本上是一个代码生成器。代理有时会在多行字符串内部编辑,误认为那是实际代码,而实际上只是嵌入的代码字符串。对于多行字符串,我知道唯一有好方案的语言是 Zig,但其基于前缀的语法对大多数人来说相当陌生。
重新格式化也经常导致构造移动到不同的行。在许多语言中,列表中的尾随逗号要么不支持(JSON),要么不常见。如果你想获得差异稳定性,你会倾向于使用需要较少重新格式化且大多避免多行构造的语法。
让它可 Grep
Go 的一大优点是,你几乎无法在不使用包名前缀的情况下将另一个包的符号导入作用域。例如:context.Context 而不是 Context。虽然有逃逸机制(导入别名和点导入),但它们相对少见且通常不被鼓励。
这极大地帮助代理理解它正在查看的内容。总体而言,通过最基本的工具使代码可查找是件好事------它能在未被索引的外部文件中工作,并且为由即时生成代码驱动的大规模自动化(例如 sed、perl 调用)带来更少的误报。
本地推理
我所说的大部分可以归结为:代理非常喜欢本地推理。它们想在部分工作,因为它们经常仅在上下文中加载少量文件,且对代码库的整体布局缺乏空间意识。它们依赖外部工具如 grep 来查找信息,任何难以 grep 或隐藏信息的情况都很棘手。
依赖感知构建
代理在许多语言中失败或成功的根本原因在于构建工具的好坏。许多语言让人难以判断真正需要重建或重新测试的部分,因为交叉引用过多。Go 在这方面做得很好:它禁止包之间的循环依赖(导入循环),包的布局清晰,测试结果被缓存。
代理讨厌的东西
宏
代理经常在宏上遇到困难。人类同样在宏上挣扎,这点已经很明显,但当初之所以支持宏,主要是因为代码生成可以减少编写代码量。由于这一点不再是大问题,我们应该寻求对宏依赖较少的语言。
关于泛型和 comptime 的问题也值得讨论。我认为它们相对更好,因为它们主要生成相同结构但占位符不同的代码,这更容易被代理理解。
重新导出与桶文件
与 Grep 可查找性相关:代理经常难以理解 桶文件,并且不喜欢它们。无法快速确定类或函数来源会导致错误导入或完全遗漏,从而浪费上下文并读取过多文件。声明位置到导入位置的 1:1 映射非常好。
而且它不必过于严格。Go 在这方面走了一条类似路线,但不至于极端。任一目录下的文件都可以定义函数,虽然不够理想,但足够快地找到,并且不需要搜索太远。之所以能奏效是因为包被强制保持足够小,足以用 grep 查找所有内容。
最坏情况是随处出现无偿重新导出,完全将实现与磁盘上任何可轻易重建的位置解耦。更糟的是:别名。
别名
代理常常讨厌别名。实际上,如果你让它们重构大量使用别名的代码,甚至会在思考块中对其提出抱怨。理想情况下,语言应鼓励良好的命名,并在导入时抑制别名。
测试不稳定与开发环境差异
没人喜欢不稳定的测试,但代理更不喜欢。讽刺的是,代理在创建不稳定测试方面尤其擅长。这是因为代理目前热衷于使用模拟,而大多数语言并不支持良好的模拟。因此,许多测试最终会意外地不具备并发安全性,或依赖于开发环境状态,而后在 CI 或生产环境中出现差异。
大多数编程语言和框架使得编写不稳定测试比稳定测试更容易。原因是它们在各处鼓励不确定性。
多重失败条件
在理想的世界里,代理只有一个命令:执行 lint 和编译,并告诉代理是否全部顺利。也许还有另一个命令来运行所有需要运行的测试。然而在实际中,大多数环境并非如此。例如,在 TypeScript 中,你往往可以运行代码,即使它 类型检查失败 [blocked]。这会让代理产生错觉。类似地,不同的打包器设置可能导致某件事在本地通过,却在 CI 上由于微小差异而失败。工具链越统一越好。
理想情况下,命令要么成功要么失败,并且对尽可能多的 lint 失败实现机械化修复,让代理不必手动完成。
我们会看到新语言吗?
我认为我们会看到新语言的出现。如今我们所写的软件比以往任何时候都多------更多的网站、更多的开源项目、各种各样的东西。即使新语言的比例保持不变,绝对数量也会增加。但我也深信,越来越多的人愿意重新思考软件工程的基础以及我们使用的语言。因为多年来,人们一直觉得需要构建大量基础设施才能让一种语言起飞;现在你可以针对一个相对狭窄的用例:先让代理满意,再向人类扩展。
我只希望能看到两件事。首先,外行艺术:那些从未设计过语言的人尝试并向我们展示新思路。其次,针对从第一原则出发,系统性记录哪些有效、哪些无效的更有计划的努力。我们实际上已经学到了很多关于什么构成优秀语言,以及如何将软件工程扩展到大团队的经验。然而,能够以可消费的概览形式记录好与坏语言设计的文字资料却很难找到。过多的内容被关于琐碎事物的主观看法所主导,而非客观事实。
现在,我们正慢慢达到一个事实更重要的阶段,因为你可以通过观察代理的表现来衡量哪些方法有效。没有人愿意接受调查,但 代理不在乎 [blocked]。我们可以看到它们的成功率以及所面临的困难。