1. 别急着换模型Agent 失败排查的认知纠偏做 Agent 开发的人大概都经历过这样的场景任务跑着跑着突然挂了日志里只留下一行冷冰冰的Agent execution terminated due to error或者一个422、一个model request failed。第一反应是什么换模型。GPT-4 换 ClaudeClaude 换国产大模型换来换去发现该挂还是挂该超时还是超时。我见过太多团队在这个阶段浪费了大量时间和 token 成本最后发现根因跟模型能力半毛钱关系都没有。这个项目标题说的就是这件事别再把 Agent 失败都算给模型。真正要做的是从一个粗粒度的错误码出发顺着执行链路一层层往下挖把那个模糊的“失败”还原成一条清晰的“失败链”。这中间涉及的核心能力包括错误码语义解析、执行路径还原、terminalBlocker 定位、工具调用链审计以及多 Agent 协作场景下的责任归属判断。这篇文章适合谁看如果你正在做 Agent 开发、Agent 框架搭建、多 Agent 协作编排或者你已经在生产环境跑着 Agent 项目但被各种莫名其妙的失败搞得焦头烂额那这篇内容就是写给你的。我会从错误码的分类逻辑讲起一步步拆到失败链的构建方法再结合 terminalBlocker 这个关键概念把排查路径给你捋清楚。全程不扯虚的都是能直接抄作业的实操思路。先说一个基本认知Agent 的失败跟传统后端服务的失败有本质区别。传统服务的错误码通常是确定的500 就是服务端错误404 就是资源不存在排查路径相对线性。但 Agent 不一样它的执行过程是一个动态决策链——模型思考、工具选择、参数生成、外部调用、结果解析、再思考任何一个环节出问题最终暴露出来的可能都是同一个笼统的错误码。这就好比你去医院看病头疼可能是没睡好也可能是高血压还可能是脑部问题你不能因为头疼就只吃止痛药。所以排查 Agent 失败的第一步不是急着修而是建立错误码到失败链的映射能力。这个能力建不起来你永远在盲人摸象。2. 错误码不是终点从粗粒度信号到细粒度链路的拆解逻辑2.1 为什么 Agent 的错误码天生就是“粗”的Agent 框架在设计错误码体系时面临一个根本矛盾错误码要通用但失败原因高度具体。一个 Agent 框架要支持多种模型服务商、多种工具类型、多种编排模式它不可能为每一种具体的失败场景都定义一个错误码。所以最终暴露给开发者的往往是几个大类模型请求失败、工具调用失败、执行超时、执行终止、参数校验失败。拿422来说在 HTTP 语义里它是“不可处理的实体”通常意味着请求格式对但内容有问题。放到 Agent 场景里422 可能来自模型服务商的 API 网关也可能来自你调用的某个外部工具接口还可能来自 Agent 框架自身的参数校验层。你只看到一个 422但背后可能是五六种完全不同的原因。再比如Agent execution terminated due to error这句话几乎等于没说。它只告诉你“执行终止了”但终止发生在哪一步、由谁触发、终止前的最后状态是什么全都没有。如果你只盯着这句话排查那跟大海捞针没区别。注意很多 Agent 框架的默认日志级别只输出最终错误码不输出中间过程。你需要在框架配置里把执行链路的日志级别调高或者接入链路追踪能力否则你永远只能看到冰山一角。2.2 错误码分类的第一层按来源划分我习惯把 Agent 错误码按来源先做第一层分类这样能快速缩小排查范围。具体分四类模型侧错误来自模型服务商的 API 返回比如限流、鉴权失败、上下文超长、内容审核拦截、模型内部错误。这类错误的特征是错误码通常带有服务商特征比如 429、401、400 等。工具侧错误来自 Agent 调用的外部工具或 API比如 HTTP 请求超时、返回格式不符合预期、工具内部异常。这类错误往往带有工具名称或调用地址的上下文。框架侧错误来自 Agent 框架自身的编排逻辑比如状态机异常、内存溢出、并发冲突、参数序列化失败。这类错误通常伴随堆栈信息。环境侧错误来自运行环境比如网络不通、DNS 解析失败、容器资源不足、端口冲突。这类错误最容易被忽略因为它们往往不直接体现在 Agent 日志里。这个分类的价值在于当你看到一个错误码时先判断它属于哪一类然后直接跳到对应的排查路径不用从头开始。2.3 错误码分类的第二层按可恢复性划分第二层分类按可恢复性来分这决定了你的处理策略可恢复性特征处理策略典型场景瞬时可恢复重试后大概率成功自动重试 退避限流、网络抖动条件可恢复修改参数后可成功调整输入后重试上下文超长、参数格式错误不可恢复重试无意义终止并告警鉴权失败、工具不存在未知信息不足无法判断记录完整现场后人工介入框架内部异常这张表看着简单但实际用起来能省大量时间。我见过太多团队对所有错误都无脑重试结果不可恢复的错误重试一百次还是失败白白烧钱。2.4 从错误码到失败链核心思路失败链的本质是把单点错误还原成时序路径。一个 Agent 执行过程可以抽象成一条时间线用户输入 - 意图理解 - 任务规划 - 工具选择 - 参数生成 - 工具调用 - 结果解析 - 状态更新 - 下一步决策 - ... - 最终输出失败链要回答的问题是错误发生在哪个节点这个节点的输入是什么上一个节点的输出是什么错误是如何传播到最终错误码的构建失败链的关键是在每个节点埋入可关联的追踪标识。我通常用trace_id贯穿整个执行链路每个节点记录节点名称、输入摘要、输出摘要、耗时、状态、错误信息。这样当最终错误码出现时你可以通过trace_id把整条链路拉出来一眼看到哪个节点是第一个出问题的。3. terminalBlocker那个真正卡死执行链的隐形节点3.1 terminalBlocker 到底是什么terminalBlocker这个词在 Agent 开发社区里越来越常见但它没有一个标准定义。我理解的 terminalBlocker 是在 Agent 执行链中导致流程无法继续推进的那个阻塞节点。它不一定是报错的那个节点但一定是让执行链“卡住”的那个节点。举个例子Agent 调用一个外部 APIAPI 返回了 200但返回内容是一个空数组。Agent 框架认为调用成功继续往下走结果下一步决策时模型发现没有可用数据开始胡编乱造最终输出一个完全错误的答案。这个案例里报错可能发生在最后但 terminalBlocker 是那个返回空数组的 API 调用节点。再比如多 Agent 协作场景下Agent A 等待 Agent B 的输出但 Agent B 因为某个工具调用超时而一直没返回。Agent A 的超时错误是表象terminalBlocker 是 Agent B 的那个工具调用。提示terminalBlocker 不一定是“错误节点”它可能是“语义阻塞节点”——节点本身没报错但它的输出让后续流程无法正常进行。3.2 如何定位 terminalBlocker定位 terminalBlocker 的核心方法是反向追溯 正向验证。反向追溯从最终错误码出发沿着失败链往回走找到第一个“状态异常”的节点。这个节点可能是报错的也可能是输出不符合预期的。正向验证从疑似 terminalBlocker 节点开始模拟它的正常输出看后续流程是否能正常走通。如果能走通说明它就是阻塞点如果还是走不通说明后面还有阻塞点。具体操作上我通常会在 Agent 框架里加一个执行快照机制每个节点执行前后都记录状态快照包括输入、输出、上下文变量、工具返回原始数据。这样追溯时不需要靠猜直接对比快照就能定位。3.3 terminalBlocker 的常见类型根据我的经验terminalBlocker 主要有以下几种空结果阻塞工具调用成功但返回空后续决策缺少依据。格式不匹配阻塞工具返回格式与 Agent 预期不符解析失败但未抛错。状态不一致阻塞多 Agent 协作时共享状态被意外修改导致后续 Agent 读到脏数据。资源耗尽阻塞容器内存不足、文件描述符耗尽、连接池满导致后续操作全部挂起。死锁阻塞多个 Agent 互相等待对方输出形成循环等待。外部依赖阻塞依赖的外部服务不可用但 Agent 没有设置合理的超时和降级策略。每一种 terminalBlocker 的排查方法不同但共同点是你不能只看错误码必须看执行链路的完整状态。4. 实操从错误码出发构建完整失败链的排查流程4.1 第一步错误码归一化与上下文补全拿到一个错误码后先别急着查文档。第一步是补全上下文。你需要回答以下问题这个错误码是在哪个节点产生的产生时的trace_id是什么这个节点的输入是什么上一个节点的输出是什么同时刻有没有其他相关日志如果框架没有自动记录这些信息你需要手动在关键节点加日志。我通常会在以下位置强制打日志# 示例在 Agent 执行节点加追踪日志 import logging import uuid logger logging.getLogger(agent.trace) def execute_node(node_name, input_data, trace_idNone): trace_id trace_id or str(uuid.uuid4()) logger.info(f[{trace_id}] node{node_name} phasestart input{summarize(input_data)}) try: result do_execute(node_name, input_data) logger.info(f[{trace_id}] node{node_name} phaseend output{summarize(result)} statussuccess) return result except Exception as e: logger.error(f[{trace_id}] node{node_name} phaseerror error{str(e)} statusfail) raise这个日志模式的关键是每个节点都有 start 和 end 两条日志通过 trace_id 关联。这样你拉日志时能清楚看到哪个节点只有 start 没有 end那就是阻塞点。4.2 第二步执行链路可视化还原有了带 trace_id 的日志后下一步是把链路还原成可读的时序图。我不建议用复杂的可视化工具直接用脚本把日志按 trace_id 聚合输出成文本时序# 按 trace_id 聚合日志并排序 grep trace_id_value agent.log | sort -k1,1 -k2,2 | awk {print $1, $2, $3, $4}输出大概长这样[trace-abc] nodeintent_parse phasestart [trace-abc] nodeintent_parse phaseend statussuccess [trace-abc] nodetask_plan phasestart [trace-abc] nodetask_plan phaseend statussuccess [trace-abc] nodetool_select phasestart [trace-abc] nodetool_select phaseend statussuccess [trace-abc] nodetool_call phasestart [trace-abc] nodetool_call phaseerror errortimeout statusfail一眼就能看出tool_call节点是阻塞点。如果日志量太大可以只过滤statusfail或只有 start 没有 end 的节点。4.3 第三步terminalBlocker 节点深度检查定位到疑似 terminalBlocker 节点后需要做深度检查。检查清单如下检查项检查内容常见问题输入检查节点输入是否符合预期参数缺失、格式错误、编码问题输出检查节点输出是否完整空结果、截断、格式不符超时检查节点是否有合理超时无超时、超时过长、超时过短重试检查失败后是否有重试无重试、重试策略不合理资源检查节点运行时资源是否充足内存不足、连接池满、CPU 打满依赖检查节点依赖的外部服务是否正常DNS 失败、网络不通、服务不可用这个清单我用了很久基本能覆盖 80% 以上的 terminalBlocker 场景。4.4 第四步失败链归因与修复验证找到 terminalBlocker 后不要急着改代码。先做归因分析这个阻塞点是偶发的还是必现的是代码问题还是配置问题是单点问题还是系统性问题归因清楚后修复策略分三种快速修复加超时、加重试、加降级先让流程能走通。根因修复修改代码逻辑、调整配置、优化资源分配。防御性修复加监控、加告警、加自动恢复机制防止同类问题再次发生。修复后必须做验证用相同的输入重新跑一遍确认失败链不再出现。如果条件允许构造边界用例再跑一遍确认修复没有引入新问题。5. 多 Agent 协作场景下的失败链排查要点5.1 多 Agent 失败链的特殊性多 Agent 协作场景下失败链的复杂度是指数级上升的。原因很简单每个 Agent 有自己的执行链Agent 之间有交互链交互链又可能嵌套。一个最终错误码可能是 Agent A 的模型问题也可能是 Agent B 的工具问题还可能是 A 和 B 之间的通信问题。我见过最离谱的案例是Agent A 等待 Agent B 的输出Agent B 等待 Agent C 的输出Agent C 因为一个空结果阻塞了但它的错误没有传播出来导致 A 和 B 一直等到超时。最终错误码显示的是 A 的超时但真正的 terminalBlocker 在 C。5.2 跨 Agent 追踪标识的设计解决这个问题的关键是跨 Agent 的追踪标识。我通常会在多 Agent 系统里设计两级追踪全局 trace_id贯穿整个任务所有 Agent 共享。Agent 级 span_id每个 Agent 的执行链有自己的 span_id但都关联到全局 trace_id。这样排查时你可以先通过全局 trace_id 拉出所有 Agent 的执行概况再通过 span_id 深入某个 Agent 的细节。# 多 Agent 追踪标识示例 class AgentContext: def __init__(self, global_trace_id, agent_name): self.global_trace_id global_trace_id self.agent_name agent_name self.span_id f{agent_name}-{uuid.uuid4().hex[:8]} def log(self, node, phase, **kwargs): logger.info( f[{self.global_trace_id}][{self.span_id}] fagent{self.agent_name} node{node} phase{phase} {kwargs} )5.3 多 Agent 死锁的排查与预防多 Agent 死锁是最难排查的 terminalBlocker 之一。它的表现是所有 Agent 都在等待没有错误码但任务永远不完成。排查死锁的方法是构建等待图把每个 Agent 的等待关系画出来看有没有环。如果有环环上的节点就是死锁参与者。预防死锁的策略设置全局超时整个任务有最大执行时间超时强制终止。设置 Agent 级超时每个 Agent 有独立超时避免单个 Agent 无限等待。避免循环等待设计协作协议时确保等待关系是有向无环图。引入仲裁者复杂协作场景下用一个仲裁 Agent 协调资源分配避免互相等待。注意死锁排查时不要只看 Agent 日志还要看系统级指标比如线程状态、连接池状态、消息队列堆积情况。有时候死锁发生在框架层Agent 日志里看不出来。6. 常见问题速查与避坑经验6.1 错误码排查速查表错误码/现象可能来源排查方向快速验证方法422模型侧/工具侧检查请求体格式、字段类型、必填项用 curl 直接调 API 复现429模型侧检查限流策略、并发数、token 消耗速率降低并发后重试timeout工具侧/环境侧检查网络、DNS、目标服务状态ping/telnet 目标地址execution terminated框架侧检查状态机、内存、并发锁拉完整堆栈日志model request failed模型侧检查鉴权、配额、模型名称用最小请求测试空结果工具侧检查工具返回原始数据、解析逻辑打印原始返回内容状态不一致框架侧检查共享状态读写、并发控制加状态快照对比6.2 避坑经验我踩过的那些坑坑一只看最终错误码不看中间日志。早期我做 Agent 排查时习惯直接搜错误码结果搜到的都是最终报错点真正的根因在几百行之前。后来强制自己先拉完整 trace再定位效率提升明显。坑二忽略环境侧问题。有一次 Agent 频繁超时查了半天模型和工具都没问题最后发现是容器内存不足导致频繁 GC执行线程被拖慢。环境侧问题最隐蔽但也最致命。坑三重试策略一刀切。所有错误都重试三次结果不可恢复的错误重试了也是白重试还放大了问题。后来按可恢复性分类处理瞬时错误重试条件错误调整后重试不可恢复错误直接告警。坑四多 Agent 场景下只看单个 Agent 日志。多 Agent 失败往往是跨 Agent 的只看一个 Agent 的日志永远找不到根因。必须拉全局 trace构建等待图。坑五没有执行快照排查靠猜。没有快照时你只能根据日志推测节点状态但日志往往不完整。加了执行快照后每个节点的输入输出都有记录排查从“猜”变成“看”。6.3 工具选型建议Agent 失败链排查的工具选型我建议分三层日志层结构化日志 trace_id 贯穿。不要用纯文本日志用 JSON 格式方便聚合和过滤。追踪层如果团队有条件接入分布式追踪系统自动采集链路数据。如果没条件用脚本聚合日志也能凑合。监控层对关键节点加监控指标比如成功率、耗时分布、错误率。出问题时先看监控大盘快速定位异常节点。工具不在多在于能串起来。日志、追踪、监控三层打通排查效率会有质的提升。7. 从失败链到防御体系让同类问题不再重复发生排查完一次失败链如果只是修了当前问题那下次同类问题还会再来。真正有价值的做法是把失败链转化为防御能力。我的做法是建一个失败模式库每次排查完把失败链的关键信息记录下来——错误码、terminalBlocker 类型、根因、修复方案、预防措施。积累多了之后你会发现很多失败是重复的只是表现形式不同。有了失败模式库下次遇到类似错误码可以直接匹配历史案例快速定位。另一个做法是在 Agent 框架里内置防御性检查。比如工具调用前检查参数完整性。工具调用后检查返回结果是否为空、格式是否符合预期。每个节点设置合理超时。多 Agent 协作时检查等待图是否有环。关键节点加状态快照便于事后追溯。这些检查会增加一点开销但相比排查失败链的时间成本完全值得。最后分享一个我个人体会Agent 失败排查的能力本质上是对系统可观测性的考验。你的日志够不够细、追踪够不够全、监控够不够准决定了你排查的效率。模型能力固然重要但一个可观测性差的 Agent 系统再强的模型也救不了。把失败链排查能力建起来你会发现很多所谓的“模型问题”其实都是工程问题。