本文是 Agent 工程里的经典架构 系列的一篇。文中的业务场景和代码为讲解设计,产品能力以所链接的官方资料为准。

一个采购任务先预留预算,再锁定库存,最后安排运输。每一步都由不同服务负责。运输失败了,前面已经扣住的预算和库存怎么办?把整个函数包进 try/except 只能捕获异常,并不能让其他系统自动回到开始之前。

Garcia-Molina 和 Salem 的 Sagas 论文讨论了如何拆分长事务,让子事务分别提交,并用补偿事务处理部分完成的情况。这里链接的是同一论文的另一份高校托管版本;原文引用的旧地址访问不稳定。下面的资源预留例子是为了说明业务补偿的含义。

首先要接受一个事实:已经发出的邮件收不回来,已经寄出的包裹也不能通过数据库回滚消失。补偿是在当前世界里再做一个动作,使业务达到可以接受的状态。取消预留、发起退款、通知取消都是补偿,它们可能有成本,也不一定能恢复全部影响。

机制图:拆分长事务,并为已完成的业务动作设计补偿。

代码按顺序预留预算和库存,再安排运输。运输明确拒绝后,逆序释放已经预留的资源。例子中的服务都是本地函数,故障明确发生在产生运输效果之前,因此可以判断哪些步骤已经完成。这个限制很重要,不能把网络超时也当成同样确定的失败。

resources = {"budget": 100, "stock": 2}
completed = []


def reserve_budget():
    resources["budget"] -= 30


def release_budget():
    resources["budget"] += 30


def reserve_stock():
    resources["stock"] -= 1


def release_stock():
    resources["stock"] += 1


def book_shipping():
    # 明确未发生运输预订,不是网络超时。
    raise ValueError("该地区不提供运输")


steps = [(reserve_budget, release_budget),
         (reserve_stock, release_stock)]
try:
    for forward, compensate in steps:
        forward()
        completed.append(compensate)
    book_shipping()
except ValueError:
    for compensate in reversed(completed):
        compensate()
assert resources == {"budget": 100, "stock": 2}
print("明确失败后,已完成的资源预留被逆序补偿")

完整示例可下载,使用 Python 3.9 或以上版本运行:python3 example.py。只依赖标准库,不调用真实业务服务。

为什么通常逆序补偿?后一步可能依赖前一步创建的资源,先撤销后一步更容易保持约束。但这只是常用策略,真实业务还需要审查依赖。假设仓库已经开始打包,释放库存之前就需要仓库确认停止处理,不能靠反转一个 Python 列表解决。

生产实现里,待执行步骤、完成结果和补偿进度都要持久化。如果完成列表只存在内存中,进程退出之后就不知道应该撤销什么。更麻烦的是外部步骤已经完成,本地来不及记录,这仍然是结果未知问题。可靠实现需要稳定操作编号、外部状态查询,必要时先登记补偿意图,并让补偿能够容忍正向动作尚未发生的情况。

补偿本身也可能失败。例如库存已经释放,但退款服务暂时不可用。任务应该进入“补偿中”,保留重试和人工处理入口,不能因为捕获了异常就返回“已回滚”。补偿动作同样需要幂等,否则一次修复可能制造新的重复退款。运营人员还需要看到当前业务影响,而不只是技术栈里的异常名称。

Saga 的另一项代价是缺少单一数据库事务那样的隔离。其他请求可能看到“预算已经预留、库存尚未锁定”的中间状态。可以用预留、待确认等业务状态限制后续操作,但需要明确这些状态持续多久、谁来释放、用户看到什么。所谓最终收敛,必须有负责把流程推进到终态的机制。

对 Agent 来说,模型可以帮助选择替代运输或联系供应商,业务补偿的许可范围仍应由工具层判断。已经确认取消的订单不能因为一封含糊邮件重新付款;重新采购也不能掩盖第一笔订单结果未知的问题。自主规划越多,越需要保留清楚的操作身份与状态。

示例用加减法释放资源,是为了看清补偿顺序,它本身不支持重复补偿。实际可以把预留建模成独立记录,只允许从“已预留”转为“已释放”,第二次释放返回同一结果。这样“取消一个预留”比“给库存再加一”更接近安全的业务接口。

人工接管也应该是流程的一种明确状态。接管后后台不能继续自动补偿,人工修复完成后要记录最终结果,避免两方同时修改。操作权限、租约和状态推进规则,都需要与自动流程保持一致。

我会在流程设计阶段为每一步写下:成功证据是什么,超时后如何查询,能否撤销,撤销失败怎么办。如果某一步不可逆,就考虑把它放在条件确认之后,或者引入明确的人工决策。把这些问题回答完,Saga 才是一套可执行的业务协议,而不是一串看起来对称的正向与反向函数。

上一篇 · 系列目录 · 下一篇