Agent 撤销按钮是 Saga,而非栈
用户点击了智能体操作的“撤销”按钮,而该操作之前已经分发(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 是在欺骗用户。
- 完全撤销(Fully reversed) —— 每一步都有可补偿的逆操作,每个逆操作都成功了,系统处于在观察上等同于“智能体从未采取过行动”的状态。这是罕见的情况,也是你在演示文稿中展示的情况。
- 带残留的部分撤销(Partially reversed with residues) —— 某些步骤被完全撤销,其他步骤留下了副作用(发给四个人的取消邮件、已提交但需要三个工作日结算的退款、已删除但已出现在某人通知栏中的 Slack 消息)。用户需要看到这份残留清单,使用通俗易懂的语言,列出受影响方以及每项处理完成的时间。
- 无法撤销(Cannot be undone) —— 一个或多个步骤不可逆,智能体确实无法让世界看起来像没发生过这件事一样。UX 必须展示需要手动更改的内容、谁可以更改,理想情况下还应提供一份用户可以发送的消息草稿。
产品团队会反对显示残留清单,因为这会让智能体看起来并非无所不能。但无论如何都要显示它。另一种选择是让用户信任撤销按钮,但在几周后通过对方的投诉发现残留问题,从此不再信任你的产品所采取的任何智能体操作。未披露的部分撤销造成的信任损失远高 于诚实告知的损失。
在补偿变得比原操作更糟之前,限制级联预算
当补偿本身也具备 Agent 特性时,一种更微妙的失败模式就会出现。当 Agent 被要求撤销操作时,它会将补偿规划为另一系列的工具调用。其中一些调用会产生连锁反应——一次取消触发了两个通知,Agent 接着又想通过后续操作来“缓和”这些通知,而每一个后续操作又会触发更多的下游影响。补偿级联的规模可能会迅速膨胀,超过原始操作的大小,结果用户点一下撤销,就可能引发一场涉及上百次工具调用的“道歉狂欢”。
缓解方案是在 Saga 级别设置一个封顶值(Cap):补偿图由明确的预算约束——工具调用次数、总 Token 数、总延迟——一旦预算耗尽,引擎就会停止并显现出“残余(Residue)”,而不是继续盲目追求操作的对称性。这个封顶限制并不是什么锦上添花的修饰,它是防止误点撤销演变成重大事故的唯一防线。
为对抗性多工具链构建评估套件
这种规范的实际落地是一个评估套件,它构建对抗性的 Saga,并断言系统在撤销请求后的有限时间内能达到已知的良好状态。值得关注的并不是那些“快乐路径(Happy Paths)”:
- 一个 Saga 中,第三步的逆操作依赖于第五步逆操作的成功。(顺序至关重要;引擎必须遵守依赖关系,而不仅仅是按时间倒序迭代。)
- 一个 Saga 在撤销过程中,某个工具的逆操作永久失败。引擎绝不能让系统处于比开始时更糟的状态——与其不断重试那些前提条件已不再成立的补偿操作,不如停止并暴露残余状态。
- 一个 Saga 在正向执行和撤销之间,被非 Agent 参与者(人类或另一个系统)修改了外部环境。预先计算的逆操作现在可能是错误的;引擎必须检测到这种偏差,并降级为“无法撤销——需要人工审核”。
- 一个 Saga 中,用户快速连续点击了两次撤销。补偿机制必须在 Saga 级别具备幂等性,而不仅仅是在单个工具级别,否则第二次点击会加剧状态的残留。
这些情况不会出现在你团队手工编写的合成轨迹中。它们只有在评估生成器被允许在正向执行和撤销之间改变周围环境,并且断言检查的是最终状态的有效性而非每一步的成功时,才会显现。
在不适用的地方,停止称之为“撤销”
最后一步是词汇层面的调整。“撤销(Undo)”是一个借用自不适用于你系统的模型的 UX 承诺。更诚实的选项应该更具体:对于可以干净回滚的状态变更使用 回滚(Revert);对于需要通过正向操作来抵消的动作使用 补偿(Compensate);对于尚未提交的进行中操作使用 取消(Cancel);而对于那些已经超出你控制范围、现在需要人工介入响应的操作,则使用 确认(Acknowledge)。产品文案通常会抵制这种做法——“撤销”在可用性研究中表现良好,因为用户已经熟悉它的含义。而它表现良好的原因,恰恰是它在系统能力上误导用户的原因。如果产品界面保留了这个词,工程层面就不应该保留,两者之间的差距需要作为已知的“UX-工程债务”进行跟踪,而不是被敷衍了事。
架构上的觉醒
Agent 的撤销不是一个功能;它是一个贯穿每个工具注册、每次工具执行、每条 Saga 日志条目以及 Agent 报告结果的每个 UX 界面的契约。把它当作一个功能——一个在项目结束时添加的按钮——会导致团队发布一个成功率只有 60% 的撤销按钮,并在三个季度后发现,用户真正记住的其实是另外 40% 失败案例留下的烂摊子。
这种模式并非新鲜事。其核心模式是 Saga,其规范是补偿,其基石是预先计算的逆操作,而难点始终在于你控制的系统与你无法逆转的世界之间的边界。新颖之处在于,现在的 Agent 会以机器速度生成跨越该边界的计划,而发布撤销按钮的团队,必须为规划器(Planner)所忽略的边界后果负责。
这项工作的前瞻性版本其实很细微且枯燥。审计你的工具目录,并为每个工具标记可逆性类别。在 Saga 日志中将预先计算的逆操作设为必填项。在构建撤销按钮之前,先构建残余状态的 UX。限制级联预算。针对对抗性 Saga 进行测试。并且,即便产品界面不改,也要在工程层面将该按钮重命 名为系统真正能够兑现承诺的名称。
- https://learn.microsoft.com/en-us/azure/architecture/patterns/saga
- https://microservices.io/patterns/data/saga.html
- https://temporal.io/blog/mastering-saga-patterns-for-distributed-transactions-in-microservices
- https://www.smashingmagazine.com/2026/02/designing-agentic-ai-practical-ux-patterns/
- https://docs.langchain.com/oss/python/langchain/human-in-the-loop
- https://www.theregister.com/2026/03/10/agentic_ai_rollback_recovery_cohesity/
- https://medium.com/airbnb-engineering/avoiding-double-payments-in-a-distributed-payments-system-2981f6b070bb
- https://blog.algomaster.io/p/idempotency-in-distributed-systems
- https://hatchworks.com/blog/ai-agents/agent-ux-patterns/
- https://www.itbrew.com/stories/2026/03/06/how-reversible-is-an-agentic-mistake
