ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent工程化实践:错误处理、重试与幂等设计

Agent工程化实践:错误处理、重试与幂等设计 1. 从一次线上事故说起为什么错误处理是Agent工程化的分水岭去年冬天我接手了一个内部Agent项目功能是自动处理用户提交的工单读取工单内容、调用大模型做意图分类、再根据分类结果调用不同的下游工具接口。Demo阶段跑得特别顺测试环境里连续跑了两百条工单成功率接近百分之百。团队里所有人都觉得这东西已经可以上线了于是我们挑了个周五下午做了灰度发布。结果周一早上打开监控面板我整个人是懵的。灰度流量里大约有百分之十七的工单卡在“处理中”状态既没有成功也没有失败就那么悬着。翻日志才发现问题出在一个特别不起眼的地方下游某个工具接口在高峰期响应变慢Agent的HTTP客户端默认超时时间是30秒超时之后抛出的异常没有被任何一层捕获整个执行链路直接中断任务状态永远停留在“处理中”。更麻烦的是有些工单实际上已经在下游执行成功了只是响应回来的时候超时了重试之后又执行了一遍导致重复扣款。那次事故让我彻底明白一件事Agent系统的复杂度不在于它能调用多少工具、能规划多复杂的任务而在于当某个环节出错时整个系统能不能优雅地兜住。错误处理和工程化实践才是Agent从“能跑的Demo”变成“能扛的生产系统”之间那道真正的分水岭。这篇内容我想聊的就是这件事。它适合已经写过至少一个Agent Demo、正准备把它推向真实业务场景的开发者也适合正在设计Agent框架、需要把容错能力做进底层的工程师。我会从错误分类、重试策略、幂等设计、可观测性、并发控制这几个角度把我在实际项目里踩过的坑和总结出来的做法完整讲一遍。核心关键词围绕错误处理、工程化实践、Agent、重试、幂等展开不玩虚的全是能直接抄作业的东西。2. Agent的错误到底分几类为什么不能一把梭全重试2.1 把错误当成一个整体是新手最容易犯的错很多人写Agent的错误处理第一反应就是“加个try-catch出错就重试”。这个思路在简单场景下能用但在Agent系统里会出大问题。原因很简单Agent执行链路里可能出现的错误性质完全不同有的重试一百次也没用有的不重试反而更安全。我习惯把Agent执行过程中遇到的错误分成四大类每一类的处理策略都不一样。第一类是瞬时性错误Transient Error。典型代表是网络抖动、下游服务短暂过载返回503、数据库连接池暂时耗尽。这类错误的特征是“过一会儿自己就好了”重试是最有效的处理方式。判断依据通常是错误码HTTP的429、502、503、504数据库的deadlock、连接超时这些都属于瞬时性错误。第二类是永久性错误Permanent Error。比如参数格式错误返回400、鉴权失败返回401、资源不存在返回404。这类错误你重试一万次结果都一样重试只会浪费时间和资源还会把日志刷得没法看。正确的做法是立即失败把错误信息清晰地抛给上层。第三类是业务逻辑错误Business Error。这类最隐蔽。比如Agent调用一个“创建订单”的工具接口返回200但响应体里写着“库存不足”。从HTTP层面看这是成功的从业务层面看这是失败的。如果你只看HTTP状态码做重试判断就会把这种错误当成成功任务状态标记为完成实际上什么都没做成。第四类是Agent自身的推理错误Reasoning Error。这是Agent系统特有的一类。比如大模型输出了格式不合法的JSON、规划出的工具调用链里引用了不存在的工具、参数类型和工具签名对不上。这类错误重试有时候有用模型有随机性换个采样可能就对了有时候没用prompt本身有歧义。需要结合具体情况判断。2.2 一张表把错误分类和处理策略说清楚错误类型典型场景是否重试重试策略备注瞬时性错误网络抖动、503、429是指数退避3-5次必须配合幂等永久性错误400、401、404否立即失败记录详细上下文业务逻辑错误库存不足、余额不够视情况通常不重试需要解析响应体推理错误JSON格式错、工具不存在视情况有限重试修正prompt需要错误反馈给模型这张表是我在项目里实际用的分类框架你可以直接拿去改。关键点在于在写重试逻辑之前先写一个错误分类函数。这个函数接收异常对象或响应对象返回一个枚举值后续所有处理逻辑都基于这个枚举值分支。这样做的好处是错误处理策略集中在一处改起来方便也不会出现“这个接口重试了那个接口没重试”的不一致问题。2.3 业务逻辑错误为什么最容易被漏掉我单独把业务逻辑错误拎出来说是因为它坑过我。当时我们有个Agent负责调用支付接口接口设计是这样的HTTP 200表示请求被接收响应体里有个status字段success表示扣款成功failed表示扣款失败。我们的重试逻辑只判断了HTTP状态码看到200就认为成功结果扣款失败的工单被标记为完成用户投诉说钱没扣但服务开通了。修复方案是在工具调用的封装层加一个“响应体校验”步骤。每个工具在注册到Agent的时候除了声明参数schema还要声明一个responseValidator函数用来判断响应体是否表示业务成功。这个validator返回false的时候抛出一个BusinessError走和永久性错误类似的“不重试、立即失败”路径。class BusinessError(Exception): def __init__(self, message, raw_response): super().__init__(message) self.raw_response raw_response def payment_response_validator(response): if response.get(status) ! success: raise BusinessError( fPayment failed: {response.get(error_msg)}, response )这段代码看起来简单但它把“HTTP成功但业务失败”这个坑堵死了。我建议你在设计任何Agent工具封装层的时候都把responseValidator作为必填项而不是可选项。3. 重试不是万能药指数退避、抖动与重试预算的实战配置3.1 固定间隔重试为什么在Agent场景下会雪崩确定了哪些错误该重试之后下一个问题是怎么重试。最朴素的做法是固定间隔比如失败后等1秒重试再失败再等1秒。这个做法在单机、低并发场景下没问题但在Agent系统里会引发雪崩。想象一下下游服务因为过载开始返回503你的Agent集群里有500个实例每个实例都在以1秒的固定间隔重试。下游服务本来只是轻微过载结果被这500个实例的同步重试打得彻底崩溃。这就是所谓的“重试风暴”。解决办法是指数退避Exponential Backoff。第一次重试等1秒第二次等2秒第三次等4秒第四次等8秒以此类推。这样重试的压力会随着失败次数增加而自然衰减给下游服务留出恢复的时间。但纯指数退避还有个问题如果多个Agent实例同时失败它们的重试时间点会高度重合还是会造成脉冲式的压力。所以需要加抖动Jitter。最常见的做法是“全抖动”每次重试的等待时间在[0, base * 2^n]之间随机取一个值。这样各个实例的重试时间点就被打散了。import random import time def retry_with_backoff(func, max_retries5, base_delay1.0, max_delay60.0): for attempt in range(max_retries): try: return func() except TransientError as e: if attempt max_retries - 1: raise delay min(base_delay * (2 ** attempt), max_delay) jittered random.uniform(0, delay) time.sleep(jittered) raise MaxRetriesExceeded()这段代码里max_delay的作用是防止退避时间无限增长。比如base_delay是1秒重试10次的话理论上是1024秒这显然不合理。设个60秒的上限超过就按60秒算。3.2 重试预算给重试行为本身设一个天花板指数退避解决了单次重试的节奏问题但没解决总量问题。如果下游服务彻底挂了每个请求都重试5次500个实例就是2500次无效请求。这时候需要**重试预算Retry Budget**的概念。重试预算的思路是给整个系统或某个下游依赖设一个重试比例上限。比如“对支付服务的重试次数不能超过总请求数的10%”。当重试比例超过这个阈值时新的失败请求直接快速失败不再重试。这样可以防止重试行为本身把系统拖垮。实现上可以用一个滑动窗口计数器class RetryBudget: def __init__(self, window_seconds60, min_requests100, retry_ratio0.1): self.window window_seconds self.min_requests min_requests self.retry_ratio retry_ratio self.requests [] # (timestamp, is_retry) def can_retry(self): now time.time() self.requests [r for r in self.requests if now - r[0] self.window] total len(self.requests) if total self.min_requests: return True retries sum(1 for r in self.requests if r[1]) return (retries / total) self.retry_ratio这个类在每次请求时记录一条记录重试请求标记为True。can_retry方法检查当前窗口内的重试比例是否超过阈值。min_requests的作用是避免在请求量很小的时候误判比如总共就3个请求1个重试比例33%超过10%但实际上这个样本量太小没有统计意义。3.3 重试和超时的配合别让重试把超时预算吃光还有一个容易忽略的点重试会消耗总超时预算。假设一个Agent任务的总超时是30秒单次请求超时是10秒重试3次。如果每次请求都跑满10秒才超时3次就是30秒总超时预算被吃光任务直接失败。但实际上可能第4次重试就能成功。我的做法是把总超时和单次超时分开管理。总超时是硬性上限单次超时根据剩余时间动态调整。比如剩余时间还有20秒单次超时设成8秒剩余时间只有5秒了单次超时设成3秒并且不再重试。def execute_with_budget(func, total_budget30.0, max_single_timeout10.0): start time.time() attempt 0 while True: remaining total_budget - (time.time() - start) if remaining 1.0: raise TotalBudgetExhausted() single_timeout min(max_single_timeout, remaining * 0.8) try: return func(timeoutsingle_timeout) except TransientError: attempt 1 if attempt 5: raise time.sleep(min(2 ** attempt, remaining * 0.3))这里remaining * 0.8的意思是单次超时不超过剩余时间的80%留20%的余量给重试的等待和后续处理。remaining * 0.3是重试等待时间不超过剩余时间的30%。这些比例是我在实际项目里调出来的你可以根据自己的场景微调。4. 幂等重试的安全带Agent工程化绕不过去的坎4.1 为什么说没有幂等就不敢重试重试和幂等是一对孪生兄弟。没有幂等保证的重试就是在制造重复数据。我前面提到的重复扣款事故根本原因就是支付接口不幂等而我们的重试逻辑又把它当成瞬时性错误处理了。幂等的定义是同一个请求执行一次和执行多次对系统状态的影响是一样的。注意这里说的是“对系统状态的影响”不是“返回值一样”。比如“查询余额”这个操作天然幂等执行多少次余额都不会变。“创建订单”这个操作天然不幂等执行两次就创建两个订单。Agent系统里工具调用是最需要幂等保证的地方。因为Agent的规划逻辑可能会因为模型输出的随机性对同一个子任务发起多次调用。再加上网络层的重试同一个工具被调用多次的概率其实不低。4.2 幂等键的设计谁来生成放在哪里实现幂等最常见的手段是幂等键Idempotency Key。客户端在发起请求时带一个唯一标识服务端记录这个标识和对应的处理结果。如果同一个标识再次到来直接返回上次的结果不重复执行。关键问题是这个幂等键由谁生成、放在哪里。在Agent系统里我推荐由Agent的执行引擎生成幂等键而不是由大模型生成。原因是模型生成的键可能重复、可能格式不对、可能每次调用都不一样。执行引擎可以在规划出一个工具调用时根据“任务ID 步骤序号 工具名 参数哈希”生成一个确定性的键。这样即使同一个步骤被重试多次键都是一样的。import hashlib import json def generate_idempotency_key(task_id, step_index, tool_name, params): payload json.dumps({ task_id: task_id, step_index: step_index, tool_name: tool_name, params: params }, sort_keysTrue) return hashlib.sha256(payload.encode()).hexdigest()用sort_keysTrue是为了保证参数顺序不影响哈希结果。这个键会随着工具调用请求一起发给下游服务下游服务用它来做去重。4.3 幂等性检查用DB还是Redis我的选型逻辑热词里有个问题问得很好“幂等性检查用DB实现好还是Redis实现好”。这个问题我在项目里纠结过最后的结论是看你的幂等窗口有多长以及你能不能接受最终一致。Redis的方案是请求进来用SET key value NX EX ttl尝试写入幂等键。如果写入成功说明是第一次请求继续执行如果写入失败说明是重复请求返回缓存的结果。这个方案快但有两个问题一是Redis如果挂了或者数据丢了幂等保证就没了二是TTL到期后键被删除如果这时候重复请求到来还是会被当成新请求。DB的方案是建一张幂等记录表幂等键做唯一索引。请求进来先INSERT如果唯一键冲突就说明是重复请求查询已有记录返回。这个方案可靠因为DB有持久化和事务保证但性能不如Redis而且每次请求都要写库。我的实际做法是两层结合Redis做第一层快速拦截DB做第二层持久化保证。请求进来先查Redis命中就直接返回没命中就查DBDB里有记录就回写Redis并返回DB里也没有就执行业务逻辑执行完把结果同时写入DB和Redis。这样既保证了性能又保证了可靠性。def check_idempotency(key, ttl3600): # 第一层Redis cached redis.get(fidem:{key}) if cached: return json.loads(cached) # 第二层DB record db.query(SELECT result FROM idempotency WHERE key %s, key) if record: redis.setex(fidem:{key}, ttl, record.result) return json.loads(record.result) return None def save_idempotency(key, result, ttl3600): db.execute( INSERT INTO idempotency (key, result, created_at) VALUES (%s, %s, NOW()), key, json.dumps(result) ) redis.setex(fidem:{key}, ttl, json.dumps(result))注意DB表的幂等键字段一定要加唯一索引否则并发情况下两个请求可能同时INSERT成功幂等就失效了。另外幂等记录的清理策略要提前想好不能无限增长。我一般保留7天用定时任务清理。4.4 下游服务不配合怎么办补偿和对账现实情况是不是所有下游服务都支持幂等键。有些老接口你改不动有些第三方服务根本不给你传幂等键的机会。这时候只能在上层做补偿。补偿的思路是记录每次工具调用的“意图”和“结果”。如果重试后发现可能产生了重复操作通过一个对账任务去检查和修正。比如支付场景记录“订单号金额时间”对账时发现同一订单有多笔支付触发退款流程。这个方案不优雅但实用。我在项目里对几个不支持幂等的下游接口都做了这种补偿逻辑虽然增加了复杂度但总比数据错乱强。5. 可观测性让Agent的每一次失败都有迹可循5.1 日志、指标、链路追踪一个都不能少Agent系统的错误处理光有重试和幂等还不够你还得知道“发生了什么”。可观测性三件套——日志、指标、链路追踪——在Agent场景下有特殊的要求。日志方面Agent的日志不能只记“请求失败”要记清楚失败的上下文当前执行到哪个步骤、调用了哪个工具、参数是什么、模型的原始输出是什么、错误分类是什么、重试了几次。这些信息在排查问题时缺一不可。我习惯用结构化日志每条日志是一个JSON方便后续检索。logger.error(tool_call_failed, extra{ task_id: task_id, step_index: step_index, tool_name: tool_name, params: sanitize(params), error_type: classify_error(e), error_message: str(e), retry_count: retry_count, elapsed_ms: elapsed })指标方面除了常规的QPS、延迟、错误率Agent系统还需要关注几个特有指标工具调用的成功率按工具维度拆分、重试率、幂等命中率、任务完成率、平均步骤数。这些指标能帮你快速定位是哪个工具在拖后腿或者是不是重试策略过于激进。链路追踪方面Agent的一个任务往往涉及多次模型调用和工具调用必须用trace_id把这些调用串起来。我一般用OpenTelemetry每个任务生成一个trace_id每个步骤生成一个span_id工具调用作为子span。这样在追踪系统里能看到完整的执行链路哪一步慢、哪一步失败一目了然。5.2 错误分类的监控看板怎么搭光有指标还不够得有一个看板能让你一眼看出问题。我搭的看板分四块第一块是错误类型分布用饼图展示瞬时性错误、永久性错误、业务错误、推理错误各占多少。如果瞬时性错误突然增多说明下游可能出问题了如果推理错误增多说明prompt可能需要调整。第二块是重试次数分布用柱状图展示重试0次、1次、2次、3次以上的请求各有多少。如果重试3次以上的请求占比超过5%说明重试策略可能太激进或者下游确实不稳定。第三块是幂等命中率用折线图展示随时间的变化。正常情况下这个值应该很低因为大部分请求都是第一次如果突然升高说明有大量重复请求可能是上游的重试逻辑出了问题。第四块是任务完成率按任务类型拆分。这个是最顶层的指标如果某个类型的任务完成率下降顺着链路追踪就能找到根因。5.3 一个真实的排查案例从指标异常到根因定位说个具体的例子。有一次看板上“瞬时性错误”的曲线突然翘起来了从平时的2%涨到15%。我先看错误类型分布发现全是503。然后看工具维度的成功率发现是“库存查询”这个工具的错误率飙升。再看链路追踪发现这个工具的响应时间从平均200ms涨到了3秒。到这里基本可以判断是库存服务过载了。但为什么过载我查了调用量发现并没有明显增长。后来发现是另一个Agent任务在批量调用库存查询而且没有做限流。那个任务的并发度设得太高把库存服务打挂了。修复方案是给库存查询工具加了独立的限流器并且把那个批量任务的并发度从50降到10。这个案例说明Agent系统的错误往往不是孤立的一个任务的错误可能是另一个任务的行为导致的。没有完善的可观测性你根本找不到根因。6. 并发场景下的错误处理限流、熔断与隔离6.1 Agent怎么扛并发先搞清楚瓶颈在哪热词里有人问“AI Agent怎么扛并发”这个问题很大我拆开说。Agent系统的并发瓶颈通常不在Agent本身而在它依赖的下游大模型API有速率限制、工具接口有承载上限、数据库连接池有限。所以扛并发的核心思路是保护下游而不是让Agent无限制地发请求。具体手段有三个限流、熔断、隔离。限流是控制请求速率。对每个下游依赖设一个QPS上限超过就排队或拒绝。Agent场景下我推荐用令牌桶算法因为它允许一定程度的突发流量。比如库存查询限流100 QPS令牌桶容量设200这样平时攒下的令牌可以应对短时突发。熔断是当下游持续失败时暂时切断对它的调用避免无效请求堆积。熔断器有三个状态关闭正常调用、打开直接失败、半开放少量请求试探。当下游错误率超过阈值比如50%时熔断器打开后续请求直接返回失败不再实际调用。等一段时间后进入半开状态放几个请求试试如果成功了就关闭熔断器恢复调用。隔离是把不同下游的调用资源分开。比如给每个下游工具分配独立的线程池或连接池这样某个下游变慢不会拖垮整个Agent。这个思路来自舱壁模式在微服务里很常见Agent系统同样适用。6.2 熔断器的实现细节和参数调优熔断器的参数调优是个经验活。我一般用这几个默认值然后根据实际情况调整参数默认值说明错误率阈值50%超过这个比例触发熔断最小请求数20样本量太小时不触发熔断时长30秒打开状态持续多久半开请求数5半开状态放几个请求试探最小请求数这个参数很重要。如果总共就3个请求2个失败错误率67%超过阈值但实际上样本量太小可能是偶然。设个20的最小请求数避免误触发。半开状态的试探请求要小心处理。如果这5个请求里有3个失败熔断器重新打开并且熔断时长翻倍。这个“翻倍”机制是为了应对下游长时间不可用的情况避免频繁试探给下游造成额外压力。class CircuitBreaker: def __init__(self, error_threshold0.5, min_requests20, open_duration30, half_open_requests5): self.error_threshold error_threshold self.min_requests min_requests self.open_duration open_duration self.half_open_requests half_open_requests self.state closed self.failures 0 self.successes 0 self.opened_at None self.half_open_count 0 def allow_request(self): if self.state closed: return True if self.state open: if time.time() - self.opened_at self.open_duration: self.state half_open self.half_open_count 0 return True return False if self.state half_open: if self.half_open_count self.half_open_requests: self.half_open_count 1 return True return False def record_result(self, success): if self.state half_open: if success: self.successes 1 if self.successes self.half_open_requests: self.state closed self.failures 0 self.successes 0 else: self.state open self.opened_at time.time() self.open_duration * 2 elif self.state closed: if success: self.failures max(0, self.failures - 1) else: self.failures 1 total self.failures self.successes if total self.min_requests: if self.failures / total self.error_threshold: self.state open self.opened_at time.time()这个实现里有个细节成功时failures减1而不是清零这是为了平滑统计避免偶发成功把失败计数清零导致熔断器永远不触发。6.3 超时、重试、熔断、限流的组合拳这四个机制不是孤立的要组合起来用。我的经验顺序是限流在最外层熔断在中间层重试在调用层超时贯穿始终。请求进来先过限流器超过速率就排队或拒绝。然后过熔断器如果熔断器打开就直接失败不浪费资源。然后进入重试循环每次调用设单次超时。如果重试过程中熔断器打开了后续重试直接失败。这个组合的关键是熔断器要能感知重试。如果重试的请求也算进熔断器的统计那重试会加速熔断触发。我的做法是只有第一次调用计入熔断器统计重试调用不计入。这样熔断器反映的是“首次调用”的健康状况更准确。7. 工程化落地从代码结构到发布流程的完整实践7.1 错误处理代码应该放在哪一层Agent系统的代码通常分几层接入层、编排层、工具层、基础设施层。错误处理代码放错层会导致逻辑混乱和重复。我的原则是错误分类和重试策略放在编排层幂等和熔断放在工具层超时和连接管理放在基础设施层。编排层负责决定“这个错误要不要重试、重试几次、失败了怎么办”。因为编排层最清楚当前任务的上下文知道这个步骤失败了是应该跳过、重试还是终止整个任务。工具层负责“这个工具调用是不是重复的、下游是不是挂了”。因为工具层最清楚下游服务的特性知道哪些接口支持幂等、哪些接口容易过载。基础设施层负责“网络请求怎么发、超时怎么设、连接怎么复用”。这些是通用的和具体业务无关。这样分层的好处是每层的职责清晰改一层不会影响其他层。比如要调整重试策略只改编排层要换HTTP客户端只改基础设施层。7.2 配置化别把重试次数硬编码在代码里我见过太多项目把重试次数、超时时间硬编码在代码里改一个参数要重新发版。这在Agent系统里是灾难因为不同工具的特性差异很大库存查询可能重试3次就行大模型调用可能重试5次都不够。正确的做法是配置化。每个工具在注册时声明自己的重试策略tools: - name: inventory_query timeout_ms: 2000 max_retries: 3 backoff_base_ms: 500 idempotent: true circuit_breaker: error_threshold: 0.5 min_requests: 20 - name: payment_create timeout_ms: 5000 max_retries: 2 backoff_base_ms: 1000 idempotent: true idempotency_key_required: true circuit_breaker: error_threshold: 0.3 min_requests: 10这份配置里支付接口的错误率阈值设得更低0.3因为支付失败的影响更大宁可早点熔断也不要持续尝试。库存查询的超时设得更短2秒因为它是个快速查询超过2秒基本就是有问题了。配置化的另一个好处是可以在运行时动态调整。比如发现某个下游服务在维护临时把它的熔断器手动打开避免无效请求。7.3 测试怎么验证错误处理逻辑真的有效错误处理逻辑的测试比普通业务逻辑难因为要模拟各种异常情况。我的做法是分三层测试单元测试用mock模拟各种异常验证错误分类函数、重试逻辑、熔断器状态机是否正确。这部分测试跑得快覆盖率高是基础。集成测试用真实的HTTP服务可以用WireMock之类的工具模拟下游的各种响应超时、503、400、业务失败。验证整个链路的行为是否符合预期。混沌测试在生产环境或预发环境注入故障比如随机让某个下游返回503、随机增加延迟、随机断开连接。验证系统在真实故障下的表现。这部分测试最有价值但要在可控范围内做。我特别推荐混沌测试。我们项目上线前做了一次混沌测试随机让库存服务返回503结果发现Agent的重试逻辑会把库存查询的失败传播到整个任务导致任务失败率飙升。后来我们改成了“库存查询失败时降级返回缓存数据”任务成功率从60%提升到95%。这个改进如果没有混沌测试根本发现不了。7.4 发布流程错误处理逻辑的灰度策略错误处理逻辑的变更比业务逻辑的变更风险更高因为它影响的是“出错时的行为”。如果改错了可能把本来能恢复的错误变成不可恢复的。我的发布策略是错误处理逻辑的变更必须走灰度而且灰度比例要比业务逻辑更低。业务逻辑灰度可能10%起步错误处理逻辑我一般1%起步观察24小时再逐步放大。灰度期间重点观察几个指标错误率、重试率、任务完成率、熔断触发次数。如果这些指标和灰度前相比有明显变化立即回滚。另外错误处理逻辑的变更要支持运行时开关。比如新的重试策略上线后如果发现问题可以通过配置中心一键切回旧策略不用重新发版。这个开关在紧急情况下能救命。8. 几个我踩过的坑和对应的解法8.1 坑一重试把下游打挂反而延长了故障时间前面提过这个坑这里补充一个细节。当时我们的重试策略是固定间隔1秒重试5次。下游服务因为过载开始返回503我们的Agent集群有200个实例每个实例都在重试。结果下游服务本来只需要30秒就能恢复被我们的重试风暴打得5分钟都没恢复。解法就是前面说的指数退避加抖动加重试预算。改完之后同样的故障场景下游服务30秒就恢复了Agent的任务成功率也从40%提升到85%。8.2 坑二幂等键生成逻辑不一致导致去重失效有一次我们发现幂等命中率异常低明明有大量重试但幂等键都没命中。排查后发现重试时生成的幂等键和第一次不一样。原因是幂等键里包含了时间戳而重试时时间戳变了。解法是把幂等键的生成逻辑改成确定性的只包含任务ID、步骤序号、工具名、参数哈希不包含任何随时间变化的值。这个坑很隐蔽因为幂等键看起来是唯一的但实际上每次重试都不同等于没有幂等。8.3 坑三熔断器误触发把健康的下游也熔断了熔断器的最小请求数设得太低导致样本量小的时候误触发。有一次某个工具在低峰期只有5个请求其中3个失败其实是参数问题不是下游问题错误率60%超过阈值熔断器打开后续所有请求都被拒绝持续了30秒。解法是把最小请求数从5提高到20并且区分“参数错误”和“下游错误”。参数错误不计入熔断器统计因为那是调用方的问题不是下游的问题。8.4 坑四日志里记了敏感信息导致安全问题Agent的工具调用参数里可能包含用户敏感信息比如手机号、身份证号、银行卡号。我们的日志一开始把这些参数原样记下来了后来安全审计的时候被指出来。解法是在日志记录前做脱敏处理。手机号保留前3后4身份证号保留前6后4银行卡号只保留后4位。这个脱敏函数要放在日志框架的入口处确保所有日志都经过脱敏。def sanitize(params): sensitive_fields [phone, id_card, bank_card, password] result {} for k, v in params.items(): if k in sensitive_fields: result[k] mask(v) else: result[k] v return result def mask(value): s str(value) if len(s) 4: return **** return s[:2] **** s[-2:]8.5 坑五任务状态机不完整导致任务卡死回到开头那个事故。任务卡在“处理中”状态根本原因是状态机不完整。我们只定义了“处理中”“成功”“失败”三个状态但实际执行中可能出现“部分成功”“超时”“取消”等情况。状态机不完整异常情况下任务就找不到对应的状态只能卡着。解法是补全状态机并且给每个状态定义明确的转换条件。特别是要有一个“超时”状态和一个“未知”状态。超时状态用于总超时预算耗尽的情况未知状态用于无法确定执行结果的情况比如网络超时但不确定下游是否执行了。未知状态的任务会进入人工排查队列而不是自动重试。9. 写在最后一些个人体会做Agent工程化这几年我最大的体会是Agent的智能程度决定它的上限错误处理决定它的下限。一个Demo可以靠模型的聪明才智跑通但一个生产系统必须靠扎实的工程化实践兜底。我见过太多团队把精力全花在prompt调优和模型选型上错误处理随便写写结果上线后各种诡异问题。其实错误处理的投入产出比很高前面说的那些手段——错误分类、指数退避、幂等键、熔断器、可观测性——每一个都不复杂但组合起来能让系统的稳定性提升一个数量级。如果让我给正在做Agent工程化的朋友一个建议那就是先把错误处理框架搭好再往上堆业务逻辑。框架搭好了后面加工具、加任务类型都是顺水推舟框架没搭好每加一个功能都要重新想一遍错误处理迟早会乱。最后分享一个我常用的检查清单每次上线新的Agent工具前过一遍这个工具的错误分类函数写了吗重试策略配置了吗指数退避和抖动加了吗幂等键生成逻辑是确定性的吗熔断器配了吗最小请求数合理吗日志脱敏了吗链路追踪接了吗超时预算算过吗总超时和单次超时配合好了吗混沌测试跑过了吗故障场景下的行为符合预期吗这七个问题都能答上来这个工具才算真正准备好上生产。
返回列表