跳到主要内容

Agent 撤销按钮是 Saga,而非栈

· 阅读需 11 分钟
Tian Pan
Software Engineer

用户点击了智能体操作的“撤销”按钮,而该操作之前已经分发(fan out)到了十二个工具调用中。智能体发送了两封电子邮件,创建了一个日历邀请,更新了一条 CRM 记录,扣除了信用卡费用,并在 Slack 频道中发布了消息。其中三个操作通过 API 是不可逆的。两个操作只能通过触发自身下游通知的反向操作来撤销。剩下的七个操作各自都有自己的幂等性(idempotency)定义,而规划器从未协调过这些定义。你发布的撤销按钮看起来令人安心,它大约 60 % 的时间静默成功,其余时间则静默失败。

这不是一个 UX(用户体验)漏洞。这是一个 Saga 模式问题,分布式系统工程师已经研究了三十年,而忽略这段历史是发现它最昂贵的方式。

当产品团队要求在智能体操作中添加撤销按钮时,本能反应是将其建模为类似文本编辑器的撤销堆栈:一个操作列表,每个操作都有一个逆操作,并按相反顺序弹出。这种心理模型可以清晰地映射到单进程的状态变更——输入一个字符、移动一个形状、删除本地文档中的一行。但它很难映射到一个每个动作都是对不同供应商的网络调用、拥有各自的一致性保证的系统;在这样的系统中,有些操作会导致对方的人类采取自己不可逆的操作,且某一步骤的逆操作取决于尚未得到补偿的步骤的结果。

当工具跨越进程边界时,“可逆”究竟意味着什么

一个有用的第一步是停止将可逆性视为二进制属性,并开始将其视为每个注册工具的契约。划分为三个粗略的类别足以让你对自己诚实:

  • 完全可逆(Cleanly reversible) —— 工具暴露了一个逆向 API 调用,执行该调用可以完全消除原始操作的影响。例如创建一个草稿文档并删除该草稿,或者更新一条记录并写回之前的值。这些是容易处理的情况,但它们比你的工具目录所暗示的要少得多。
  • 带残留的补偿(Compensable with residue) —— 原始动作无法被抹去,但一个向前的补偿性动作可以恢复到一个已知的良好业务状态。付款无法撤销,但可以发起退款;会议无法静默地取消,但可以发送一封取消通知给每个受邀者。补偿是一个具有真实副作用的真实操作,而不是数据库回滚。
  • 不可逆(Non-reversible) —— 一旦执行,该操作的效果无法通过任何 API 调用序列中和。例如被人类阅读过的电子邮件、触发了回调的短信,或者超过了撤回窗口的电汇。唯一诚实的“撤销”是由人类撰写的后续消息,承认这一错误。

注册到智能体的每个工具都应该声明它属于哪一类,而注册信息应该是单一事实来源。如果工具作者无法回答这个问题,安全的默认选择是不可逆——将不可逆工具视为可补偿工具的失败模式,就是静默地发布一个“用户可以撤回”的谎言。

在执行时预计算逆操作,而非在撤销时

团队最先尝试的另一种模式是错误的形式:将执行步骤存储在日志中,在撤销时遍历日志并为每个步骤合成逆操作。这失败的原因与当状态已经逃离数据库时数据库回滚失败的原因相同:构建逆操作所需的信息已无法恢复。

如果智能体更新了一条 CRM 记录,逆操作是“将字段 X 设回其之前的值 Y”。值 Y 必须在写入的那一刻被捕获,因为到撤销时,该记录可能已被人类、另一个智能体或来自下游系统的 Webhook 修改,现在字段的值是 Z。从智能体操作日志中重建 Y 是行不通的——日志中从未记录过 Y,只记录了新值。

必须落地的纪律是,每个执行步骤都会写入一条 Saga 日志条目,其中包含正向操作和预计算的逆操作,这些操作是针对执行时观察到的世界状态捕获的。逆操作是数据,而不是稍后合成的代码。在撤销时,引擎遍历日志并按相反顺序分发每个步骤的预计算逆操作,每个补偿操作都是幂等且可重试的。

这正是像 Temporal 这样的编排器所规范化的纪律,如果开发者不使用现有的框架,就必须在自研架构中发明同样的纪律。跳过这一步意味着你的撤销按钮在演示流程中运行良好,因为演示流程没有并发写入者;但在第一个 CRM 同时也由其电子邮件集成更新的用户那里,它就会崩溃。

UX 必须如实告知部分撤销的情况

即使有了预计算的逆操作,真实的撤销也会处于三种状态,而将其合并为一个“已撤销”气泡提示的 UX 是在欺骗用户。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates