跳到主要内容

取消安全的智能体:你的“停止”按钮背后已经产生的副作用

· 阅读需 12 分钟
Tian Pan
Software Engineer

用户点击“停止”,因为智能体(agent)误解了请求。UI 界面闪烁着“已停止”。在加载图标消失时,智能体已经发送了两封邮件,在你的日历上安排了周二的会议,针对错误的分支开启了一个草稿拉取请求(pull request),并排队发送了一条正在通过工具层追赶取消信号的 Slack 消息。模型已经听话地停止了生成 token。但外部世界并未停止对它三十秒前生成的 token 做出反应。

这是智能体演示中没人提及的失败模式。同步代码中的取消操作本身就是一个难题,背后有一整代协作式取消理论的支持:Go contexts、Python 的 asyncio.cancel、带有任务组的结构化并发,以及“礼貌请求、谨慎升级、不留资源”的整套语法。智能体在这个本就困难的问题上又增加了一层复杂性:规划器不知道用户在第 4 步和第 5 步之间撤回了授权,而它在第 4 步启动的工具在第 5 步被取消时也不会收到通知。“停止”只是一个 UI 交互功能。其背后的系统必须经过专门设计。

除非能明确已经发生的情况,否则“停止”就是一个谎言

大多数智能体 UI 将取消视为一种二元状态:一个按钮,一个消失的加载图标,一个“已停止”的提示。用户看到“已停止”后,会假定系统不再代表他们执行任何操作。这种假定正是 Bug 所在。

推理取消(inference cancellation)是简单的一半。token 流可以干净地停止,因为模型是一个运行时可以中断的函数。工具执行则是困难的一半。当取消信号到达工具层时,发往日历 API 的 HTTP 请求可能已经在传输中,一封邮件可能已经在队列中等待处理,而数据库迁移可能已经进行到了一半,智能体永远不会看到事务的提交或回滚。一个值得信赖的停止按钮必须体现这些情况。取消后正确的 UI 交互不应该是“已停止”——而是“这是你的智能体在取消前所做的操作,这是被中断的操作,这是仍在进行中的操作,这是你可以撤销的操作”。

构建这样的 UI 需要底层系统知道答案。但系统几乎从不掌握这些,因为副作用清单是隐式的:分散在工具调用日志、可能已写入也可能未写入的供应商请求 ID,以及描述智能体尝试了什么而非最终落地了什么的异常追踪中。第一个架构动作是让清单显性化。每一个工具调用在发出前都必须记录日志,并带有足够的元数据,以便在取消后的审计中进行重建:尝试了什么、什么到达了上游系统、返回了什么,以及存在哪些补偿动作(如果有的话)。

第 4 步和第 5 步之间的授权窗口

运行时间较长的智能体存在一个同步工具从未遇到过的微妙授权问题:用户在运行过程中可能会改变主意。他们看着智能体执行第 1 步、第 2 步、第 3 步,然后在第 4 步意识到智能体误读了目标。他们点击取消。此时规划器正处于第 5 步工具调用的 token 生成中。工具调用随后落地。

在设计良好的系统中,工具的执行权限不是在会话开始时的一次性授权,而是在执行瞬间重新检查的。这种架构原语更像是一个可以被撤销的 OAuth 风格作用域令牌,工具层在每次调用时都会出示该令牌,而不是规划器只读取一次的全局会话标志。授权应在请求时评估,而不只是在初始连接期间。这样,当智能体偏离目标(即用户已撤回授权的目标)时,就无法继续凭旧授权行事。具体而言:取消信号不仅停止规划器,还会使挂起和未来的工具调用必须持有的执行授权失效。

这将失败模式从“取消后工具调用成功”转变为了“取消后尝试工具调用但被授权层拒绝”。第二种失败模式是可恢复的,而第一种则是事故。

一个有用的思路:每个工具调用都是一个两阶段提交(two-phase commit),其中第二阶段取决于“授权依然有效”的信号。工具准备副作用(撰写邮件、锁定日历时段、开启数据库事务),但在重新验证授权之前不会提交。取消操作会翻转状态位,已准备但未提交的副作用将作废,而不是逃逸到外部世界。这种模式并非没有代价——每个工具集成都必须设计准备/提交边界——但对于不可逆的操作,这是让“取消”符合用户预期的唯一方法。

正向计划需要逆向计划

二十年来,分布式事务中的 Saga 模式一直是解决“一系列副作用必须部分失败时该怎么办”的答案。无论开发者是否意识到,智能体本质上就是 Saga。标准的 Saga 原则同样适用:当某个步骤产生副作用时,在发布该步骤之前,请定义在语义上撤销该副作用的补偿动作。退款、取消日历邀请、如果协议支持则撤回邮件。将数据库记录标记为已回滚并添加墓碑行,因为原始写入仍保留在审计日志中。

更难且属于智能体特有的点是:补偿并不总是能在事后推导出来。一个可以调用五十种工具的规划器无法通用地知道如何撤销这五十种工具。撤销方案必须由集成工具的人编写,而不是由模型生成。在生产环境中有效的架构模式是,在注册每个工具的同时,注册一个配对的补偿动作和可逆性分类:

  • 可逆(Reversible):工具提供清晰的逆向操作(取消草稿、删除创建的记录、作废待处理费用)。
  • 可补偿(Compensable):没有逆向操作,但存在语义上的撤销动作(退款、发送更正邮件、发布撤回声明)。
  • 不可逆(Irreversible):系统无法撤销的操作(向外部合作伙伴触发的 Webhook、已被索引的已发布文章、在非版本化文件系统上执行的破坏性命令)。

不可逆工具应被置于确认步骤之后,挂起智能体直到人工批准;或者至少标记为“此操作一旦开始即无法取消”,并在取消 UI 中如实展示。做得好的系统会将可逆性类别视为工具注册的一个属性,而不是模型做出的运行时决策。模型无法判断它刚刚发送的邮件是否可以撤销。

还存在一种真实的二阶失败:补偿动作本身也可能失败,系统必须能够从失败点恢复并重试。取消后的状态机是一个独立的子系统:它是一个反向 Saga,遍历已落地的副作用清单,尝试为每个副作用执行注册的补偿,并向用户(在显示“已停止”的同一个 UI 中)报告哪些补偿成功了、哪些仍在进行中,以及哪些系统已经放弃、需要人工介入。

持久化账本胜过内存信任

大多数智能体运行时无法清晰地回答“已经发生了什么”,原因在于它们信任模型的循环变量来保存真相。智能体认为它运行了工具 A,然后是工具 B;它的暂存区里有一个列表。但这个列表不是持久化的。如果工作进程崩溃、重启、触发部署,或者被非协作式的取消操作中断,列表就会消失。更糟糕的是,即便列表存续下来,它也不是事实真相——模型相信工具 B 成功了,因为响应解析成功了,但上游系统提交的数据形态可能与智能体预想的并不一致。

在过去两年中,持久化执行系统里已经形成了一种稳固的模式——Temporal 风格的工作流日志记录、LangGraph 检查点机制、DBOS 风格的事务事件日志。这些模式将副作用清单推入一个独立于智能体进程、能够存续的预写式账本(write-ahead ledger)。规划器形成的每一个意图在执行前都会被写入;每一个结果在影响下一个计划前也会被写入。如果运行时重启,它会回放账本以重建状态。如果用户取消,账本就是取消后 UI 读取的清单。

账本并不华丽。它只是一个数据库表,每一行对应一次工具调用,列包括意图、请求负载、请求 ID、响应、最终状态和补偿动作状态。这就是一个能回答“发生了什么”的系统与一个寄希望于模型能记住的系统之间的区别。对于任何会产生现实世界副作用的智能体来说,账本不是可选的;它是取消正确性赖以存在的基石。

一个细微的好处:账本为取消 UI 提供了展示内容。用户看到的不再是消失的追踪记录,而是一个列表——三个工具调用已完成,一个正在运行(现在正在取消),两个补偿动作已排队。他们可以决定撤销日历邀请但保留电子邮件。他们可以用修正后的目标重新提示智能体,而智能体可以读取账本以了解哪些早期工作不需要重复执行。取消不再是一个破坏性的操作,而变成了一个状态转换。

在每个边界测试取消,而不仅仅是在结尾

大多数智能体评估套件只测试理想路径:输入完整提示词,输出完整响应,判断答案。取消评估要少一个数量级,而且现有的评估通常在智能体完成后才触发取消——这证明不了任何东西,因为已经没有东西可以取消了。捕捉取消正确性漏洞的评估原则是在每个步骤边界进行取消:在步骤 1 和步骤 2 之间、步骤 3 的工具调用过程中、工具已提交但结果尚未到达规划器之前、以及在之前取消后的补偿阶段。这些都是具有不同失败模式的不同代码路径。

断言(Asserts)也不同。取消评估不是在问“智能体是否给出了正确答案”——因为根本没有答案。它在问:取消信号是否停止了新的工具调用?清单账本是否反映了所有已落地的内容?注册的补偿动作是否针对可补偿的副作用运行了?UI 显示是否与账本一致?授权层是否拒绝了取消后的任何尝试?这些是伪装成评估的集成测试,认真对待这些测试的团队会在用户发现之前,暴露出一整类事故。

一个实用的启发式方法:任何涉及带有副作用工具的智能体步骤,都应该配对一个执行期间的取消测试。这种测试不需要很昂贵——它可以模拟具有确定性延迟的工具并断言账本状态——但它必须存在。发布时缺乏这种规范的智能体,将会像二十年前分布式数据库让用户感到意外那样,让他们的用户大吃一惊。原因在结构上是相同的:一个在多个地方做很多事情的系统,不可避免地会被要求“半停止”(half-stop),而“半停止”是一个功能特性,而不是一个边缘案例。

真正的能力是协商,而非撤销

在智能体时代,“停止”不是“撤销”的同义词。它是协商的第一步:用户说“我想让这一切现在结束”,系统说“这是已经发生的情况,这是我可以撤销的,这是我无法撤销的,你打算怎么做”,然后由用户决定。能够做对这一点的系统,投资于让清单保持真实的持久化账本、让撤销成为一种注册能力而非愿景的单工具补偿动作、让撤回真正起作用的授权层,以及用非工程师也能操作的语言呈现这一切的 UX。

如果不在这方面投入,当用户点击取消后 Slack 消息依然发出的那一刻,系统就会失去用户的信任。这种失败模式是无法挽回的,因为用户的心理模型——停止就意味着停止——是让你放心把权限交给智能体的唯一前提。智能体即将被寄予厚望处理更多事务,而取消按钮就是用户的生命线。请像构建生命线一样构建它。

References:Let's stay in touch and Follow me for more thoughts and updates