前阵子我们的某个Agent调度系统在生产环境上让我印象特别深同一个任务被自动重试了七次最后不仅没成功还把下游数据搞得一塌糊涂。当时团队里第一反应是“重试策略有问题把重试关掉就好了”但冷静下来一看真正的问题不是“该不该重试”而是我们根本不知道一次重试到底会带来什么影响。做Agent落地的人应该都有类似体验LLM调用不稳定工具执行偶尔超时网络抖动说出现就出现。于是很多人第一反应就是“加个重试”这确实是最常见、成本最低的容错手段。但Agent和普通接口不一样它每一次执行都有状态变化、上下文累积、外部副作用无差别重试的结果往往不是“再试一次就成功了”而是“试了N次错得更离谱了”。这篇文章我想把这套问题的根源掰开揉碎结合我的实际调参和排障经验说说Agent重试为什么会在某些场景下“越帮越乱”以及我们自己总结出来的一套相对可靠的重试治理思路。适合正在做Agent工程化、任务编排、自动化工作流的同学们参考。1. 重试不是万能药先从一次线上事故说起1.1 事故现场同一个任务Agent跑了七次那个出问题的任务本身不复杂Agent需要从某业务系统读取一批数据做目录整理和分类打标最后把结果写回数据库并通知下游。事情发生在凌晨的一次批量调度里Redis短暂抖动导致Agent在调用某个工具时拿不到响应。我们当时的重试策略很简单只要任务状态是“失败”就整体重试最多重试3次。但因为批量任务里每个子任务都各自计数实际跑下来最后这个任务被不同层级的重试机制加起来执行了七次。七次意味着什么Agent每次都重新读取数据、重新做分类、重新写库。因为LLM的非确定性每次的分类结果不完全一样后一次执行覆盖了前一次的部分产出。有些数据被打标两次、有些被改来改去更麻烦的是下游通知接口每成功一次就发一条消息下游系统收到了七条内容还互相矛盾的消息。用户看到的结果就是“这个Agent是不是脑子有问题反复横跳还乱发通知。”我们当时没有立刻关掉重试因为一旦关闭网络抖动等瞬时故障就没法自动恢复了。但保留原策略又意味着继续产生脏数据。最后只能先停线、手动回滚、补数据花了整个上午才恢复。从那次之后我意识到Agent的重试问题需要我们换一种完全不同的看待方式。1.2 为什么团队下意识选择“加重试”来解决我要先说一句公道话重试本身没有错错误的点在于我们用了“接口思维”来处理“任务执行”。传统接口的重试假设场景是“这次请求失败了我们再发一次同样的请求”。在无状态、幂等的接口世界里这个假设基本成立。但Agent不一样Agent执行一次任务包含多次LLM推理、多次工具调用、多次内存状态更新。每一次执行都不是简单重复上一次的输入而是基于新的上下文重新做决策。所以当下意识“加个重试”时实际掩盖了一个关键问题我们根本不了解失败了之后系统停留在什么状态。是停在第一步没开始是执行到一半工具返回了超时还是已经写到数据库但Agent自己以为失败了这三种情况对应完全不同的处理手段。第一种可以放心重试第二种需要先判断中间步骤的外部副作用再决定是否续跑第三种则绝对不能盲目重试否则就是重复提交。而大部分团队在初始阶段都懒得区分直接封装一个“失败就重试N次”的通用逻辑。这个逻辑在Demo、在演示环境不会出大问题但一上生产、一接真实下游各种“越帮越乱”的戏码就全来了。2. “越帮越乱”的三个核心机制非确定性、副作用与上下文污染2.1 非确定性同一条路径两次执行走出两条路LLM本身是概率模型同一个Prompt、同样的参数采样结果几乎不可能逐字一致。这本身不是缺陷但在重试场景下就被放大了。假设一个Agent在第一步做“数据分类”第一次运行时判断某个项目属于A类执行到写库时报错。重试之后Agent重新读取数据基于当时的上下文可能判断它是B类。从任务视角看两次都是“成功完成了分类”但结果是同一个数据在两个表里各出现了一次或者后一次直接覆盖了前一次的结果。更有意思的是重试之前的错误信息也会被Agent带入上下文。如果我们在重试时把上一次的报错原封不动地塞回提示词里Agent很可能会为了“修正”上一次的结果而做出额外动作。表面上它在干活实际上它在给一个它自己都没完全搞清楚的烂摊子做收拾。这就是非确定性带来的第一层麻烦我们不能假设“重试重新执行同一件事”而必须承认“重试重新做一次可能产生不同结果的决策”。2.2 副作用重复执行不是“试错”是“闯祸”第二层麻烦更实际Agent执行过程中的工具调用可能已经产生了外部副作用。举个例子Agent的任务是“调用支付接口给用户退款”。第一次调用时接口实际上已经处理成功了但因为网络原因响应没有返回Agent侧看到的是超时于是判定失败并触发重试。第二次调用又成功退款了一次。用户收到了两笔退款。这个例子比较极端但类似场景每天都在发生发邮件发了两次、写工单建了两条、状态流转改了两遍。很多做工程的同学第一反应是“那就在工具层做幂等”这当然是对的但问题在于你能控制自己写的工具控制不了下游所有系统。第三方接口未必支持幂等键老系统的接口也不一定接受请求ID。一旦工具层幂等做不彻底重试的风险就完全暴露出来了。我们需要意识到Every retry is a new execution它不仅重新跑一次LLM推理还会重新触发所有已经跑过一遍的外部动作。这也是“越帮越乱”最直接的表现。2.3 上下文污染错误日志喂出了更差的模型判断第三个机制相对隐蔽但对结果的影响同样致命就是上下文污染。Agent的状态存放在上下文中重用上一次的上下文会导致Agent认为“上一次的任务还在进行中”。如果我们在重试时保留记忆Agent会带着上一次失败的挫败感继续操作它可能会尝试去“修复”第一个错误但这个修复动作往往基于错误的推测。比如我见过一个Agent在重试时因为看到了上一次的超时记录居然自动去关闭了某个服务理由是“它觉得可能是服务阻塞了”。另一种上下文污染来自重试日志。一些工程实现会把“上次调用tool X失败”直接作为一条新的消息插入到对话记录中。模型看到这条消息后会把它当作用户需求的一部分来响应而不是当作一条系统级的故障信息。于是它的行为从“继续完成原任务”变成了“先处理这条错误消息”思路被带偏产出自然越来越乱。所以“上下文要不要保留、保留多少、重试时如何标记状态”在Agent工程里是一个绕不开的设计议题。这是普通接口重试完全不会遇到的场景。3. 踩坑实录一次订单类Agent重试引发的完整排查链路3.1 从“偶发失败”到“必现错乱”的复现路径说回我们自己的经历。事故暴露后我们并没有立刻改代码而是先复现问题、定位根因。复现路径想了很多办法最终是这样一个过程第一步把重试次数从3改成1跑同样的批量任务发现偶发失败还在但“错乱”明显减少了。这说明错乱和多次重试强相关。第二步把重试次数改回3但在每次重试前清空Agent的执行内存只保留任务元数据再跑同一批任务发现错乱基本消失但覆盖率上有些任务在重试时确实重新完成了整体结果变干净了很多。第三步分别在“保留上下文重试”和“清空上下文重试”两种模式下收集Agent的中间推理过程对比发现保留上下文时Agent会在第三次、第四次推理中频繁提及“上一次执行时出现了XX错误”然后主动去调整策略而清空上下文时Agent就像一个新人按部就班地重新执行任务没有额外的“修复动作”。这三步走完基本可以确认错乱的主要来源是非确定性行为 上下文残留的组合效应而不只是简单的重复执行。3.2 逐步排查日志、快照、调用链三者对照排查过程中我们踩过的另一个坑是日志太分散很难把几次重试串联起来看。建议团队在做这类Agent时把“任务执行轨迹”构建成一条完整的大日志包含每次重试的重试原因、触发层、当时的上下文摘要、已完成的工具调用序列、实际副作用记录。当时我们用了一个不算复杂但很有用的方法给每个任务建一个执行快照目录里面有task.json任务元数据、memory.jsonl每次工具调用后的核心状态、tools.log所有工具调用的入参出参。排查时把三次重试的任务ID拿出来逐个对照这三个文件就能看出每次执行哪里分叉了。这种排查方式比单纯看Prompt日志靠谱很多。因为Agent的重试问题往往不是单个模型输出问题而是多个环节的组合结果零散日志很难支撑全局分析。3.3 根因定性重试不背锅背锅的是无差别重试整个排查链路走完我们的结论其实非常朴素重试机制不是问题的根源根源是我们没有区分“可安全重试”和“不可安全重试”的场景。对一次幂等查询类的工具调用重试十次都没问题。对一次有副作用的写操作重试二次就是在制造脏数据。对一个在错误上下文里继续推理的Agent重试只是在给它更多机会犯新错误。从那之后我们把团队的容错设计原则改成了“先定位再分类最后决定是否重试”。这个原则听起来简单但落到工程实现上牵扯到任务模型设计、工具定义规范、调度策略等多个层面接下来我详细说。4. 构建可靠重试体系的四个工程动作4.1 设计任务范围给每个Agent动作画边界要让重试可靠第一步不是写重试代码而是先定义任务的最小执行单元。大多数“越帮越乱”都源于重试粒度太大——整个Agent任务从头到尾重来一遍结果前面已完成的步骤又重复一遍形成连锁反应。我建议把一次Agent执行拆分成多个“原子操作”每个原子操作有明确的输入、输出、副作用描述。比如“读取用户输入”是一个原子操作“调用工具执行查询”是一个原子操作“生成最终回复”是另一个原子操作。调度层记录当前执行到哪个原子操作重试时从失败点续跑而不是从头再来。对应到实际调度系统里就是维护一个“执行游标execution cursor”每完成一个原子操作就持久化一下。重试发生后先从持久化的游标位置恢复判断该原子操作是否已经被执行过再决定是续跑还是重跑。这个游标机制比简单轮询要可靠得多。4.2 幂等化改造让重复执行的结果一致虽然我们做不到所有下游都幂等但至少要在自己能控制的范围内把幂等做扎实。具体有三个动作第一给每个Agent任务生成全局唯一的执行ID并在调用所有下游工具时携带这个ID。下游系统只要能支持“同一个ID只处理一次”幂等就基本成立了。第二对于不支持幂等的下游工具在执行前先做“是否存在同ID结果”的检查。比如发邮件之前先查询发件记录写数据库之前先查唯一键是否存在。这个动作会让Agent多一次工具调用但换取的是重试的安全性。第三在Agent自己的工具定义里明确标注“副作用类型”字段例如只读、写前自检、幂等可重放、不可重放。调度层基于这个字段决定能不能自动重试。把工具的“性格”描述清楚重试策略才有的放矢。4.3 失败分类与策略决策表重试前先回答三个问题现在很多Agent框架都内置了重试机制但它们大多是“固定次数退避重试”对失败类型不够敏感。我的做法是引入一个重试前决策环节每次失败后先回答三个问题这次失败的阶段是什么是LLM调用失败、工具执行失败、还是结果校验失败这个阶段的副作用是否可能已经发生发生了多少如果重试Agent是否需要在干净上下文中重新理解任务根据这三个问题的答案按下面的决策表来判断失败阶段副作用可能发生推荐策略说明LLM调用失败否直接重试白指数退避未产生外部影响安全工具执行超时不确定先查询结果再决定用幂等查询确认是否已生效工具执行明确失败否重试或降级可安全重试工具成功后响应丢失是禁止自动重试查询或人工介入结果校验失败可能清理上下文后重试重置内存避免污染这个表的本质是把“重试”从一种无脑的容错机制变成一种有节制、有判断的治理策略。同时也是给Agent团队一个很好的沟通工具——产品、算法、工程对着同一张表讨论要比听一句“重试策略出问题了”清楚得多。4.4 全局兜底超时熔断、人工介入与最终一致性就算有了上面的分层重试我们依然要在全局层面设一道保险。这套保险包含三件事超时熔断是底线任何重试都不能无限进行。我们自己设定的是单次工具调用最长等待时间、单任务最大执行时长、单任务最大重试次数这三项必须同时存在缺一个都不要上线。否则一旦上游故障时间拉长重试风暴会直接拖垮整个调度集群。人工介入是救生圈。当任务重试两次依然失败时不要交给Agent自己继续挣扎直接把任务流转到人工处理队列。Agent可以持续被优化但在生产环境里最稳妥的兜底永远是人。最终一致性是视角问题。对于“写数据”任务我们要把“是否重试成功”的定义从“工具返回成功”改成“最终状态到达预期”。比如一个发送通知的任务第一次因超时判失败第二次工具返回成功那任务结束前需要去查一次下游的投递记录确认发送成功且没有重复。我们把这个校验步骤内置到任务收尾逻辑里避免重试后“看起来成功、实际上已经重复”。5. 几个让重试真正可靠的补充技巧5.1 重试次数的上限不是一个固定数而是看故障来源我以前习惯把重试次数固定为3后来发现这个3太武断。网络闪断可能1次就能恢复模型服务排队可能要等更长时间下游数据库超时则需要更谨慎的次数上限。更合理的做法是让重试参数跟着失败类型走。读操作可以多试几次写操作必须保守网络类故障可以快速连试资源类故障必须退避等待。把重试参数拆到每条工具定义里虽然配置繁琐但长期来看是值得的因为它让系统更具弹性而不是更脆弱。5.2 重试时要给模型“新任务”的启动提示而不是“续跑”的缝合感实践里还有一个很有效但容易忽略的小技巧当Agent是从失败点续跑时不要直接丢“继续完成”这种模糊指令。最好重新构造一个完整的任务Prompt让Agent像一个接收到新任务的人一样从任务目标开始理解而不是把自己当成那个刚刚失败过一次的“失败者”。我们内部管这个叫“重启思维cold restart”。哪怕不能完全清空上下文至少要在重试时把上下文压缩成一份“任务摘要”并把之前的错误排除出去。这个做法的具体效果可以用一个数据说明在我们的一次对比测试里保留完整错误上下文的重试成功率只有61%而清空错误上下文、用任务摘要重试的成功率达到了84%。5.3 重试审计每次重试都要留下“为什么”的记录最后这个建议更像工程管理层面任何一次重试都必须留下结构化的审计记录。记录的内容至少包括触发重试的异常类型、当前执行步骤、相关上下文摘要、重试次数、最终处理结果。有了这些记录团队才能定期复盘“重试到底帮了多少忙、惹了多少祸”。我们每月会跑一次重试归因分析统计哪些工具的重试成功率最高、哪些重试反而引入了新问题。这不仅是技术优化也是团队认知的迭代。没有审计的重试策略基本就是在盲人摸象。我自己现在看一个Agent系统的容错水平不是看它有多少重试手段而是看它能不能做到“重试有据可依、结果有迹可循、边界有守有为”。重试不是灵丹妙药但也不该成为事故替罪羊问题只在于你有没有想清楚为什么要重试、重试会踩出哪些新的脚印、以及这些脚印能不能被准确感知和兜得住。这个方向还值得继续细化。比如任务游标机制在不同Agent框架下的落地方案、上下文压缩的具体策略、以及多Agent协作场景下重试风暴的隔离手段这些话题后面如果实践有了新的结论我再来分享。