1. 从一次“删不掉的通知”说起:Agent操作回滚难在哪
我接手这个项目,起因是一张“发了就收不回”的通知。
当时业务方提了一个需求:用户取消订单之后,系统要把之前结算页上“已发放”的满减券、积分、运费券一并收回来。听起来很常规,对吧?订单、优惠券、积分、物流,各自都有微服务,老一套的TCC或者本地消息表方案都能做。问题出在,这件事被改造成由智能体(Agent)来执行之后,一切都变得不那么确定了。
用户下单时,AI调度官会先驱动一个“订单履约Agent”去创建订单,再驱动“库存Agent”去预占库存,然后驱动“营销Agent”去核销优惠券,最后让“物流Agent”去生成配送单。如果其中某一步失败,前面已经执行成功的操作就必须全部回滚。听起来依然是分布式事务里最常见的Saga模式场景,但真正动起手来才发现,被编排的对象从“数据库事务”变成了“Agent操作”,整个游戏的规则都不一样了。
数据库事务提交后,数据就在那里,你可以UPDATE回去,也可以DELETE掉。但Agent做了什么?它可能调用了第三方物流平台的接口,可能在外部通知群里发了一条消息,可能真的把优惠券从用户账户里扣掉了,也可能只是“以为自己扣掉了”就返回了一个成功。更麻烦的是,Agent的执行过程是黑盒的,它内部可能还有子Agent,子Agent又调用了MCP工具或者外部API。等到你要回滚的时候,你首先要回答一个问题:它到底干成了什么?干到了哪一步?
这就是本文要聊的核心:把Saga模式用在Agent这种不可完全确定的操作之上,再用状态机给整个流程立规矩。下面这套方案,是我在“智能体来了(西南总部)”这个项目里实际落地并跑过生产流量的做法,不追求理论完美,只讲怎么把一个又长又容易出岔子的Agent事务链,变成能查、能控、能补、能回滚的工程实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Saga模式没变,变的是被编排的对象从SQL变成了智能体
2.1 为什么传统分布式事务方案在这里失效
先对齐一个认知:在Agent执行场景下,TCC和两阶段提交(2PC)基本可以放弃。为什么?因为TCC要求每个参与者提供Try、Confirm、Cancel三个方法,这对数据库资源、缓存资源还可行,但Agent操作的本质是“一次带意图的外部调用”,结果不可预知,资源状态也不由你完全掌控。
举个实际例子。库存Agent的Try阶段去预占库存,如果只是把数据库里的库存字段减一,那确实可以做TCC。但当这个Agent背后接的是第三方供应链系统的配额接口时,对方只给你一个“申请成功”的回执,你根本没有一个和它精密对齐的Cancel接口,即使有,Cancel也未必能保证在对方系统里成功执行。你说我改它?改不了,第三方接口又不是我们家的。
2PC就更不用说了,它要求所有参与者先进入可提交状态,再由协调者统一提交或回滚。Agent的执行时长极其不稳定——大模型推理可能要几秒到几十秒,外部API还可能超时——让所有参与者一直持有资源锁等协调者指令,这在工程上就是自杀式设计。
所以最终的落地方案只能是Saga。
2.2 Saga在这里的务实形态
Saga的核心思想很简单:把一个长事务拆成一串本地事务,每执行一步都记录下来;如果某一步失败,就沿着执行顺序反向调用前面各步骤的补偿操作。它的灵魂是“补偿”,不是“回滚”。补偿不保证把世界恢复原状,只保证业务上回到一个可接受的“对账一致”状态。
在这个项目里,我把整个Saga执行链设计成如下形态:
- 主事务步骤(T1, T2, ..., Tn):由不同Agent执行,每一步完成一个独立的业务动作。
- 补偿步骤(C1, C2, ..., C(n-1)):也由Agent执行,但会是“反向操作”Agent,比如“库存恢复Agent”“优惠券退回Agent”。
- 执行过程统一接入调度官:调度官维护全链路状态,决定下一步是向前执行还是向后补偿。
- 任何一步的最终状态必须有持久化记录,不能只放在内存里。
这看起来和传统的Saga编排差不多,但Agent场景还有三个新问题要单独处理。
2.3 问题一:Agent执行结果的“不确定成功”怎么判
数据库事务的成功是确定的,事务提交了就成功了,没提交就还在那儿。但Agent返回一个“成功”,可能只是它认为自己成功了,不代表外部系统真的成功了。更麻烦的是,Agent执行超时了,不代表它没执行,可能只是响应消息丢了。
在传统Saga里,你根据数据库的返回码就能判断这一步走没走。在Agent场景里,我们必须把每一个环节的判定从“调用成功”降级为“状态更新成功”。什么意思?调用了、出参拿到了、状态落库了,才算这一步成功;如果Agent返回的是“未知”“超时”“疑似执行但未确认”,那就不能当作失败去触发补偿,而是要进入一个待确认状态,让对账巡检任务去问外部系统“你到底做没做”。
这一点我在最开始没想清楚,后面连续踩了好几个坑,具体细节会在第五部分展开。
2.4 问题二:Agent的行为有副作用,补偿不能只看数据
数据库回滚只要把数据改回去就行。但Agent操作会触发真实世界的副作用:发出去的短信、推送给第三方的单据、被扣减掉的权益。这些副作用有些是可以撤销的,有些是“不可逆”的。
所以项目里把Agent操作按可补偿性分成三类:
| 类型 | 含义 | 补偿方式 | 示例 |
|---|---|---|---|
| 可补偿操作 | 有明确的反向接口,可以撤销 | 调用反向Agent恢复 | 库存预占可释放、优惠券可退回 |
| 幂等可覆盖操作 | 没有直接撤销接口,但可以通过“冲正”或“覆盖”达到一致 | 发送冲正单据,或把状态置为“已取消” | 更新订单状态为已取消 |
| 不可补偿操作 | 副作用一旦发生就不可逆转 | 只能记录痕迹,人工介入或发布后续纠正通知 | 发送“订单已发货”短信 |
| 关键 | 不可补偿操作必须放在Saga链的末尾执行 | 前置步骤只要没有它,都可以安心补偿 | 优惠券核销可以补偿,但没有特殊必要就放后面 |
这个分类会直接影响我们怎么编排Agent的执行顺序。原则只有一个:把不可补偿的步骤尽量往后放,因为越往后,需要补偿的前置步骤越少;一旦不可补偿步骤执行完了,整个Saga就不能再进入补偿分支了。
这也是为什么“整个链路最后才发通知”,而不是一边执行一边发。
3. 状态机不是流程图:AI调度官与Agent指挥官的职责边界
3.1 我们为什么要两个“领导角色”
项目名里有“AI调度官”,又有“AI agent指挥官”,很多同事一开始没搞明白这俩角色是不是重复设计了。实际跑了几个场景之后,大家才认可:这俩不是一回事,一个管过程,一个管决策。
AI agent指挥官的角色是“决定怎么达成目标”。它接收一个业务目标,拆解出有哪些Action需要做、先后顺序是什么、需要考虑哪些异常分支、在某个Agent失败之后是重试还是换一条路。它更像大脑:不执行具体操作,而是决定“做什么”。
AI调度官的角色是“确保执行过程不跑偏”。它负责按照指挥官给出的Saga执行路径,把一个个Agent行动严格有序地驱动起来,记录每一步的状态,检测异常并触发补偿。它更像小脑加脊椎反射弧:不决定方向,但保证每一步都落到肌肉上,走错了就拉回来。
如果只让一个Agent既当决策者又当执行调度者,会出现一个问题:当链路出错时,它可能会“灵活应变”改变计划,而不是严格走补偿流程。这在传统开发里叫“逻辑灵活但不可控”,在Agent世界里会变成灾难——因为你根本不知道它会在哪一步自行其是。
所以我在系统设计里定了一条铁律:AI agent指挥官只负责编排“事务蓝图”,AI调度官只负责执行“事务蓝图”,Agent操作的具体执行由各业务Agent完成。一条链路上,只有调度官能修改运行状态,其他任何Agent都不能自己改Saga的执行轨迹。
3.2 Agent级的完整状态机定义
状态机在这套系统里的核心作用,就是给Agent的每一步操作限定“合法状态迁移”。我见过很多团队把状态机画成一张花里胡哨的流程图,但落地的时候到处都是goto式的跳转,最后状态图变成摆设。真正的状态机是要配合代码和数据库约束一起生效的。
我给系统的Saga全局状态设计了八个状态:
| 状态 | 含义 |
|---|---|
| INIT | 事务蓝图已创建,尚未开始执行 |
| RUNNING | Saga链正在向前执行中 |
| WAITING_CONFIRM | 某个Agent操作疑似执行但状态未知,等待对账确认 |
| COMPENSATING | 正在执行反向补偿流程 |
| COMPENSATED | 补偿已全部完成,事务最终失败但业务一致 |
| COMPLETED | 所有主事务步骤成功,事务完成 |
| FAILED | 无法继续执行且无法补偿,需要人工介入 |
| UNKNOWN | 系统异常导致状态未知,由巡检任务重新恢复 |
每个Agent步骤也有自己的状态机:PENDING、EXECUTING、SUCCEEDED、FAILED、COMPENSATING、COMPENSATED、SKIPPED。
关键设计是:Agent步骤只能在合法路径上迁移。比如EXECUTING状态只能迁到SUCCEEDED、FAILED或WAITING_CONFIRM,不能直接跳到COMPENSATED。任何非法迁移都会被底层状态机引擎拦截,并记录一条异常日志。
这些状态迁移不是靠Agent自觉执行,而是靠数据库里的状态字段和历史记录表强约束。每个Agent在修改状态之前,必须提交一个“状态迁移申请”,调度官校验当前状态是否允许迁移到目标状态,允许才落库。
3.3 为什么状态机必须加上“版本号”和“操作人”
这里分享一个踩坑经验:单纯的状态字段不够,必须加上版本号(version)和操作人(actor)。
在Agent并发或补偿链路并发的时候,很容易出现两个流程同时去改同一个Saga实例的状态。比如主流程Agent刚返回成功,正在把步骤状态从EXECUTING改为SUCCEEDED,而另一个超时看护任务以为这个步骤超时了,同时在把Saga全局状态改为COMPENSATING。如果不加版本号,后一次更新会直接覆盖前一次,状态就乱了。
我们的做法是:saga_activity表里加version字段,每次更新都走
sql复制UPDATE saga_activity SET status = 'COMPENSATING', version = version + 1
WHERE id = ? AND version = ?
影响行数为0时,说明有并发修改,当前更新作废,交给冲突处理逻辑来决定谁赢。同时,每次状态变更都记录actor字段——是调度官改的,还是对账任务改的,还是人工运维改的——排查问题的时候能少吵很多架。
另外,Agent状态机里还引入了“看门狗”的概念。调度官本身是一个不断循环的事件驱动引擎,它不依赖Agent回调才能推进状态;每个状态都有对应的超时时间,超时未收到回执就自动触发确认或补偿流程。这个机制让整个系统不会被某个Agent卡死。
4. 实战拆解:订单履约场景的Saga全链路落地
4.1 业务场景和链路拆分
拿我们上线后的一个核心场景说事:用户在小程序上下单购买商品,支付成功后,系统需要同时完成五个动作:创建订单记录、预占库存、核销优惠券、发放积分、创建物流配送单。
按照传统微服务的思路,这五个动作可以做成一个本地事务,但随着业务发展,库存、优惠券、积分、物流早就拆成了独立的服务,甚至库存和物流的一部分逻辑已经代理给了外部系统。于是这个场景变成了一个标准的分布式事务问题。
在AI agent指挥官的拆解下,这条Saga链被设计成:
T1:订单服务Agent创建订单状态为“待履约”
T2:库存Agent预占库存
T3:营销Agent核销优惠券
T4:积分Agent给用户发放积分
T5:物流Agent申请运单号
对应的补偿链就是:
C4:积分Agent扣回积分
C3:营销Agent退回优惠券
C2:库存Agent释放预占库存
C1:订单服务Agent将订单状态置为“已取消”
注意,这里的Agent都只是“执行者”,背后还是对接原有微服务的工具调用。真正新增的部分是:每个步骤都多了状态上报、幂等控制和可补偿性检查。
4.2 调度官的驱动循环和代码骨架
调度官的核心执行逻辑,可以简化成下面这段伪代码。它的输入是AI agent指挥官生成的Saga蓝图,输出是一个走完整个执行链或补偿链的结果。
python复制class SagaOrchestrator:
def __init__(self, saga_repo, agent_gateway, state_machine):
self.saga_repo = saga_repo
self.agent_gateway = agent_gateway
self.state_machine = state_machine
def start(self, saga_id):
saga = self.saga_repo.load(saga_id)
if not self.state_machine.can_transition(saga.status, 'RUNNING'):
return self._handle_conflict(saga)
saga.status = 'RUNNING'
self.saga_repo.save(saga)
self._drive(saga)
def _drive(self, saga):
for step in saga.planned_steps:
if step.status == 'SKIPPED':
continue
if step.status == 'SUCCEEDED':
continue
self._execute_step(step)
outcome = self._wait_for_outcome(step)
if outcome in ('SUCCESS', 'CONFIRMED'):
step.status = 'SUCCEEDED'
self.saga_repo.save(step)
elif outcome == 'UNKNOWN':
step.status = 'WAITING_CONFIRM'
self.saga_repo.save(step)
self._trigger_confirm_job(step)
return
elif outcome == 'FAILED':
self._trigger_compensation(saga)
return
else:
step.status = 'FAILED'
self.saga_repo.save(step)
self._trigger_compensation(saga)
return
saga.status = 'COMPLETED'
self.saga_repo.save(saga)
def _trigger_compensation(self, saga):
saga.status = 'COMPENSATING'
self.saga_repo.save(saga)
# 按照Saga链的反向顺序,逐个调用补偿Agent
for step in reversed(saga.completed_steps):
if not step.compensable:
saga.status = 'FAILED'
self.saga_repo.save(saga)
self._notify_human(saga)
return
comp_agent = self.agent_gateway.get_compensation_agent(step)
resp = comp_agent.execute(step.context, compensation_id=saga.gen_id())
if resp.status == 'SUCCESS':
step.status = 'COMPENSATED'
self.saga_repo.save(step)
else:
# 补偿失败要重试,重试也失败进死信队列
self._enqueue_dead_letter(step, resp)
return
saga.status = 'COMPENSATED'
self.saga_repo.save(saga)
这段代码有几个工程细节值得注意。
第一,每一步执行后不立即改主事务状态,要先等子Agent回执。回执是异步的还是同步的,取决于Agent本身的实现。我们大部分Agent通过MQ上报执行结果,调度官监听结果事件后更新状态。这样即使调度官服务重启,事件还在MQ里,不会丢。
第二,补偿操作里强制带了compensation_id,这是我们做幂等控制的凭证。后续补偿Agent收到请求时,先查本地有没有执行过这个compensation_id,执行过就直接返回成功,不重复扣积分、不重复回库存。
第三,不可补偿操作的检查被放在补偿链最开始。如果已经执行了一个不可逆动作,整个Saga不能再走自动补偿,只能标为FAILED,进入人工处理队列。这比一路补偿到最后发现某步补不了要稳得多。
4.3 空补偿、悬挂和幂等:老三板斧一个都不能少
Agent场景下的空补偿问题比传统Saga更隐蔽。
举个例子:库存Agent执行预占时,网络超时了。调度官把它标记为WAITING_CONFIRM,然后对账任务去库存系统查,发现预占没成功。可是此时有一个已启动的补偿流程已经走到了“释放库存”这一步,它会尝试去释放一个根本不存在的预占记录。如果没有空补偿保护,这里可能把别人的库存给释放了。
我们的保护手段是一个“预占申请单”机制。每个Agent在执行前,先向调度官申请一个唯一的operation_id,并把这个operation_id写入agent_step表的pending状态。Agent执行成功后再把结果带回来。补偿流程启动时,先检查该步骤是否真的执行成功过:
- 如果pending和回调都不存在:判定为“未执行”,补偿直接标记为COMPENSATED(保证事务最终一致)。
- 如果只有成功回调没有补偿记录:正常执行补偿。
- 如果既有成功回调又有补偿记录:直接幂等返回。
悬挂问题则是另一个方向。某一步Agent执行超时后,调度官启动补偿,但Agent的操作请求其实只是卡在网络里,过一会儿又真实执行成功了。这时候补偿和成功回调同时在系统里跑,如果没有判断机制,库存会被释放两次,或者补偿成功后成功回调又把状态改为SUCCEEDED,状态机就崩了。
我们的解法是:因为每个Agent都有唯一的operation_id,补偿流程一旦启动,就先把该operation_id标记为“已补偿”;后续如果真的有成功回调到达,系统会先检查这个标记,发现已经被补偿了,就不再执行正向操作,而只记录一条“迟到成功回调”。
这一切的前提,都是状态机与数据库约束共同生效:任何一个状态修改,都要求当前状态符合预期的迁移路径。
5. 回滚踩坑实录:空补偿、补偿失败、乱序回执的三条排查链路
5.1 空补偿在优惠券场景的“灵异事件”
有一段时间,运维同事报了一个案例:用户取消订单后,收到的短信说“优惠券已退回”,但用户打开券包发现根本没有券,反而在后台日志里看到了一串奇怪的“扣券记录”。
排查链路是这样的。
第一步:定位到该订单的Saga日志,发现T3(核销优惠券)步骤状态是FAILED,补偿很快就跑了,C3补偿Agent也返回了成功,但问题是:T3真的执行成功过吗?
第二步:查agent_step表,发现T3的pending时间是有的,但SUCCEEDED回调时间却是空的。再看补偿日志,补偿Agent在本地查券的时候没带operation_id,只是按优惠券号去“冲正”,结果它把另一笔历史核销记录给冲掉了。
这个问题的本质就是空补偿:原操作没成功,补偿操作却执行了。修复方案是两层:
- 补偿Agent必须带上原操作的operation_id,并且只对同一operation_id做过成功核销的券做退回。
- 补偿Agent内部增加状态检查:先查原步骤是否存在SUCCEEDED状态,不存在则直接返回“无需补偿”。
5.2 补偿失败之后的死信队列:不重试比失败更可怕
补偿操作本身也会失败。库存Agent释放预占时,如果下游库存服务刚好在发布升级,接口直接超时,补偿流程会卡在COMPENSATING状态。
我们最开始的设计是补偿失败就立即重试5次,重试完还失败就标记FAILED,交给人工。上线之后发现这个策略还是太粗暴,因为有些失败是消息丢失导致的,有些是下游短暂不可用导致的,有些是补偿Agent本身的上下文缺失导致的。
后来我把它改成了一套分层策略,主要看失败类型:
- 可重试的临时错误(网络超时、下游503):进入重试队列,按指数退避重试。前5次间隔为1秒、3秒、9秒、27秒、81秒。
- 需要人工介入的错误(参数非法、上下文缺失、依赖外部人工审批):直接进入死信队列,发告警通知到运维群。
- 超过24小时仍然无法补偿成功的,进入“待人工巡检”表,每天定时任务扫描并提醒。
这个分层救了我们很多次。有一次物流Agent创建运单后返回成功,但订单Agent的补偿把它置为已取消后,物流Agent又因为重试机制把运单创建了一遍。如果当时没有按失败类型分流,这个重复创建可能会被无限重试放大。分类越清晰,后续排查越省力。
5.3 乱序回执引发的状态机死锁
这是整个项目里最让人头疼的一个问题。
场景是这样的:T2库存Agent超时了,调度官判定为疑似超时,发起对账确认。但就在确认过程中,T2的成功回执通过MQ到达了。正常情况下,成功回执会把T2状态从EXECUTING改为SUCCEEDED;但此时调度官已经决定把全局Saga状态改为COMPENSATING,于是状态机面临两个拉锯信号。
最开始我们的状态机引擎是“先到先得”,谁先更新状态谁就赢。结果就会出现:成功回执先把T2改成SUCCEEDED,然后把Saga推进到下一步T3,可这时候全局状态已经是COMPENSATING了,两个状态互相矛盾——Saga已经进入补偿流程,但步骤状态显示全部成功。这会让整个事务既不能正常执行完,又不敢轻易补偿,等于卡死了。
排查链路是这样的:
- 第一步,拉出该Saga实例的完整事件时间线,发现成功回执和补偿触发几乎同一秒。
- 第二步,查到事件顺序:全局状态先被改为COMPENSATING,随后T2的回执才把步骤改成SUCCEEDED。
- 第三步,确认问题根源是状态机没有做“全局状态与步骤状态一致性校验”。
修复方案是在状态机引擎里增加一条规则:当全局Saga状态已经进入COMPENSATING或COMPENSATED时,任何正向步骤都不能再从EXECUTING迁移到SUCCEEDED。如果出现了“迟到的成功回执”,一律把步骤标记为SUCCEEDED_BUT_LATE,由对账任务去核实是否要额外补偿。同时,补偿流程必须等待所有疑似步骤都完成最终确认之后才能执行真正的补偿动作,避免补偿动作和迟到的成功回执互相打架。
6. 工程化落地:表结构、对账机制与灰度开关
6.1 三张核心表:Saga实例、Agent步�骤、补偿记录
这套系统能不能长期稳定运行,数据库设计比Agent代码更重要。我用了三张核心表:
saga_activity表用于记录一次完整的事务执行:
sql复制CREATE TABLE saga_activity (
id VARCHAR(64) PRIMARY KEY,
biz_type VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
blueprint_json TEXT NOT NULL,
version INT NOT NULL DEFAULT 0,
actor VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
KEY idx_status_updated (status, updated_at)
);
saga_activity_step表用于记录每一个Agent步骤的执行状态:
sql复制CREATE TABLE saga_activity_step (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL,
step_name VARCHAR(128) NOT NULL,
agent_name VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
operation_id VARCHAR(64) NOT NULL,
compensation_id VARCHAR(64) DEFAULT NULL,
context_json TEXT,
result_json TEXT,
retry_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_saga_step (saga_id, step_name),
UNIQUE KEY uk_operation_id (operation_id),
KEY idx_status (status)
);
saga_compensation_log表用于记录补偿操作的执行历史:
sql复制CREATE TABLE saga_compensation_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL,
step_id BIGINT NOT NULL,
compensation_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
result_json TEXT,
retry_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_compensation_id (compensation_id)
);
operation_id是幂等关键,compensation_id则保证每次补偿操作本身也是幂等的。当补偿Agent收到请求时,先查compensation_log里是否已存在相同compensation_id,存在就直接返回成功。这个机制防止了补偿重试导致的双重释放问题。
6.2 对账巡检任务:不能只靠事件回执
Agent回执不可靠,所以必须有兜底的对账巡检任务。我们的定时任务每5分钟扫描一次所有处于WAITING_CONFIRM、RUNNING但超过步骤超时时间的Saga实例,然后调用各业务Agent的“查询执行结果”接口,确认Agent操作到底成没成功。
这个查询接口是Agent必须实现的基础能力。如果一个Agent不能查询自己的历史操作结果,那它就不适合接入Saga链。我在Agent接入规范里把这个列成了硬性要求。
对账的判定逻辑如下:
- 查询结果是成功:把步骤状态更新为SUCCEEDED,继续向前推进后续步骤。
- 查询结果是失败:把步骤状态更新为FAILED,触发补偿流程。
- 查询结果还是未知:继续保留WAITING_CONFIRM状态,重试次数加1,超过3次则上报人工。
6.3 灰度开关:让Saga和状态机先小范围跑起来
最后一点经验是:不要把整套状态机和Saga方案一次性在所有流量上打开。Agent本身的不确定性决定了这套系统需要充足的灰度验证期。
我们上了三个开关:
- 业务流量灰度开关:只对内部测试账号、定向白名单用户开启Agent Saga链路,其余用户继续走旧版事务流程。
- 补偿策略开关:灰度初期,所有自动补偿动作先降级为“只记录补偿计划,不真正执行”,由人工审核确认后再放开。这一步非常有用,它帮我提前发现了至少三个我没考虑到的补偿异常场景。
- 状态机严格模式开关:灰度初期可以打开宽松模式,状态迁移不合规只告警不强制拦截;跑一周后看告警数量,再切换成严格模式。这个开关帮我避免了一上来就因为状态机太严而把正常流量卡死。
这套系统上线三个月,处理了上万条Agent事务链,真正走到FAILED需要人工介入的比例不到千分之一。但说句实在话,即使到现在,每次看到“AI调度官”把一条复杂事务链干净利落地补偿回去,我还是会觉得这套状态机和Saga的组合,确实是Agent落地少有的、能让人睡个安稳觉的基础设施。
最后分享一个个人习惯:我工位旁边一直贴着一张状态迁移表,不是那种高大上的架构图,就是A3纸打印的九宫格状态表。每次线上出问题,第一步永远是去查当前Saga实例挂在哪个状态格子里。状态定住了,问题就解决一半了。这条经验,比任何框架选型都管用。
