Agent故障恢复实战:重试陷阱、幂等设计与对账机制

发布时间:2026/10/10 9:34:47
Agent故障恢复实战:重试陷阱、幂等设计与对账机制
1. 重试的本能是错的从一次重复扣款事故说起1.1 确定性消失之后重试就从救生圈变成了绞索做后端开发的人对重试这两个字都有本能的好感。网络抖动、连接池用完、下游超时代码里只要写了重试很多偶发故障就自动消失了。这种习惯长期成立是因为传统代码的每一次调用都是确定的同一份代码、同样的入参执行路径几乎可以预期重试一百次结果也一样。即使某次调用真的改了什么东西我们也能靠代码逻辑反推出来。但到了Agent场景这个前提整个垮掉了。Agent的每一步动作背后是模型生成的结果不是确定性的计算。同样的系统提示词、同样的上下文两次运行生成的工具调用未必一致。更麻烦的是一个Agent在工作流的中间步骤失败后它自己能感知到的信息极其有限往往只有一个超时异常或者一个状态码。此时人类最自然的反应是让它再试一次而Agent最大的风险恰恰在于它根本不知道上一次动作到底有没有在外部系统中产生影响。我见过一个最典型的案例。某个自动化客服Agent负责处理退款业务它调用支付通道接口时遇到了超时于是系统层自动重试了一次。第一次调用其实已经成功扣款但由于响应包在网络中丢失Agent误判为失败重试导致用户被重复扣款。事后排查发现支付通道那边明确有订单号但是Agent每次重试都重新生成了订单号——因为它的重试逻辑是重新执行这个步骤而不是补查上一次的结果。这就是Agent 出错以后不能只重试这句话的真实含义。传统代码的重试是在一个确定性世界里重新执行确定性的逻辑Agent的重试是在一个充满未知副作用的世界里盲目重复一个可能已经产生后果的动作。重试在这里不再是救生圈而是一根绞索。1.2 三类动作的重试安全级完全不同把Agent的动作拆开看其实可以分成三类每一类面对重试的态度都不同。我根据自己的实操经验给它们做过一个重试安全度分级动作类型典型例子重试风险安全度只读查询类查订单状态、查库存数量、拉取配置几乎无副作用重试只是增加负载高可以放心重试写入但可覆盖类更新缓存、保存草稿、写入日志重复执行可能导致数据被覆盖或产生冗余记录中需要幂等保护创建/提交/扣款类下单、退款、转账、发送通知邮件重复执行极可能造成重复扣款、重复下单、重复发信低严禁盲目重试这个表看着简单但真到Agent执行引擎里去落地困难就出现了Agent自己往往无法判断当前这个工具调用到底属于哪一类。它不是人不会像我们一样看到这是扣款接口不能随便重试。它只知道自己调用失败了需要想办法完成目标。这引出了一个关键结论重试策略不该由Agent在执行时临时决策而应该在工具注册和服务编排时就明确写好。每个工具暴露给Agent之前就要给它标注清楚该动作是否允许自动重试、重试前是否需要先执行状态确认而不是把一个裸的API交给Agent让它自己赌运气。2. 未知副作用恢复决策的真正分歧点2.1 未知才是问题而不是副作用副作用这个词并不新鲜。任何有外部I/O的操作都有副作用关键其实在于它是否已知。如果你知道某个动作在超时时可能产生副作用并且能查询、能抵消那它就不再是危险的而是可控的。真正让Agent恢复变得复杂的是那些不知道到底有没有发生的副作用。举例来说一个Agent调用文件上传接口超时了。此时文件有没有传到对象存储如果传了一部分是否留了残缺对象这些信息Agent不知道。更麻烦的是如果上传接口设计得不好它甚至不能通过查询接口确认。这种状态下任何自动恢复策略都是在盲人摸象。所以我在设计Agent故障恢复机制时第一个动作不是搭重试框架而是清点所有被调用的外部系统逐一确认一个问题如果调用超时我能不能通过一个独立于调用链的查询接口拿到这笔操作的确切结果拿不到确切结果的一律视为高不确定性操作严禁自动重试。这个独立于调用链的查询很重要。因为Agent原来的调用链本身可能已经断了或者返回了误导性的错误码继续沿着这条链查是查不出真相的。必须用另一条路比如直接查数据库里的订单记录、直接调支付通道的对账单接口才能还原出事实。2.2 三种副作用的处置策略把副作用按可观察性和可抵消性分成三类是我目前觉得最实用的分类法。它直接决定了恢复动作的下限。第一类可探测副作用。特征是操作会留下可查询的痕迹Agent可以通过其他接口确认它到底做没做。例如发送记录表、日志条目、订单状态字段。这类副作用恢复策略很清晰先查查清楚了再决定是否重试。第二类可补偿副作用。特征是即使副作用真实存在也可以用另一个操作把它抵消掉。典型例子是创建订单失败但生成了草稿记录这时补偿动作就是删除草稿。补偿型副作用在业务系统里非常多也是最值得做预案的一类。每个高风险的写入型工具都应该在注册时附带一个补偿动作配置就像数据库事务里的undo操作。没有补偿方案的工具从源头上就不该让Agent调用。第三类不可逆副作用。发出去的邮件、已完成的转账、物理世界里的机械动作都算这类。一旦执行成功无论带来的是不是预期结果都无法通过程序自动回到上一状态。对这类操作恢复策略只有一条人工介入接受现实尽量减少损失。任何自动化重试在这里都应该被硬性禁止只能走人工审批流程让人类来评估这一步要不要继续。我在一个模拟项目X里做过一个测试让Agent同时处理三类动作结果非常明显凡是预先把工具标注了副作用类型的故障恢复的平均时间基本稳定凡是没标注的几乎全部在重试中把副作用放大了一轮。标注这件事本身不复杂但它是最容易被忽略的一步。3. 幂等不是重试的前提而是恢复机制的地基3.1 真幂等与看起来幂等之间隔着一个查询接口说到幂等很多人第一反应是同一个请求执行多次结果都一样。这个概念本身没错但落地时偷懒的人太多。最常见的偷懒是把接口设计成如果已经处理过就返回成功但判断是否处理过靠的是内存里的一个Set。服务重启之后这个Set没了同一笔请求再进来就被当成新的。这哪是幂等这叫假装幂等。真幂等必须至少做到两点。第一判断依据必须持久化不能依赖进程内存。第二必须给客户端也就是Agent提供一个手段让它用一个稳定不变的业务键去映射每一次操作。Agent在执行一个动作之前先生成一个全局唯一键这个键从请求发出到补偿、对账全程不变服务端用这个键判断请求是否重复重复时直接返回第一次执行的结果。在Agent场景里经常出现一个很隐蔽的问题Agent的执行引擎在不同轮次之间重新生成了请求ID。每轮失败后重启执行就生成一个新的ID上游服务根本不认识这个ID自然无法判断它是不是重试。两笔扣款就这么产生了。正确做法是让请求ID的生成时机放在任务编排最高层而不是放在Agent每次工具调用的函数里。整个工作流只有一个ID所有子步骤共享它这才叫端到端幂等。3.2 外部系统的幂等Agent自己是控制不了的这里必须泼一盆冷水即便Agent这边把幂等键管理做得完美如果第三方系统不支持幂等整个链路依然不保险。很多外部API是你传一个幂等键我就记住重复调用只返回原结果但也有不少API根本没有这个能力接入的时候要特别确认。我踩过一次坑。某个短信通知服务不提供幂等键Agent在超时后重试结果用户收到了两条一模一样的短信。事后我们想通过消息内容哈希去重来补救但因为两条消息内容完全相同查询接口又暴露不出消息ID根本没法清理。这件事之后我把所有外部依赖分成了两类支持幂等键的才允许Agent自动重试不支持的统一标记为重试前必须先通过查询接口确认是否已送达没有查询接口的就直接转人工。所以评估一个Agent能否安全恢复其实是在评估整条链路的幂等能力而不是只看Agent自己。用到幂等这个词的时候建议大家心里自动补一句端到端幂等——从Agent的决策层到工具调用层再到外部服务的存储层每一层都要有对应的幂等机制缺一段都不算数。3.3 读-改-写模式是幂等的隐形杀手还有一种幂等陷阱特别容易被忽视那就是先读取再修改再写回的操作模式。Agent在处理业务流程时经常需要先拉取一个资源的当前状态基于这个状态做出判断然后把判断结果写回去。如果中间发生了重试第二次执行时会重新读取最新状态——注意这个最新状态可能已经被第一次执行改过了。于是同样的业务逻辑在新状态下会得出不同的结论操作自然不再幂等。举一个生活化的例子。一个Agent管理待办清单它收到指令把第一个未完成任务标为完成。第一次执行读取列表第一个任务是A把A标记完成写入成功。但响应超时Agent重试。第二次执行读取列表第一个任务已经变成B于是B被误标为完成。从接口层面看每次执行都只操作了一次完全符合幂等定义但从业务结果看这份清单的状态被搅乱了。这类问题靠加幂等键解决不了必须从操作语义上规避。我的做法是凡是基于当前状态决策的动作都要求Agent在决策时把读取到的状态连同业务判断一起写入操作记录重试时不重新读取状态而是先核对之前操作记录。通俗讲就是以第一条记录为准而不是以最新状态为准。这在设计上更重但换来了真正的确定性和可追溯性。4. 对账恢复的真正指挥中心4.1 对账解决的是重试决策的信息缺失前面讲了未知副作用和幂等但这两个能力即使都到位Agent仍然缺一个东西全局视角。它知道自己想做什么也知道自己的动作有没有被记录下来但它不知道整个业务流程的期望状态和实际状态之间的差距。而差距定位就是对账要干的事。我理解的Agent对账分两层。第一层是操作层对账每次工具调用之后Agent主动查询外部系统的状态确认实际结果与预期是否一致。第二层是业务层对账整个任务执行完毕之前Agent把当前所有相关实体的实际状态与业务预期目标比对看看有没有漏掉的子任务、有没有重复执行的动作、有没有残留的中间态。举个例子一个供应链Agent负责完成采购订单并同步库存。它的操作层对账是调用创建订单接口后回来主动查询订单是否已创建成功同步库存后查询库存快照。而业务层对账是在任务收尾前检查订单状态是否为已确认、库存是否减少对应数量、是否有未处理的失败回调。这两层对账结合起来才能形成一个完整的恢复决策依据。4.2 最后一步前后盘点法说到业务层对账我想分享一个非常实用的技巧我把它叫做最后一步前后盘点法。核心思路是在多步骤业务流程里有一个最后一个关键动作比如支付、提交订单、发布配置在执行这个动作之前强制Agent停下来做一次全面盘点执行完成之后再做一次全面盘点。两次盘点结果比对可以快速发现异常。为什么卡在这个节点因为最后一步往往是不可逆操作的分水岭。之前的动作大多可以补偿之后的错误往往代价巨大。盘点本身用什么手段呢我建议用实体状态对照表让Agent把涉及到的每个业务实体列出来标注期望状态和实际状态一列一列看。有一次一个Agent在处理完一个迁移任务后复盘时发现目标环境里的一个配置文件居然还没被更新但任务逻辑显示已完成。就是因为盘点表上配置文件版本这一栏实际状态填成了未知触发了人工介入才避免了带着旧配置直接上线。这种方法本质上不是Agent自带的技能而是工程上强制规定的一个执行节点。Agent本来就是指令跟随者你给了它这个检查点它就会严格执行。不给它它根本不会主动想到在提交之前再核一遍。4.3 独立审计线索别让Agent拿自己的日记本当证据对账的另一个要点是审计线索必须独立。什么意思呢如果Agent判断上一步执行成功依据是它自己写进日志里的我成功了四个字那这个判断就只能用一个词形容自说自话。因为Agent的日志本身可能就是不完整的、甚至被错误生成的。独立审计线索指的是从外部系统直接获取的客观事实比如数据库里的订单状态、支付通道的对账单、对象存储里的文件列表。我在做某跨平台系统的故障恢复组件时特意为每个Agent配了一份外部事实清单。清单上列着每个关键动作应该去哪个外部系统查什么字段而不是去Agent自己内存里翻记录。再配合上操作前记录预期状态、操作后记录实际状态的对比整个恢复流程才真正有了依据。对账还有一个容易被忽略的作用可以用来反向发现重试导致的副作用。一次自动重试后把重试前后的两次数值摆在一起看如果出现了不该出现的差值那基本可以断定重试产生了额外副作用应立即冻结该工作流并转人工复核。这种能力靠重试框架本身是长不出来的必须专门设计。5. 恢复决策应该是个分诊过程不是布尔判断5.1 把恢复路径分成三色通道现实中的故障恢复很少是要么重试、要么不重试二选一。更合理的做法是设一个分诊机制根据前面积累的信息把故障归入三个通道之一。我倾向于用红黄绿三种颜色来表达绿色通道允许自动重试。条件是该动作明确只读或已经完全确认副作用并能够做到端到端幂等。重试前最好还能做一次前置状态确认确认无事发生再重试。黄色通道允许自动补偿或有限重试。条件是副作用可探测且可补偿。如果重试失败转入补偿流程补偿也失败再升级人工。红色通道禁止所有自动化恢复动作。条件是动作存在不可逆副作用或副作用状态未知。此时Agent唯一被允许的动作是停止并请求人类介入同时提交一份尽可能完整的对账报告。这套分诊体系看着机械但好处非常明显它把能不能恢复这个问题从技术层面剥离出来变成了一个配置和管理层面的问题。只要在工具注册阶段把每个工具的分诊策略写清楚运行时的Agent就不需要做复杂判断直接按规则执行。5.2 补偿回滚并不等于反向调用一次黄色通道里的补偿操作最容易踩的坑是把它理解成反向调用一次。比如创建订单失败就调用删除订单扣款失败就调用退款——听起来顺理成章但真实世界里这个对应关系经常不成立。我举一个退款的例子。假设Agent先创建了一笔订单然后执行扣款扣款后订单状态需要更新但更新调用超时了。此时补一笔退款确实能把钱退回去但订单本身处于什么状态是否被用户看到是否触发了后续发货逻辑如果这些不知道就直接退款就可能出现订单显示已完成、钱却退了的怪状。补偿操作必须是一个仔细设计的逆流程要按正确的顺序执行每一步还要重新对账不是简单地把原操作取反。再退一步说补偿流程本身也需要幂等。如果补偿执行到一半又失败了怎么办我见过不少团队给补偿流程留了重试但补偿动作没有自己的幂等键结果重复执行了两次退款账又乱了。补偿和正向操作一样需要完整的幂等与对账设计它也是一个一等公民流程而不是副驾驶位上的工具。5.3 人工介入不是失败而是恢复机制的高阶形态很多人潜意识里觉得出了事要人工处理就是系统不够智能甚至会在产品设计时拼命压缩人工介入的入口。但在Agent场景里我反而认为人工介入是恢复机制的一部分而且是不可省略的一层。当一个故障所涉及的副作用不可逆或信息不足无法判断时人类介入是唯一还能保住底线的方式。关键是人工介入也要设计得顺畅。Agent不能只丢一句我失败了请处理而是应该在停下之前完成它所有能自动完成的工作总结发生了什么、列出已知事实、标注不确定点、给出建议选项比如你可能需要人工确认是否退款。我在某公司搭过的一个人工复核队列就是这样Agent在触到红色通道后自动生成一份结构化工单附带上对账结果和候选操作列表人类只需要做判断题而不是重新做一遍调查。这套流程上线之后处理一个故障的平均时间从小时级降到了分钟级。把人工介入看成分诊流程里的最高级别处置而不是自动化的失败整个恢复机制的心理负担和实际压力都会小很多——Agent可以更大胆地执行低风险操作因为系统知道最坏情况下还有人工托底。5.4 多智能体团队里的对账与恢复人其实是共享的我最后想强调一个观察。越来越多的工作流开始使用多个Agent协作比如一个负责规划一个负责写代码一个负责测试或者一个负责订单处理一个负责物流同步。在这种多个智能体里每个Agent的容错判断不能只看自己的局部状态。这时候恢复机制的核心就不是单个Agent的重试策略了而是Agent之间如何对账。我的做法是给每个Agent分配一个对外可见状态页其他Agent在执行依赖动作之前先读取这个状态页而不是直接信任一次调用结果。一个Agent失败了它更新的状态页会反映出来下游Agent能感知到这个失败就不会盲目地延续错误。这样在错误传导之前就形成了隔离。有过一次挺有意思的实测某图像处理Demo系统里A Agent负责下载图片B Agent负责处理图片。A下载超时后悄悄重试了一次恰好成功了但处理模块这边因为等待超时已经自行失败并进入补偿流程。两边各干各的结果图片被处理了两次。后来我们在两个Agent之间加了一个共享任务状态表统一记录这个文件应该被处理几次、当前已经处理几次这个问题立刻消失了。多Agent的恢复本质上不是让每个Agent都更聪明而是让它们共享一份可靠的事实记录。我在实践中最深的体会是重试按钮要放在所有恢复手段的最底层而不是最前面。真正支撑起出错后能恢复的是整个系统对未知副作用的管理能力、端到端的幂等设计以及永远不离场的人的对账检查。这三件事做好了Agent犯错的代价就不是灾难而只是一次值得分析的日志。