☰
Agent 工程化分水岭:错误处理、重试与幂等设计实践
2026/10/1 12:21:31 网站建设 项目流程

1. 为什么错误处理才是 Agent 工程化的分水岭

做 Agent 开发的人大概都有过这种体验:Demo 阶段一切丝滑,工具调用、多轮推理、记忆读写全都跑得通,可一旦放到真实环境里跑上几天,各种稀奇古怪的报错就开始冒出来。模型这一轮只输出了思考过程没产出正文、工具调用超时、外部接口返回 429、上下文超长被截断、沙盒环境失联、token 刷新失败……这些问题单看每一个都不难,但它们叠加在一起,就足以让一个看起来能用的 Agent 变成"三天两头挂"的半成品。

我自己的判断是:Agent 从玩具到产品的分界线,不是模型能力,而是错误处理与工程化程度。模型能力决定上限,工程化决定下限,而绝大多数线上事故都发生在下限这一侧。一个 Agent 系统里,模型调用、工具执行、记忆读写、状态持久化、并发调度,每一环都可能失败,而且失败方式千奇百怪——有的报错清晰,有的只给你一句"请稍后重试",有的干脆静默返回空结果。

这篇内容我想聊的就是这块最不性感、但最要命的部分:Agent 的错误处理机制怎么设计,工程化实践里有哪些坑,重试和幂等这两个核心手段到底该怎么落地。适合已经写过基础 Agent、准备把它推向真实业务场景的开发者,也适合正在做 Agent 框架选型和架构设计的人。我会尽量把每个决策背后的"为什么"讲清楚,而不是只丢一堆代码让你抄。

先说一个我踩过的坑作为引子。早期我写的一个 Agent,工具调用失败就直接抛异常终止整个流程,结果用户看到的就是"agent execution terminated due to error"。后来改成失败重试,又遇到重复扣款的问题——因为工具本身不幂等,重试把同一个订单提交了两次。再后来加了幂等键,又发现并发场景下幂等检查本身有竞态。这一路踩下来,我才真正理解为什么说错误处理是 Agent 工程化的分水岭。

2. Agent 错误的全景分类与应对思路

2.1 按错误来源划分的四大类

在动手写任何错误处理代码之前,我建议先把错误分类搞清楚。分类不清,处理策略就一定是拍脑袋的。根据我实际项目里的统计,Agent 的错误大致可以归到四类,每类的处理逻辑完全不同。

错误类别典型表现是否可重试处理策略
模型层错误只输出思考无正文、请求失败 4054、上下文超限多数可重试逐级提升输出预算、截断上下文、降级模型
工具层错误接口超时、429、返回格式异常视幂等性而定幂等则重试,非幂等需补偿
环境层错误沙盒失联、设备离线、网络连接失败 3002可重试但需退避指数退避、健康检查、熔断
逻辑层错误参数校验失败、状态机非法转移不可重试直接失败并记录,触发人工介入

模型层错误里最典型的就是"模型本轮只输出了思考过程、没有产出正文"。这个错误很多人第一次遇到会懵,其实原因通常是输出预算被思考过程吃光了。系统自动重试并逐级提升输出预算,就是针对这个的标准解法。我后面会详细讲这个重试策略怎么设计。

工具层错误是重灾区。因为工具往往对接外部系统,而外部系统的失败模式你控制不了。这里最关键的一个判断就是:这个工具调用是不是幂等的。幂等就能放心重试,不幂等就得走补偿或者去重逻辑。这个判断直接决定了你的重试策略,后面单独开一节讲。

2.2 可重试与不可重试的判定标准

很多人写重试逻辑的通病是"无脑重试",结果把不可重试的错误也重试了,浪费资源还放大问题。我的经验是建立一个明确的判定标准,落到代码里就是一个函数。

判定一个错误是否可重试,我一般看三个维度:

  • 错误是否瞬时:网络抖动、限流、临时不可用属于瞬时,参数错误、权限不足属于持久。
  • 操作是否幂等:查询、读取天然幂等;写入、扣款、发消息需要额外保证。
  • 重试是否有副作用:重试会不会导致重复执行、状态错乱、资源泄漏。

只有三个维度都过关,才允许自动重试。任何一个不过关,就应该走失败路径或者人工介入。这个标准听起来简单,但真正落到每个工具上,需要你逐个去分析,没有捷径。

提示:不要试图用一个通用的重试装饰器套在所有工具上。工具之间的幂等性和副作用差异巨大,通用装饰器看着优雅,实际会埋雷。我建议按工具粒度配置重试策略。

2.3 错误处理的分层架构

从架构角度,我习惯把错误处理分成三层,每层职责清晰,互不越界。

第一层是调用层,负责单次调用的即时重试和超时控制。这一层最贴近具体操作,知道这次调用是什么、能不能重试。第二层是编排层,负责整个 Agent 执行流程的错误恢复,比如某个步骤失败后是回退、跳过还是终止。第三层是系统层,负责熔断、降级、告警和可观测性,从全局视角看错误趋势。

分层的价值在于:调用层的重试不会污染编排逻辑,编排层的决策不会干扰系统层的全局判断。我见过太多项目把这三层揉在一起,结果一个工具的重试逻辑改了,整个流程都受影响。分层之后,每层可以独立演进,测试也好写。

3. 重试机制的设计与落地细节

3.1 指数退避与抖动:为什么不能固定间隔重试

重试策略里最基础也最容易被做错的就是退避算法。新手最常见的写法是固定间隔重试,比如失败后等 1 秒再试,连试 3 次。这个策略在单机、低并发场景下勉强能用,但一旦并发上来,就会引发"重试风暴"——所有失败的请求在同一时刻一起重试,把本来只是抖动的下游直接打挂。

正确的做法是指数退避加随机抖动。指数退避让重试间隔随次数增长,给下游恢复的时间;随机抖动打散重试时刻,避免同步。公式大致是:

delay = min(base * (2 ^ attempt), max_delay) * (1 + random_jitter)

其中 base 是基础间隔,attempt 是第几次重试,max_delay 是上限,random_jitter 是 0 到 1 之间的随机数。举个具体例子,base 取 0.5 秒,max_delay 取 30 秒:

重试次数基础延迟加抖动后范围
第 1 次0.5s0.5s ~ 1.0s
第 2 次1.0s1.0s ~ 2.0s
第 3 次2.0s2.0s ~ 4.0s
第 4 次4.0s4.0s ~ 8.0s
第 5 次8.0s8.0s ~ 16.0s

抖动系数我一般取 1,也就是延迟在基础值的 1 到 2 倍之间随机。这个范围足够打散,又不会让延迟失控。max_delay 一定要设,否则重试次数一多,延迟会指数爆炸到不可接受。

3.2 重试预算与熔断:防止重试拖垮系统

光有退避还不够,还得有重试预算的概念。所谓重试预算,就是给整个系统或单个下游设定一个重试总量上限,超过就停止重试直接失败。这个思路来自 SRE 里的错误预算,本质是承认"重试不是免费的,它消耗资源"。

我通常会在两个维度设预算:单次请求的重试次数上限(比如 5 次),以及单位时间内对某个下游的重试总量上限。前者防止单个请求无限重试,后者防止某个下游故障时被重试流量淹没。

和重试预算配套的是熔断器。当某个下游连续失败达到阈值,熔断器直接打开,后续请求快速失败,不再尝试。等过一段时间进入半开状态,放少量请求试探,成功就恢复,失败就继续熔断。熔断器的价值在于:下游已经挂了的时候,继续重试只是浪费自己的资源,快速失败反而能让上游有时间做降级处理。

注意:熔断阈值不要设得太敏感。我见过阈值设成连续 3 次失败就熔断的,结果正常波动也会触发,反而增加了失败率。一般连续失败 10 次或者失败率超过 50% 且样本足够,才考虑熔断。

3.3 模型层重试的特殊处理:输出预算逐级提升

模型层的重试和普通接口重试不太一样,因为有些"失败"其实是模型行为问题,不是网络问题。最典型的就是"只输出思考过程、没有产出正文"。这种情况重试时如果参数不变,大概率还是同样的结果,所以需要逐级提升输出预算。

具体做法是:第一次失败后,把 max_tokens 提升一个档位再试;再失败再提升,直到达到上限。同时可以配合调整提示词,比如在重试时追加一句"请直接给出最终答案,不要输出思考过程"。我实测下来,这个组合策略对"思考吃光预算"类问题的解决率很高。

还有一种模型层错误是上下文超限。这时候重试前必须先做上下文压缩或截断,否则重试多少次都一样。压缩策略可以是摘要历史、丢弃最旧的消息、或者只保留关键状态。这块和记忆管理强相关,后面会展开。

3.4 重试的代码骨架

下面给一个我常用的重试骨架,Python 写的,核心逻辑清晰,可以直接改。

import time import random from functools import wraps class RetryExhausted(Exception): pass def retry_with_backoff( max_attempts=5, base_delay=0.5, max_delay=30.0, jitter=1.0, retryable=lambda e: True, on_retry=None, ): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exc = None for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception as e: last_exc = e if not retryable(e) or attempt == max_attempts - 1: raise delay = min(base_delay * (2 ** attempt), max_delay) delay = delay * (1 + random.random() * jitter) if on_retry: on_retry(attempt, e, delay) time.sleep(delay) raise RetryExhausted() from last_exc return wrapper return decorator

这个骨架的关键点在于retryable回调,它把"能不能重试"的判断交给调用方,而不是写死在装饰器里。这样不同工具可以传不同的判定函数,灵活又安全。on_retry回调则用来做日志和监控,每次重试都记录一次,方便事后分析重试分布。

4. 幂等性:重试的安全底线

4.1 幂等性到底解决什么问题

重试最大的风险就是重复执行。一个查询接口重试一百次都没事,但一个下单接口重试两次就可能出大问题。幂等性就是保证同一个操作执行多次和执行一次的效果相同。它是重试的安全底线,没有幂等保证的重试就是在赌博。

在 Agent 场景里,幂等性问题尤其突出,因为 Agent 会自主决定调用哪些工具、调用几次。模型可能因为一次超时就重新发起同样的工具调用,如果工具不幂等,就会产生重复副作用。我遇到过最离谱的一次是 Agent 在重试逻辑下把同一封邮件发了三遍,因为邮件发送接口没有幂等保证。

所以我的原则很明确:凡是会产生副作用的工具,必须实现幂等,否则不允许自动重试。这条原则听起来严格,但能避免绝大多数线上事故。

4.2 幂等键的设计与传递

实现幂等最常见的手段是幂等键。客户端为每个逻辑操作生成一个唯一键,服务端记录这个键的处理结果,重复请求直接返回首次结果。幂等键的设计有几个要点:

  • 唯一性:同一个逻辑操作必须生成同一个键,不同操作必须不同。通常用业务 ID 加操作类型组合,或者用 UUID。
  • 可传递:幂等键要能穿过整个调用链,从 Agent 编排层一直传到最底层的工具实现。
  • 有生命周期:幂等键不能永久保存,否则存储会爆炸。一般保留 24 小时到 7 天,取决于业务对重复窗口的容忍度。

在 Agent 里,幂等键的生成时机很关键。我建议在编排层生成,而不是在工具层。因为编排层才知道这是一个逻辑操作,工具层只看到一次调用。编排层生成键后,通过上下文传递给工具,工具用它去重。

4.3 幂等检查用 DB 还是 Redis

这是热词里出现的一个经典问题,我直接给结论:看你的持久化要求和并发量,多数场景两者结合。

方案优势劣势适用场景
纯 DB强一致、可持久化、事务支持高并发下性能瓶颈、锁竞争金融、订单等强一致场景
纯 Redis高性能、天然支持过期可能丢数据、一致性弱高并发、可容忍极低概率重复
DB + Redis兼顾性能与一致实现复杂、需处理缓存失效大多数生产场景

纯 Redis 的问题是它本质是缓存,宕机或主从切换时可能丢数据,导致幂等键丢失,重复请求就漏过去了。纯 DB 的问题是每次幂等检查都要读写数据库,高并发下压力大。我常用的方案是 Redis 做第一层快速去重,DB 做最终兜底。请求先查 Redis,命中就直接返回;未命中则查 DB 并加唯一约束,DB 插入成功才真正执行,插入冲突说明重复。

提示:DB 层一定要加唯一索引,这是最后一道防线。我见过只靠 Redis 去重、结果 Redis 抖动时重复扣款的案例,加了唯一索引就能彻底堵死。

4.4 幂等与并发的竞态处理

幂等检查本身也有竞态。两个相同请求几乎同时到达,都查了 Redis 没命中,都去查 DB 也没命中,然后都执行了。这就是典型的 check-then-act 竞态。

解决办法有两个方向。一是用原子操作,比如 Redis 的 SET NX,设置成功才继续,失败说明有并发请求。二是用数据库唯一约束,让数据库来保证原子性,插入冲突的那个请求直接返回已有结果。我一般两个都用:Redis SET NX 做快速拦截,DB 唯一索引做最终保证。

在 Agent 场景下,还要注意工具调用的并发。如果 Agent 支持并行工具调用,同一个逻辑操作可能被拆成多个并发子调用,这时候幂等键的粒度要设计好,确保子调用之间不会互相干扰。

5. Agent 工程化的其他关键实践

5.1 状态持久化与断点恢复

Agent 执行往往是有状态的,多轮对话、工具调用链、中间结果都需要保存。如果执行到一半挂了,没有持久化就只能从头再来,用户体验极差。状态持久化是断点恢复的前提。

我的做法是把 Agent 的执行状态抽象成一个状态机,每一步的状态变更都持久化。持久化的粒度要权衡:太粗,恢复时丢失太多;太细,写入压力大。一般按"步骤"粒度持久化,一个工具调用完成就存一次。存储介质用 DB 或 Redis 都行,看恢复时效要求。

断点恢复时,从最后一个持久化状态继续执行。这里要注意幂等:恢复后重新执行的那一步,可能之前已经执行过了,所以每一步都要幂等。这也是为什么幂等是工程化的基础。

5.2 可观测性:日志、指标与追踪

错误处理做得好不好,很大程度上取决于你能不能看到错误。可观测性三件套:日志、指标、追踪,一个都不能少。

日志要结构化,每条日志带上 trace_id、step、tool、attempt 等字段,方便聚合分析。指标要覆盖关键维度:调用成功率、重试率、平均重试次数、熔断触发次数、各错误码分布。追踪要能串起整个调用链,从用户请求到模型调用到工具执行,一眼看出瓶颈在哪。

我特别想强调重试率这个指标。重试率突然升高,往往是下游出问题的早期信号。如果只监控最终成功率,可能因为重试兜底而看不出问题,等重试也兜不住时就晚了。

5.3 降级与兜底策略

不是所有错误都能靠重试解决。当重试和熔断都失效时,需要有降级方案。降级的思路是:用次优但可用的方案替代失败的主方案。

比如主模型不可用时,降级到备用模型;工具调用失败时,返回缓存结果或默认值;记忆服务不可用时,退化为无记忆模式。降级的关键是提前设计好,而不是等出事了临时想。每个关键依赖都应该有对应的降级预案,并且定期演练。

注意:降级方案本身也要有监控。降级是临时手段,如果长期处于降级状态,说明主方案有根本问题,需要修复而不是一直降级。

5.4 沙盒与执行环境隔离

Agent 执行代码或操作文件时,沙盒隔离是安全底线。热词里提到的"更新 agent 沙盒""docker 容器里的 ros2"都指向这个方向。沙盒要做到资源隔离、网络隔离、文件系统隔离,防止 Agent 的误操作影响宿主环境。

工程化上,沙盒的生命周期管理很重要:创建、复用、销毁都要有明确策略。频繁创建销毁开销大,长期复用又有状态污染风险。我一般用池化方案,维护一个沙盒池,用完重置而不是销毁。

6. 常见问题排查与避坑实录

6.1 典型错误速查表

下面这张表是我从实际项目里整理出来的,覆盖了 Agent 开发中最常遇到的错误和对应处理。

错误现象可能原因排查方向处理建议
只输出思考无正文输出预算被思考吃光检查 max_tokens 与思考长度逐级提升预算 + 提示词约束
请求失败 4054模型服务临时不可用查看服务状态与错误码分布指数退避重试 + 熔断
网络连接失败 3002网络抖动或下游不可达检查网络与下游健康退避重试 + 健康检查
设备离线/沙盒失联执行环境异常检查沙盒状态与资源重建沙盒 + 状态恢复
token 刷新失败凭证过期或服务异常检查凭证有效期重新获取凭证 + 重试
重复执行副作用工具不幂等 + 重试检查幂等键与唯一约束补幂等键 + DB 唯一索引
上下文超限历史消息过长检查上下文长度压缩/截断 + 摘要
并发下重复扣款幂等检查竞态检查 check-then-actRedis SET NX + DB 唯一约束

6.2 我踩过的三个坑

第一个坑:无脑重试导致重复副作用。前面提过,早期我的重试装饰器套在所有工具上,结果邮件发了三遍。教训是重试必须和幂等绑定,不幂等的工具宁可不重试。

第二个坑:熔断阈值太敏感。设成连续 3 次失败就熔断,结果正常波动频繁触发,反而增加了失败率。后来改成滑动窗口统计失败率,样本足够才熔断,稳定多了。

第三个坑:幂等键生成位置错误。一开始在工具层生成幂等键,结果每次重试都生成新键,幂等完全失效。后来改到编排层生成,通过上下文传递,才真正生效。这个坑很隐蔽,因为代码看起来没问题,但逻辑上键的粒度错了。

6.3 避坑清单

  • 重试前先判断幂等性,不幂等不重试。
  • 退避算法必须加抖动,避免重试风暴。
  • 熔断阈值用滑动窗口,别用连续计数。
  • 幂等键在编排层生成,不在工具层。
  • 幂等检查用 Redis + DB 双层,DB 加唯一索引。
  • 状态持久化按步骤粒度,恢复时每步都要幂等。
  • 重试率要单独监控,它是下游故障的早期信号。
  • 降级方案提前设计,定期演练。

7. 从错误处理看 Agent 架构的演进方向

聊到这里,我想说一个更宏观的观察。错误处理和工程化实践,其实反过来在塑造 Agent 的架构。早期 Agent 框架大多假设"模型调用会成功、工具会返回正常结果",所以架构很简洁。但真实世界的失败率逼着框架必须内建重试、幂等、熔断、状态恢复这些能力。

我看到的趋势是,Agent 框架正在从"编排逻辑"向"运行时平台"演进。编排只是其中一部分,运行时还要负责错误处理、资源管理、可观测性、安全隔离。这也是为什么现在很多 Agent 项目开始强调"工程化最佳实践",因为大家发现光有编排能力不够,跑不稳。

对开发者来说,这意味着两件事。一是选型时要看框架的工程化能力,不只是看它支持多少模型、多少工具。二是自己写 Agent 时,要把错误处理当成一等公民,而不是事后补丁。我个人的经验是,错误处理代码量往往占到整个 Agent 项目的三成以上,这个比例是合理的,不是浪费。

最后分享一个我自己的小习惯:每次上线新工具,我都会先写一个"故障注入"测试,人为让工具失败、超时、返回异常,看 Agent 的反应是否符合预期。这个习惯帮我提前发现了无数问题,比等线上出事再修划算得多。错误处理这东西,平时看不出价值,出事的时候才知道有没有。

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

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

立即咨询