资讯动态

Agent失败恢复:副作用、幂等与对账的工程实践

发布时间:2026/10/20 7:39:24 来源:尧图企业网站定制
Agent 又失败了。这句话我在好几个实际项目里反复听到。说这句话的人通常手里已经抓着一份满是 timeout、空返回、工具调用异常的日志然后做下一个动作时几乎一模一样点重试。我不止一次见过失败的 Agent 在被重试之后看起来“成功”了但用户那边收到了两笔扣款、两条通知、两条工单数据表里多出一堆重复记录。问题其实不在“要不要重试”而在“这次失败到底改了什么”。如果上一次执行只做了一半外部系统里的副作用已经发生那重试大概率会把副作用再放大一遍。Agent 出错以后的恢复策略本质上是三件事的博弈未知副作用、幂等设计、对账机制。三者共同决定一个操作能不能重试、要不要补偿、还是只能停下来等人工介入。这篇文章我想把做多步骤 Agent、流程自动化系统时踩过的坑和沉淀下来的一套判断方法完整讲一遍。适合正在搭多步骤 Agent 应用的后端工程师适合被“重试大法”坑过的数据一致性负责人也适合刚接触 Agent 编排、还没把失败恢复当成一等设计的同学。1. 重试不是安全网有时是连环雷先看清Agent失败时到底发生了什么1.1 把Agent失败先分清楚瞬时故障、逻辑错误、状态漂移Agent 的一次执行往往不是单次 API 调用那么简单。它可能先调模型再调工具再写数据库再发通知中间还夹着异步回调。表面看都是“这次执行没成功”但底层原因差得很远。只给所有失败统一贴一张“重试”标签是很多系统后面出大事的起点。我习惯先把失败分成三类每一类的处理逻辑完全不同失败类型典型表现直接重试的后果瞬时故障网络抖动、服务端 503、超时、限流大概率能成功但如果前序步骤已生效容易重复逻辑错误参数非法、工具调用结构不对、业务规则不满足会得到同样的错误浪费一次执行通常更贵状态漂移前置状态被别的流程改了Agent 拿着过期数据继续跑轻则操作无效重则覆盖别人刚更新的数据瞬时故障看着人畜无害但危险就藏在“前序步骤已生效”这半句里。逻辑错误相对好办因为它的执行往往没有真正触达外部系统重试只是白花钱不会造成数据混乱。状态漂移最麻烦因为它不是“这次代码写错”的问题而是整个运行的上下文已经不再成立这时候重试等于逼一个拿着旧地图的人继续开车。1.2 盲目重试为什么比失败本身更贵失败本身只留下一条失败记录代价是一次执行成本重试则会把一次失败变成一串互相纠缠的副作用。我举一个很常见的例子一个“下单 Agent”第一步锁库存第二步创建支付单第三步通知仓库。第二步超时了Agent 报错。到此为止系统只损失了一次尝试。如果操作者直接点重试会发生什么第一步锁库存可能又被执行一次——如果第一次没有幂等库存被扣两次第三步虽然第一次没跑但第二次跑成功了仓库收到通知而第二步在第一次执行时支付单到底创建了没有没人知道。接下来库存、支付单、仓库通知三份数据对不上每一份看起来都“成功”了。更让人头疼的是这种问题往往不在运行时立刻爆炸而是在对账、结算、库存盘点的时候才暴露。那时候已经离原始日志很远了你连当时发生什么都拼不完整更别说定位是哪一次重试搞出来的。按我的经验十次因为错误恢复引发的事故里有七八次不是第一次失败导致的而是“重试出去的那一下”导致的。1.3 用“执行签名”来判断这次重试允许不允许既然不能闭眼重试那能不能有一套机械化的判定方法我在项目里用的办法是给每次 Agent 执行构造一个“执行签名”。执行签名不是请求 ID而是把这次执行的输入参数、目标对象 ID、阶段状态、已登记副作用列表、失败信息这几样拼在一起做一个哈希。重试之前先拿签名去查上一轮执行里到底有哪些副作用已经被登记、哪些还处于“未知”状态。只要签名里出现“未知副作用”标记系统就拒绝自动重试把恢复权交给人或者对账流程。这套思路的本质是把“失败恢复”从一个凭感觉的动作变成一个有依据的决策。你在重试按钮上多花一天做设计换来的是将来少熬好几个凌晨。2. 副作用的未知性才是判断“能不能重试”的真正标尺2.1 每次 Agent 调用都在真实世界里留下了痕迹我一直跟团队强调一个观点Agent 不是纯函数。纯函数传入相同参数永远返回相同结果不会对外部世界造成影响。但 Agent 恰恰相反它每一步都可能触发写库、调用第三方接口、发消息、更新指标、产生计费事件。这些操作有一个共同特点一旦发生就无法假装没发生。问题在于一次失败了的执行并不等于什么都没做。最常见的形态是这样的Agent 调了一个外部接口外部接口其实已经收到请求并把业务办了但因为网络回包丢失Agent 这边看到的是超时。这种情况下系统里的真实状态是“业务已发生”但 Agent 的状态是“失败”。一个看起来失败的执行可能一半副作用都成功了。2.2 为什么说“副作用未知”是高风险信号错误处理里最怕的不是“确定失败”而是“结果不确定”。确定失败你知道该怎么补偿结果不确定你连补偿的依据都没有。外部接口超时就是典型的“结果不确定”。你只知道请求发出去了不知道对方有没有处理。这时候盲目重试有两种错误可能一是重复副作用——对方已经处理完你又发了一次二是遗漏副作用——对方其实没处理但你也不敢再发。两种错误方向相反处理手段也完全相反所以你必须先搞清楚到底属于哪一种否则盲目动作就是在赌。从系统设计角度看“未知副作用”意味着这条执行的日志、状态、事件流是不完整的。我在项目里的底线是一个执行如果出现了未知副作用默认当作高风险处理宁可停下来让对账流程去查证也不允许直接自动重试。一句话不知道做过什么的操作没资格重做。2.3 第一步把副作用登记成一张显式清单想管理副作用先得让副作用可见。最直接的做法是要求 Agent 所有对外的工具调用都统一走一个调度层不允许工具直接裸调。调度层在每次调用前、调用后各写一条副作用日志记录工具名、参数、目标对象、预计影响、实际结果状态。我举个例子一张副作用登记表大致长这样执行 ID工具名目标对象参数摘要预计影响实际结果exec_1001lock_inventorySKU_2233qty3库存预占 3成功exec_1001create_paymentorder_889amount299生成支付单未知exec_1001notify_warehouseorder_889...发送通知未执行这张表本身不解决任何问题但它是后续所有恢复决策的地基。有了它你才知道重试会带来哪些影响也才知道对账时要对着哪些记录去找差异。如果一个 Agent 系统连这样的登记都没有我会认为它还不具备做自动恢复的条件。2.4 重试前的副作用风险评估有了登记表下一步是给每种情况定恢复规则。我在项目里用的是一张简单的评估矩阵副作用状态幂等情况能否自动重试理由确认无副作用不涉及可以重试只是重新执行没有重复风险副作用全部成功全部幂等可以执行结果可重复重试不会放大影响副作用全部成功部分非幂等不可以非幂等部分会被重复执行副作用部分未知无论是否幂等不可以未知意味着无法决定补偿方向副作用部分成功有补偿手段允许补偿式恢复通过补偿操作把系统带回一致状态这张矩阵我会直接写进恢复引擎的判断逻辑里。说白了能让重试的窗口比大多数人想象的小但只有把这个窗口收窄系统才敢在别的地方放开自动恢复。3. 幂等设计做对了重试才算候选方案从请求幂等到业务幂等3.1 幂等的含义是同一结果不是同一时刻很多同学把幂等理解成“同一个操作执行多次返回内容一样”。这其实只说对了一半。真正的业务幂等是指无论请求执行多少次对外呈现的业务效果和第一次成功执行时一致。返回的报文可能不同但账不能多记一笔、库存不能多扣一次、邮件不能多发一封。有个生活化的类比电梯里的楼层按钮是幂等的——你按十次十楼电梯还是只上到十楼不会因为你多按几次就多跑一趟但一个开关灯的按钮不是幂等的——按一下开、按两下关多按一次效果完全变了。设计 Agent 的外部调用时要往“电梯按钮”方向努力。3.2 幂等键怎么生成、放哪里、怎么检验幂等设计的核心是幂等键。它不是一个随机字符串而应该由三部分构成请求发起方 ID、目标对象 ID、操作类型。比如同样是对订单 12345 执行“取消”同一家系统发起的取消请求幂等键必须是同一个如果是另一个业务方发起的就应该是不同的键。幂等键放哪里三种常见位置请求 Header、消息体、数据库唯一索引。我的建议是三处都放但以数据库唯一索引作为最终防线。不管 Header 和消息体怎么传最终写入时靠唯一索引兜底只要同一幂等键第二次进来直接拒绝这才算真正落地。检验流程要放进事务里。伪代码大概长这样def call_tool_with_idempotency(request_id, tool_name, payload): # 幂等键由 操作类型 请求方 目标对象 三者拼接 idem_key fidem:{tool_name}:{request_id}:{payload[object_id]} # 先查幂等记录命中就直接返回历史结果 previous idem_store.get(idem_key) if previous is not None: return previous, True # 执行工具调用并把结果和幂等键放在同一事务里写入 with db.transaction(): result tool_client.execute(payload) idem_store.set(idem_key, result) return result, False这段代码最重要的细节是幂等记录的写入必须和业务结果写入在同一个事务里。否则会出现一种尴尬情况工具调用成功了但幂等记录因为进程崩溃没写进去下一次重试又执行了一遍。事务把这两件事绑在一起要么都成功要么都失败。3.3 幂等没覆盖到的角落幂等设计不是万能的有几个角落它天然覆盖不到。第一个是跨工具的组合操作。单个工具幂等很容易但 Agent 的一次执行往往要调三四个工具它们的组合未必幂等。比如先判断库存、再锁库存、再下单这个组合整体跑两遍结果就乱套了即便其中每个工具单独都是幂等的。第二个是时间语义相关的操作。像“按当前市场价买入”这种操作天然每跑一次价格都不同给它的执行做幂等没有意义重试就等于一次新交易。对这类操作唯一正确的手段是承认它不可重试失败后直接进入人工或对账流程。第三个更难处理第三方接口不支持幂等键。你可以自己生成幂等键但对方接口设计里根本没有这个参数它每次收到请求都会执行一次业务。这时你再怎么设计也没有用唯一能做的是用上一节说的副作用登记和对账手段去发现重复、再去补偿。4. 对账是最后一道兜底系统不会自动恢复但可以对账找回秩序4.1 对账的本质用审计流水还原事实幂等做得好能挡住大部分重复副作用但总有漏网之鱼第三方不支持幂等、状态漂移、人为误操作。这些情况里系统靠自动重试已经无能为力了唯一能做的是先把发生过的事搞清楚。对账不是修复动作而是一个还原动作。它的输入是所有执行留下的审计流水输出是一份“账实差异清单”。我在系统里要求每个 Agent 执行都落审计事件内容包括谁调了什么工具、传了什么参数、返回了什么、执行当时的业务状态快照。这些事件只追加、不修改是对账时唯一可信的事实来源。4.2 对账窗口、差异分类与告警阈值对账要有节奏。我们一般把对账窗口设成两档实时对账针对高风险操作比如支付、退款用消息队列触发几秒内完成比对批量对账针对普通操作每小时或者每天跑一次。有了对账窗口还要给差异分类。我见过的差异无非这几类差异类型含义处理方向缺失Agent 认为没执行外部系统其实执行了以外部为准补登记或补状态重复Agent 认为执行了外部系统执行了多次发起补偿操作撤销多余部分金额不一致双方记录的数值对不上拉出原始流水逐笔核对状态悬挂执行停留在中间状态没有终态按业务规则补一个终态迁移告警阈值不能设死。我习惯的做法是差异量为零时不打扰差异量小于 1% 时记录并走日结超过 1% 或者出现金额差异时立刻升级成工单进人工。阈值还要根据业务量动态调整大促时期的绝对值和平时绝对值不可同日而语。4.3 从对账结果反推补偿操作对账本身不产生修复修复靠它给出的结论。某个“发送邮件”Agent 的执行日志显示“已发送”但邮件服务商的投递日志里没有这条记录对账就会把它标记为“缺失”结论是“需要补发”。反过来如果两边都显示已发送但用户收到了两封一模一样的邮件对账把它标成“重复”结论是“无需处理或发送撤销”。这个“对账 → 结论 → 补偿”的闭环是整套恢复体系里最关键的一环。它不追求实时恢复但能在事后把系统带回一致状态。对账驱动的补偿比盲目重试优雅得多因为它每一步都有据可依不会产生新的操作风险。4.4 对账脚本的实践要点最后说几个做对账时的落地细节。第一对账要离线跑不能和线上主流程抢资源第二对账基于的审计事件流必须不可变所有日志只能追加不能修改否则对账结果本身就不可信第三对账任务本身要幂等同一个差异清单跑两遍不能生成两份补偿工单第四对账结果必须持久化因为补偿操作可能要跨天才能完成中间需要有人能查到这个差异一直在被跟进。5. 恢复行动的分级决策能自动重试、能补偿、还是要等人工5.1 把风险分成三档低、中、高对不同恢复动作的取舍核心指标只有一个风险等级。我把风险简单分成三档每档对应一套默认动作。风险档典型场景默认恢复动作低纯读操作、副作用全部幂等自动重试指数退避上限 3 次中有副作用但可补偿如通知、状态流转走补偿流程通过反向操作修正高副作用未知、涉及资金、对外已宣称结果暂停实例人工介入对账后决定分档的原则不一定非要搞多复杂关键是每一档都要有明确的默认动作不能在线上临时讨论。比如资金这档我的默认规则就是暂停。宁可让人晚十分钟处理也不能让自动恢复把问题放大。5.2 不同档位对应的恢复动作低风险的自动重试我建议用指数退避加抖动避免所有失败实例在同一个时间点一起打向外部系统。参数可以参考这样第一次 1 秒第二次 2 秒第三次 4 秒最多三次超过后直接转人工工单。中风险的补偿流程要提前为每个工具写反向操作。比如“锁定库存”的反向操作是“释放库存”“发送通知”的反向操作是“发送一条撤稿或更正说明”。补偿操作同样要走幂等设计否则补偿本身也会出问题。高风险的暂停不是简单停掉一个任务而是要把相关 Agent 实例收进一个“半自动隔离区”它不再对外发起调用但内部状态保留等人工确认后选择继续、回滚或补偿。5.3 该不该加熔断和降级失败放大效应的第一道闸门重试策略里最容易被忽略的一个问题是失败放大。一个 Agent 失败如果同一批请求都在重试外部系统可能瞬间收到几倍流量触发新的限流和故障形成雪崩。所以无论低风险还是中风险自动重试都必须在熔断器保护下运行。熔断器的参数我习惯按下面的思路设置连续 N 次失败就打开熔断进入不再调用的“打开”状态过一个冷却窗口后放少量请求探测外部系统是否恢复恢复则认为“半开”否则继续保持熔断。N 的取值通常设为 5 或 10冷却窗口设为 30 秒具体要看你的业务对延迟的容忍度。这个机制不需要每次失败都人工干预但能把失败的影响范围限制住。5.4 决策表一个可实际部署的恢复策略示例结尾放一张我真正在项目里用过的决策表场景是一个包含“扣库存→创建支付单→发送通知”的多步骤 Agent。以下决策逻辑可以直接抄进恢复引擎失败场景副作用指纹幂等状态推荐动作扣库存超时无外部副作用幂等键命中指数退避重试最多 3 次创建支付单超时可能已生成支付单有幂等键不重试直接查询幂等键对应的支付单状态发送通知超时发送状态未知第三方不支持幂等不重试标记待对账人工确认后补发资金转账失败部分划转幂等键缺口熔断并暂停 Agent人工作业后补偿这张表最有价值的地方不是每个场景的具体结论而是它逼着你在出事之前把每种可能的失败预处理了一遍。真到了凌晨三点出故障的时候你不需要临场决策只需要按表执行。6. 我在实践中的体会重试文化、度量指标和团队协作6.1 团队普遍犯的错误是默认 Agent 可重试我观察到一个普遍现象很多团队把重试按钮做得特别显眼却把副作用日志做得非常敷衍。运维同学点一个“重跑”很容易但想知道这个 Agent 上次到底对哪些外部系统做过什么却得翻好几个系统才能拼出来。这两件事的投入程度完全反了。还有一个常见的思维惯性把 Agent 当成普通的、无状态的函数来处理。普通函数失败重试一百次都没关系因为它不改世界但 Agent 不一样它对世界有影响所以每次重跑都得有据可查。团队里最危险的对话就是那句“重试一下应该就好了吧”。我一般会要求给出这个判断的同时必须附上副作用清单和幂等说明拿不出来就不允许点重试。6.2 度量指标把重试率、失败率和对账命中率都展示出来恢复体系做得好不好不能只靠感觉。我建议团队至少盯三组指标。第一是执行失败率区分瞬时故障和逻辑错误观察整体稳定性。第二是自动恢复成功率也就是失败后通过重试或补偿最终处理成功的比例这个数能看出你的恢复策略有没有价值。第三是对账差异率这个数直接反映系统里有多少隐藏的不一致它比失败率更值得关注因为它是事后才发现的问题。这三组指标要放在同一个看板里。曾经有一次对账差异率突然上涨顺着差异清单查下去发现是一个 Agent 在重试时不带幂等键导致一批发送操作重复执行。如果没有对账指标这个问题要等用户主动投诉才会暴露。监控的目的不只是报警而是让这些“隐藏账”尽早显形。6.3 给一线开发者的落地清单最后分享一份我从项目里总结出来的落地清单跟着做基本不会偏把 Agent 的所有外部调用统一收口到调度层禁止工具裸调让副作用先可见。给每次执行生成执行签名重试前必须比对签名里的副作用状态和幂等情况。对所有写操作设计幂等键并且把幂等记录和业务结果放在同一事务里写入。对高风险操作默认不重试直接进入人工或对账流程。给自动重试加退避和熔断宁可慢一点也不能放大失败流量。建立审计事件流对所有执行留痕让对账有可靠的事实来源。设计定期对账任务让差异清单自动生成、自动升级而不是靠人工发现。老实说这一整套东西做起来并不性感也没有“让 Agent 能力变强”的爽感。但它决定了你的 Agent 系统在真实业务里能不能站得住。我以前也痴迷于让 Agent 跑出更复杂的推理后来被几次夜间事故教育之后才明白一个可以安全失败的 Agent才配谈自动恢复。我现在接手一个新的 Agent 项目第一天先不问它的能力有多强而是先问如果它失败了我们知不知道它刚才做过什么把重试按钮藏起来把审计日志放到首页。这套东西看起来没那么炫酷但在半夜出故障的时候它是唯一能让人安稳处理问题的底气。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑