ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent 报错排查:从粗错误码到失败链与 terminalBlocker 定位

Agent 报错排查:从粗错误码到失败链与 terminalBlocker 定位 1. 当 Agent 报错时我们到底在排查什么做 Agent 开发的人大概都经历过这种场景任务跑了一半突然挂掉日志里只留下一行冷冰冰的agent execution terminated due to error或者一个语焉不详的错误码。第一反应往往是——模型又抽风了于是换模型、调温度、加提示词折腾半天问题依旧。我见过太多团队把这类失败一股脑归因于模型能力不行结果真正的病灶藏在工具调用、状态管理或者编排逻辑里模型只是那个最后背锅的。这篇内容想聊的就是怎么把这种粗错误码拆成一条可追溯的失败链。核心思路很简单Agent 的失败从来不是单点事件而是一连串环节依次劣化后的最终表现。你要做的不是盯着最后那个报错而是顺着链条往回找看第一个偏离预期的节点出现在哪里。这套方法适合所有在做 Agent 开发、编排、部署和测试的同学不管你是刚上手的新人还是已经踩过几轮坑的老手都能从中找到可复用的排查路径。关键词里的terminalBlocker、失败链、错误码这几个词其实指向了同一个问题域Agent 执行过程中的可观测性和归因能力。很多人把可观测性理解成加日志但日志多不等于能定位问题。真正的难点在于Agent 的执行是异步的、多阶段的、带状态的一个错误码背后可能横跨了模型推理、工具调用、上下文拼接、沙盒执行、结果回传等好几个环节。你需要的是一套结构化的排查框架而不是靠运气翻日志。我先给一个结论性的判断绝大多数被归为模型失败的 Agent 问题根因都在模型之外。模型是概率系统它的输出波动是特性不是 bug。真正让任务崩掉的往往是那些确定性的工程环节——参数没对齐、状态没同步、超时没处理、错误没分类。把这些环节理顺你会发现 Agent 的稳定性会有质的提升。2. 粗错误码为什么几乎无法直接定位问题2.1 错误码的抽象层级决定了它的信息量先说说错误码这件事。一个错误码本质上是一种抽象抽象层级越高信息量越少。agent execution terminated due to error这种错误码抽象层级高到几乎等于没说——它只告诉你挂了没告诉你在哪挂的为什么挂的挂之前发生了什么。这就像你去医院看病医生只给你一个诊断结果你不舒服你没法据此治疗。你需要的是分层的诊断信息是哪个器官、什么指标异常、异常到什么程度、什么时候开始的。Agent 的错误码也一样它需要分层。我通常把 Agent 的错误分成三个层级来看层级典型错误码形态能定位到什么排查难度L1 粗粒度execution terminated / task failed只知道整体失败极高基本靠猜L2 环节级tool_call_timeout / llm_rate_limit / sandbox_init_failed能定位到具体环节中等需要环节日志L3 根因级connection_refused:port_8080 / json_decode_error:pos_142能定位到具体原因低可直接修复问题在于很多 Agent 框架默认只暴露 L1 错误码。为什么因为框架设计者想让错误处理简单把所有异常都收敛成一个统一的失败信号。这个设计在 demo 阶段很友好在生产环境就是灾难。你拿到 L1 错误码等于拿到一个黑盒只能靠复现和猜测。2.2 错误码收敛带来的信息丢失错误码收敛的过程本质上是信息丢失的过程。一个完整的失败链可能是这样的模型返回了一个工具调用请求参数里有个字段类型不对工具执行器尝试解析参数抛出一个类型错误执行器的异常被上层捕获包装成一个通用的tool_execution_failed编排层收到这个错误决定重试重试三次都失败编排层放弃抛出agent execution terminated最终日志里你只看到第 6 步的错误码从第 1 步到第 6 步信息一层层被吞掉。等你看到日志时原始的类型错误早就消失了。这就是为什么我说粗错误码几乎无法直接定位问题——它不是不能定位而是它把定位所需的信息在传递过程中丢光了。2.3 一个真实的排查困境我遇到过这样一个案例一个 Agent 任务在特定输入下必然失败错误码永远是agent execution terminated due to error。团队换了三个模型调了无数提示词问题依旧。后来我们把日志级别调到最细才发现真正的失败点是沙盒初始化时的一个端口占用——前一个任务没清理干净后一个任务启动时端口冲突沙盒起不来Agent 自然跑不动。这个案例里模型从头到尾都是无辜的。但因为错误码收敛所有矛头都指向了模型。如果一开始就有 L2 甚至 L3 的错误码这个问题可能十分钟就解决了。提示如果你正在设计 Agent 框架千万不要把所有异常都收敛成一个错误码。宁可错误码多一点、杂一点也要保留环节信息和根因信息。错误码的简洁是给使用者看的不是给排查者看的。3. 失败链的构建从单点错误到因果序列3.1 什么是失败链失败链这个概念是我从分布式系统的链路追踪里借过来的。在微服务架构里一个请求会经过多个服务每个服务都会产生 span这些 span 串起来就是一条 trace。当请求失败时你能沿着 trace 看到是哪个服务、哪个环节出的问题。Agent 的失败链是同样的道理。一次 Agent 执行会经过多个阶段任务解析、规划、工具选择、工具调用、结果处理、状态更新、下一轮推理……每个阶段都可能失败。失败链就是把这些阶段串起来标出第一个偏离预期的节点以及后续的连锁反应。构建失败链的关键是给每个阶段打上可关联的标识。最基础的做法是给每次 Agent 执行分配一个trace_id给每个阶段分配一个span_id所有日志都带上这两个 ID。这样你就能把散落在不同文件、不同时间的日志重新拼成一条完整的执行路径。3.2 失败链的四个关键节点一条完整的失败链通常包含四个关键节点触发点第一个出现异常的环节。这是你要重点关注的因为它是根因所在。传播路径异常从触发点向上传播的路径。理解传播路径能帮你判断哪些错误是衍生错误可以忽略。收敛点异常被捕获并包装成粗错误码的地方。这是信息丢失最严重的地方也是你要重点改造的地方。终止点Agent 最终放弃并报错的地方。这是你看到错误码的地方但通常不是问题所在。排查的时候你的目光要越过终止点直接去找触发点。终止点只是结果触发点才是原因。3.3 用 trace_id 把失败链串起来具体怎么落地我给一个最小可行的方案。首先在 Agent 执行的入口处生成一个trace_id可以用 UUID也可以用时间戳加随机数。然后把这个trace_id注入到所有下游调用里——模型调用、工具调用、沙盒执行、状态存储一个都不能漏。import uuid import logging class AgentTracer: def __init__(self): self.trace_id str(uuid.uuid4()) self.spans [] def start_span(self, name, metadataNone): span { span_id: str(uuid.uuid4()), trace_id: self.trace_id, name: name, start_time: time.time(), metadata: metadata or {} } self.spans.append(span) return span def end_span(self, span, status, errorNone): span[end_time] time.time() span[status] status span[error] error logging.info(f[{self.trace_id}] span{span[name]} status{status} error{error})这段代码很粗糙但核心思想到位了每个阶段都有独立的 span所有 span 共享同一个 trace_id失败时能记录错误信息。有了这个基础你就能在日志里按 trace_id 过滤把一次执行的完整路径捞出来。3.4 失败链的可视化光有日志还不够你需要能直观地看到失败链。最简单的做法是把 span 按时间顺序排好标出每个 span 的状态失败的标红。这样一眼就能看出第一个失败的 span 在哪里。我通常会用这样的表格来呈现失败链序号阶段状态耗时错误信息1task_parsesuccess12ms-2planningsuccess340ms-3tool_selectsuccess89ms-4tool_callfailed5001mstimeout after 5000ms5retry_1failed5002mstimeout after 5000ms6retry_2failed5003mstimeout after 5000ms7terminatefailed1msagent execution terminated看到这张表你立刻就知道问题出在 tool_call 的超时上而不是模型。这就是失败链的价值——它把模型失败这个模糊判断变成了工具调用超时这个具体问题。4. terminalBlocker那些让 Agent 彻底卡死的隐形杀手4.1 terminalBlocker 的定义和特征terminalBlocker是我自己造的一个词指的是那些会让 Agent 执行彻底终止、且无法通过重试恢复的阻塞性问题。它和普通的可恢复错误不一样——普通错误重试一下可能就过了terminalBlocker 是你重试一百次也没用必须人工介入。terminalBlocker 有几个典型特征确定性同样的输入必然触发不是概率问题。不可恢复性重试、换模型、调参数都无效。隐蔽性错误码通常很粗看不出真正原因。连锁性一旦触发后续所有阶段都会失败。常见的 terminalBlocker 包括沙盒环境初始化失败、关键依赖服务不可用、状态存储损坏、权限配置错误、资源耗尽磁盘满、内存溢出等。这些东西和模型能力毫无关系但它们造成的失败往往被算在模型头上。4.2 沙盒初始化失败最常见的 terminalBlocker沙盒初始化失败是我见过最多的 terminalBlocker。Agent 执行代码、调用工具通常需要一个隔离的沙盒环境。这个环境要创建、要配置、要挂载资源任何一个环节出问题沙盒就起不来Agent 直接卡死。沙盒初始化失败的常见原因端口冲突前一个沙盒没清理干净端口被占用资源不足磁盘空间不够、内存不够、文件描述符耗尽权限问题沙盒进程没有权限访问挂载的目录镜像问题基础镜像损坏或版本不匹配网络问题沙盒需要拉取依赖但网络不通这些问题里端口冲突和资源不足是最隐蔽的因为它们的错误码往往很粗比如sandbox_init_failed你根本不知道是端口问题还是内存问题。4.3 排查 terminalBlocker 的实操路径排查 terminalBlocker我的经验是先隔离再复现后定位。第一步隔离。把 Agent 执行环境和其它任务隔离开确保没有资源竞争。如果是容器化部署给每个 Agent 任务分配独立的容器和端口段。第二步复现。用最小输入复现问题。如果最小输入也能复现说明是环境问题如果只有特定输入能复现说明是逻辑问题。第三步定位。打开最细粒度的日志看沙盒初始化的每一步。通常我会在沙盒初始化的关键节点加日志# 沙盒初始化脚本的日志埋点 echo [sandbox] checking port availability # 检查端口 echo [sandbox] checking disk space df -h /sandbox echo [sandbox] checking memory free -m echo [sandbox] mounting volumes mount | grep sandbox echo [sandbox] starting sandbox process这些日志看起来笨但关键时刻能救命。端口冲突、磁盘满、内存不足这些问题在日志里一目了然。4.4 预防 terminalBlocker 的工程实践排查是被动的预防才是主动的。我在项目里会做几件事来预防 terminalBlocker资源预检Agent 执行前先检查端口、磁盘、内存是否满足要求不满足就直接报明确的错误而不是等到沙盒初始化时才失败。资源清理每个任务结束后强制清理沙盒、释放端口、删除临时文件。用finally块保证清理一定执行。健康检查关键依赖服务加健康检查不健康时快速失败避免 Agent 卡在等待上。错误分类把 terminalBlocker 和可恢复错误分开处理。terminalBlocker 直接终止并告警可恢复错误才走重试逻辑。注意不要把 terminalBlocker 放进重试队列。重试一个注定失败的操作只会浪费资源、拉长故障时间。识别出 terminalBlocker 后应该立即终止并上报让人工介入。5. 从错误码到失败链的改造实操5.1 错误码分层设计改造的第一步是重新设计错误码。我的建议是三层结构L1 领域码标识失败发生在哪个领域如LLM、TOOL、SANDBOX、STATE、ORCHESTRATE。L2 环节码标识失败发生在该领域的哪个环节如TOOL.CALL、TOOL.PARSE、TOOL.RESULT。L3 根因码标识具体的失败原因如TOOL.CALL.TIMEOUT、TOOL.PARSE.TYPE_ERROR。组合起来就是TOOL.CALL.TIMEOUT这样的错误码。看到这个码你立刻知道是工具调用超时不用再猜。class AgentError(Exception): def __init__(self, domain, stage, reason, detailNone): self.code f{domain}.{stage}.{reason} self.detail detail or {} super().__init__(self.code) def to_dict(self): return { code: self.code, detail: self.detail, trace_id: self.detail.get(trace_id) } # 使用示例 raise AgentError( domainTOOL, stageCALL, reasonTIMEOUT, detail{tool_name: web_search, timeout_ms: 5000, trace_id: tracer.trace_id} )5.2 异常传播时的信息保留错误码分层之后还要保证异常在传播过程中不丢信息。很多框架在捕获异常后会重新抛出一个新异常把原始异常吞掉。这是信息丢失的重灾区。正确的做法是保留原始异常用raise ... from ...链式抛出try: result tool.execute(params) except TimeoutError as e: raise AgentError( domainTOOL, stageCALL, reasonTIMEOUT, detail{original_error: str(e), trace_id: tracer.trace_id} ) from e这样原始异常会作为__cause__保留下来排查时能看到完整的异常链。5.3 失败链的自动重建有了分层错误码和 trace_id失败链的自动重建就水到渠成了。你可以写一个简单的分析脚本按 trace_id 把所有 span 捞出来按时间排序标出第一个失败的 spandef rebuild_failure_chain(trace_id, log_store): spans log_store.query(trace_idtrace_id) spans.sort(keylambda s: s[start_time]) chain [] first_failure None for span in spans: chain.append({ name: span[name], status: span[status], error: span.get(error) }) if span[status] failed and first_failure is None: first_failure span return { trace_id: trace_id, chain: chain, root_cause: first_failure }这个脚本不复杂但能帮你把排查时间从小时级降到分钟级。5.4 改造前后的对比我拿一个真实项目做过对比。改造前平均故障定位时间是 45 分钟因为错误码太粗只能靠复现和猜测。改造后平均定位时间降到 8 分钟因为失败链直接告诉你根因在哪。指标改造前改造后平均定位时间45 分钟8 分钟误判为模型问题的比例60%15%需要人工复现的比例80%25%重试浪费的资源高低这个对比说明错误码和失败链的改造投入产出比非常高。前期花几天时间做后期能省下大量的排查时间。6. 几个容易踩的坑和我的应对经验6.1 日志太多反而找不到重点改造初期我犯过一个错把所有能打的日志都打上结果日志量爆炸排查时反而找不到重点。后来我学乖了日志要分级ERROR只打 terminalBlocker 和需要人工介入的错误WARN打可恢复错误和重试INFO打关键阶段的状态变更DEBUG打详细的参数和中间结果默认关闭这样默认情况下日志量可控需要深挖时再开 DEBUG。6.2 重试逻辑掩盖了真正的错误重试是个好东西但滥用重试会掩盖真正的错误。我见过一个项目所有错误都重试三次结果 terminalBlocker 也被重试白白浪费了三倍时间。后来我们加了错误分类只有可恢复错误才重试terminalBlocker 直接失败。判断一个错误是否可恢复我的经验是看三点错误是否确定性的、重试是否可能改变结果、重试成本是否可接受。三个都满足才重试。6.3 状态不一致导致的诡异失败Agent 是有状态的状态不一致会导致非常诡异的失败。比如上一轮的工具调用结果没正确写入状态下一轮推理时读到了旧状态导致决策错误。这种失败往往没有明显的错误码只是结果不对。应对方法是给状态加版本号和校验和每次读写都校验。状态不一致时立即报错而不是带着错误状态继续跑。6.4 超时设置的两难超时设置是个两难设太短正常操作被误杀设太长terminalBlocker 卡住太久。我的做法是分级超时模型调用一个超时工具调用一个超时整体执行一个超时。而且超时时间要根据历史数据动态调整不能拍脑袋定。6.5 跨进程排查的难点Agent 经常跨进程、跨容器执行这给排查带来很大困难。日志分散在不同地方trace_id 可能没正确传递。我的经验是trace_id 必须在所有跨进程调用的入口和出口都打日志确保能串起来。如果用了消息队列trace_id 要放在消息头里传递。7. 把排查能力沉淀成团队资产排查能力不应该只存在于个别人的经验里而应该沉淀成团队的资产。我做了几件事第一把失败链的构建脚本做成内部工具任何人输入 trace_id 就能看到完整失败链。第二把常见 terminalBlocker 和对应的排查步骤写成 runbook新人照着做就能定位。第三把错误码规范写成文档新代码必须遵守。第四定期做故障复盘把新的失败模式补充进 runbook。这样做的效果是团队的排查能力不再依赖某个大神而是变成了可复制、可传承的流程。新人上手快老人负担轻。我个人在实际操作中的体会是Agent 的稳定性问题八成以上是工程问题不是模型问题。把错误码分层、把失败链串起来、把 terminalBlocker 识别出来这三件事做完你会发现 Agent 的可靠性有质的飞跃。模型该换还是换但别再让它背不属于它的锅了。最后分享一个小技巧每次遇到新的失败模式别急着修先把它记录下来补充到你的失败链分析工具里。日积月累你的排查工具会越来越强排查时间会越来越短。这个过程本身就是团队能力建设的一部分。
返回列表