上线一个 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。降级链路是这样跑的:
- 第一层:工具适配层重试两次,失败。
- 第二层:健康评分触发,物流 API 被标记为不健康。
- 第三层:编排层检测到工具不可用,跳过物流查询步骤,改用缓存的历史查询结果(如果用户问的是同一单号),否则直接走人工工单兜底。
- 第四层:用户收到的回复是"物流接口暂时不稳定,我们已经记录您的查询需求,将在恢复后第一时间通知您",而不是一个错误页。
整个切换过程用户无感,客服工单没有堆积太多,等物流 API 恢复后健康评分自动回升,系统平滑切回正常流程。
4. 超时与重试参数的"气血调理":那些需要精细配置的数字
很多 Agent 系统挂在超时和重试参数上——要么超时设太短导致经常误判失败,要么重试太凶打得下游接口嗷嗷叫。这部分全是实操细节,参数怎么设、为什么这么设,直接说清楚。
4.1 超时参数精细化
HTTP 调用不要只设一个总超时,要分开设三个阶段的超时:
| 参数 | 建议默认值 | 设置思路 |
|---|---|---|
| connect_timeout | 3~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 += 14.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 系统"该有的样子。