1. 别急着换模型,先看看错误码在说什么
Agent 项目跑崩了,很多人的第一反应是“模型不行”,然后换模型、调温度、加提示词,折腾一圈发现该崩还是崩。我踩过太多次这种坑了,后来才慢慢意识到:大部分 Agent 失败根本不是模型能力问题,而是失败链路上游某个环节先出了问题,模型只是最后一个背锅的。
举个很典型的场景。你写了一个 Agent,让它调用工具查数据、做分析、生成报告。跑着跑着突然报错,日志里只有一行agent execution terminated due to error,或者更粗暴的model request failed。这时候你去看模型服务商的错误信息,可能显示的是 422、超时、连接中断。但如果你只盯着模型这一层排查,永远找不到根因。
我后来总结了一套方法:把 Agent 的失败当成一条链来看,从最外层的粗错误码开始,一层一层往里剥,直到定位到真正断掉的那个环节。这条链大致是这样的:
用户输入 → Agent 编排层 → 工具调用层 → 外部服务/API 层 → 模型请求层 → 结果解析层 → 输出层
任何一层出问题,最终表现出来的都可能是“Agent 失败了”。但错误码的粒度和位置,能告诉你该往哪个方向查。
这篇文章适合两类人:一是刚开始做 Agent 开发、遇到报错就懵的新手;二是已经跑过几个 Agent 项目、但排查效率一直不高的开发者。我会用实际案例拆解,从错误码怎么读、失败链怎么建、每一层怎么排查,到常见坑和速查表,尽量把这件事讲透。
1.1 为什么“换模型”是最差的排查策略
先说说为什么不能一上来就换模型。原因很简单:换模型会掩盖问题,而不是解决问题。
你换了一个模型,可能因为它的超时阈值更宽、对畸形输入的容忍度更高、或者恰好那次请求没触发某个边界条件,于是“看起来好了”。但失败链上游的问题还在,过几天换个场景又崩了。更糟糕的是,你换模型之后,原来的错误码和日志对不上了,排查难度反而更大。
我自己的经验是:除非确认是模型本身的能力边界问题(比如上下文长度不够、不支持某种工具调用格式),否则不要动模型。先把错误码读明白,把失败链建起来,找到真正断掉的那一环。
1.2 粗错误码到底粗在哪里
大部分 Agent 框架和平台返回的错误码,粒度都很粗。常见的几类:
| 错误码/信息 | 实际含义 | 可能根因 |
|---|---|---|
agent execution terminated due to error | Agent 执行被终止 | 工具调用失败、超时、解析异常、模型返回异常 |
model request failed | 模型请求失败 | 网络问题、鉴权失败、请求格式错误、限流 |
422 | 请求实体无法处理 | 参数格式错误、必填字段缺失、类型不匹配 |
timeout | 超时 | 外部服务慢、模型响应慢、死锁 |
connection refused | 连接被拒 | 目标服务没启动、端口不对、防火墙 |
out of memory | 内存不足 | 容器内存限制、大对象未释放、并发过高 |
你看,这些错误码没有一个能直接告诉你“是哪一行代码、哪个工具、哪个参数出了问题”。所以我们需要把粗错误码展开成失败链,逐层缩小范围。
2. 失败链的搭建:从错误码到根因的映射方法
失败链的核心思想是:每一个错误码都不是孤立的,它一定对应链路上某个节点的异常状态。我们要做的,就是把这个对应关系建起来。
2.1 先给 Agent 执行过程打上“层标签”
我在自己的项目里,会给 Agent 的每个执行阶段打上标签,日志里必须带这些标签。这样一旦出错,我能立刻知道是哪一层的问题。
# 示例:给 Agent 执行阶段打标签 import logging logger = logging.getLogger("agent") def execute_agent(user_input): logger.info("[LAYER=INPUT] user_input=%s", user_input) try: plan = planner.plan(user_input) logger.info("[LAYER=PLAN] plan=%s", plan) except Exception as e: logger.error("[LAYER=PLAN] error=%s", e) raise for step in plan.steps: try: logger.info("[LAYER=TOOL] tool=%s args=%s", step.tool, step.args) result = tool_registry.call(step.tool, step.args) logger.info("[LAYER=TOOL] result=%s", result) except Exception as e: logger.error("[LAYER=TOOL] tool=%s error=%s", step.tool, e) raise try: logger.info("[LAYER=MODEL] prompt=%s", prompt) response = model.call(prompt) logger.info("[LAYER=MODEL] response=%s", response) except Exception as e: logger.error("[LAYER=MODEL] error=%s", e) raise return response这样做的目的是:当粗错误码出现时,你能通过日志里的 LAYER 标签,快速定位到是哪一层先出的问题。没有这个标签,你只能看到“Agent 失败了”,然后从头开始猜。
2.2 错误码到失败链的映射表
基于我自己的项目经验,整理了一张映射表。这张表不是绝对的,但能覆盖大部分常见情况。
| 粗错误码 | 优先排查层 | 次要排查层 | 常见根因 |
|---|---|---|---|
agent execution terminated due to error | TOOL 层 | PLAN 层 | 工具参数错误、工具内部异常、工具超时 |
model request failed | MODEL 层 | 网络层 | 鉴权、限流、请求格式、网络不通 |
422 | TOOL 层 / MODEL 层 | 输入层 | 参数类型错误、必填缺失、JSON 格式错误 |
timeout | TOOL 层 | MODEL 层 | 外部 API 慢、模型响应慢、死锁 |
connection refused | 外部服务层 | 网络层 | 服务未启动、端口错误、防火墙 |
out of memory | 运行时层 | TOOL 层 | 容器内存限制、大对象、并发过高 |
permission denied | 鉴权层 | TOOL 层 | API Key 错误、权限不足、角色配置错误 |
这张表的使用方法是:先看错误码,确定优先排查层,然后去日志里找对应 LAYER 标签的最后一条记录。最后一条记录的位置,就是断点所在。
2.3 一个真实案例:422 错误背后的三层问题
我之前遇到过一个 422 错误,表面上看是模型请求失败,但实际排查下来,根因在工具层。
现象:Agent 执行到某一步时,报422 Unprocessable Entity,模型服务商返回的错误信息是“请求体格式错误”。
第一层排查(MODEL 层):检查发给模型的请求体,发现 JSON 格式没问题,字段也齐全。但注意到一个细节:请求体里有一个字段tool_result,它的值是一个很长的字符串,里面包含了换行符和特殊字符。
第二层排查(TOOL 层):去看工具返回的结果,发现工具返回的是一个未经过滤的原始文本,里面包含了\n、\t、引号等字符。这些字符在拼接进 JSON 时,没有被正确转义,导致模型服务端解析失败。
第三层排查(PLAN 层):为什么工具会返回这种未过滤的文本?因为规划层在调用工具时,没有指定输出格式,工具就按默认方式返回了原始数据。
根因:规划层缺少输出格式约束 → 工具层返回未转义文本 → 模型层 JSON 解析失败 → 422。
修复:在规划层给工具调用加上output_format="json"约束,工具层统一做转义处理,模型层增加请求体校验。
这个案例说明:422 不一定真的是模型的问题,它可能是上游工具层和规划层的问题,只是最终在模型层暴露出来。
3. 逐层排查实操:从 terminalBlocker 到根因定位
这一章我按失败链的顺序,逐层讲怎么排查。每一层我都会给出具体的操作步骤、命令、代码片段和注意事项。
3.1 输入层:最容易被忽略的起点
输入层的问题往往最隐蔽,因为用户输入看起来“很正常”,但可能包含一些边界情况。
排查要点:
- 输入长度是否超过限制
- 是否包含特殊字符(换行、引号、反斜杠、Unicode 控制字符)
- 是否包含注入类内容(比如让 Agent 忽略之前指令的文本)
- 编码是否统一(UTF-8 vs GBK)
实操命令:
# 检查输入文件的编码 file -i input.txt # 查看输入中的特殊字符 cat -A input.txt | head -50 # 统计输入长度 wc -c input.txt注意事项:
我遇到过好几次因为输入里有一个不可见的 Unicode 字符(比如零宽空格),导致工具调用参数解析失败。这种问题用肉眼根本看不出来,必须用
cat -A或者十六进制查看器才能发现。
3.2 规划层:Agent 的“大脑”怎么排查
规划层负责把用户输入拆解成可执行的步骤。这一层出问题,通常表现为:步骤缺失、步骤顺序错误、工具选择错误、参数生成错误。
排查方法:
- 把规划层的输出完整打印出来,包括每一步的工具名、参数、预期输出。
- 检查每一步的参数是否满足工具的输入要求。
- 检查步骤之间是否有依赖关系被遗漏。
# 打印规划结果 plan = planner.plan(user_input) for i, step in enumerate(plan.steps): print(f"Step {i}: tool={step.tool}, args={step.args}, expected={step.expected}")常见问题:
| 问题 | 表现 | 修复 |
|---|---|---|
| 步骤缺失 | 最终结果不完整 | 优化规划提示词,增加步骤校验 |
| 工具选择错误 | 调用了不存在的工具 | 限制工具列表,增加工具描述 |
| 参数类型错误 | 工具调用报 422 | 在规划层做参数类型校验 |
| 循环依赖 | Agent 卡死 | 增加最大步骤数限制 |
3.3 工具层:terminalBlocker 和外部服务的排查
工具层是失败链上最容易出问题的一层,因为它涉及外部服务、网络、鉴权、数据格式等多个变量。
terminalBlocker 是什么:在我的项目里,terminalBlocker 指的是工具调用过程中,导致整个 Agent 执行被阻塞或终止的异常状态。它可能是一个超时、一个连接拒绝、一个未捕获的异常,或者一个死循环。
排查步骤:
- 确认工具是否被正确注册:检查工具注册表里有没有这个工具,名称是否匹配。
- 确认工具参数是否正确:把参数打印出来,和工具文档对比。
- 确认外部服务是否可达:用 curl 或 telnet 测试。
- 确认鉴权是否有效:检查 API Key、Token 是否过期。
- 确认超时设置是否合理:工具调用超时、外部服务超时、模型超时。
# 测试外部服务可达性 curl -v -X POST https://api.example.com/endpoint \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"key": "value"}' \ --max-time 10 # 检查端口是否开放 telnet api.example.com 443 # 查看 DNS 解析 nslookup api.example.com注意事项:
工具层的超时设置一定要比外部服务的超时设置长。比如外部 API 超时是 10 秒,工具层超时至少设 15 秒。否则外部服务还没返回,工具层就先超时了,你会误以为是外部服务的问题。
3.4 模型层:请求失败的真实原因
模型层的错误码通常比较明确,但“明确”不等于“好排查”。因为模型层的错误可能是上游传过来的,也可能是模型服务本身的问题。
排查清单:
- 请求体是否符合模型 API 的格式要求
- 请求体大小是否超过限制
- 是否触发了限流(429)
- 鉴权是否有效(401/403)
- 模型名称是否正确
- 是否支持请求中的参数(比如某些模型不支持
tools参数)
# 模型请求排查示例 import requests payload = { "model": "gpt-4", "messages": [{"role": "user", "content": prompt}], "temperature": 0.7, "max_tokens": 1000 } try: resp = requests.post( "https://api.example.com/v1/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json=payload, timeout=30 ) print("Status:", resp.status_code) print("Body:", resp.text) except requests.exceptions.Timeout: print("请求超时") except requests.exceptions.ConnectionError: print("连接失败")常见问题速查:
| 错误码 | 含义 | 排查方向 |
|---|---|---|
| 401 | 未授权 | API Key 是否正确、是否过期 |
| 403 | 禁止访问 | 权限是否足够、IP 是否在白名单 |
| 429 | 限流 | 降低请求频率、申请提额 |
| 500 | 服务端错误 | 模型服务商问题,重试或联系支持 |
| 502/503 | 网关错误 | 服务暂时不可用,重试 |
| 504 | 网关超时 | 请求处理时间过长,优化请求 |
3.5 结果解析层:模型返回了,但解析失败
这一层经常被忽略。模型返回了 200,你以为成功了,结果解析的时候报错。
常见问题:
- 模型返回的不是合法 JSON
- 模型返回的 JSON 结构和你预期的不一样
- 模型返回了多余的解释文字,导致 JSON 解析失败
- 模型返回了空内容
排查方法:
import json response_text = model.call(prompt) print("Raw response:", repr(response_text)) try: data = json.loads(response_text) except json.JSONDecodeError as e: print("JSON 解析失败:", e) # 尝试提取 JSON 部分 import re match = re.search(r'\{.*\}', response_text, re.DOTALL) if match: data = json.loads(match.group()) else: raise注意事项:
不要假设模型一定会返回合法 JSON。即使你在提示词里明确要求了,模型也可能返回带 markdown 代码块标记的 JSON,或者前后加了解释文字。解析层一定要做容错处理。
4. 常见问题与排查技巧实录
这一章我把实际项目中遇到的高频问题整理成速查表,并补充一些独家避坑技巧。
4.1 高频问题速查表
| 现象 | 可能根因 | 排查动作 | 修复方案 |
|---|---|---|---|
| Agent 突然终止 | 工具层未捕获异常 | 查看 TOOL 层最后一条日志 | 增加 try-catch,返回结构化错误 |
| 模型请求 422 | 请求体格式错误 | 打印请求体,检查特殊字符 | 转义特殊字符,校验 JSON |
| 工具调用超时 | 外部服务慢 | curl 测试外部服务 | 增加超时,增加重试 |
| 内存持续增长 | 大对象未释放 | 查看容器内存监控 | 限制并发,及时释放对象 |
| Agent 卡死 | 循环依赖或死锁 | 查看步骤日志 | 增加最大步骤数,增加超时 |
| 结果不完整 | 规划层步骤缺失 | 打印规划结果 | 优化提示词,增加校验 |
| 鉴权失败 | Token 过期 | 检查 Token 有效期 | 自动刷新 Token |
| 连接被拒 | 服务未启动 | telnet 测试端口 | 启动服务,检查端口 |
4.2 独家避坑技巧
技巧一:给每个工具调用加唯一 ID
这样在日志里可以追踪一次工具调用的完整生命周期:开始、参数、结果、耗时、异常。
import uuid call_id = str(uuid.uuid4())[:8] logger.info("[TOOL_CALL=%s] start tool=%s args=%s", call_id, tool_name, args) try: result = tool.call(args) logger.info("[TOOL_CALL=%s] success result=%s", call_id, result) except Exception as e: logger.error("[TOOL_CALL=%s] failed error=%s", call_id, e)技巧二:失败链日志要包含上下文
不要只记录错误信息,还要记录当时的输入、规划结果、工具参数、模型请求体。这样你才能复现问题。
技巧三:给 Agent 执行加“黑匣子”
把每次 Agent 执行的完整日志(输入、规划、工具调用、模型请求、模型响应、解析结果)写入一个单独的文件。出问题时,直接看这个文件,不用在大量日志里翻找。
import json from datetime import datetime def save_blackbox(execution_id, data): filename = f"blackbox/{execution_id}_{datetime.now().isoformat()}.json" with open(filename, "w") as f: json.dump(data, f, ensure_ascii=False, indent=2)技巧四:区分“可重试”和“不可重试”错误
不是所有错误都值得重试。网络超时、限流可以重试;参数错误、鉴权失败重试也没用。
RETRYABLE_ERRORS = ["timeout", "429", "502", "503", "504"] def should_retry(error_code): return any(code in str(error_code) for code in RETRYABLE_ERRORS)技巧五:用最小复现法定位问题
当失败链很长时,不要从头到尾查。把问题拆成最小可复现单元:单独调用工具、单独调用模型、单独解析结果。哪个单元失败,问题就在哪里。
4.3 一个完整的排查实录
最后分享一个我最近排查的案例,把上面的方法串起来。
现象:Agent 在执行到第 3 步时,报agent execution terminated due to error,日志里只有这一行。
第一步:看错误码。粗错误码,没有更多信息。需要看 LAYER 标签。
第二步:看日志。发现最后一条日志是[LAYER=TOOL] tool=search_api args={...},没有对应的 result 或 error。说明工具调用开始了,但没有返回。
第三步:查工具层。单独调用 search_api,发现它依赖一个外部服务。用 curl 测试外部服务,返回 504 网关超时。
第四步:查外部服务。联系外部服务方,确认是他们那边的一个数据库查询慢,导致接口超时。
第五步:修复。在工具层增加超时设置(15 秒),增加重试机制(最多 2 次),增加降级方案(超时后返回缓存数据)。
结果:Agent 不再因为外部服务慢而整体失败,而是能在超时后走降级路径,继续执行。
这个案例的关键点是:粗错误码只是入口,真正的根因在失败链的最末端。如果没有 LAYER 标签和工具调用日志,我可能还在怀疑模型。
4.4 工具选型:排查辅助工具推荐
| 工具 | 用途 | 备注 |
|---|---|---|
curl -v | 测试 HTTP 接口 | 看请求头、响应头、状态码 |
telnet | 测试端口连通性 | 快速判断服务是否可达 |
jq | 格式化 JSON | 方便查看请求体和响应体 |
htop | 查看进程资源 | 排查内存、CPU 问题 |
docker stats | 查看容器资源 | 排查容器内存、CPU 限制 |
tcpdump | 抓包 | 排查网络层问题 |
strace | 跟踪系统调用 | 排查文件、网络、信号问题 |
提示:
tcpdump和strace需要一定权限,生产环境慎用。优先用日志和监控排查。
5. 把失败链变成排查习惯
排查 Agent 失败,最怕的就是“猜”。猜模型不行、猜网络不好、猜工具挂了。猜来猜去,时间浪费了,问题还在。
我现在的习惯是:每次 Agent 失败,先建失败链,再逐层排查。失败链不是画给别人看的,是画给自己看的。把链路写下来,把每一层的状态标出来,断点自然就出现了。
具体来说,我会做三件事:
第一,日志必须带层标签。没有层标签的日志,在排查时等于没有日志。
第二,每个工具调用必须有唯一 ID 和完整上下文。这样我才能追踪一次调用的完整生命周期。
第三,每次排查完,把根因和修复方案记下来。下次遇到类似错误码,直接查记录,不用重新排查。
这套方法我用了大半年,Agent 项目的平均排查时间从原来的几个小时降到了十几分钟。不是因为我变聪明了,而是因为我不再瞎猜了。
最后再分享一个小技巧:如果你的 Agent 框架支持,给每个执行阶段加一个“健康检查”钩子。比如工具调用前检查外部服务是否可达,模型请求前检查 Token 是否有效,结果解析前检查 JSON 是否合法。这样很多问题在发生之前就能被拦截,而不是等到 Agent 整体失败才发现。
这个内容后续还可以这样扩展:把失败链和监控系统对接,每个层的异常自动上报,形成告警。这样你甚至不用等用户反馈,系统自己就能告诉你哪里快断了。