☰
高可用 Agent 系统如何落地:错误处理与降级设计全解析
2026/10/5 3:47:30 网站建设 项目流程

上线一个 Agent 项目,翻车方式远远多于写代码那几天。链路上任何一环——模型接口、工具服务、编排逻辑、状态存储——出一点小问题,整个对话过程就可能卡死、重复执行、甚至静默丢失用户请求。单靠 try-except 包一层远远不够。这篇把我做高可用 Agent 系统的思路整理出来,从错误来源分类、分层处理架构、降级策略设计到超时重试参数、状态一致性和可观测性,一套能直接落地的方案。

1. Agent系统到底会摔在哪:错误来源全景图

先别急着写异常处理代码。理解整个系统的故障形态,比堆一百个异常捕获更管用。Agent 系统的链路比传统接口服务长得多:用户消息进来,大模型理解意图,规划步骤,调用工具,拿到结果再交给模型,循环到任务收尾。每个环节的错误形态都不同,混合在一起排查难度翻倍。

1.1 大模型层的故障形态

模型接口是整个链路上最脆弱的环节,尤其是走第三方 API 的时候。常见故障大致分四类:

  • 超时类:连接超时、读取超时、思考超时。大模型生成长文本时耗时波动很大,网络抖动一下,请求就可能挂在半路。
  • 限流类:最典型的是 HTTP 429。不同服务商的限流策略不一样,有些按每分钟请求数限,有些按 token 吞吐量限,有些是账号维度或并发维度限。触发限流如果不退避重试,只会越撞越狠。
  • 上下文类:输入 token 超限、输出 token 截断。Agent 系统特别容易踩这个坑,因为每轮都要把历史消息、工具返回结果、系统提示词拼进去。工具返回一大段 JSON,几轮下来上下文直接爆掉。
  • 输出非法类:模型返回了格式不符合预期的内容。常见的是要求输出 JSON 却返回了带 Markdown 代码块的文本,或者 JSON 里混了多余的逗号,更隐蔽的是结构合法但字段缺失。

1.2 工具与服务层的故障形态

Agent 核心价值之一是调用工具。工具的数量越多,故障面越大。典型的包括:

  • 下游服务 5xx:工具背后的业务接口崩了、超时了、限流了。
  • 参数校验失败:模型生成的参数不符合工具的 schema。比如工具要求一个日期字符串,模型传了"明天"或者"2025/02/30"。
  • 部分成功:批量操作中,一部分成功一部分失败。比如一批发消息,三条成功两条失败,这时候整个请求算成功还是失败?
  • 鉴权失效:token 过期、权限被回收,这个经常发生在长时间运行的 Agent 任务中。

1.3 编排层和状态层的隐性错误

这部分的问题不像接口报错那么显眼,但往往破坏力更大。

编排层常见的是:步骤依赖判断错误、循环没有收敛(Agent 陷入了死循环)、步骤超时没有中断机制、上下文被无意识污染(比如某一步的调试信息被塞进了下一步的 prompt)。

状态层的风险集中在:状态存储服务不可用、状态读写出现脏数据、多实例并发时状态被互相覆盖、任务执行中断后没有恢复机制。

排查经验告诉我:大多数看起来"玄学"的 Agent 故障,最终都落在状态层的设计缺陷上,而不是模型不够聪明。

2. 错误处理的分层架构:每层只该管自己该管的事

传统服务的异常处理,一个 try-except 包住业务逻辑就完了。Agent 系统不行——错误来源分散在不同层级,处理策略也完全不同。正确的做法是分层处理,每层只负责自己的错误面和兜底策略。

2.1 分层模型设计

我这里把 Agent 系统的错误处理拆成四层:

层级职责错误来源处理策略
边缘接入层接收用户输入,做基础校验参数缺失、类型错误、格式非法快速失败,直接返回错误信息
模型调用层封装模型 API 调用超时、限流、上下文超限、输出非法重试、退避、上下文裁剪、输出修复
工具适配层调用外部工具并标准化返回下游 5xx、参数错误、部分失败重试、缓存、降级替代方案
流程编排层规划、执行、状态流转步骤失败、循环、状态冲突步骤级重试、任务降级、状态恢复

关键原则:底层只抛结构化错误,不做决策;上层根据错误码和错误类型做策略判断。底层把"发生了什么"讲清楚,上层决定"怎么办"。

举个例子。模型调用层遇到 429,不应该自己无限重试——它应该把错误码 429 抛上去,上层根据当前任务的重要级别决定是重试、换备用模型还是降级到简化流程。

2.2 结构化异常体系

所有层级的异常必须结构化,否则排查时全靠猜。我在项目里一般用统一的异常基类,携带关键字段:

class AgentError(Exception): def __init__( self, error_code: str, # 统一错误码,如 LLM_TIMEOUT message: str, # 人可读的错误描述 retryable: bool, # 当前错误是否可重试 severity: str = "warning", # debug/info/warning/error/critical upstream_error: dict | None = None, # 原始错误细节 context: dict | None = None, # 当前步骤、工具名、重试次数等 ): super().__init__(message) self.error_code = error_code self.retryable = retryable self.severity = severity self.upstream_error = upstream_error self.context = context or {}

设计时有一点要特别注意:retryable这个开关。LLM 超时可重试,参数校验失败不可重试,429 可重试但必须退避,工具业务层 5xx 可重试但要注意重试次数上限。这个字段能避免上层无脑重试导致雪崩。

2.3 快速失败与优雅兜底并存

不是所有错误都需要重试。边缘接入层的输入校验错误、工具参数校验错误、流程中不可恢复的逻辑错误,越早抛出越好。无谓的重试既浪费 token 又拖慢响应。

但另一方面,最终用户感知的不能是一堆堆异常堆栈。兜底设计要确保:系统即使整体不可用,也要给用户一个体面的交代。比如"当前服务请求过多,请稍后再试",而不是一大段密密麻麻的报错 JSON。

3. 优雅降级不是简单重试:从任务级到链路级的降级阶梯

重试只能解决瞬时故障。当模型服务连续报错、下游工具批量超时的时候,硬扛是扛不住的。这时候需要一套降级阶梯——根据当前系统健康状态,自动把一个复杂的 Agent 任务逐级切换到更简单、更稳定的处理方式。

3.1 降级阶梯分层

我习惯把降级动作排成一条阶梯,从低风险到高风险依次切换:

第一级:模型冗余备胎主模型请求失败后,切换到备用模型或降级模型。比如主用大参数旗舰模型,备用小参数模型;或者主用 API,备用本地部署模型。切换时注意提示词兼容性,不同模型对 system prompt 的遵循能力不一样,可能需要同步简化指令。

第二级:上下文压缩降级上下文超限是 Agent 高频故障。降级方式是做上下文裁剪:把对话历史从"完整保留"降级为"摘要 + 最近N轮",把工具返回结果从"完整内容"降级为"关键字段抽取"。这个降级对 token 消耗的影响立竿见影。

第三级:任务拆分降级当完整任务链路连续失败时,尝试把任务拆成子任务逐段执行。比如一个"调研竞品并生成周报"的任务,拆成"竞品信息检索"和"周报内容生成"两步。前者失败不影响后者的兜底,大幅度提高整体完成率。

第四级:流程简化降级从强规划模式降级为弱规划模式。比如从规划式 Agent(ReAct/Plan-and-Solve)降级为直接调用型——跳过规划步骤,直接把用户请求匹配到一个已有模板上。功能是简单了,但起码能响应。

第五级:人工兜底走到这一级说明降级阶梯已经到底了。保留一个队列或工单通道,引导用户提交人工处理请求。有些任务宁可排队也不要让用户拿到一个错误的结果。

3.2 降级开关的触发机制

降级不能人工拍脑袋,要让系统自动判断。我用的是一套健康度评分机制:

  • 每个关键依赖持续上报健康状态:模型服务成功率、平均时延、工具服务错误率。
  • 一个滑动窗口内,如果某依赖的错误率超过阈值(比如 5 分钟窗口内超过 30%),自动标记为"不健康"。
  • 编排层做路由决策时绕过不健康的依赖,直接走降级分支。

注意:健康状态不能实时重置。引入冷却时间和状态平滑恢复,避免依赖在"健康/不健康"之间抖动,导致系统频繁切换降级策略。

3.3 一个降级实例

项目里一个真实场景:自动客服 Agent,负责查询物流信息并解答售后问题。一次下游物流 API 大面积故障,所有查询工具都在报 5xx。降级链路是这样跑的:

  1. 第一层:工具适配层重试两次,失败。
  2. 第二层:健康评分触发,物流 API 被标记为不健康。
  3. 第三层:编排层检测到工具不可用,跳过物流查询步骤,改用缓存的历史查询结果(如果用户问的是同一单号),否则直接走人工工单兜底。
  4. 第四层:用户收到的回复是"物流接口暂时不稳定,我们已经记录您的查询需求,将在恢复后第一时间通知您",而不是一个错误页。

整个切换过程用户无感,客服工单没有堆积太多,等物流 API 恢复后健康评分自动回升,系统平滑切回正常流程。

4. 超时与重试参数的"气血调理":那些需要精细配置的数字

很多 Agent 系统挂在超时和重试参数上——要么超时设太短导致经常误判失败,要么重试太凶打得下游接口嗷嗷叫。这部分全是实操细节,参数怎么设、为什么这么设,直接说清楚。

4.1 超时参数精细化

HTTP 调用不要只设一个总超时,要分开设三个阶段的超时:

参数建议默认值设置思路
connect_timeout3~5 秒网络连接建立。正常情况毫秒级完成,超过 3 秒基本说明网络有问题
read_timeout取决于任务复杂度模型请求的读取超时要跟模型能力挂钩。复杂推理任务给 60~120 秒,简单工具调用给 10~15 秒
think_timeout / 总超时任务级别整个 Agent 步骤允许的最长执行时间,超过需要中断

有一个容易踩的坑:模型流式输出时,如果只设 read_timeout,模型长时间不吐 token(但连接还开着)会被误判为超时。流式场景建议从"最后收到一个字节的时间"开始计时,而不是从请求发起时开始计时。

4.2 重试策略与退避公式

重试的核心是退避,退避的核心是加抖动(jitter)。没有抖动的固定间隔重试,会让所有客户端在同一时间打爆下游接口。

经典公式:

sleep = min(cap, base * multiplier ** attempt) + random(0, jitter)

实际配置建议:

  • base:1 秒
  • multiplier:2
  • cap:最大退避时间 30 秒(防止退避时间长得离谱)
  • jitter:随机值,范围通常在 0~500ms 之间
  • 总重试次数:3 次以内。多了没意义,反而拖慢整体响应

一个执行步骤的大致流程:

max_attempts = 3 attempt = 0 while attempt < max_attempts: try: result = call_llm_with_timeout(...) return result except LLMError as e: if not e.retryable or attempt >= max_attempts - 1: raise sleep_time = min(30, 1 * (2 ** attempt)) + random.uniform(0, 0.5) time.sleep(sleep_time) attempt += 1

4.3 不同错误码的处理差异

超时是不分错误码的,但 HTTP 错误码要区分对待:

  • 429:限流。必须退避重试,而且要读响应头里的Retry-After字段。服务商具体限流策略可能不同,有的按分钟配额,有的按并发,退避 5~60 秒比较安全。
  • 5xx:服务端故障。可重试,但要留意是否连续失败。连续 3 次 5xx,说明大概率是持续故障,可以触发降级而不是继续重试。
  • 4xx(除 429):参数问题、鉴权失败。通常不可重试,重试只会浪费请求。

4.4 智能超时:按模型和任务差异化配置

一个常见失误是全局用一个超时值。实际上不同任务类型、不同模型能力差很多。一个普通信息抽取任务和一个深度推理任务,耗时可能差 5 倍以上。

我的做法是维护一份模型能力画像:

{ "model": "gpt-4o-class", "default_timeout": 60, "complex_reasoning_timeout": 120, "long_context_timeout": 90 }

编排层根据任务的复杂度标签,选择对应的超时档位。复杂任务给足时间,简单任务快速失败。评估任务复杂度的方式可以很朴素:步骤依赖数、上下文长度、是否涉及多轮工具调用。

5. Agent状态管理与数据一致性:最容易被忽视的高可用盲区

接口错误看得见摸得着,状态错误才是无声杀手。Agent 的技术核心是状态流转:用户消息进来,状态从"待理解"到"已规划"到"执行中"到"已完成"。状态一旦错乱,做再多重试都救不回来。

5.1 状态持久化的正确姿势

在本地内存里维护状态只适合开发和单实例 demo。生产环境只要有多个实例,内存态必然出问题:用户请求打到实例 A,状态却存在实例 B 的内存里,互相看不到对方。

生产级做法是把状态外置到 Redis 或者数据库。我倾向用 Redis Hash 存状态快照,配合 TTL 处理会话过期:

agent:session:{session_id} 字段: status 当前状态 current_step 当前步骤号 history 已执行步骤摘要 context 上下文数据(JSON) updated_at 最后更新时间

步骤级的原子更新用 Lua 脚本或者事务保证,避免两个实例同时修改同一个会话状态。很多并发问题表面上是"Agent 怎么能这样思考",实际是 Redis 里状态被覆盖了。

5.2 幂等机制

Agent 最容易出问题的是重试导致的重复执行。比如工具调用成功返回了结果,但响应在网络上丢了,客户端重试。如果工具不是幂等的(比如发消息、扣费用),一次成功被当成失败重试,重复执行就产生了。

解决办法是给每一步执行分配唯一标识(step_id),在状态存储里记录当前步骤的幂等键。重试同一个步骤时,先查幂等键是否已有结果,有就直接返回,不重复执行。

幂等键的生命周期要管理好。会话结束或长期空闲后清理,否则 Redis 里全是陈旧的幂等记录,占用内存。

5.3 多 Agent 协作时的状态隔离

多 Agent 系统里,子 Agent 的状态不能跟主 Agent 混在一起。我的习惯是:主会话状态和子任务状态分开存储,子任务只保留自己的上下文和结果,通过任务 ID 关联回主会话。这样任何一个子任务失败,可以单独重跑而不污染主流程的上下文环境。

还有一点:子 Agent 的失败不要直接中断整个任务。子任务可控的话,记录失败原因,交给编排层决定是重跑、跳过还是降级到备用方案。

6. 全链路可观测性:让故障发生在你的眼皮底下

写再多错误处理逻辑,如果观测不到,等于盲飞。Agent 系统的链路那么长,排查一个问题经常要跨模型调用、工具调用、状态读写多个环节,没有一套贯穿全程的可观测体系,排查基本靠猜。

6.1 TraceID 贯穿全链路

所有的模型调用、工具调用、状态操作都要带上同一个 TraceID。用户在对话中触发一次 Agent 执行,这整个过程就对应一个 TraceID。日志里按 TraceID 一搜,能精确还原整个执行过程。

配合日志的结构化,把关键流程都记录下来:

{ "trace_id": "t_20250225_abc123", "span": "tool_call", "step": 2, "tool": "order_query", "status": "success", "duration_ms": 850, "error_code": null }

6.2 三类关键指标

除了常规的 QPS、成功率、时延,Agent 系统要额外关注三类指标:

决策质量指标:任务完成率、步骤收敛率、循环打断次数。这几个指标反映 Agent"思考"得顺不顺畅。

上下文消耗指标:每一步的 token 消耗、上下文占比、裁剪触发次数。上下文裁剪频繁说明系统的记忆管理策略有问题,需要优化摘要节奏。

降级触发指标:降级路径被触发的次数、各类降级原因的分布。降级量突然飙升,往往是最早的系统故障信号。

6.3 告警分级

告警必须分级,否则一有风吹草动就拉群报警,时间长了大家都麻木了。

  • P1(立即处理):关键依赖不可用超过 5 分钟、任务完成率低于 70%、上下文故障率超过阈值。
  • P2(重点观察):时延明显增长、降级触发率升到正常值的 2 倍以上。
  • P3(记录追踪):偶发超时、单次工具失败、重试次数上升。

P1 告警才走即时通知,P2/P3 进看板。别把所有告警都搞成即时通知,真正的故障反而会被淹没在噪音里。

6.4 番茄炒蛋式排查链路复盘

有一次线上故障排查,印象很深。用户投诉 Agent 间歇性无响应,单看日志一切正常。后来靠 TraceID 串联比对才发现:所有出问题的请求,都发生在 Redis 主从切换的那几秒窗口——状态读取超时了,编排层直接抛出异常,但异常被上层吞掉,变成了"无响应"。

如果没有全链路追踪,这种跨 Redis 和编排层的隐蔽问题,排查时长会拉长几倍。这也是为什么可观测性必须跟错误处理一体设计,不能等出故障了再补。

7. 实测量产教训:那些只有跑过才知道的坑

最后分享几个我在真实项目里踩过的坑,每一个都付出了真金白银的 token 成本。

7.1 模型从 JSON 改返回 Markdown,解析直接崩

模型服务商更新了模型行为,要求输出 JSON 的接口,新模型开始返回带 Markdown 代码块的内容。解析器没做容错,任务大面积失败。

修复方案是双保险:解析失败时先剥离 Markdown 标记再重试;同时维护一个小的修复规则库——比如修正多余逗号、补全缺失的引号。另外,抓到模型输出变化要第一时间告警,老模型下线前要主动做兼容测试。

7.2 重试风暴把下游接口打懵了

一个批量任务对下游工具做了同步重试,重试退避逻辑写的 base 时间又太短,大量并发任务一起退避重试,直接把下游 API 打到了限流阈值。这其实是用错误处理机制制造了新的故障。

解决办法:全局信号量限制同一时刻的调用并发量,退避参数从"任务内均匀重试"改成"全局统一抖动"。在最外层再加一层熔断,下游接口连续失败时直接切断后续请求,让接口喘口气。

7.3 降级开关自己先挂了

熔断降级的实现没考虑自身的资源消耗。一次故障发生时,所有请求同时触发降级逻辑,熔断器自身的监控存储被打爆了,降级完全失效。

经验教训:降级组件本身必须也是一等公民,独立监控、独立容量规划。所有降级策略触发时都要有日志和指标,这样才清楚这个系统到底因为什么降级、降了好不好使。

7.4 "重试就好"是对高可用的最大误解

这是我最想强调的一个观点。重试解决的是瞬时抖动,降级解决的是局部不可用,状态一致性解决的是持久化层的可靠性,可观测性解决的是故障定位的效率。整套高可用方案是一层层叠起来的,单独重试只是让故障晚一点暴露,并不是让它不发生。

把这些沉淀成规范之后,团队新接手 Agent 项目的同学,照着这个框架就能搭出一套有防护的底座——健康检查、错误码、降级阶梯、超时策略、状态幂等、TraceID、告警分级,一个都不缺。这才是"高可用 Agent 系统"该有的样子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询