
上线一个 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 消耗的影响立竿见影。第三级任务拆分降级当完整任务链路连续失败时尝试把任务拆成子任务逐段执行。比如一个调研竞品并生成周报的任务拆成竞品信息检索和周报内容生成两步。前者失败不影响后者的兜底大幅度提高整体完成率。第四级流程简化降级从强规划模式降级为弱规划模式。比如从规划式 AgentReAct/Plan-and-Solve降级为直接调用型——跳过规划步骤直接把用户请求匹配到一个已有模板上。功能是简单了但起码能响应。第五级人工兜底走到这一级说明降级阶梯已经到底了。保留一个队列或工单通道引导用户提交人工处理请求。有些任务宁可排队也不要让用户拿到一个错误的结果。3.2 降级开关的触发机制降级不能人工拍脑袋要让系统自动判断。我用的是一套健康度评分机制每个关键依赖持续上报健康状态模型服务成功率、平均时延、工具服务错误率。一个滑动窗口内如果某依赖的错误率超过阈值比如 5 分钟窗口内超过 30%自动标记为不健康。编排层做路由决策时绕过不健康的依赖直接走降级分支。注意健康状态不能实时重置。引入冷却时间和状态平滑恢复避免依赖在健康/不健康之间抖动导致系统频繁切换降级策略。3.3 一个降级实例项目里一个真实场景自动客服 Agent负责查询物流信息并解答售后问题。一次下游物流 API 大面积故障所有查询工具都在报 5xx。降级链路是这样跑的第一层工具适配层重试两次失败。第二层健康评分触发物流 API 被标记为不健康。第三层编排层检测到工具不可用跳过物流查询步骤改用缓存的历史查询结果如果用户问的是同一单号否则直接走人工工单兜底。第四层用户收到的回复是物流接口暂时不稳定我们已经记录您的查询需求将在恢复后第一时间通知您而不是一个错误页。整个切换过程用户无感客服工单没有堆积太多等物流 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)实际配置建议base1 秒multiplier2cap最大退避时间 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 系统该有的样子。