1. 当Agent报错时,我们到底在排查什么
做过Agent开发的人大概都有过这种经历:线上告警响了,某个任务执行失败,日志里躺着一条agent execution terminated due to error,然后整个团队开始围着模型转——是不是prompt写得不好?是不是模型能力不够?要不要换个更大的模型试试?
我一开始也是这么干的。直到有一次,我花了两天时间反复调prompt、换模型、加few-shot示例,最后发现真正的问题出在一个根本不起眼的地方:工具调用返回了一个格式不规范的JSON,导致后续解析直接崩了。模型从头到尾都没问题,它老老实实按照要求输出了内容,只是下游的处理链路没接住。
这件事让我意识到一个很关键的问题:Agent的失败,绝大多数时候不是模型的问题,而是链路上某个环节断了。但我们习惯性地把锅甩给模型,因为模型是黑盒,是最容易怀疑的对象,也是最容易“换一个试试”的对象。可实际上,从用户发起请求到Agent返回结果,中间经过的环节远比我们想象的多——意图理解、任务规划、工具选择、参数构造、API调用、结果解析、状态管理、重试逻辑、超时控制……任何一环出问题,最终表现出来的都是“Agent失败了”。
这篇文章想聊的就是这件事:怎么从一条粗糙的错误码出发,一步步定位到真正的失败链。不管你是刚接触Agent开发的新手,还是已经踩过几轮坑的老手,这套排查思路应该都能帮你少走一些弯路。关键词里的terminalBlocker、失败链、错误码,本质上都是同一个问题的不同侧面——我们需要一套系统化的方法,把“Agent挂了”这个笼统的现象,拆解成可定位、可复现、可修复的具体问题。
2. 错误码为什么总是“不够用”
2.1 从一条真实的错误日志说起
先看一条典型的Agent执行日志:
[2024-11-15 14:23:07] ERROR agent_executor - Agent execution terminated due to error. [2024-11-15 14:23:07] ERROR agent_executor - Error code: 422 [2024-11-15 14:23:07] INFO agent_executor - Task: 查询北京明天天气并发送邮件通知 [2024-11-15 14:23:07] INFO agent_executor - Steps completed: 2/5 [2024-11-15 14:23:07] INFO agent_executor - Last tool: weather_api这条日志告诉我们的信息其实不少:任务执行到第2步就挂了,最后调用的工具是天气API,错误码是422。但问题是,422这个码太笼统了。它可能是请求参数格式不对,可能是API的认证过期了,可能是请求频率超限,也可能是返回的数据结构跟预期不一致导致解析失败。光看这个码,你根本不知道下一步该往哪查。
这就是“粗错误码”的典型困境:错误码的设计初衷是给调用方一个快速判断的依据,但在Agent这种多环节、多工具、多状态的场景下,单一错误码承载的信息量远远不够。它就像医院急诊的分诊台,只告诉你“内科”还是“外科”,但具体是什么病,还得靠进一步检查。
2.2 错误码在Agent链路中的三个盲区
我总结下来,粗错误码在Agent排查中主要有三个盲区:
第一个盲区是环节归属不清。一个422可能来自模型服务商的API网关,也可能来自你调用的第三方工具,还可能来自你自己写的中间件。如果不记录错误发生的具体环节,你连该找谁都不知道。我见过有团队因为这个问题,把模型服务商的工单系统翻了个底朝天,最后发现是自己工具层的参数校验写错了。
第二个盲区是上下文丢失。错误码只告诉你“失败了”,但不告诉你“在什么状态下失败的”。比如同样是工具调用失败,Agent是在第一次尝试时失败的,还是在重试了三次之后失败的?是在拿到了部分结果之后失败的,还是在发起请求之前就失败了?这些上下文决定了排查的方向完全不同。
第三个盲区是因果链断裂。Agent的执行是一个链式过程,A步骤的输出是B步骤的输入。如果B失败了,错误码只记录B的问题,但真正的原因可能在A——A输出的数据格式不对、A遗漏了某个字段、A的结果被截断了。没有完整的链路追踪,你只能看到“最后一根稻草”,看不到“骆驼是怎么被压垮的”。
2.3 一个改进的错误码设计思路
那怎么让错误码变得“有用”?我的经验是,不要试图用一个码解决所有问题,而是采用分层错误码 + 结构化上下文的方式。
具体来说,错误码本身可以设计成三段式:[环节标识]-[错误类型]-[严重级别]。比如TOOL-PARAM-ERROR表示工具调用环节的参数错误,MODEL-TIMEOUT-WARN表示模型调用环节的超时警告。这样一眼就能看出问题出在哪个环节、是什么性质、有多严重。
但光有错误码还不够,关键是要把上下文一起记录下来。我通常会在错误日志里强制包含这几个字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| trace_id | 全链路追踪ID | abc-123-def |
| step_index | 当前执行到第几步 | 2 |
| step_name | 当前步骤名称 | weather_query |
| tool_name | 调用的工具名 | weather_api |
| input_digest | 输入参数的摘要 | {"city":"北京","date":"2024-11-16"} |
| output_digest | 输出结果的摘要 | {"error":"invalid_date_format"} |
| retry_count | 已重试次数 | 0 |
| elapsed_ms | 已耗时(毫秒) | 3200 |
有了这些字段,你再看错误日志的时候,就不是在看一条孤零零的错误码,而是在看一个完整的现场快照。哪个环节、什么输入、什么输出、重试了几次、花了多长时间,一目了然。
提示:
input_digest和output_digest不要记录完整内容,避免日志膨胀和敏感信息泄露。摘要长度控制在200字符以内,保留关键字段即可。
3. 失败链的拆解方法:从终点往回追
3.1 为什么正向排查容易迷路
大部分人排查Agent失败的习惯是正向的:从用户请求开始,一步步往下看,看哪一步出了问题。这个方法在简单场景下没问题,但在Agent这种多分支、多工具、多状态的场景下,很容易迷路。因为Agent的执行路径不是线性的,它可能走了A分支又回到主干,可能调了工具X又调了工具Y,可能重试了三次才成功。你顺着日志往下读,读着读着就不知道读到哪了。
我的做法是反过来:从失败点往回追。先找到最终报错的那一步,然后问三个问题:这一步的输入是什么?这个输入是谁给的?给输入的那一步有没有问题?这样一层层往回推,直到找到真正的根因。
3.2 往回追的三个关键节点
往回追的过程中,有三个节点是必须检查的:
第一个节点是失败步骤的直接输入。比如天气API返回了422,那就要看它收到的请求参数是什么。是城市名写错了?是日期格式不对?还是缺少了必填字段?这一步的输入往往来自上一步的输出,所以你要把上一步的输出原样拿出来对比。
第二个节点是上一步的输出生成逻辑。如果上一步是模型生成的工具调用参数,那就要看模型输出的原始内容是什么。有时候模型输出的参数是对的,但被中间的解析层处理错了;有时候模型输出的参数本身就带了多余的空格或换行,导致API拒绝。这里特别要注意的是,模型输出的JSON经常会有一些“小毛病”——比如用了单引号而不是双引号、末尾多了逗号、数字被引号包起来了。这些在人类看来无所谓,但严格的API会直接拒绝。
第三个节点是状态传递的完整性。Agent执行过程中,状态是在不断传递和更新的。如果某一步的状态没有被正确传递到下一步,就会出现“上一步明明成功了,下一步却说缺少参数”的情况。我遇到过好几次,都是因为状态对象在序列化和反序列化过程中丢了字段,导致后续步骤拿不到必要的信息。
3.3 一个实际的排查案例
说一个我最近处理的案例。有个Agent任务是“查询指定城市的空气质量并生成报告”,执行到第三步的时候报错了,错误码是TOOL-RESP-500,意思是工具返回了500错误。
按照往回追的思路,我先看第三步的输入。第三步是调用报告生成工具,它的输入是第二步的空气质量查询结果。我把第二步的输出拿出来看,发现空气质量数据是拿到了,但里面有一个字段是null——具体是primary_pollutant这个字段,当空气质量为优的时候,这个字段就是空的。
报告生成工具在收到null的时候,没有做空值处理,直接去拼接字符串,结果抛了异常。但异常被工具层捕获后,包装成了一个500错误返回给Agent。Agent看到500,以为是工具服务挂了,就触发了重试逻辑,重试了三次还是同样的错误,最后整个任务失败。
真正的根因是什么?是第二步的输出没有做空值兜底。如果第二步在输出primary_pollutant的时候,把null转成“无”或者空字符串,第三步就不会挂。模型从头到尾都没问题,它只是忠实地执行了“查询空气质量”这个指令。
这个案例告诉我们:失败链的终点往往不是根因,根因通常在更早的环节。如果你只盯着报错的那一步修,可能修了表面,下次换个任务又会在别的地方爆出来。
4. terminalBlocker:那些让Agent彻底卡死的场景
4.1 什么是terminalBlocker
terminalBlocker这个词是我自己在排查过程中总结出来的,指的是那些让Agent彻底无法继续执行、且无法通过重试恢复的阻塞性故障。它和普通的“执行失败”不一样——普通失败可能重试一下就好了,但terminalBlocker是重试多少次都没用,因为问题的性质决定了它不可能通过重复执行来解决。
典型的terminalBlocker包括:认证凭证过期且无法自动刷新、依赖的外部服务永久下线、请求的参数格式根本不符合API要求、状态机进入了死循环、资源配额耗尽且无法自动扩容。这些问题的共同特点是:不改变外部条件或代码逻辑,Agent永远不可能成功。
4.2 三类常见的terminalBlocker
第一类是凭证类阻塞。比如Agent调用某个第三方API时,access token过期了。如果Agent的重试逻辑只是简单地重新发起请求,而不去刷新token,那重试一万次也是401。更糟糕的是,有些Agent框架会把401当成普通错误,触发无限重试,最后把配额耗光。
第二类是参数类阻塞。比如API要求日期格式是YYYY-MM-DD,但模型输出的是YYYY/MM/DD。如果Agent没有参数校验和修正逻辑,每次调用都会被拒绝。这种阻塞最隐蔽,因为错误码可能只是400或422,看起来像是“临时性错误”,但实际上它是确定性的——只要参数不改,永远失败。
第三类是状态类阻塞。比如Agent的任务规划进入了一个循环:A步骤依赖B步骤的输出,B步骤又依赖A步骤的输出,两者互相等待,形成死锁。这种阻塞在日志上可能表现为“一直在执行中”,没有明确的错误码,但任务永远不会完成。
4.3 怎么识别和拦截terminalBlocker
识别terminalBlocker的关键是区分“可恢复错误”和“不可恢复错误”。我的做法是在错误处理层加一个判断逻辑:
def is_terminal_blocker(error_code, error_context): # 凭证类:401/403且没有刷新机制 if error_code in [401, 403] and not error_context.get("can_refresh_token"): return True # 参数类:400/422且参数格式错误 if error_code in [400, 422] and error_context.get("error_type") == "invalid_format": return True # 状态类:检测到循环依赖 if error_context.get("cycle_detected"): return True # 资源类:配额耗尽且无法扩容 if error_code == 429 and error_context.get("quota_exhausted"): return True return False一旦判定为terminalBlocker,就不要走重试逻辑了,直接终止任务并上报。上报的时候要把完整的上下文带上,让运维人员能快速定位。同时,Agent应该给用户一个明确的反馈,而不是让用户一直等。
注意:terminalBlocker的判定条件需要根据你的实际业务场景调整。比如有些API的401是可以通过刷新token恢复的,那就不能简单判定为terminalBlocker。关键是要理解每个错误码背后的语义。
4.4 一个terminalBlocker的实战处理
我之前遇到过一个典型的terminalBlocker:Agent需要调用一个内部服务来获取用户画像数据,但这个服务的认证方式是双向TLS。某次证书轮换之后,Agent的证书没有及时更新,导致所有请求都被拒绝。错误码是SSL_HANDSHAKE_FAILED,看起来像是网络问题,但实际上证书不更新就永远连不上。
当时的处理过程是这样的:首先,通过错误码和上下文确认了是TLS握手失败;然后,检查了证书的有效期,发现已经过期;接着,确认了证书轮换的流程,发现Agent的证书更新没有自动化;最后,手动更新了证书,并加了一个证书过期前的自动告警。
这件事的教训是:terminalBlocker往往不是代码逻辑的问题,而是运维流程的问题。代码写得再好,证书过期了就是过期了。所以Agent的排查不能只看代码,还要看依赖的外部条件。
5. 从错误码到失败链的完整排查链路
5.1 第一步:锁定失败环节
拿到一个错误码之后,第一件事是确定它来自哪个环节。Agent的链路通常包括:用户输入解析、任务规划、工具选择、参数构造、工具调用、结果解析、状态更新、输出生成。每个环节都可能产生错误码,但错误码的格式和来源往往不同。
我的做法是给每个环节分配一个唯一的标识前缀。比如PLAN-开头的是任务规划环节,TOOL-开头的是工具调用环节,PARSE-开头的是结果解析环节。这样一看错误码就知道该去哪个环节的日志里找。
如果错误码没有这样的前缀,那就需要看错误发生的堆栈信息。堆栈里通常会包含模块名和函数名,能帮你快速定位。如果连堆栈都没有,那就只能靠时间戳和上下文来推断了。
5.2 第二步:还原失败现场
锁定环节之后,下一步是还原失败时的现场。这包括:当时的输入是什么、期望的输出是什么、实际的输出是什么、执行了多长时间、重试了几次、有没有并发操作。
还原现场最怕的是信息不足。所以我强烈建议在Agent的每个关键步骤都打结构化日志,至少包含前面提到的那些字段。日志的级别要合理,INFO级别记录正常流程,ERROR级别记录异常,DEBUG级别记录详细的输入输出。线上环境一般开INFO,排查问题时可以临时调到DEBUG。
5.3 第三步:构建失败链
有了失败现场的信息,就可以开始构建失败链了。失败链的核心是因果关系:A导致了B,B导致了C,C最终导致了失败。构建失败链的过程,就是不断问“为什么”的过程。
比如:工具调用失败了,为什么?因为参数格式不对。参数为什么不对?因为模型输出的JSON有语法错误。模型为什么输出有语法错误?因为prompt里没有明确要求JSON格式。Prompt为什么没有要求?因为模板里漏了这一条。
这样一层层问下来,你就能从表面的错误码,追到深层的根因。根因可能是一个prompt模板的遗漏,可能是一个参数校验的缺失,也可能是一个状态管理的bug。
5.4 第四步:验证和修复
找到根因之后,不要急着改代码。先做一个最小化的复现,确认你的判断是对的。复现的方法可以是写一个单元测试,也可以是手动构造一个请求。复现的目的是排除“巧合”的可能性——有时候你以为是A导致的,改完A之后发现还是失败,那就说明真正的根因在别处。
修复的时候,我建议遵循“先止血、再治本”的原则。如果线上正在出问题,先加一个临时的兜底逻辑让任务能跑通,然后再去修根因。兜底逻辑可以是参数修正、重试策略调整、降级处理等。但兜底逻辑一定要有日志记录,方便后续追踪。
5.5 第五步:沉淀排查经验
每次排查完一个失败链,我都会把过程和结论记录下来。记录的内容包括:错误码是什么、失败链是什么、根因是什么、修复方案是什么、有没有类似的坑。这些记录积累多了,就形成了一套排查手册。下次遇到类似的错误码,直接翻手册就能找到方向。
更重要的是,这些记录能帮你发现系统性的问题。比如你发现最近三个月有五次失败都是因为参数格式问题,那就说明参数校验这一块需要加强。如果发现某个工具的错误率特别高,那就说明这个工具的集成方式需要优化。
6. 工具选型:用什么来追踪失败链
6.1 日志系统是基础
追踪失败链最基础的工具是日志系统。不管是ELK、Loki还是云服务商提供的日志服务,核心是要能做到结构化查询和链路关联。结构化查询让你能按字段过滤,比如查所有tool_name=weather_api且error_code=422的日志。链路关联让你能通过trace_id把同一个任务的所有日志串起来。
我在选日志系统的时候,最看重的两个能力是:第一,支持高基数字段的索引,比如trace_id这种每个请求都不同的字段;第二,支持快速的上下文查询,比如给我看某条日志前后各10条。这两个能力直接决定了排查效率。
6.2 链路追踪是进阶
如果条件允许,建议上链路追踪系统,比如Jaeger、Zipkin或者云服务商的APM。链路追踪能把Agent的每个步骤画成一张时序图,让你一眼看出哪一步耗时最长、哪一步出了错、步骤之间的依赖关系是什么。
链路追踪的关键是埋点要全。Agent的每个工具调用、每次模型请求、每次状态更新,都应该生成一个span。span的标签里要带上关键信息,比如工具名、模型名、输入输出摘要、错误码等。这样你就能在追踪系统里直接看到失败链的全貌。
6.3 自定义的失败链分析工具
通用的日志和追踪工具虽然强大,但有时候不够贴合Agent的场景。比如你想快速统计“最近一小时哪些工具的错误率最高”,或者“哪些错误码最容易导致terminalBlocker”,通用的工具可能需要写复杂的查询语句。
我的做法是在通用工具之上,再搭一个轻量的分析层。这个分析层定期从日志系统拉数据,做聚合和统计,生成一些固定的报表。比如:
| 报表名称 | 统计维度 | 更新频率 |
|---|---|---|
| 工具错误率排行 | 工具名、错误码 | 每小时 |
| terminalBlocker统计 | 阻塞类型、发生次数 | 每天 |
| 失败链TOP10 | 根因、影响任务数 | 每天 |
| 平均恢复时间 | 错误类型、修复耗时 | 每周 |
这些报表能帮你从宏观上把握系统的健康度,也能帮你发现一些隐藏的问题。
6.4 一个容易被忽略的工具:回放系统
排查失败链的时候,最痛苦的事情是“无法复现”。日志里看到失败了,但手动去跑同样的请求,又成功了。这种情况往往是因为失败依赖于特定的时序、特定的状态或者特定的并发条件。
解决这个问题的办法是建一个回放系统。回放系统的核心是记录每次执行的完整输入和状态快照,然后能在需要的时候原样重放。重放的时候可以逐步执行,观察每一步的输出,从而定位问题。
回放系统的实现难度取决于Agent的复杂度。如果Agent的状态是纯函数式的,那回放很简单,把输入重新喂进去就行。如果Agent有副作用(比如调用了外部API),那就需要mock这些副作用,或者记录副作用的返回值。
7. 那些只有踩过才知道的坑
7.1 模型输出的“隐形”格式问题
模型输出的JSON经常有一些肉眼难以察觉的问题。比如:用了全角引号而不是半角引号、数字被引号包起来了、布尔值是字符串“true”而不是布尔值true、嵌套对象里多了尾随逗号。这些问题在人类看来微不足道,但严格的JSON解析器会直接报错。
我的应对策略是在解析之前先做一轮清洗。清洗的规则包括:把全角引号替换成半角、把被引号包起来的数字转成数字类型、把字符串“true”/“false”转成布尔值、去掉尾随逗号。清洗之后再用严格的解析器解析,成功率会高很多。
但清洗也有风险——万一模型输出的就是字符串“true”呢?所以清洗规则要结合具体的字段类型来定。如果字段在schema里定义的是布尔值,那就转;如果定义的是字符串,那就不转。
7.2 重试逻辑的“雪崩”效应
Agent的重试逻辑如果设计不当,很容易引发雪崩。比如某个工具调用失败了,Agent重试三次,每次重试都超时30秒,那一个任务就卡了90秒。如果并发量大的话,线程池很快就被占满了,整个系统都会变慢。
我的做法是给重试加上指数退避和最大重试次数。第一次重试等1秒,第二次等2秒,第三次等4秒,最多重试三次。同时,重试要有超时控制,单次重试不能超过一个阈值(比如10秒)。如果超过阈值还没返回,就直接放弃,走降级逻辑。
另外,重试要区分错误类型。对于terminalBlocker,不要重试;对于网络抖动这种临时性错误,可以重试;对于参数错误,重试也没用,应该直接报错。
7.3 状态管理的“幽灵”字段
Agent的状态在传递过程中,有时候会多出一些“幽灵”字段——这些字段不是当前步骤产生的,而是之前某个步骤遗留的。如果后续步骤不小心读到了这些字段,就可能产生错误的行为。
比如,第一步查询了天气,状态里存了temperature字段。第二步查询空气质量,状态里应该只有空气质量相关的字段。但如果状态没有清理,第二步的输出里可能还带着temperature。第三步生成报告的时候,如果误用了temperature,就会生成错误的内容。
解决这个问题的办法是状态隔离。每个步骤只应该看到它需要的字段,不应该看到无关的字段。实现上可以用命名空间,比如weather.temperature和air_quality.pm25,这样就不会混淆了。
7.4 并发场景下的时序问题
Agent如果支持并发执行多个任务,时序问题就会变得很棘手。比如两个任务同时修改同一个状态对象,一个写入了A,一个写入了B,最后状态里是A还是B取决于谁后写。这种问题在单线程下不会出现,但在并发下很容易爆。
我的做法是给状态加上版本号或者时间戳,每次更新的时候检查版本号,如果版本号不是最新的,就拒绝更新或者合并更新。另外,对于共享资源(比如数据库连接、API配额),要用锁或者信号量来控制并发访问。
7.5 日志本身的“坑”
日志是排查的基础,但日志本身也可能出问题。比如:日志量太大导致磁盘写满、日志级别设置不当导致关键信息没记录、日志格式不统一导致查询困难、日志里包含了敏感信息导致安全风险。
我的经验是:日志要分级,ERROR级别只记录真正的错误,INFO级别记录关键流程,DEBUG级别记录详细信息但默认关闭。日志要轮转,避免单个文件过大。日志要脱敏,手机号、邮箱、身份证号等敏感信息要打码。日志要统一格式,最好用JSON,方便结构化查询。
8. 把排查能力变成系统能力
8.1 从“人肉排查”到“自动诊断”
一开始,排查失败链全靠人肉看日志、猜原因、试修复。但随着Agent的任务越来越复杂、调用链越来越长,人肉排查的效率越来越低。这时候就需要把排查能力沉淀成系统能力。
自动诊断的核心是规则引擎。把常见的失败模式和对应的根因整理成规则,当错误发生时,自动匹配规则并给出诊断建议。比如:错误码是422且工具是weather_api,规则引擎就提示“检查日期格式是否为YYYY-MM-DD”。错误码是401且工具是内部服务,规则引擎就提示“检查token是否过期”。
规则引擎的规则可以不断积累。每次人工排查出一个新的失败模式,就把它加进规则库。时间长了,规则库越来越丰富,自动诊断的准确率也越来越高。
8.2 从“事后排查”到“事前预防”
排查能力再强,也是事后补救。更好的做法是事前预防。预防的手段包括:在Agent执行之前做参数校验、在工具调用之前做连通性检查、在状态更新之前做一致性检查、在任务规划阶段做可行性分析。
比如,对于参数校验,可以在prompt里明确要求模型输出特定格式的JSON,并在解析层做严格的schema校验。对于连通性检查,可以在任务开始之前先ping一下依赖的服务,如果服务不可用就直接拒绝任务,而不是等到执行到一半才发现。
8.3 从“单点修复”到“系统优化”
每次排查完一个失败链,除了修复当前的问题,还要思考:这个问题是不是系统性的?有没有类似的隐患?能不能通过架构调整来避免?
比如,如果发现多个工具都有参数格式问题,那就应该统一参数校验的逻辑,而不是每个工具单独修。如果发现多个任务都因为超时失败,那就应该调整超时策略,或者优化任务的拆分方式。如果发现terminalBlocker频繁发生,那就应该加强依赖服务的管理,比如加证书自动轮换、加配额预警。
8.4 一个实际的系统优化案例
我之前负责的一个Agent系统,上线初期失败率很高,每天都有几十个任务失败。排查之后发现,大部分失败都是因为工具调用的参数格式问题。模型输出的参数有时候是字符串,有时候是数字,有时候是布尔值,而工具API对参数类型有严格要求。
当时的修复方案是:在工具调用层加一个统一的参数适配器,根据工具的schema自动转换参数类型。比如schema要求数字,就把字符串转成数字;schema要求布尔值,就把字符串“true”/“false”转成布尔值。这个适配器上线之后,参数相关的失败率下降了80%以上。
这件事让我意识到:很多失败不是模型的问题,而是工程的问题。模型已经尽力了,它输出了它认为正确的参数,但工程层没有做好适配和兜底。把工程层做扎实,Agent的稳定性会有质的提升。
9. 一些实用的排查清单和模板
9.1 快速排查清单
遇到Agent失败时,按这个清单走一遍,能覆盖大部分场景:
- 看错误码:确定错误发生的环节和类型。
- 看trace_id:把同一个任务的所有日志串起来。
- 看失败步骤的输入:输入是否符合预期?有没有格式问题?
- 看失败步骤的输出:输出是什么?和期望的差异在哪?
- 看上一步的输出:上一步的输出是否完整?有没有null或空值?
- 看重试记录:重试了几次?每次的错误是否相同?
- 看耗时:是否超时?超时阈值是否合理?
- 看并发:是否有并发冲突?状态是否被覆盖?
- 看依赖服务:依赖的服务是否正常?凭证是否有效?
- 看最近变更:最近有没有代码发布、配置修改、证书轮换?
9.2 失败链记录模板
每次排查完,用这个模板记录:
失败链编号:FL-2024-1115-001 发现时间:2024-11-15 14:23 影响任务:查询天气并发送邮件 错误码:TOOL-RESP-422 失败环节:工具调用(weather_api) 失败现象:API返回422,提示日期格式错误 根因:模型输出的日期格式为YYYY/MM/DD,API要求YYYY-MM-DD 修复方案:在参数适配层增加日期格式转换 预防措施:在prompt中明确日期格式要求,并在解析层做格式校验9.3 常见错误码对照表
| 错误码 | 可能环节 | 常见原因 | 排查方向 |
|---|---|---|---|
| 400 | 工具调用 | 参数缺失或格式错误 | 检查请求参数 |
| 401 | 工具调用 | 认证失败 | 检查token/证书 |
| 403 | 工具调用 | 权限不足 | 检查权限配置 |
| 404 | 工具调用 | 资源不存在 | 检查URL/资源ID |
| 422 | 工具调用 | 参数语义错误 | 检查参数格式和取值 |
| 429 | 工具调用 | 频率超限 | 检查配额和限流策略 |
| 500 | 工具调用 | 服务端错误 | 检查依赖服务状态 |
| TIMEOUT | 任意环节 | 超时 | 检查网络和超时阈值 |
| PARSE_ERROR | 结果解析 | 解析失败 | 检查输出格式 |
| CYCLE_DETECTED | 任务规划 | 循环依赖 | 检查任务依赖图 |
这张表不是万能的,但能帮你快速缩小排查范围。实际排查中,还要结合具体的上下文来判断。
10. 写在最后的一些个人体会
做Agent开发这几年,我最大的体会是:Agent的稳定性,三分靠模型,七分靠工程。模型的能力在不断提升,但工程上的坑是永远填不完的。与其天天想着换个更强的模型,不如先把错误处理、状态管理、参数校验、日志追踪这些基础功做扎实。
另一个体会是:排查能力是练出来的,不是看出来的。你看再多的排查指南,不如亲自处理几个真实的失败案例。每次排查都是一次学习的机会,把过程记录下来,把根因搞清楚,把修复方案沉淀下来,你的排查能力就会越来越强。
最后一个体会是:不要害怕失败,要害怕不知道为什么失败。Agent失败不可怕,可怕的是失败了却找不到原因,只能靠重启和重试来碰运气。把失败链搞清楚,把错误码用起来,把排查流程标准化,Agent的稳定性自然就上来了。
希望这篇内容能帮到正在被Agent失败困扰的你。如果你也有类似的排查经历,或者有更好的方法,欢迎一起交流。