第二周了,我把上个月启动的那个Agent项目又往前推了一段,结果差点把自己推坑里。翻完两周的项目日志,我发现一个特别扎心的现象:大多数Agent项目根本轮不到"模型不够聪明"这个阶段,而是倒在"失败"这一步就集体阵亡了。我这话不是随便说的——在真实跑批量任务的过程中,工具调用超时、输出格式解析失败、上下文错乱、外部接口限流、Agent自己把错误信息当成正常结果……这些乱七八糟的"失败"几乎每天都会把任务链直接逼停。
这篇总结,我打算把"失败"这件事彻底拆开讲清楚:Agent项目里失败到底有哪几类,为什么传统程序那套try-catch根本救不了它,以及我第二周实打实落地的失败处理方案是什么。文章会穿插大量代码、配置和真实踩坑记录,正在做Agent应用开发、被工具调用和长链路稳定性折磨的朋友,可以直接照着抄作业,至少能让你少走两三个星期的弯路。
1. 项目现状:一个内容自动化Agent,是怎么被"失败"逼停的
1.1 第一周很顺利,第二周全是意外
我做的这个项目,目标是让Agent自动完成一套"检索资料→写大纲→生成成稿→输出配图建议"的内容生产链路。选型上没有纠结太久,用LangChain做整体编排,挂了几个常用工具:网页搜索接口、数据库查询、文本生成模型,后边还接了一个多Agent节点负责章节拆分和交叉验证。
第一周搭框架的时候一切都很简单。Agent按照剧本调两个工具、生成一段文本,demo演示给同事看,大家拍手叫好,甚至有人已经开始预测项目上线时间了。但第二周进入真实任务批量测试后,问题开始密集爆发。一个任务失败了,整条链路就停在原地;某个接口偶发返回500,Agent傻乎乎地重试三次还是失败,然后直接把任务丢弃;最让我崩溃的是,有一次Agent把工具返回的错误提示原封不动地当成"检索结果"写进文章正文里,乍一看内容还挺通顺,仔细读才发现全是报错信息。
1.2 "90%都死在失败这一步"的判断依据
这个比例不是我拍脑袋编的,是这两周跑出来的实感。我在第二周做了个小统计:连续投喂60个真实生产任务,观察Agent从开始到结束的表现。结果发现,只有约72%的任务能一次跑通;剩下的28%里,有将近一半属于"模型能力不足导致的产出质量差",这个我认,模型上限摆在那;但另一半完全属于"失败处理机制缺失"——工具超时没有重试策略、解析失败没有兜底方案、Agent判断不了当前错误可不可恢复、卡死之后没有任何看门狗机制去中断它。
我后来复盘时越来越确定一件事:如果把失败处理做好,这28%的失败里至少能救回来一大半。换句话说,很多Agent项目表面上死于"模型不够聪明",实际死于"面对失败时毫无作为"。这种死亡方式尤其冤,因为模型能力短期提不上来,但失败处理能力是可以立刻补上的。
1.3 市面上所有"XX失败"的报错,其实都指向同一个痛点
顺手搜了一下最近的报错热词,特别有意思:网络共享失败、MySQL安装失败、单片机下载失败、Harbor推送失败、Ubuntu安装GCC失败……从底层环境到单体应用,各种"失败"铺天盖地。传统项目里你遇到一次安装失败,修好环境变量就一劳永逸了,问题生命周期很短。但Agent项目完全是另一个物种:它把各种可能出现在开发、运维、业务逻辑里的失败类型,全部揉进了一条长链路里,并且在每次任务执行时随机复现。这也解释了为什么新手做Agent总觉得"系统飘忽不定"——不是错觉,是整个链路里的失败本来就有那么多。
2. 先搞懂Agent的"失败"到底有哪几类
2.1 按执行阶段拆分:规划期、执行期、产出期
我把Agent运行过程分成三个阶段:规划期、执行期、产出期,每个阶段的失败形态都不一样,需要完全不同的处理策略。
规划期的失败,最典型的表现是"任务拆解错了"。比如我让Agent"搜集社区里关于儿童编程教育的讨论,并写一份趋势报告",它一本正经地把任务拆成了"搜索儿童编程教育政策文件"和"分析全国各省市政策差异"——这完全跑偏了,因为原始需求根本没说政策。这种失败很隐蔽,因为Agent执行得一丝不苟,代码跑了很久,最后产出一份数据丰富但答非所问的报告。
执行期的失败大家最熟悉:工具调用报错、超时、参数拼错、限流限频。比如搜索接口偶尔返回504,数据库连接池满了抛异常,图片生成服务动不动就Rate Limit。这类失败的共同点是"错误信号明确",Agent能看到异常码和异常信息,问题在于它不知道该怎么从异常里恢复。
产出期的失败分为两种:一是输出格式不合格,比如我要求JSON格式的配图建议,它偶尔会在JSON外面包一层markdown代码块,解析器直接炸掉;二是结果内容本身有问题,比如生成的文章里出现了事实性错误,或者是把报错文本当成了检索资料(这个我前面提过)。格式类失败靠解析容错就能解决,内容类失败才是真正的难题,它需要引入验证机制或者人工抽查。
2.2 按错误性质拆分:可重试、需修正、不可恢复
按阶段分类能帮你快速定位失败发生在哪一层,但我建议第二层分类也一起做:按错误性质,把失败分成可重试、需修正、不可恢复三类。
可重试型错误,特点是"换个时机再试一次大概率能成功"。典型的就是网络抖动、服务端超时、限流降级、偶发500。这类错误处理起来最轻松,直接上指数退避重试就好。我第二周给所有HTTP类工具都加了三层重试,搜索接口的偶发超时就这么吞掉了。
需修正型错误,特点是"盲目重试没有意义,必须先调整参数或策略"。比如Agent调用工具时拼错了参数、把字符串格式的时间戳传给了需要整数的接口、目标页面需要登录但Agent没带认证信息。这类错误需要让Agent先看一眼报错信息,自己判断要修改什么,再发起下一轮尝试。
不可恢复型错误,特点是"当前环境下无论如何都不会成功"。比如权限不足、目标数据根本不存在、账号余额用尽、外部服务已经永久下线。遇到这类错误,正确的做法是立即停止、标记失败并通知人,而不是一次一次地撞南墙。
2.3 为什么传统的try-catch在Agent这里几乎失效
很多从传统后端转过来做Agent的同学,一开始都会条件反射式地搬出try-catch、异常处理、熔断降级这些老一套。这些机制不是没用,但远远不够。最核心的原因是:传统程序是确定性的,函数要么返回正确结果,要么抛异常,异常被catch住之后,程序状态是明确可控的;而Agent的每一步都依赖大模型做决策,模型的输出永远有不确定性。
举个我自己遇到的例子:一个工具调用逻辑上完全正常,HTTP状态码200,返回的数据结构也合法,但返回内容里的一段话是错的。传统程序根本发现不了这类问题,因为程序层面没有任何异常信号;Agent也不会发现,它只会把这段错误内容作为"事实"继续推理下去,越滚越歪。
如果把传统程序比作一条铁轨上的火车,轨道坏了轮子就会报警;Agent更像是在野外开车,前方道路断了不会给你正式的路障提示,你需要自己看路面、判断方向、决定绕路还是回头。Agent项目里最需要建的,不是异常捕获机制,而是一整套"让Agent在信息不完整、错误不确定的环境里做出恢复决策"的机制。
3. 核心实操:给Agent装上一颗"失败处理大脑"
3.1 第一步:把错误从Exception文本变成结构化信息
让Agent学会处理错误之前,你得先让它看懂错误。第二周我做的第一个改动,就是统一所有工具的错误返回格式。以前工具报错就是简单抛一个Exception,堆栈信息往日志里一扔;现在所有的工具都返回一个标准化的错误结构,无论是给程序用的还是给大模型看的,都能一眼读明白。
from pydantic import BaseModel from typing import Optional class ToolResult(BaseModel): """所有工具的标准化返回结构""" success: bool code: str # 错误码:TOOL_TIMEOUT / INVALID_PARAM / PERMISSION_DENIED ... message: str # 人类可读的错误说明 retryable: bool # 是否值得重试 payload: Optional[dict] = None # 成功时的业务数据 hint: Optional[str] = None # 给LLM的修复建议结构里有几个字段我特别想强调。retryable这个布尔值相当于一个"方向标",它告诉Agent这个错误重试有没有意义;hint则是给大模型的"提示词",比如"参数date需要格式化为YYYY-MM-DD后再调用"。实测下来,把错误信息从一堆堆栈文本变成这种结构化对象后,Agent后续自我修正的成功率明显提高,因为它不用再从混乱文本里猜自己到底错哪了。
3.2 第二步:设计分区重试策略,让Agent"有耐心但不傻"
重试不是无脑重试。我以前见过一个项目,工具调用失败后,Agent在同一个报错上重试了整整11次,API账单看着都肉疼。第二周我定了三条铁律:区分瞬时错误和永久错误、采用指数退避加抖动、设置最大重试次数,超过就不再重试。
import random import time from tenacity import ( retry, stop_after_attempt, wait_exponential, retry_if_exception, wait_random ) # 只对"可重试型"错误进行重试 def is_retryable_error(exception: Exception) -> bool: return getattr(exception, "retryable", False) @retry( stop=stop_after_attempt(3), # 最多重试3次 wait=wait_exponential(multiplier=1, min=2, max=30) + wait_random(0, 1), retry=retry_if_exception(is_retryable_error), ) def call_tool_with_retry(tool_name: str, **kwargs): # 实际调用工具,如果工具返回 retryable=True,则抛出带标记的异常 ...这里有个很多人容易忽略的点:重试必须搭配"幂等性设计"。什么意思?同一个操作执行两次和执行一次,结果应该一样,或者第二次能被明确识别为重复操作。比如你的Agent工具里有一个"创建订单"的接口,如果第一次调用超时了,你以为失败了就重试了一次,结果订单被创建了两笔,这就是灾难。我现在做Agent工具设计时,一定会给自己留一个幂等键——每次请求带上业务ID,接口收到重复ID直接返回上一次的结果。
3.3 第三步:给Agent装"后悔药"——反思与修正循环
光重试是治标不治本的。很多失败不是因为运气不好,而是Agent的调用方式一开始就错了,重试一百次也一样错。第二周我引入了一个"反思-修正"环节:当Agent收到错误信息后,不是马上自动再试,而是先让大模型读一遍错误,自己分析原因并给出新的行动计划。
你刚才的一次操作失败了。 工具调用信息如下: 工具名: {tool_name} 入参: {tool_args} 报错信息: {error_message} 当前重试次数: {retry_count} 请执行两步操作: 1. 分析这次失败的根本原因,判断下面哪种情况更匹配: - 工具参数错误,需要调整后再调用 - 工具选择不合适,应该使用另一个工具 - 当前条件不具备,需要先做另一件事 - 错误不可恢复,需要终止并报告 2. 基于你的分析,输出下一步行动(JSON格式): {"action": "retry" | "switch_tool" | "precondition" | "stop", "reason": "简要原因", "modified_params": {...} 或 "new_tool": "..." 或 empty}这个循环的思想其实和Agent研究里的Reflection机制一脉相承:让Agent看到自己刚才的失败,把失败当成一次"观察数据",而不是直接吞掉。实测下来,加了反思环节后,Agent对"参数修正型失败"的处理能力提升非常明显——以前是同一姿势撞墙三次,现在是第一次撞了,第二次换个姿势尝试,第三次基本就能穿过去。
3.4 第四步:留好"人类后门",让Agent学会求助
第二周给我最大教训的,是"Agent不该硬扛的时候一定要学会叫人"。我遇到过一个案例:Agent在批量生产文章时,某个数据源需要登录权限,Agent重试了三次、换了两种工具都没办法,然后它安静地跳过了这一步,在输出文章时悄悄编造了一段看起来很像样但完全没有数据支撑的内容。这比直接报错可怕一百倍。
所以我在Agent流程里加了"失败分级处理"规则:可自动恢复的失败走重试和反思;自动恢复不了的,先评估有没有替代方案;连替代方案都没有的,立刻把任务标记为"HUMAN_REVIEW_REQUIRED",暂停后续动作,进入人工确认队列。同时在Prompt里明确告诉Agent:遇到不可恢复的错误,主动停下来找人是正确行为,不会受到惩罚;但编造内容填补空缺是严重违规。
def route_on_failure(tool_result: ToolResult) -> str: """失败分级路由:决定下一步走重试、反思、还是停止等人工""" if tool_result.retryable: return "retry_with_backoff" if tool_result.hint: return "reflection_and_correct" # 兜底:不管怎样,先别让Agent自己决定编造数据 return "human_review"这个"主动求助"机制在实操中很关键。别忘了给Agent一个明确的行为约束:不确定就停,停下来找人,比自作聪明往前跑安全得多。
4. 工具选型与落地:我第二周实际用到的方案
4.1 框架选型:LangChain、Dify、CrewAI哪个更扛得住失败
很多人在选型阶段就卡住了。我第二周专门花时间把三个主流框架的"失败处理能力"过了一遍,结论如下:
| 特性 | LangChain | Dify | CrewAI |
|---|---|---|---|
| 失败重试 | 需自行封装Retry逻辑,灵活但费事 | 提供节点级错误分支,可视化操作 | 任务级重试配置简单,细粒度控制弱 |
| 反思/修正能力 | 高度可控,可插入自定义循环 | 图形化节点里做人工设定 | Agent内部有基础重试机制 |
| 可观测性 | 需要接入LangSmith或自建日志 | 自带运行时追踪,界面直观 | 有执行日志,但信息颗粒度一般 |
| 定制灵活度 | 极高,适合生产级深度定制 | 中低,复杂逻辑容易绕不开GUI限制 | 中,适合多Agent编排场景 |
我的建议很明确:如果你是技术团队做生产系统、对失败处理有深度定制需求,选LangChain,因为它把"过程控制权"完完全全交到你手里,但前提是你愿意花精力把容错机制自己搭起来;如果你想快速验证业务逻辑,或者团队里没有很强的后端能力,Dify是个好选择,它的错误分支节点拖一拖就能用;CrewAI适合多Agent协作编排,但别指望它对单次调用的失败能做细粒度干预。
我的项目最后选型是LangChain为主、Dify为辅。主流程在LangChain里做深度定制的失败处理中间件,Dify只用来做快速原型验证,验证通过后再把逻辑搬到主流程。这样既有灵活性,又有速度。
4.2 一套可复用的"失败处理中间件"实现
下面这套东西,是我第二周反复打磨后的核心资产。核心思路是:把失败处理从Agent的业务逻辑中独立出来,成为一个"中间件层",任何工具调用都先经过中间件,再决定下一步往哪走。
import time from typing import Callable from dataclasses import dataclass, field @dataclass class ExecutionContext: trace_id: str # 链路追踪ID step_id: int = 0 history: list = field(default_factory=list) class AgentFailureMiddleware: """失败处理中间件:统一拦截工具调用结果""" def __init__(self, max_reflections=2, timeout_per_step=60): self.max_reflections = max_reflections # 最大反思轮数 self.timeout_per_step = timeout_per_step # 单步超时(看门狗) def execute(self, ctx: ExecutionContext, tool_func: Callable, **kwargs): ctx.step_id += 1 start = time.time() # 看门狗:单次工具执行最长60秒,超时直接认定失败 # 防止接口挂起时整个Agent任务跟着挂死 if time.time() - start > self.timeout_per_step: ctx.history.append({"step": ctx.step_id, "event": "watchdog_timeout"}) return "human_review" # 第一次调用 result = tool_func(**kwargs) ctx.history.append({ "step": ctx.step_id, "tool": tool_func.__name__, "args": kwargs, "result": result.model_dump(), }) # 失败处理:按错误分级路由 if not result.success: if result.retryable: # 走指数退避重试 return self._retry_with_backoff(ctx, tool_func, **kwargs) elif result.hint and ctx.step_id <= self.max_reflections: # 走反思-修正循环 return self._reflect_and_retry(ctx, tool_func, **kwargs) else: # 不可恢复:转人工审核 return "human_review" return result里面有两个点我想单独强调:
第一,history列表是整个中间件和可观测性的灵魂。每次工具调用,无论成功失败,我都会把"参数、结果、耗时"原样记录下来。这样出了问题后,我可以完整回放Agent的执行过程,精准定位是哪一步把任务带偏了。传统开发里这叫链路追踪,在Agent项目里它更是必需品——没有回放能力,你排查 Agent 行为只能靠猜。
第二,看门狗超时。这个机制看似简单,实际救我无数次。有些第三方接口会在极端情况下"假死"——不报错也不返回,挂在那里干耗。如果不在中间件层加这个60秒超时,Agent任务就会无限阻塞,整条流水线跟着罢工。
4.3 实战记录:搜索接口偶发超时,我如何把成功率从72%拉到91%
说点具体的数字。第二周中段,我针对最常用的网页搜索工具做了一次专项优化,整个优化过程可以直接复现。
问题很典型:Agent在批量写文章时,每篇文章平均调用6到8次搜索接口,其中有大约5%的情况接口会偶发超时或限流。第一次跑60个任务时,成功率只有72%。
第一轮改动:加了指数退避重试。给搜索工具配置了最多3次重试,第一次失败后等2秒再试,第二次失败等4秒,第三次失败等8秒。改动之后,成功率从72%提升到了82%。但还有一批失败,原因是搜索接口返回了200但内容是空的,程序看HTTP状态码认为成功,实际业务上却拿到空结果。
第二轮改动:在工具层增加"结果有效性校验"。我把"返回内容为空、或内容长度低于某阈值"也定义成一种可重试失败,同样纳入退避机制。再加上结构化错误里的retryable标记,成功率从82%提升到了87%。
第三轮改动:引入反思循环。当搜索空结果重试三次仍然失败时,Agent会先停下来分析上下文,判断这次搜索是不是关键词本身有问题,然后调整关键词重新搜索。这轮改动让成功率提升到了91%。剩下9%,要么是数据源本身不存在,要么是需要权限才能访问的内容,这些我统一都转给了人工审核。
你看,同样是"失败"这两个字,第一轮只看重不重试,第二轮看结果是否有效,第三轮看要不要改变策略,每一层的深度都不一样。这个优化过程,本质上就是把"失败处理大脑"一层层装进系统里。
5. 常见问题与排查技巧实录
5.1 高频失败场景速查表
下面是我第二周整理的速查表,专门对付Agent项目里最常出现的几个失败场景:
| 失败现象 | 常见原因 | 排查手段 | 解决方案 |
|---|---|---|---|
| Agent反复调用同一个工具并报同样的错 | 参数未修正就不断重试 | 查看重试参数和反思策略 | 对"需修正型"错误强制先反思再重试 |
| 任务执行到一半卡死,没有报错也没进展 | 外部接口假死,或Agent陷入无限Loop | 加看门狗超时、记录单步耗时 | 中间件加单步超时,超时转人工 |
| 工具返回成功,但Agent生成内容明显错误 | 工具返回内容未经校验,被当成事实 | 对比工具返回数据与输出内容 | 在工具层加结果有效性校验 |
| 同一批次任务大量限流 | 重试策略太激进,触发服务方限频 | 看日志里错误码是否都是429 | 指数退避+随机抖动,限制并发数 |
| Agent把报错信息当作业务数据写入结果 | Prompt没有约束,且缺少输出验证 | 人工阅读产出发现异常 | 加结果验证节点,明确禁止把非业务内容写进产出 |
| 上下文越来越长,Agent开始答非所问 | 失败重试和反思不断往上下文塞内容 | 观察单次任务的Token消耗趋势 | 设置上下文精简策略,重试时只保留关键错误信息 |
这张表建议你直接贴到项目文档里,遇到了问题按图索骥,能省很多排查时间。
5.2 排查方法论:可观测性是第一生产力,别用猜的
Agent项目调试和传统项目最大的区别是:传统程序出错你能看堆栈,Agent出错你经常只能看到一个"结果不对"。这时候可观测性就是你最重要的武器。
我的做法非常朴素:所有发生在Agent里的关键事件,都写成结构化的日志,存进一个独立的数据表。每次工具调用记录一行:trace_id、步骤编号、工具名、入参、返回结果、错误码、耗时、LLM推理摘要。这样排查问题时,我只需要根据trace_id调出整条执行链路,从头到尾看一遍,马上就知道是哪一步开始偏离的。
有一句我特别认可的话:不会回放Agent行为的项目,出问题时负责人只能靠掷骰子。第二周我深刻体会到,Agent的自己解释不可靠,运行日志才是唯一的真相。所以现在做任何Agent模块,我第一件事不是优化Prompt,而是先把日志埋点埋齐。
5.3 几个值得反思的教训:重试也会上瘾
优化失败处理的过程中,我也踩了几个让人哭笑不得的坑。第一个坑就是过度自信地把重试参数调得太大,结果代理解决策遇到困难的时候,系统会疯狂重试同一个不可恢复的动作,API账单肉眼可见地膨胀。后来我才彻底明白:重试的次数上限,不取决于你多有耐心,而取决于这个动作的"失败是否值得再试"。
第二个坑是"反思"过度。在Prompt里加了反思环节之后,Agent每次失败都要调用大模型分析原因,本身也消耗大量token和时间。后来我设了一个反思轮数上限,只在第二次以后的重试间隔里才允许调用反思,这样成本就控制住了。
第三个坑是有段时间我把所有失败都往"人工审核"里丢,结果人工队列堆积了几百个任务。后来我意识到,人工介入是最后的兜底,不能当垃圾桶。我加了一个前置筛选:凡是能通过简单参数修正解决的,都让Agent自己完成;只有代码级无法处理的(权限、数据缺失、需求跑偏),才放行到人工队列。
5.4 何时应该果断放弃?止损也是一种"失败处理"
越来越多人开始讨论一个话题:Agent项目做到什么时候应该止损。从失败处理的角度看,我也补一刀:不是所有的失败都能被挽留,项目层面同样如此。
我从第二周的数据里得出一个判断标准:如果某个任务类型连续重复失败超过12次,且每一次失败的原因都属于"不可恢复型",那就不要再往里面填重试参数了。果断停止该任务类型、回到调研层面去确认需求本身是否成立,这才是理性的选择。处理失败的最高境界,是知道哪些失败值得处理,哪些失败应该快速放弃。
止损的勇气和止损的方法同样重要。我现在做批量任务时,都会给整个批次加一个总的失败率阈值,任务整体失败率超过某个数值就立刻停止全流程并拉响警报,避免在系统性的错误上继续消耗资源。
6. 第二周结束后的心态变化:把Agent当成可靠性工程来做
第二周最大的认知转变,是我开始把Agent项目当成"可靠性工程"而不是"提示词工程"来做。第一周我每天都在琢磨怎么把Prompt写得更精妙、怎么让模型输出更结构化;第二周我大部分时间都在写中间件、调重试参数、看日志、设计失败路由。进步反而比第一周大得多。
如果你现在正被Agent项目的各种失败折磨,我的建议是先别急着去改模型或者Prompt。不妨把项目里所有"失败"收集起来,分类、打标、设计不同的恢复策略。先把"失败这关"过了,你会发现Agent项目的含金量瞬间提高一个档次。
最后再分享一个实用的小技巧:不要一上来就做多Agent协作。很多人喜欢一上来就搞一个复杂的多Agent系统,几个Agent互相调用,失败了互相传染,排查起来难如登天。我更推荐的做法是,先把一个单Agent闭环的失败处理做得扎扎实实,中间件、重试、反思、人工兜底全部到位之后,再逐步演进到多Agent协同。单Agent的失败逻辑都不清,多Agent只会把混乱放大十倍。
第二周的记录到这里算是讲透了。下一轮我计划把失败处理方案扩展到Agent的记忆模块和知识库检索上,到时候再跟大家汇报具体踩到的坑和结果。