☰
Agent 失败排查实战:从粗错误码到失败链根因定位
2026/11/2 12:36:58 网站建设 项目流程

1. 为什么“Agent 失败”不能只盯着模型看

做 Agent 开发的人,大概都经历过这种场景:任务跑着跑着突然挂了,日志里只留下一句agent execution terminated due to error,或者一个孤零零的ssl_get_error返回 1,再或者干脆什么都没有,进程直接消失。第一反应往往是——模型又抽风了?是不是 prompt 写得不够好?是不是该换个更强的模型?

我一开始也这么想。直到有一次,一个跑了三个月的 Agent 项目突然开始频繁失败,换了模型、改了 prompt、加了重试,全都没用。最后花了两天时间把整条链路拆开看,才发现根因是容器里的一个环境变量在某次部署时被覆盖了,导致 Agent 调用的某个外部工具一直超时。模型从头到尾都没问题,它只是在老老实实地报告“我拿不到结果”。

这件事让我意识到一个很关键的问题:Agent 的失败,绝大多数时候不是模型的问题,而是链路上某个环节断了,模型只是最后一个接锅的人。就像一辆车抛锚了,你不能只怪司机,得看是没油了、轮胎爆了、还是发动机进水了。

这篇文章想聊的就是这件事——怎么从那种粗粒度的错误码出发,一步步把 Agent 的失败链拆出来,找到真正该负责的那个环节。适合谁看?如果你正在做 Agent 开发、搭过 AI Agent 框架、或者被terminalBlocker这类问题卡过,那这篇应该能帮你省下不少瞎调模型的时间。

2. 粗错误码为什么让人抓瞎

2.1 错误码的本质:一个被压缩过的信号

先说说错误码这个东西。不管是ssl_get_error返回 1,还是 HTTP 的 422,还是进程退出码 137,它们本质上都是一个被高度压缩的信号。压缩的目的是让程序能快速判断“要不要继续”,但代价是丢失了大量上下文。

拿ssl_get_error返回 1 来说,这个 1 对应的是SSL_ERROR_SSL,意思是 SSL 协议层出了错。但具体是证书过期、握手失败、还是对端直接断了?这个错误码本身不告诉你。你只知道“SSL 这层有问题”,至于问题在哪,得自己往下挖。

Agent 场景下这个问题更严重。因为 Agent 的执行链路通常很长:

用户输入 → 意图解析 → 工具选择 → 工具调用 → 外部服务响应 → 结果解析 → 模型生成 → 输出

任何一个环节出问题,最终暴露给你的可能都是同一句agent execution terminated due to error。这就像医院只告诉你“病人不舒服”,但不说哪里不舒服、什么时候开始的、之前吃了什么。

2.2 常见粗错误码对照表

我把实际项目中遇到过的高频粗错误码整理了一下,方便你对照排查:

错误码/信息表面含义实际可能的原因
ssl_get_error返回 1SSL 协议错误证书链不完整、TLS 版本不匹配、对端主动断开
HTTP 422请求格式正确但语义错误参数类型不对、必填字段缺失、字段值超出范围
退出码 137进程被 SIGKILL 终止OOM 被杀、容器资源限制、手动 kill
terminalBlocker终端阻塞子进程未退出、stdin 未关闭、管道缓冲区满
agent execution terminatedAgent 执行终止上游超时、工具返回异常、内存溢出
连接超时网络不通DNS 解析失败、防火墙拦截、对端服务未启动

这张表的关键不是让你背下来,而是让你建立一个意识:看到粗错误码的第一反应,不应该是“改代码”,而应该是“这个码背后可能藏着哪几种情况”。

2.3 为什么 Agent 场景下错误码特别“粗”

普通后端服务的错误码通常还算精确,因为调用链路短、依赖少。但 Agent 不一样,它有几个特点让错误码天然变粗:

第一,Agent 的调用链是动态的。它不像传统 API 那样路径固定,而是根据模型判断决定下一步调什么工具。这意味着同一条错误信息,可能来自完全不同的执行路径。

第二,Agent 依赖的外部服务多。搜索、数据库、代码执行环境、文件系统,任何一个抖动都会传导上来。而这些外部服务各自的错误码体系不一样,Agent 框架往往做了统一封装,封装的过程就是信息丢失的过程。

第三,Agent 的执行是异步的。很多失败不是即时发生的,而是某个后台任务超时后才暴露。这时候你看到的错误码,和真正的根因之间可能隔了好几层。

所以,想靠一个错误码定位问题,基本不可能。你需要的是失败链——把从触发到暴露的整条路径还原出来。

3. 失败链排查法:从结果反推根因

3.1 什么是失败链

失败链这个词是我自己习惯的叫法,指的是一次失败从最初触发点到最终暴露点之间的完整因果路径。它和调用链的区别在于:调用链记录的是“谁调了谁”,失败链记录的是“谁把谁搞挂了”。

举个例子。你看到 Agent 报agent execution terminated due to error,这是暴露点。往回推:

  • 为什么终止?因为工具调用超时了。
  • 为什么超时?因为工具内部在等一个 HTTP 响应。
  • 为什么等不到响应?因为对端服务返回了 422。
  • 为什么返回 422?因为 Agent 传过去的参数里有个字段类型不对。
  • 为什么类型不对?因为模型生成的参数格式和工具定义的 schema 不一致。

这条链走下来,根因是“模型生成的参数格式不对”,但暴露出来的是“Agent 执行终止”。如果你只看暴露点,可能会去改超时时间、加重试,治标不治本。

3.2 失败链的三个关键节点

在实际排查中,我把失败链拆成三个必须盯住的节点:

触发点:最早出现异常的那个环节。它可能不报错,只是行为偏离预期。比如工具返回了一个空结果,但没抛异常。

传导点:异常从触发点往下传的过程中,被放大或变形的地方。比如一个空结果被上层当成“成功”,继续往下走,直到某个环节因为拿不到数据而崩溃。

暴露点:最终让你看到错误的地方。通常是日志里的那行错误码,或者用户看到的失败提示。

排查的核心工作,就是从暴露点出发,找到传导点,最终定位触发点。这个过程需要日志、需要埋点、需要对整条链路的理解。

3.3 排查前的准备工作

在开始排查之前,有几件事必须先做好,否则你会像无头苍蝇一样乱撞:

第一,确保日志是完整的。很多 Agent 框架默认只记录最终结果,中间过程不落盘。这种情况下你根本看不到失败链。我的做法是在每个关键节点加结构化日志,至少记录:时间戳、节点名称、输入摘要、输出摘要、耗时、状态。

第二,确保有请求 ID 贯穿全链路。一个 Agent 任务可能触发几十次工具调用,如果没有统一的 trace ID,你根本分不清哪条日志属于哪次执行。这个 ID 要在任务开始时生成,一路透传到所有子调用。

第三,确保能复现。如果一个问题只出现一次就再也复现不了,排查难度会指数级上升。所以关键任务是:把失败时的输入、环境、配置完整保存下来,方便重放。

提示:如果你的 Agent 框架不支持结构化日志和 trace ID,先别急着排查具体问题,先把这两个基础设施补上。否则你每次排查都是在碰运气。

4. 实操:一次完整的失败链拆解过程

4.1 现场还原:一个典型的 Agent 失败案例

说一个我实际处理过的案例。项目是一个代码助手 Agent,用户让它“帮我重构这个函数”。正常情况下,Agent 会读取文件、分析代码、生成新版本、写回文件。但某天开始,这个任务频繁失败,日志里只有:

agent execution terminated due to error

没有堆栈,没有具体错误,就这一行。而且不是每次都失败,大概十次里有三次。

4.2 第一步:给暴露点加“上下文”

首先做的是在报错的地方加日志。原来代码大概是这样的:

try: result = agent.run(task) except Exception as e: logger.error("agent execution terminated due to error") raise

这里的问题是把原始异常吞掉了。改成:

try: result = agent.run(task) except Exception as e: logger.error( "agent execution terminated", exc_info=True, extra={ "task_id": task.id, "step": agent.current_step, "tool": agent.last_tool, "elapsed": time.time() - start_time, } ) raise

改完之后重新跑,失败时日志变成了:

agent execution terminated step: tool_execution tool: file_writer elapsed: 30.2s

这就有了第一个关键信息:失败发生在file_writer工具执行阶段,而且耗时 30 秒,明显是超时。

4.3 第二步:顺着传导点往下挖

知道是file_writer超时后,去看这个工具的实现。它内部是调用一个本地文件服务来写文件的。继续加日志:

def write_file(path, content): start = time.time() try: resp = requests.post( FILE_SERVICE_URL, json={"path": path, "content": content}, timeout=30 ) logger.info("file write response", extra={ "status": resp.status_code, "elapsed": time.time() - start }) return resp.json() except requests.Timeout: logger.error("file write timeout", extra={ "path": path, "content_size": len(content), "elapsed": time.time() - start }) raise

再跑,失败时看到:

file write timeout path: /workspace/src/utils.py content_size: 48210 elapsed: 30.0s

到这里,传导点清楚了:文件写入超时,而且内容大小是 48KB,不算特别大,但也不小。

4.4 第三步:定位触发点

现在问题变成:为什么写一个 48KB 的文件要超过 30 秒?去看文件服务的日志,发现它收到请求后,卡在了某个环节。继续挖,发现文件服务在写入前会做一个“内容校验”,而校验逻辑里有个正则表达式,在处理某些特定字符时会发生灾难性回溯。

具体来说,校验逻辑里有一行:

if re.match(r'^(\w+\s*)*$', content): ...

这个正则在处理包含大量空格和特殊字符的长文本时,会进入指数级回溯。48KB 的内容里恰好有一段生成的代码包含大量缩进和空行,触发了这个问题。

到这里,触发点找到了:文件服务的内容校验正则存在性能缺陷,在特定输入下会卡死。模型生成的代码内容恰好触发了这个缺陷,导致写入超时,最终 Agent 报错。

4.5 第四步:验证与修复

定位到根因后,修复就简单了。把那个正则换成非回溯的写法:

if re.match(r'^[\w\s]*$', content): ...

或者更稳妥的做法是用字符串方法判断,避免正则:

if all(c.isalnum() or c.isspace() for c in content): ...

改完之后,同样的任务跑一百次,零失败。

这个案例里,模型从头到尾都没问题。它生成的代码是正确的,只是内容里恰好有触发下游 bug 的字符。如果一开始就去调模型、改 prompt,可能永远解决不了。

5. 高频失败场景与排查速查

5.1 工具调用类失败

工具调用是 Agent 失败的重灾区。常见表现和排查方向:

表现可能原因排查动作
工具超时对端服务慢、网络抖动、参数过大看工具内部耗时分布,检查请求体大小
工具返回空参数不对、对端无数据、权限不足打印实际请求参数,手动调一次
工具报 422参数类型/格式不符对比工具 schema 和实际传参
工具间歇失败并发限制、资源竞争看失败是否集中在高并发时段

我踩过的一个坑是:工具定义里某个参数写的是string,但模型有时候会生成一个数字。大部分时候框架会自动转,但某些边界情况下转换失败,直接报 422。后来在工具入口加了一层参数校验和自动转换,这类问题就少了。

5.2 终端阻塞类失败

terminalBlocker这个问题值得单独说。它的典型表现是 Agent 执行到某个命令后卡住,不报错也不继续,直到超时。

根因通常是这几种:

子进程没有正确退出。比如启动了一个交互式命令,但没给它传 stdin,它一直在等输入。

管道缓冲区满了。如果子进程输出大量数据,而父进程没有及时读取,管道缓冲区写满后子进程会阻塞。

文件锁未释放。前一个进程持有某个文件的锁,后一个进程一直在等。

排查这类问题的关键是:看进程树。用ps -ef --forest或者pstree看当前有哪些子进程还活着,它们的父进程是谁,状态是什么。如果看到某个进程状态是D(不可中断睡眠)或者S(可中断睡眠)但一直不退出,基本就是它了。

处理方式:

# 查看进程树 ps -ef --forest | grep -A 5 agent # 查看进程打开的文件和管道 lsof -p <pid> # 查看进程状态 cat /proc/<pid>/status | grep State

注意:不要一上来就kill -9。先看清楚进程在等什么,否则可能丢失关键现场。我一般会先strace -p <pid>看它卡在哪个系统调用上,再决定怎么处理。

5.3 网络与协议类失败

ssl_get_error返回 1、连接超时、422 这些都属于这一类。排查思路:

先确认网络通不通。用curl -v手动调一次对端接口,看握手过程是否正常。如果curl也失败,那就是网络或对端问题,跟 Agent 无关。

再看证书。openssl s_client -connect host:port可以看证书链是否完整、是否过期。

最后看协议版本。有些老服务只支持 TLS 1.2,而客户端默认用 1.3,握手就会失败。这种情况在容器环境里特别常见,因为不同基础镜像的 OpenSSL 版本不一样。

5.4 资源类失败

退出码 137、OOM、CPU 打满,这些属于资源类。Agent 场景下资源问题往往被低估,因为大家觉得“不就是调个 API 吗,能占多少资源”。

实际上,Agent 如果涉及代码执行、文件处理、大量文本拼接,内存占用可以很高。我见过一个 Agent 在处理大文件时,因为把整个文件读进内存做字符串操作,直接把容器撑爆。

排查资源问题:

# 实时看容器资源 docker stats # 看容器内存限制 docker inspect <container> | grep -i memory # 看进程内存占用 ps aux --sort=-%mem | head -20

如果确认是资源问题,要么加限制,要么改代码。加限制是治标,改代码是治本。比如把大文件操作改成流式处理,把一次性拼接改成生成器。

6. 把失败链排查变成日常习惯

6.1 埋点:让失败链自己浮出来

排查能力再强,也不如让问题自己暴露出来。我的做法是在 Agent 的关键节点强制埋点,具体包括:

任务级埋点:任务开始、任务结束、任务失败,记录 task_id、总耗时、最终状态。

步骤级埋点:每个 step 开始和结束,记录 step 名称、输入摘要、输出摘要、耗时。

工具级埋点:每次工具调用,记录工具名、参数摘要、返回状态、耗时。

异常级埋点:任何异常捕获处,记录异常类型、消息、堆栈、当前上下文。

这些埋点不需要很复杂,关键是结构化。用 JSON 格式输出,方便后续检索和聚合。比如:

logger.info("step_complete", extra={ "task_id": task_id, "step": "tool_execution", "tool": "file_writer", "status": "success", "elapsed_ms": 120, "input_size": 48210, "output_size": 12, })

有了这些,出问题时你不需要猜,直接按 task_id 把所有日志拉出来,失败链一目了然。

6.2 复现:把失败现场保存下来

Agent 的失败很多时候是偶发的,跟输入、环境、时序都有关。所以一旦失败,第一件事是把现场保存下来:

  • 完整的输入(用户请求、上下文、历史消息)
  • 完整的配置(模型参数、工具配置、环境变量)
  • 完整的日志(所有节点的结构化日志)
  • 环境快照(容器镜像版本、依赖版本、资源限制)

我习惯把这些打包成一个failure_bundle,存到对象存储里。后续排查时直接加载这个 bundle 重放,比靠记忆和零散日志高效得多。

6.3 回归:让修过的 bug 不再回来

每次定位到一个失败链并修复后,我都会做一件事:把这个 case 加到回归测试里。具体做法是保存失败时的输入,写一个测试用例,断言修复后能正常完成。

这样做的好处是,以后如果有人改了相关代码,导致同样的失败链重新出现,测试会直接拦住。Agent 项目的依赖多、变化快,没有回归测试的话,同一个坑可能踩好几次。

7. 几个容易踩的坑和我的处理方式

7.1 不要急着加重试

看到失败就加重试,是很多人的本能反应。但重试有个前提:你得知道失败是不是可重试的。

如果是网络抖动、临时限流,重试有用。但如果是参数错误、逻辑 bug、资源不足,重试只会让问题更严重——比如一个 OOM 的任务,重试三次就是 OOM 三次,还可能把整个节点拖垮。

我的做法是给异常分类:

RETRYABLE = (TimeoutError, ConnectionError, RateLimitError) NON_RETRYABLE = (ValueError, TypeError, PermissionError) def should_retry(exc): return isinstance(exc, RETRYABLE)

只有明确可重试的才重试,其他的直接失败并报警。

7.2 不要忽略“成功但结果不对”

有些失败不会报错,只是结果不对。比如 Agent 说“已完成”,但实际什么都没做。这种问题比报错更难排查,因为它不触发任何异常。

我的处理方式是在关键步骤加结果校验。比如文件写入后,检查文件是否真的存在、大小是否合理。工具调用后,检查返回结构是否符合预期。校验失败就主动抛异常,把“静默失败”变成“显式失败”。

7.3 不要只在一个环境里排查

Agent 在开发环境跑得好好的,一到生产就挂,这种情况太常见了。原因通常是环境差异:依赖版本、网络策略、资源限制、并发量。

所以排查时一定要确认:问题是在哪个环境出现的?如果只在生产出现,那就要对比两个环境的差异。我一般会列一个环境对照表:

维度开发环境生产环境
依赖版本最新锁定版本
网络直连经过网关
资源限制无CPU/内存限制
并发单请求高并发

对比之后,往往能快速缩小范围。

7.4 不要一个人闷头查

Agent 的失败链可能跨越多个团队:模型团队、平台团队、业务团队。如果问题涉及外部服务,自己查效率很低。我的经验是:尽早拉相关方一起看,把日志和现场共享出来,让最了解那个环节的人参与排查。

当然,前提是你已经把失败链梳理清楚了,能明确指出“问题出在哪个环节”。否则拉再多人也是互相甩锅。

8. 最后分享几个实用技巧

第一个技巧:给每个工具调用加一个“预期耗时”。如果实际耗时超过预期的三倍,就打警告日志。这样能在问题变成失败之前就发现苗头。

第二个技巧:用terminalBlocker这类问题做关键词告警。日志里出现特定关键词时直接触发告警,不用等用户反馈。

第三个技巧:定期做失败链复盘。每周把失败的 case 拉出来,按根因分类,看看哪类问题最多。往往你会发现,80% 的失败集中在 20% 的环节上,优化那几个环节就能大幅提升稳定性。

第四个技巧:保留“失败样本库”。把典型的失败 case 保存下来,新同事入职时用来做排查训练,比看文档有效得多。

这些方法都不复杂,关键是坚持做。Agent 的稳定性不是靠某个神奇的工具或框架,而是靠对失败链的持续理解和优化。模型会换、框架会变,但“从暴露点反推根因”这套思路,一直管用。

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

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

立即咨询