当一个 AI 代理读取某个函数时,它看到的是输入列表返回输出列表的签名,写出的测试在隔离环境中通过,但生产环境中却失败了 —— 因为该函数依赖了一个全局配置对象和一个数据库单例,而这些依赖从未在函数签名中声明过。这不是一个模型问题,而是架构问题。函数式编程程序员在 1980 年代就已经解决了这类问题。

代理失败的根本原因

大多数代理项目从未进入生产阶段。根据 Gartner 的预测,到 2027 年底超过 40% 的代理 AI 项目将被取消。MIT 的研究发现 95% 的 AI 试点项目未能实现投资回报。业界的第一反应往往是指责模型本身 ——“GPT-5 会解决这个问题” 或者 “我们需要更好的提示词”。但真正的失败原因是架构性的:当代理将代码写入一个可变的、紧耦合的代码库时,它产生的是非确定性输出,依赖于它无法看到的隐藏状态。

一个经过数月积累的人类开发者会构建出关于代码库的心智模型。他们知道哪些函数会改变状态、哪些模块共享全局变量、哪些测试是不稳定的。但代理没有这种奢侈 —— 每次会话都从零开始。代理只能阅读眼前的代码,遵循显式的契约,基于它能验证的内容生成输出。这意味着任何隐含的东西、任何隐藏的状态、任何埋藏在 “纯” 函数内部的副作用,都会成为陷阱。

想象一个看起来很正常的函数:接收一个选项列表和一个阈值,返回过滤后的选项。团队中的开发者知道配置是从启动时加载的 YAML 文件中读取的,数据库访问器是一个需要初始化的单例。但代理看到的只是一个接收列表返回列表的函数。它根据这个契约编写测试,测试在隔离环境中通过,然后函数在生产环境中失败 —— 因为全局配置没有被加载。将这种情况乘以一个代码库中数百个隐藏依赖的情况。每个代理触及的函数都有一个看不见的爆炸半径,每次修改都可能破坏一个它从未读过的模块中的东西。这解释了为什么代理项目会逐渐退化:每次迭代都会引入微妙的状态损坏,而且会不断累积。

函数式编程的核心优势

函数式编程之所以能解决这些问题,是因为它生来就是为了消除那些让代码抗拒自动化推理的特性。这并不是什么新见解。ML 研究者从 1980 年代起就知道引用透明的代码更容易被机器分析、优化和转换。我们只是还没有将对代理编写代码的应用付诸实践。

纯函数在相同输入下总是返回相同输出,没有全局状态、数据库调用或函数体内的日志记录。代理可以通过直接调用来测试纯函数,无需任何设置或模拟。显式数据流意味着你可以线性阅读代码来追踪输入如何变成输出,无需远距离操作或在三层回调深处发生的突变。代理可以遵循数据管道并理解每一步做了什么。副作用边界意味着 I/O、数据库访问和外部 API 调用发生在一个薄薄的外层。核心业务逻辑是确定性的,代理可以重写核心逻辑而无需担心意外触发支付或发送邮件。组合优于耦合意味着小函数像乐高积木一样可以 snap 在一起,代理可以替换一个函数而无需理解整个模块图。

SUPER:代理友好代码的五项原则

将代码约束组织成一个易记的缩写是让原则在组织中存活下来的方式。SUPER 是对代码编写方式的五项约束:Side Effects at the Edge(边缘副作用)意味着 I/O 发生在薄薄的外层,绝不在业务逻辑内部;Uncoupled Logic(解耦逻辑)意味着依赖被传入,而不是从全局变量中拉取;Pure & Total Functions(纯且总体函数)意味着确定性函数处理每一种输入;Explicit Data Flow(显式数据流)意味着你可以线性追踪数据从输入到输出的流动;Replaceable by Value(可替换为值)意味着任何表达式都可以用其计算结果替换。

实际效果是:代理在 SUPER 兼容的代码上工作时,可以通过只阅读该函数及其类型签名来修改任何函数。没有隐藏状态需要追踪,没有全局配置需要发现,没有副作用需要意外触发。在典型的命令式代码中,代理修改函数需要理解整个依赖图,因为任何地方都可能隐藏着全局状态或副作用。而在 SUPER 兼容的代码中,爆炸半径恰好是一个函数。

每项原则都有具体的失败模式。边缘副作用意味着如果函数在业务逻辑内部发送通知,那么每次测试、每次代理运行、每次 dry run 都会触发真正的通知。将副作用移到调用者 —— 函数负责计算发送什么,边界层负责发送它。解耦逻辑意味着如果函数导入模块来获取依赖,它就与那个模块绑在一起了。将依赖作为参数传入,这样代理可以为了测试而交换实现,而无需触及导入图。纯且总体函数意味着如果函数在意外输入时抛出异常,那它就是在隐瞒返回类型。一个总体函数处理每一种情况,代理无法捕获它不知道的异常,但它可以阅读说 “可能失败” 的返回类型。显式数据流意味着当数据通过嵌套回调或在多个方法中改变一个对象时,代理无法追踪管道。可替换为值意味着如果你可以用函数的返回值替换函数调用而程序行为不变,该函数就是引用透明的。

SPIRALS:人机协作的流程环

SUPER 处理代码。但代理也需要一个结构化流程,否则它们会漂移。任何看过 Auto-GPT 消耗 API 配额陷入无限循环的人都知道无结构的代理自主性是什么样子。SPIRALS 是一个七步循环,在每个任务上运行代理。这不是瀑布式,而是一个紧密的循环,通常不到一分钟,让代理保持专注并给人类自然的干预点。

Sense 收集上下文:阅读相关文件,检查 git 状态,识别已经存在什么。跳过这一步的代理会重建已经能工作的东西。Plan 草拟方法,考虑权衡,并定义 “完成” 是什么样子。人类在写任何代码之前验证这个计划。Inquire 识别知识空白。代理做什么假设?它不知道什么?这防止了自信的幻觉问题,即代理基于错误的假设横冲直撞。Refine 简化计划。应用 80/20 法则。如果一个 ticket 大于 3 个 story points,就拆分开。复杂性在这里被消灭,在它进入代码库之前。Act 编写代码,遵循 SUPER 原则,作为带有测试的小的有界变更。Learn 运行测试并检查输出。如果有什么失败了,代理记录具体什么出了错,供下一次迭代使用。Scan 代理放大视角,寻找重复、新的风险和变更可能破坏的其他东西。这是 Auto-GPT 从未有过的步骤 —— 它从不检查是否真的在取得进展。

这七个步骤分为两个阶段:S・P・I・R 是计划阶段,A・L・S 是执行阶段。在实践中,我将这些作为两个单独的命令运行。计划阶段(Sense, Plan, Inquire, Refine)产生设计文档、ticket 和燃尽图。人类审查并批准。只有然后执行阶段(Act, Learn, Scan)才开始,并且是按 ticket 运行的:写代码,验证它有效,检查回归,提交,移动到下一个 ticket。SPIR 和 ALS 之间的门是我要求人类批准的唯一一点。其他一切代理都能处理。

循环在 Scan 确认目标达成时终止。如果它不收敛,Scan 标记它,人类决定下一步做什么,所以你不会一觉醒来发现一个通宵运行消耗完你 API 预算的无限循环。

为什么它们一起工作

SUPER 没有 SPIRALS 给出的是没有流程的干净代码。代理写一个完美的函数,然后写出九个不需要的函数。或者它重构了不需要重构的东西。代码中的纪律没有工作流中的纪律重要。SPIRALS 没有 SUPER 给出的是应用于混乱代码的结构化流程。代理遵循所有七步,但 Act 步产生带有隐藏依赖的代码,在下一次迭代中会腐蚀。循环会退化,因为底层代码不能支持可靠的自动化修改。

当两者结合时,边缘副作用意味着只有 Act 步接触真实世界。Sense、Plan、Inquire 和 Refine 是纯推理,可以安全地重试和廉价地测试。解耦逻辑意味着每个 SPIRALS 步骤可以是它自己的模块或自己的代理。你可以插入一个更好的 planner 而无需重新接线系统。纯意味着 Plan 和 Refine 是确定性的。相同输入状态,相同计划。你可以通过重放输入来重现 bug。显式数据流意味着你可以精确追踪每一步发生了什么。当长时间运行的第 47 分钟出现问题时,你线性阅读日志并找到它。引用透明意味着中间结果是可缓存的。如果 Sense 返回相同的上下文,跳过它直接到 Plan。

实践中的具体差异

代理在 SUPER 兼容的代码上产生首次测试通过的变化的频率大约是在典型的带有全局状态的命令式代码上工作的三倍。我没有对这进行严格的研究;这是我在过去一年跨项目观察到的。调试时间下降得更多,因为当 something 确实失败时,故障只局部化在一个函数中,而不是分布在共享状态图上。使用 SPIRALS 的流程差异:代理曾经需要大量的看护,我会检查每个输出并在幻觉落地之前尝试捕捉它们。使用 SPIRALS,Scan 步骤在大多数回归在我看到之前就捕获了它们。我在 Plan 和 Learn 步骤审查并跳过其余部分,除非 Scan 标记了什么。我每个任务的参与从持续下降到两个检查点。

两个框架都不需要从头重写你的代码库。从 SUPER 的 “S” 开始:将副作用移出你最常修改的三个模块。这本身就能使代理修改更安全。在你的代理工作流中添加 Scan 步骤。你会在它们让你付出代价之前捕获无限循环和自信但错误的输出。

行业正在朝着更多代理自主性的方向发展,而不是减少。如果你的代码不能被机器推理,无论模型改进多少都救不了你。这个修复已经在你的 CS 教科书里待了四十年。只是代理让它变得紧迫了。


资料来源:Cyrus Radfar, AI agents keep failing. The fix is 40 years old