随笔
追求 agent 的幸福
为 agent 设计理想的语言
错误的问题
七十年来,编程语言设计一直在问一个问题:什么让人类程序员高效且幸福?答案各有不同 — 表达力、人体工程学、安全性、迭代速度 — 但问题的主语从未改变。程序员是一个人。
这个假设已经悄悄地失效了。越来越多的代码现在由在循环中运行的大语言模型编写:生成、编译、测试、观察、修改。这些 agent 不是更慢的人类或更快的人类。它们是一种完全不同的程序员,有不同的失败画像,而为人类幸福优化的语言可能对 agent 的幸福是主动敌对的。现在值得问的问题是:当一门语言的主要用户是一台自信地、合理地、且频繁地犯错的机器时,它该长什么样?
agent 如何失败
一个 LLM 程序员的决定性特征不是无知,而是自信的幻觉。一个 agent 通常不是不知道某个 API;它发明了一个看起来对的。它不是误解了类型系统;它产出与正确代码模式匹配却违反了一个它从未检查过的不变式的代码。它的错误是流畅的。
这颠覆了通常的设计优先级。人类从一门让正确代码容易书写的语言中受益。agent 从一门让错误代码响亮、可归因、且修复成本低的语言中受益 — 因为 agent 的工作流不是"一次写对",而是"通过迭代收敛到对"。一门语言的质量,从 agent 的视角看,是它在 agent 错的时候与 agent 进行的对话的质量。
天真的结论是 agent 想要最大化的静态保证:最严格的编译器能抓到最多的幻觉。按这个理论,Rust 应该是完美的 agent 语言 — 它的编译器是有史以来最激进的"看起来对但不是"检测器,而它的错误信息读起来像纠正性提示。
Rust 悖论
从经验上看,这个理论失败了。agent 在 Rust 上挣扎。在一个更简单的、带垃圾回收的语言里一个 agent 两轮就能修好的 bug,在 Rust 里可能要十轮,或者永远不收敛。这个悖论的解答是 agent 导向语言设计的核心洞见:错误检测和错误解决是不同的指标,一门语言可以在一个上占主导地位同时在另一个上失败。
Rust 的 borrow checker 验证全程序所有权不变式。这意味着一个在局部逻辑上正确的修复 — 对正确的那一行的正确修改 — 仍然可能被拒绝,因为它违反了别处的所有权架构。诊断是精确的;修复是一次重构。Rust 把局部编辑转化为全局义务,而这对于一个基于 transformer 的 agent 是灾难性的,它的注意力机制从根本上偏向局部上下文,而它在失败循环下的偏向是极小编辑 — 恰恰是被全局纠缠约束惩罚的策略。更糟的是,编译器出了名有用的建议成了陷阱:agent 贪婪地应用每个局部最优的修复,在代码库里追着错误前沿跑却永远不收敛,因为真正的问题是架构性的,而没有任何单条建议这么说。
Rust 的保证是前置的惩罚换取后置的安全。这笔交易相对于静默内存损坏的替代方案是极好的。但它揭示了"最大静态检查"不是 agent 的最优解。还有别的。
三条轴,不是一条
"编译器能抓到错误"把必须分开才能看清设计空间的属性打包在了一起。有三条:
检测延迟 — 错误引入后多久有东西失败?在编译时、首次运行时,还是生产中?
检测保真度 — 失败指向原因,还是只指向下游症状?一条离 bug 三帧的栈追踪是低保真度检测,即使它瞬间到达。
修复局部性 — 正确的修改是否限定在诊断附近,还是向外辐射到签名、模块和架构?
这些轴是独立的,而现有语言占据了揭示性地不同的角落。Rust:出色的延迟和保真度,差的局部性。Python:好的局部性 — 修复通常是一行 — 但差的延迟和保真度,因为错误在运行时浮现,远离原因,且只在测试恰好覆盖的路径上。C 对内存 bug 同时在三条上失败:晚检测、症状远离原因、以及爆炸半径让连修复都变成非局部的。Go 之类的"无聊"语言命中了一个可行的中间地带:适度的保证,但显式控制流、没有魔法、修复留在错误所在的地方。
对 agent 而言尤其如此,修复局部性是承重的轴。 agent 的调试循环是一个搜索过程,而局部性约束了它的分支因子。局部修复意味着在一小撮行的编辑上搜索,每次迭代要么收敛要么廉价地证伪一个假设。非局部修复意味着在程序架构上搜索 — 而且,更糟的是,中间状态编不过,所以 agent 失去了让它保持诚实的增量验证信号。期望修复时间随迭代次数乘以每次迭代成本而缩放,而非局部性同时攻击两个因子。
确定性作为伟大的均衡器
如果局部性是承重的轴,那么确定性是让运行时检测能与编译时检测竞争的属性 — 而这是设计空间里最被低估的杠杆。
确定性主要买的是保真度。一个可复现的失败可以被二分、最小化、机械地追踪;即使一个检测得晚的错误也能被归因到原因。非确定性才是让运行时 bug 昂贵的根源:不是它们被发现得晚,而是它们无法被可靠地再次发现。一门保证确定性执行的语言把"检测得晚"转化为"检测得晚但可归因",而可归因是 agent 需要的大部分 — 因为 agent 的核心认识论是科学方法,而一个 Heisenbug 是一个假设无法被证伪的环境。
这打开了设计空间里一个真正不同的点,也就是像 SuperJ 这样的语言占据的点:拒绝 borrow checker 的迭代税,转而用确定性和测试把验证买回来。一个内存 bug 仍然是 bug — 但它是一个带行号、可复现的 bug,在运行时几乎能和静态检查器在编译时一样可靠地被抓到,而每次迭代成本只是它的一小部分,且修复不会升级为重构。诚实的告诫随之同行:这笔交易只在测试覆盖替代了静态证明本会保证的东西时成立。Rust 里那个要十轮的 bug 仍然不能以错误状态发布;宽松语言里的两轮修复可以,如果测试套件有漏洞的话。但一个带确定性复现的快循环在实践中缩小这个差距的程度,远超静态保证最大化主义所假设的。
设计原则:局部性从哪里来
修复局部性不是语法的偶然。它由语言让程序员对哪些全局不变式负责来决定。任何跨越整个程序的不变式,按构造,在被违反时都会有全程序的修复。因此语言设计者对每类不变式有四个选项,其中只有一个是不好的:
最强的招是让这个 bug 类无法表达。垃圾回收并不比 borrow checker 更好地检查生命周期;它把生命周期 bug 从可写程序的空间里删掉了。局部性的最强形式是一个不可能存在的错误。
第二是在显式边界处检查不变式 — 契约、检查接口、模块边缘的标注 — 这样修复半径被最近的声明而非整个程序所约束。
第三是在确定性下动态地解除不变式,可复现性让调试保持局部,即使被检查的属性是全局的。
第四 — Rust 的选择 — 是在整个程序上静态地证明全局不变式:完美的检测,代价是让每次违反都成为架构事件。
还有一个防止过拟合的护栏:一些正确性属性确实是全局的 — 并发交错、资源协议、跨模块契约 — 而一门语言可以通过简单地不检查它们实现完美的修复局部性,这把错误从"非局部修复"移到"未检测",一个严格更糟的象限。原则不是"最小化检查"。它是:对每类不变式,选择保真度与修复半径之比最好的执行机制,并偏好那些能缩小程序可违反的全局不变式集合的设计。
幸福是可衡量的
这个框架最后的优点是它是可证伪的。agent 的幸福不必停留在哲学层面;它可以是一个基准,而一门控制自己工具链的语言可以执行它。
两个指标就够。修复半径:编译器发出的每条诊断,最终的修复相对于报告的跨度触及了多少行和多少文件?一门服务 agent 良好的语言在诊断周围显示紧密的分布;一门引入了 Rust 失败模式的语言显示一条胖尾巴。到绿为止的循环数:从首次错误到通过状态的编辑-编译-测试迭代的中位数。两者一起把"agent 在这里收敛更快"从轶事变成回归测试 — 而且,至关重要的是,变成对语言自身演化的约束。任何加胖修复半径尾巴的提议特性都在引入非局部性,无论它别的优点如何,可以在被测量的理由上被拒绝。
结论:一门语言是推理的 API
人类按书写感觉给语言排名。agent 必须按它们在 agent 错的时候如何回应来排名 — 因为 agent 会错,自信地且频繁地,而语言的工作是让犯错变得可存活且廉价。
那么,理想的 agent 语言不是最富表达力的,也不是最静态铁腕的。它是那个最大化每单位迭代成本的可读性和可复现性的语言:错误被足够早地检测,忠实地归因于原因,且在 agent 的注意力能真正持有的半径内可修复。它删除能删的 bug 类,把必须的围在显式边界处,并用确定性让剩下的一切可复现。它把编译器不当作守门人,而是当作与一个会犯错的对话者的对话的另一半。
追求 agent 的幸福不是追求一个永远不让 agent 错的编译器。它是追求一门语言,在其中犯错是快速的、响亮的、诚实的、且局部的 — 因为对一个通过循环学习的心智来说,那不是对正确性的妥协。那是通往它的道路。