十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent长任务失败:直接重试的隐藏风险与幂等恢复策略

AI Agent长任务失败:直接重试的隐藏风险与幂等恢复策略 前几天晚上我在调一个自动化流程任务跑到第27个步骤时上游接口突然返回 500整个 AI Agent 会话直接中断。我盯着控制台里那行报错手悬在重试按钮上空犹豫了大概十秒钟。我犹豫不是因为怕它再挂而是因为我清楚地知道这个任务已经不是第一次跑了前一次它跑到第13步时就已经挂过一次而那一次我毫不犹豫地点了重试。结果就是原本只需要执行一次的创建订单工具被模型调用了两次生产环境里多了一条重复数据。这就是今天想聊的问题AI Agent 跑长任务中途挂掉之后直接重试这四个字看起来人畜无害实际上是一个技术决策而且是一个很容易做错的技术决策。这篇文章我不会讲那些Agent 很强大的空话我只想把 AI Agent 长任务失败的底层原因、直接重试的隐患、以及一套真正能落地的重试与恢复方案掰开揉碎讲清楚。适合正在做 AI Agent 开发、用 LangGraph / Spring AI / MCP 协议做复杂工作流、或者在生产环境里部署过 Agent 服务的读者参考。1. 真实世界里Agent 任务是怎么挂掉的在讨论重试之前得先把挂掉这件事分类。我在实际开发和运维 AI Agent 的过程中发现长任务崩溃的原因几乎逃不出下面这四类。每一类的处理方式完全不同如果不加区分地统一用重试解决就是在给自己埋雷。1.1 第一类外部 API 的软失败这是最常见的一种。你的 Agent 要调用上游服务——不管是模型 API、业务系统接口、还是第三方 SaaS——都会遇到限流429、服务端错误5xx、网络超时timeout。这类失败的典型特征是它不是你 Agent 本身逻辑的问题你重试的时候上游可能已经恢复了也可能还在抖动。比如模型 API 返回了您最近作出的请求太多了请稍候再重试这类错误说明你触发了速率限制。这时候如果你拿着同样的请求立刻重试大概率还是会被限流。反之如果是对端服务的偶发 5xx隔一两秒重试往往就能成功。这类失败看着简单但有个隐蔽的坑超时并不等于请求没到达上游。也就是说你的 Agent 端等不及断开了但上游可能已经收到请求并且正在处理甚至已经处理完了。这时候重试就会造成同一个操作被执行两次。这是后文要深入展开的重点。1.2 第二类上下文失控AI Agent 跑长任务本质上是在一个不断膨胀的上下文里做推理。任务越长中间的中间结果、工具返回内容、用户提供的参考资料就越多。当你达到模型的上下文窗口上限Context Window Exceeded时调用会直接报错整个会话的状态也会变得极其脆弱。我见过不少团队Agent 任务设计的时候没有做记忆分层所有内容一股脑往上下文里塞。跑到中途上下文超限整个任务就废了。这时候你点重试没有用——因为重试会把同样的上下文再次塞进去然后再次超限。1.3 第三类工具调用的副作用被重复执行AI Agent 长任务和传统程序最大的区别就是模型会自主决定调用哪些工具。而工具是有副作用的比如发邮件、扣费、创建数据库记录、调用第三方写接口。如果一个长任务在工具调用完成之后、模型接收结果之前崩溃了你为了让它继续而重试就会出现同一个工具被调用两次。我做过一个电商客服 Agent中间需要调用创建售后工单这个工具。有一次系统在工具返回响应后、模型还没生成下一轮回复时网络中断我重试了整个任务。结果就是模型从头开始推理又把售后工单建了一次。客户最后收到了两条完全相同的工单通知。这就是副作用重复执行。1.4 第四类模型自身的行为异常还有一类失败不是来自外部而是模型发疯了输出格式不稳定、陷入死循环比如反复调用同一个工具却无法收敛、产生幻觉导致下一步操作完全偏离主线。这类问题在一次会话里可能不会导致硬报错但会让长任务卡在某个步骤上一直消耗 token。这类失败最麻烦的地方在于你没法通过简单的重试解决因为问题出在推理策略层面不是网络请求层面。这时候需要的是给 Agent 加约束、加校验、加回退机制。2. 直接重试为什么是个技术上的坑现在可以正面回答标题里的问题了。直接点重试本质上是在做一个没有任何保险措施的状态恢复行为。为什么说它危险我拆成三点来看。2.1 你以为的重新开始其实是从零再来大多数 AI Agent 平台的重试按钮逻辑非常简单把当前这轮对话从头跑一遍或者把这个任务节点重新执行一遍。它不会智能地判断哪些步骤已经完成了哪些还没完成。也就是说当你点下重试的那一刹那Agent 会带着新的或者旧的上下文从任务的开头重新开始推理。问题是长任务里的很多步骤并不具备从头再来的资格。举个例子一个内容生成 Agent要先搜索资料、再写大纲、再生成正文、最后自动发布。如果发布那一步挂了你重试整个任务那它大概率会再搜索一遍资料、再写一遍大纲、再生成一遍正文、然后再发一次。如果发布是按篇数计费的你的账单会很好看。如果 Agent 在重写正文的时候模型温度参数有随机性那第二次生成的正文可能和第一次完全不一样——你原本要的是修一下发布失败的这篇结果它给你端上来一篇全新的。这个行为在业务上几乎不可接受。2.2 非幂等操作的放大效应幂等性这个概念是所有重试工程的核心。一个操作如果被重复执行多次、结果和只执行一次完全相同它就是幂等的。GET 请求读数据是幂等的但转账发消息创建资源这类操作天然不是幂等的。AI Agent 长任务里充满了非幂等操作。模型每一次工具调用都可能是在真实世界里做一次一锤子买卖。你点了重试就好比开会的时候网络不好投影仪没显示出来你对着会议室喊了一句再讲一遍——然后全场所有人把刚才的话又听了一遍。有个比较反直觉的结论即使你是在同一个节点上重试只要这个节点内部包含非幂等工具调用它依然可能导致副作用重复。重试的粒度决定了风险的大小。整体重试的风险 节点重试的风险 单次工具调用重试的风险。2.3 时间与 token 的双重浪费很多人只看重重试之后能不能跑通忽略了成本。AI Agent 的每一步推理都在消耗 token而 token 的背后是时间和钱。一个长任务如果在最后一步挂了你重试整个任务就等于把前面几十步的推理成本全部重付一遍。我曾给一个长任务做过统计正常跑完需要约 80 万 token 的输入输出因为一次失败导致全量重试最后的实际消耗接近 150 万。很多时候重试一次的成本甚至比把这个任务单独用一个新会话重新跑一次还高。3. 重试之前先回答三个问题幂等、定位、断点如果你正在维护一个 AI Agent 任务系统并且已经遇到挂了之后不知道能不能重试的困扰我建议你先不要急着优化重试的代码而是先回答下面三个问题。这三个问题全部想清楚了重试方案自然就浮出水面了。3.1 问题一任务里的每一个操作都幂等吗这是整个重试工程的地基。如果地基没打好别的都白搭。要做的事情很具体把你 Agent 里能调用的每一个工具都过一遍逐个问自己这个工具被重复调用会不会出问题如果是只读类操作、查询类操作那没问题天然幂等如果是写操作、扣费操作、通知操作就得想办法让它幂等。一个非常实用的做法是幂等键机制。要求所有非幂等工具在上岗之前都必须支持客户端传入一个 request_id。服务端在处理请求时如果发现同一个 request_id 已经处理过了就直接返回第一次处理的结果而不是再处理一遍。这样客户端即使因为超时而发起重试服务端也能识别出来并去重。下面是我在实践中反复使用的一个工具调用幂等封装思路import uuid class ToolInvocation: def __init__(self, tool_name: str, payload: dict): self.tool_name tool_name self.payload payload # 幂等键从调用来源和参数中生成同一逻辑操作稳定不变 self.idempotency_key uuid.uuid5( uuid.NAMESPACE_URL, f{tool_name}:{sorted(payload.items())} ).hex注意这里生成幂等键的方式不一定适合所有业务场景。如果你的工具调用有状态依赖比如给指定用户加积分用户积分会变化用入参排序生成 key 可能不够稳定需要业务侧自己定义。核心原则是同一个逻辑操作在重试时生成同一个幂等键。3.2 问题二失败发生的时候你能定位到具体是哪一步吗很多团队做 AI Agent日志打得极其简陋。控制台输出几行 prompt、几段模型返回然后就没有然后了。等到任务挂了想找挂在哪一步都无从下手。这种情况下讨论重试策略基本等于盲人摸象。我建议至少维护一张 Agent 运行轨迹表。这张表记录每一次会话中的每一个节点执行情况哪怕只是简单地写进日志文件也行。关键字段包括字段说明示例session_id会话 ID长任务全程不变agent_20250607_2047node_id节点/步骤 IDstep_27_tool_send_emailparent_id父节点 ID用于还原调用链step_12_plan_creationstatus节点状态pending/running/success/failedrequest_snapshot请求的完整内容含工具参数{to:userx.com,title:...}response_snapshot响应的完整内容{code:0,id:1234}attempt_count当前节点的重试次数2error_message错误信息timeout after 30s有了这张表当任务挂了你能清楚地看到最后一个 success 状态节点在哪、失败节点是哪一个、这个节点的入参和出参分别是什么。在这个基础上谈恢复才是有意义的。3.3 问题三你希望任务从哪里恢复这是最核心的问题。设计 AI Agent 长任务的恢复逻辑时有三个粒度可选会话级恢复整个会话从头重新跑简单但是成本高、副作用风险大。节点级恢复从失败的节点开始重跑失败之前的节点结果直接复用。这是大多数场景下性价比最高的方案。工具调用级恢复只重试失败的那一次工具调用模型上下文不变。这个粒度最精细但对系统设计要求最高。我的经验是大部分业务场景做到节点级恢复就已经足够了。要支持节点级恢复需要把每个节点的输出持久化下来。当任务恢复时先查一下当前节点的输出是否已经存在存在就直接读缓存不存在才重新执行。3.4 一份可直接照抄的重试预检清单在实际工作中我会把下面这份清单作为任务挂掉之后的第一步动作。你完全可以照着她来[ ] 这个任务挂了之后有没有已产生但未确认结果的工具调用超时场景最需要排查[ ] 这些工具调用是否带了幂等键如果带了重试是否安全[ ] 失败的具体原因是什么是限流、超时、上下文超限还是模型幻觉[ ] 当前已经执行成功的步骤有哪些它们的输出是否已被持久化[ ] 如果从失败节点恢复它的输入依赖有没有发生变化[ ] 重试的成本估算token 消耗是否低于重跑整个任务的成本4. 按错误类型分开处理别用一套重试逻辑打天下如果你已经做好了幂等、日志和断点这三件事那就可以开始设计具体的重试策略了。这里有一个很重要的理念不要对所有失败统一套用退避重试三遍的逻辑不同错误类型要区别对待。4.1 限流与配额错误指数退避 抖动配合请求预算当遇到 429 或请求过多请稍后再试这类响应时说明上游服务正在保护自己。你要是立刻重试只会继续被限流甚至可能因为反复尝试导致被封禁更长时间。标准做法是使用指数退避Exponential Backoff同时加入随机抖动Jitter。退避时间不是简单地越来越长而是要在每次重试前加入一个随机偏移。原因在于如果多个客户端同时失败、同时按相同的时间间隔重试会在上游形成新的请求风暴。import random import time def retry_with_backoff(func, max_attempts5, base_delay1.0): for attempt in range(max_attempts): try: return func() except RateLimitError: if attempt max_attempts - 1: raise # 指数退避 全抖动delay 在 [0, base_delay * 2^attempt) 范围内随机 delay random.uniform(0, base_delay * (2 ** attempt)) time.sleep(delay)除了退避策略还要给整个 Agent 任务设置请求预算。比如在任务开始之前就设定本轮最多调用模型接口 N 次超过这个次数直接转人工或失败退出。这能有效防止 Agent 在异常状态下反复重试烧掉大量 token。4.2 超时与网络抖动有限次快重试先确认上游是否已执行超时是最尴尬的错误。因为你无法确定请求到底有没有被上游处理。处理这类错误首要动作不是重发请求而是查询上一次请求的状态。具体做法是在业务工具层面对所有写操作统一要求提供 query 接口。如果调用 create 接口超时了先调用 query 接口查一下确认资源是不是已经建出来了。如果已经建出来了直接把结果当作成功返回不再发起 create如果确认没有创建成功再进行重试。查不到状态的情况怎么办那就只能在接受副作用风险重试和放弃并转人工之间做选择。我的建议是对于业务影响大的操作扣款、发消息、下单宁可转人工也不要盲目重试对于影响小的操作比如给备注加个标签可以重试一次。4.3 上下文超限错误压缩、裁剪、分层记忆一旦遇到 Context Window Exceeded 这类错误你要知道这不是重试一下能解决的。此时应该做的是上下文瘦身。常用的手段有几种一是将已经完成的历史步骤摘要化用一小段摘要替代大段原始内容二是把工具返回的长文本做裁剪只保留结构化字段三是把上下文分层核心指令永远保留中间过程按优先级淘汰最新的内容保留最全。我在一个多阶段的研究类 Agent 里使用的方法是每完成一个大阶段就把这个阶段内的消息列表压缩成一段约 200 字的摘要连同关键结论一起存回上下文。这样任务即使跑了 50 个步骤上下文也不会无限制膨胀。压缩之后如果再触发超限再考虑裁剪和移除。4.4 工具执行失败区分业务错误与基础设施错误当 Agent 调用工具返回错误时要看错误码属于哪一类。如果是对端服务 5xx、网络连接失败这类基础设施错误可以进行有限次重试如果是业务错误比如用户不存在余额不足重试没有意义反而应该把错误信息回传给模型让它调整策略。这个区分非常重要。我知道有团队发生过这样的情况工具返回了库存不足Agent 系统却自动重试了三次每次都是同样的错误白白浪费了时间和 token。浪费还在其次更麻烦的是如果库存不足这个错误信息触发了模型产生新的推理它可能会绕过库存检查尝试下单造成更严重的业务问题。所以我的原则是基础设施错误可以重试业务错误必须回传模型做决策。4.5 模型输出异常校验、修正、再重试对于模型发疯导致的失败策略跟前几类完全不同。这类问题的核心不是请求没送到而是模型输出的质量不符合预期。处理思路是校验-修正-再试的循环第一层强校验解析模型的结构化输出如果 JSON 格式不对、必填字段缺失、字段值不在枚举范围内直接判定为失败。第二层给模型回传错误信息让它修正自己的输出。这比无脑重试有效得多因为模型能看到上次错在哪里。第三层设定修正的最大轮数超限则终止。我见过最经典的模型发疯场景是让模型输出一个包含步骤列表的 JSON它突然在字符串里多写了几个换行和注释。我用一段基于 JSON Schema 的校验逻辑拦截住了然后把这个校验错误回传给模型模型很快就修正了。这个过程中没有任何网络层面的重试但确实解决了问题。5. 长任务可靠性的根本解法把重试设计进架构里聊到这里你应该已经意识到重试能不能做、怎么做、做到什么粒度根本不是一个策略函数能解决的问题。它考验的是你系统的整体架构设计。只有把可恢复性设计进骨子里AI Agent 长任务才能真正可靠。5.1 中间态持久化事件溯源与状态机长任务为什么挂了之后让人不敢重试最核心的原因是系统失忆了——它不知道自己已经做到哪一步了。解决的思路不是让它别忘而是让它把每一步都记下来。事件溯源是一个被验证过很多次的模式。每次 Agent 执行一个关键动作就把这个动作的事件追加到事件流中Event Store。Agent 的当前状态就是这些事件按顺序应用之后的结果。这样任务无论挂在哪一步只要把事件从 Event Store 里重放一遍就能恢复到挂掉之前的状态。举个实操例子。假设 Agent 任务包含收集需求 → 生成方案 → 发送审批 → 执行发布四个节点。用状态机来管理这四个节点from enum import Enum class TaskState(Enum): COLLECTING collecting DRAFTING drafting APPROVING approving RELEASING releasing DONE done FAILED failed # 状态机的转移规则明确每个状态下允许的下一步 TRANSITIONS { TaskState.COLLECTING: [TaskState.DRAFTING, TaskState.FAILED], TaskState.DRAFTING: [TaskState.APPROVING, TaskState.FAILED], TaskState.APPROVING: [TaskState.RELEASING, TaskState.DRAFTING, TaskState.FAILED], TaskState.RELEASING: [TaskState.DONE, TaskState.FAILED], }有了状态机和事件流恢复逻辑就变成了重放事件 → 恢复状态 → 读取当前状态 → 从当前状态继续。这比什么都靠重试要优雅得多也安全得多。5.2 编排层与执行层分离很多团队的 AI Agent把决策和执行混在一层代码里模型直接调用业务工具业务工具里又包含后续的推理逻辑。这样做的结果是一旦任务挂了不仅业务动作没完成连决策状态也丢了。更好的架构是把系统拆成两层。编排层Orchestrator负责决策它用模型规划任务步骤、决定调用哪个工具、判断任务是否完成执行层Worker负责干活每一个工具调用都是一个独立的任务单元由执行层去调度、去重试、去记录结果。这样设计的好处是编排层的模型调用失败了执行层已经完成的工作不受影响可以直接复用执行层的某个工作单元失败了编排层可以重新规划策略换一种方式完成任务而不是从头再来。这就是重试粒度在架构层面的体现。5.3 系统的可恢复性设计队列、定时任务与人工介入通道高阶的 AI Agent 长任务系统最终会走向异步化。任务不是一次性同步跑完的而是被拆成多个消息体塞进消息队列由 Worker 逐步消费。这样任何一个 Worker 宕机了任务只是停留在队列里等待其他 Worker 来拾取。这种模式本质上是一种天然的重试机制。如果你不想引入太重的消息队列也可以用数据库表加定时任务的模式做一个轻量级的任务队列。表结构足够记录任务状态和重试次数定时任务每隔几十秒扫一次把超时的任务重新投递。在小规模场景下这个方案完全够用。另外任何自动重试机制都要有止损底线。重试次数到达上限之后怎么办我的答案永远是转人工。给运维或业务同学留一个人工介入的通道让他们能在界面上查看失败原因、手动触发恢复或终止任务。自动化做得再好也不能把最后一道闸门焊死。6. 一次真实的挂掉-恢复复盘从崩溃现场到自动续跑理论讲了这么多最后分享一个我自己的实战复盘。虽然细节做了一些脱敏但整个过程非常典型你可以对照着看如果自己遇到类似情况可以按这个排查链路走。6.1 事故现场任务跑到第 27 个节点时挂了当时我在做的是一个竞品分析Agent。任务流程大概是收集竞品官网信息 → 抓取产品文档 → 调用模型分析功能差异 → 生成对比表格 → 汇总成报告 → 通过邮件发送。任务启动之后前 26 个节点都顺利跑完了报告也生成好了。第 27 个节点是调用邮件服务发送报告。也就是在这个节点上邮件服务的 API 超时了Agent 直接报错退出。从运行轨迹表来看节点状态是 running但实际上邮件有没有发出去系统里没有记录。6.2 排查链路先看日志再看状态最后定重试方案遇到这种情况我的第一步不是点重试而是去查邮件服务那边的日志。查完之后发现邮件服务已经成功收到了创建发送任务的请求并且已经把邮件发出去了。也就是说超时发生在上游已经执行成功之后。如果这时候我直接重试整个任务Agent 会从第 1 个节点重新跑一遍重新抓取页面、重新调用模型分析、重新生成报告、再发一次邮件。结果是收件人会收到两封一模一样的邮件。这是绝对不能接受的。正确的处理方式是把第 27 个节点标记为 success因为上游已经执行成功把任务状态推进到下一个节点。由于我的系统里已经持久化了前 26 个节点的输出所以不需要重跑任何节点只需要做一个确认动作然后继续后面未执行的节点。6.3 修复与验证加入幂等保护和断点恢复后再次压测事故处理完之后我做了三件事来防止类似问题再发生。第一件事给邮件发送工具加幂等键。调用邮件 API 时带上一个由任务 ID 和节点 ID 生成的幂等键。邮件服务端存储这个键如果收到重复请求直接返回第一次请求的结果。第二件事在系统里加入重试预检逻辑任务失败后先查询上游状态再决定是恢复、重试还是终止。第三件事把断点恢复的能力做了完善——节点输出全部持久化模型可以从任意节点续跑。改完之后我专门设计了一个压测场景在第 30 个节点的地方人为注入超时然后观察系统行为。结果是系统在发现超时后先调用查询接口确认状态确认资源已创建后把当前节点标记为成功自动继续往下跑。整个过程没有再产生明显的副作用。那次之后我心里就有底了AI Agent 长任务不怕挂怕的是挂了之后不知道怎么安全地爬起来。写在最后对于大部分刚接触 AI Agent 开发的人来说可能不需要一上来就上事件溯源、消息队列这些重型方案。我个人的建议是从日志完整、节点可查、操作幂等这三件事做起。先把这三个地基打好再逐步加入断点恢复、自动重试、状态机管理。等你的 Agent 真正开始在业务里跑关键任务了你会发现这些当初觉得过度设计的东西每一样都在帮你兜底。下次你的 AI Agent 跑长任务突然挂了先别急着把鼠标移到重试按钮上。喝口水查一下日志想一想这一步到底有没有真正执行过然后再决定要不要点下去。
返回列表