资讯动态

AI Agent生产级容错:异常处理、重试与降级全攻略

发布时间:2026/10/13 11:13:49 来源:尧图企业网站定制
Agent开发做到一定深度容错这块一定是绕不开的坎。我见过太多项目Agent在Demo阶段跑得顺风顺水一上真实流量就花式翻车——模型输出偶尔不按套路出牌某个工具接口慢了几秒重试一多直接把下游打挂用户那边看到的就是“转圈圈”和“已断开”。我自己也在这个坑里反复打磨过所以想把这套容错优化的经验系统整理下来聊清楚异常处理、重试机制、降级策略这三件事到底该怎么做以及它们之间怎么配合。这篇文章适合两类人看一类是正在把Agent从原型推向生产的开发者你已经跑通基础流程但担心稳定性另一类是接手了遗留Agent项目、被动处理线上故障的维护者。我会把每个环节的设计思路、代码写法、踩坑教训都讲透尽量让看完的你能直接照搬出一套容错方案。1. 先搞清Agent的故障域错误到底从哪来1.1 一段线上事故后的复盘先说一个我印象很深的真实场景。某团队在给一个客服Agent做上线前的最后压测任务流是“识别用户诉求 → 查订单 → 查物流 → 生成答复”。压测跑到第40分钟突然一大片请求开始失败日志里全是同一个工具报出的超时异常。查下来发现这个工具依赖的仓储系统在做夜间维护本身就慢而Agent层用的是“失败就重试3次”的通用逻辑所有并发请求一起重试等于把本来就紧张的接口又踩了一脚。事故持续了将近半小时最核心的查单功能完全不可用。复盘的时候我们讨论出一个共识Agent的故障和传统后端服务很不一样它不是一个“请求-响应”的确定性过程而是一条多步决策链链上每个环节都可能出问题而且问题之间会互相放大。你很难用一套统一的异常处理逻辑去包住所有情况。1.2 四个异常来源和它们的特征我习惯把Agent运行中的异常来源分成四类每一类的发生频率、危害方式和应对思路都不同。第一类是输入侧异常。用户传来的消息千奇百怪缺上下文、格式不对、内容超长、夹带无关信息甚至故意构造的极端输入。这类异常不会让流程崩掉但会让模型的理解产生偏差最终表现为“答非所问”或“多轮对话跑题”。第二类是模型侧异常。包括上下文超长触发token截断、模型返回的内容不符合结构化约定比如该返回JSON却返回一段自然语言、function call参数拼装错误、或者干脆遇到服务端限流与超时。这类异常最隐蔽因为它不是直接抛出来的exception而是“看起来正常但内容不能用”。第三类是工具侧异常。外部API超时、返回5xx、限流、数据格式变更、权限失效、业务规则拒绝。这是最像传统后端故障的一类也是重试和降级的主要战场。第四类是编排侧异常。状态丢失、循环调用Agent反复调用同一个工具始终不满足终止条件、上下文膨胀导致成本爆炸、并行子任务之间的条件竞争、以及workflow因上一步结果异常而无法继续。四类异常往往不是独立出现的例如工具超时会导致模型多次重试进而推高上下文消耗又引发新的token超限问题。容错设计如果只盯着单一环节很容易按下葫芦浮起瓢。1.3 传统后端容错经验在Agent上为什么不管用在传统后端异常处理是程序员的职责。你会用try-catch包住可能出错的代码块判断错误码决定是重试、降级还是直接失败。系统的行为是可枚举的你可以在编码时把所有分支都写进代码里。但在Agent里模型是一个基于概率的决策者你没法枚举它的所有输出。同一个问题它这次能规规矩矩输出JSON下一次可能在JSON前后多写一句“好的我来帮你查询”。此外Agent的每一步决策还受上下文影响同样的错误信息模型每次解读出来的行动可能都不一样。所以Agent容错的核心策略发生了变化传统后端靠“代码兜底”Agent必须靠“信息兜底 策略兜底”。你要给模型和编排层足够的、结构化的错误上下文让它有能力做出正确判断同时用多级策略兜底覆盖模型判断失效的情况。这也是下面三个核心机制要解决的问题。2. 异常处理错误信息是给模型看的第一手材料2.1 工具层的职责边界抛出还是反馈很多人在给Agent写工具函数的时候习惯把传统后端的习惯带进来出错了就throw一个异常让上层捕获。这在Agent开发里是一个典型的反模式。原因很简单Agent的编排器不是“程序员逻辑”它能感知的只有提示词上下文。如果你把一个异常直接throw掉外层catch住后能做的无非就是记录日志、返回一句“抱歉出了点问题”——但此时模型并不知道具体出了什么错也就没有机会尝试另一种路径来完成任务。我建议把工具层的职责定义为“如实反馈错误状态”即工具函数捕获底层异常把错误信息转换成结构化的结果对象返回给模型而不是直接中断流程。举个例子def call_order_service(order_id: str, ctx) - dict: try: resp http_client.post(/query_order, json{order_id: order_id}) if resp.status_code ! 200: return { ok: False, error_code: fHTTP_{resp.status_code}, recoverable: resp.status_code 500, message: 订单服务暂时不可用当前时间为业务高峰系统提示稍后重试。, } return {ok: True, data: resp.json()} except TimeoutError: return { ok: False, error_code: TIMEOUT, recoverable: True, message: 查询订单接口响应超时可能是系统繁忙建议间隔几秒后重试。, } except Exception as e: return { ok: False, error_code: UNKNOWN, recoverable: False, message: f内部错误:{type(e).__name__}请稍后再试或转人工处理。, }注意这不是普通的返回值它是给模型看的“错误情报”。模型拿到这个东西之后就会知道“哦现在查不到系统建议我待会儿再试或者问问用户是否愿意走别的流程”。2.2 设计一个模型能读懂的Error Packet既然错误信息是给模型看的第一手材料那它的格式就必须精心设计。我在实践里总结了一个“Error Packet”的字段规范尽量保持精简但信息完整ok布尔值表示这次调用是否成功是模型判断的最直接信号。error_code稳定的机器可读错误码方便编排层做规则判断。recoverable这个错误是否值得重试或换路直接引导模型决策。message人类和模型都能看懂的错误描述含可能的原因与建议动作。suggestion可选的行动建议例如“可以尝试查询历史订单”、“建议转人工”等。这套结构的好处是你可以在编排层写一些硬规则来阻止模型乱来比如recoverableFalse时禁止重试同时也可以把完整结构塞进上下文里让模型灵活理解。硬规则和模型灵活性并不矛盾它们是两道保险。2.3 用户视角的异常呈现进度、提示与兜底异常处理不只是系统内部的事情用户侧的体验同样关键。Agent产品里最常见的翻车现场就是Agent已经在后台重试了两三次工具调用但用户界面还停留在“正在为你查询…”的转圈状态没有任何新信息。稍有延迟用户就会失去耐心。我在实践中坚持一个原则每一条会超过3秒的内部处理路径都必须向用户同步阶段性状态。具体有两种做法一种是流式输出中间状态让用户看到Agent正在“检查订单状态”、“尝试连接物流接口”另一种是在检测到异常时主动输出安抚文案比如“这个物流接口暂时响应较慢我再试一次稍等”。最后也是最重要的一层是为异常情况准备“人工兜底”出口。Agent可以答错但不能把用户堵死。在对话卡片上保留“转人工”按钮或者在多次失败后主动推送人工入口这应该是Agent产品的标配能力而不是选配。3. 重试机制把“再来一次”变成可控策略3.1 可重试与不可重试重试前先做分类重试是容错里最直观的手段但很多人做错的第一件事就是不加区分地重试。重试消耗资源、增加下游压力、推高整体延迟所以第一步必须对错误做分类。一般来说可重试的错误包括网络超时、5xx服务器错误、限流429、暂时性依赖不可用。不可重试的错误包括4xx客户端错误参数错误、鉴权失败、业务规则拒绝比如订单不允许修改、数据格式异常模型给的工具参数不符合schema。一个朴素但有效的规则是错误码在recoverable字段里标记为True才允许重试否则直接进入失败分支。这比在代码里硬编码几百个错误码要清爽得多。我常用的重试判断逻辑长这样def should_retry(result: dict, retry_count: int) - bool: if retry_count MAX_RETRY: return False if not result.get(ok): return bool(result.get(recoverable)) return False记住这个反直觉的结论多数情况下重试造成的整体成本远大于它挽回的请求。所以重试次数宁少勿多默认2次就好最多不超过4次。3.2 指数退避与全抖动给自己留余地给下游留呼吸如果系统决定重试那重试的节奏就是关键。最糟糕的重试方式是“立即重试三次”因为如果下游已经过载你这三次重试大概率是同一时间点踩进去的只会加重过载而且三次都失败的概率很高。标准的做法是指数退避第一次重试等待1秒第二次等待2秒第三次等待4秒。但纯指数退避有个问题——当大量客户端同时失败时它们的重试时间会对齐形成“惊群效应”。解决办法是引入全抖动Full Jitter让等待时间在0到上限之间随机取值import random import time def retry_with_backoff(fn, max_retries3, base_delay1.0): for attempt in range(max_retries 1): try: return fn() except Exception: if attempt max_retries: raise delay random.uniform(0, base_delay * (2 ** attempt)) time.sleep(delay) # unreachable全抖动的效果在Agent这种高度并发调用的场景里特别明显。每个请求的重试时间被随机打散下游的过载就有机会恢复而不是被重试请求持续轰击。3.3 幂等与去重重试风暴的刹车片重试还有一个容易被忽略的隐患重试可能导致重复执行。如果你的工具操作不是幂等的——比如“创建订单”接口重试一次就可能创建两笔订单“转账”接口重试则可能重复扣款。这类事故可比服务失败严重多了。通用解法是在每次Agent会话开始时生成一个全局唯一的request_id或者叫trace_id这个ID会随工具调用传给下游下游用它做去重。下游如果发现同一ID已经处理过就直接返回上一次的结果而不是再执行一次。在我做过的系统里这个request_id的规范是agent_session_idstep_indexretry_attempt组合。这样哪怕外部系统要做精确去重也有足够信息区分“同一工具的不同调用”和“同一调用的不同重试”。另外一个防重试风暴的手段是全局限频在Agent级别设置一个“重试预算”。比如单个任务最多允许重试5次超过这个额度就强制进入降级流程哪怕每个环节单看都还可以重试。这相当于给失控的指数叠加按下暂停键。3.4 模型层重试换一个说法再问比原样重试更有效前面讲的重试都是针对工具调用层的但Agent还有一个独特的重试层级模型本身。当模型的输出不符合预期时——格式不对、参数错误、答非所问——你重试的方式不应该是“原样再调一次模型”那样大概率还是得到同样的问题。更有效的做法是带反馈的重试。把上一次的错误信息、校验结果作为额外上下文塞回提示词里让模型知道自己哪里错了然后重新生成。比如messages [ {role: user, content: 查询订单20240901001的物流状态}, # ... 此前Agent的思考和工具调用记录 ] if invalid_output: messages.append({ role: system, content: 你上一次返回的工具参数不符合JSONSchema要求具体错误字段order_id必须为字符串。请纠正后重新返回。 })这种重试本质上是在引导模型自我修正比单纯机械重试的成功率高得多。两次修正都失败就要考虑换模型、换提示词模板或者直接走降级路径。4. 降级策略从“必须成功”到“尽力而为”4.1 模型降级链保住核心链路放弃边缘体验当Agent依赖的主模型发生故障限流、超时、服务不可用很多团队的方案是“等它恢复”或者“转人工”这两种都是体验最差的方案。更好的思路是建立一条“模型降级链”。我常用的模型降级链长这样高精度大模型 → 性价比模型 → 本地小模型 → 规则兜底。平时用高精度模型保证Agent的复杂推理质量一旦发现高精度模型连续失败通常连续2次即可判定就把请求切到性价比模型还不行就切到本地小模型本地小模型虽然智力上限低但只用来处理简单任务比如查单、查天气这类意图明确的操作完全够用。有一个关键细节模型降级不是全局切换而应该是链路级别的。比如对话管理仍然用高精度模型但意图识别这类子任务可以单独降级到小模型。这样能在故障期间保住80%的能力而不是直接哑掉。降级链还必须配合模型侧的熔断器。连续失败N次后打开熔断一段时间内不再发起对主模型的请求只走降级路径同时用半开启状态做小流量探测恢复成功则逐渐切回。4.2 工具降级链实时代价太高就先给缓存答案工具降级的核心是在无法获取确定结果时给出一个“足够好”的替代方案。我最常用的降级链是实时接口 → 带保鲜期的缓存 → 启发式默认值 → 明确告知限制。举个例子假设Agent要获取当前实时金价来做投资建议。实时接口超时后可以降级到缓存数据——前5分钟的数据对用户来说基本没有差别。如果缓存也没有就明确告诉用户“实时数据暂不可用我根据最近一次公开价格给出参考”而不是沉默或报错。这里还很重要的是给降级数据的“信心等级”做一个标记数据来源信心等级提示文案示例实时接口高“根据最新行情当前价格为…”缓存60秒中高“根据1分钟内的行情数据…”缓存30分钟中“以下为最近一次刷新结果可能与当前有出入…”启发式/经验值低“实时数据暂不可用以下为估算参考值…”信任是一点一点建立的但一次“拿过时数据冒充实时”就能把信任清零。所以降级文案的措辞必须诚实。4.3 流程降级从“全自动”回落到“人机协作”除了模型和工具Agent的处理流程本身也是可以降级的。当核心工具持续不可用时你要考虑的不是“把同样的流程再降级一次”而是“能不能换一条路径完成任务”。举一个实际案例。一个售后Agent的默认流程是识别用户诉求 → 查订单 → 判断是否符合退换货规则 → 生成退换货单。在订单查询接口故障时完整流程无法推进。此时可以降级为一个“半自动流程”先引导用户在输入框填入订单号Agent把订单号记录下来生成一个预约工单注明“规则判断将在订单查询恢复后补充”同时给出一个转人工的快捷入口。这类流程降级的关键是把该让用户知道的决策透明化。你需要让用户理解“现在系统不完整但你的事情已经被记录下来了”而不是让用户在一串“抱歉”里打转。降级不是放弃而是换一条路继续把事情往前推。4.4 降级预案要能量化触发条件和恢复条件我发现很多团队不是没有降级方案而是降级方案写得太模糊——“当接口不可用时走备用通道”——但什么算“不可用”连续失败几次什么条件算恢复这些问题不量化降级机制就没法自动化只能靠运维半夜盯监控手动切。我建议每个降级链路都要配备三个明确参数触发阈值、熔断时长、恢复策略。例如触发阈值连续失败3次或错误率超过50%且持续30秒。熔断时长开启熔断后持续60秒不接受主路径请求。恢复策略每10秒放一个小比例探测请求成功则逐步恢复失败则继续熔断。这组参数要能按业务调整也需要在压测中验证——下一节我会讲到压测里这几个数字是怎么被验证和修正的。5. 隔离与补偿别让一个工具故障带走整个工作流5.1 超时控制Agent里最重要的那个参数如果要选一个Agent开发中“最容易被低估但最重要”的配置我会选超时时间。Agent的调用链非常长——模型推理、多轮工具调用、上下文重组任何一个环节卡住整体体验都会崩掉。我把超时分成三档来治理单次工具调用超时、单轮Agent决策超时、整个任务超时。单次工具调用通常控制在3-5秒单轮决策模型生成结束到执行完一个工具控制在10-15秒整个任务根据复杂度控制在60-120秒超时就强制结束并给用户输出一个兜底回复同时把任务状态持久化以便后续追踪。这里要特别提醒一件事工具调用超时不能只靠HTTP客户端超时还必须结合Agent编排层的“看门狗”。有时候底层HTTP没有超时是因为请求已经被某个队列卡住了你在上面用asyncio.wait_for或者concurrent.futures包一层才能真正把它切断。5.2 并发保护与下游限流一个Agent实例能力有限但线上通常是一排Agent实例在跑如果它们同时调用同一个外部工具下游瞬间就可能被打爆。这是“重试风暴”之外第二常见的故障放大器。我在编排层引入了两个机制一是信号量控并发。给每个工具分配一个最大并发数——比如某个物流查询接口的单实例并发上限是20Agent编排器会用信号量把同时打到这个接口的调用数限制住多出来的请求排队等待而不是并发涌入。二是容器级限流。在对外部服务的调用层做一个令牌桶比如“每秒最多30次、突发允许50次”。这个限流是Agent侧主动自律避免把外部依赖压垮后触发更严重的连锁故障。实践中这个令牌桶的容量通常要根据压测得出的下游真实承受能力来配置不能拍脑袋。5.3 部分成功与状态补偿多步Agent流程里可能会出现“部分成功”的尴尬局面。比如一个“查订单并申请退款”的流程订单查到了但退款接口超时了如果你的状态管理只记录“成功/失败”二元状态这个任务就会处于一个无法恢复的中间态。我的做法是引入步骤级状态记录每一步执行完都把该步骤的结果成功/失败/降级和关键数据写入会话状态。这样当某一步失败时后续流程可以清楚地知道“前面已经做了哪些事”然后决定继续执行、回滚还是跳到降级路径。以退款流程为例如果订单查到但退款未生成降级策略可以是“保留查询结果只生成退款申请记录并给用户一个链接去人工确认”。而不是把这个任务整体置为失败让用户从头再来一遍。在涉及真正会改变业务状态的工具比如创建订单、扣款时必须额外检查幂等。如果接口不支持幂等务必要在进入该步骤前加一个二次确认的user turn——多问用户一句“确认要下单吗”——比事后跟用户道歉一万句都管用。5.4 长任务要把重试状态搬出内存最后一个隔离建议针对那些会跑几分钟甚至更长的Agent任务——批量处理、数据采集、复杂的多轮审批。如果Agent实例在任务跑到一半时崩溃、重启、或被调度器迁移内存里的任务状态就全丢了这时重试机制不仅毫无帮助反而会让用户看到“任务突然消失”。解决办法是把任务状态持久化到Redis或数据库每个任务的进度、当前步骤、已调用工具、产物路径都写入存储。Agent实例重启后可以通过任务ID恢复进度或者至少能让用户明确知道“任务中断在哪里是否需要重新发起”。这本质上是一种“让重试发生在更可靠的位置”的思路。6. 一次容错压测的完整记录6.1 预置故障注入理论讲得再多最终还是要靠压测来验证。我搭建了一个标准的容错压测环境试验对象是一个多轮对话的客服Agent核心链路包含意图识别、查订单、查物流、生成答复四个步骤。为了让捉虫更有针对性我主动做了三类故障注入模型层让意图识别模型以15%概率返回非标准格式模拟token截断或输出格式漂移。工具层让订单查询接口随机返回超时概率从5%到30%逐步上调。网络层在查询物流的接口上人为加1秒延迟模拟上游过载时的排队效应。压测持续了2小时模拟了500个并发会话每个会话平均5轮。我自己盯着实时指标面板重点看几个关键数字任务完成率、平均延迟、重试率、降级触发率以及用户侧看到的“失败回复”占比。6.2 压测里跑出来的关键结论第一版压测结果不算好看但暴露的问题特别典型。首先是完成率下滑曲线太陡当工具超时率从10%升到30%时任务完成率从92%掉到了61%这说明容错机制没有形成有效缓冲而是把故障放大到了用户端。其次我发现重试数量在错误率上升时暴增。由于每个Agent会话有多个步骤每个步骤失败都会触发重试而重试失败又会触发下一个步骤的重试最终导致单任务重试次数远超预期。日志里最离谱的一条一个简单查询任务竟然对同一个接口重试了9次。还有一个有意思的现象降级路径的触发条件被打破了。设计时预计“连续失败3次进入降级”但实际上因为模型在重试之间还会插入新的tool call请求连续失败判断经常被中间行为打断导致降级始终不触发一直在无效重试里打转。6.3 优化后的效果根据压测暴露的问题我做了三处调整效果非常明显指标优化前30%故障率优化后30%故障率任务完成率61%88%平均会话延迟38s21s用户侧失败回复占比27%9%单任务最大重试次数9次3次第一处调整是把“降级触发”的判断改为滑动窗口统计窗口内最近5个工具调用有3个失败就视为故障而不是依赖连续失败。这个改动直接让降级链路真正派上了用场。第二处是把每个会话的重试预算下调到3次。这个数字看似损失了“多一次机会”但实际上避免了大量无效重试。因为对于可恢复错误指数退避抖动之后的前两次重试已经覆盖了80%以上的恢复可能性超过3次的重试大部分只会带来延迟放大。第三处是补上了模型层反馈重试的prompt模板。之前格式异常的重试是原样再发一次成效很差改成把错误信息直接反馈给模型之后格式修正的成功率从不到一半提升到了七成以上。6.4 让我印象最深的两个细节压测过程里有两个细节直到现在我都觉得特别值得分享。一个是限流器的位置。一开始我把令牌桶放在Agent的API网关层压测发现这挡不住问题因为真正打爆下游的不是入口流量而是Agent内部多步调用对同一工具的重复请求。把限流器下移到工具调用层之后效果立刻好了很多。这个案例让我强烈意识到Agent的容错点必须沿着它自己的调用链去定位而不是沿用传统网关的思维。另一个是关于错误信息里的争议。第一次压测时工具错误返回的message字段写的是技术细节比如“java.net.ConnectException: Connection refused”。结果模型经常被这些术语带跑生成给用户的回复里也冒出这些词。后来我把message改成了面向模型和用户的双层设计技术细节放到debug_info字段里message只放业务可读的描述。模型引用错误信息的“污染率”立刻降了下来。这算是一个字数虽小、影响很大的细节。收尾容错优化的边界与“够用”原则全套机制上线之后后台的稳定性确实上了一个台阶但我也有必要泼点冷水——容错不是做得越多越好。每多一层重试、多一条降级链路系统复杂度和维护成本都在上升。我把这个度把握为三个“看得见”触发看得见有日志和指标、行为看得见降级时用户明确知道、成本看得见每一次重试和降级都有成本记录。只要这三个都看得见容错方案就不会失控变成另一个故障源。如果在你的项目里只能先做到三件事我会建议优先做第一给所有工具调用加上统一的Error Packet和信息反馈第二把重试改成指数退避全抖动并把重试预算压到一个吝啬的数字第三为最核心的2-3条链路设计降级预案并量化触发条件。其他的等真出了问题再去补齐也来得及。Agent的稳定性一定是在真实故障里一寸一寸打磨出来的但先用“容错三板斧”把底线兜住后面就不会那么被动。

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

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

免费获取报价 →
↑