资讯动态

Agent失败不全是模型的锅:一条TLS握手引发的失败链追踪

发布时间:2026/10/3 10:14:33 来源:尧图企业网站定制
开头先交代一下背景。前一阵我负责的一个多 Agent 协作服务频繁出问题业务方拿着一张截图来找我上面就一行错误码AGENT_EXECUTION_TERMINATED。没有堆栈没有节点信息没有上下文快照连是哪个子 Agent 挂的都不知道。我当时的第一反应就是“模型又抽风了换个更强的模型试试。”结果换完模型照样失败调低 temperature 也失败把 prompt 重写三遍还是失败。这让我非常挫败但也逼着我换了思路不再盯着模型猜而是把这次失败的完整链路挖出来。后来我才发现真正的根因根本不在模型而在一条被我忽略的“失败链”上——从外部 API 的 TLS 握手失败到工具层返回空列表再到编排器吞掉异常信息最后才表现为那个粗错误码。这篇文章就是这次排查的完整复盘我把每一步的思路、操作、踩坑都写出来希望能帮你少走几个弯路。文章适合所有在做 Agent 开发、尤其是被各种诡异失败折磨过的朋友。1. 事故现场一行粗错误码把我带进了死胡同1.1 症状与第一反应换模型、调参、重试先说事发时的具体场景。服务里有一个主 Agent负责接收用户请求拆解任务后派发给两个子 Agent一个负责调用外部搜索工具获取资料另一个负责把资料整理成结构化答案。整个链路跑在自研的 Agent 编排框架上每个子 Agent 内部是“大模型 工具调用”的循环。那天线上开始陆续出现失败请求日志里只有主 Agent 抛出的AGENT_EXECUTION_TERMINATED后面啥也没有。我的第一反应和大多数人一样先重试重试不行就换模型从原来的 7B 级模型换到更大参数的模型还把 temperature 从 0.3 调到 0.1又给子 Agent 的 system prompt 里塞了好几条 few-shot 示例。结果完全没有用。同一个请求无论怎么调都在同一个位置失败。这时候我才意识到问题可能压根不在模型身上。1.2 为什么换模型救不了一个脏链路我后来想明白了一件事模型只是 Agent 链路里的一个处理器它吃进去的是前序节点给的输入输出的结果又会被后续节点处理。如果上游给的输入本身就是脏的——“搜索成功但没有内容”的空列表、被截断的历史记录、错误的工具返回格式——那模型再强也只能基于垃圾输入生成垃圾输出甚至直接拒绝生成。打个比方餐厅上菜慢你可能先怪厨师炒得慢但真正的原因可能是采购没买到菜、传菜员把单子送错了、或者后厨的炉子坏了。厨师只是链路里最后一个动手的人他看着最可疑但未必是根因。Agent 系统的复杂性就在这里一次最终失败往往是多个节点的问题叠加出来的。模型输出的异常只是最外层的表现真正的问题可能藏在工具调用、上下文组装、外部服务依赖甚至编排器的异常处理逻辑里。只盯着模型调参等于只骂厨师根本不解决问题。1.3 破局从“猜错误”转向“收集链路证据”意识到这一点之后我停止了所有“盲试”开始老老实实收集证据。我给整个链路补了全量日志把每个关键节点的输入输出、耗时、异常状态全部记录下来。我梳理了一下需要采集的信息至少包括这些主 Agent 收到请求的时间、请求 ID、任务类型。任务拆解后派发了哪些子任务每个子任务去了哪个子 Agent。每个子 Agent 的模型调用记录输入 prompt 的摘要、输出内容、finish_reason、token 消耗。工具调用记录工具名、入参、原始返回结构、异常类型、耗时。上下文组装记录最终喂给模型的 prompt 长度、内容摘要、是否有被截断或丢失的字段。编排器的聚合逻辑子 Agent 返回后主 Agent 如何合并结果、何时触发异常分支。这些日志加上去之后链路开始“显形”。我后来发现之前之所以难排查不是因为问题有多深而是因为整个链路几乎是一个黑盒——你只看得到最外层的结果根本不知道里面发生了什么。把可观测性补上之后问题很快就浮出来了。2. 粗错误码为什么会误导人失败链的底层逻辑2.1 粗错误码到底丢了多少信息先说一个扎心的事实很多 Agent 框架默认抛出的错误码信息量低到可怜。就拿我遇到的AGENT_EXECUTION_TERMINATED来说它只是一个布尔级别的信号——“挂了”。至于为什么挂、挂在哪一环、是外部依赖挂了还是模型拒答了、是超时还是上下文溢出了全都不知道。我整理了一张对比表可以直观看到粗错误码和细错误码的信息量差异信息维度粗错误码常见默认细错误码推荐状态只有成功/失败成功、失败、部分成功、重试中失败阶段无意图解析、规划、工具调用、模型生成、聚合失败节点无具体是哪个子 Agent、哪个工具根因类型无超时、空结果、格式错误、外部依赖不可用上下文关联无关联 request_id、trace_id、任务快照重试建议无是否可重试、建议的退避策略粗错误码之所以这么“粗”往往不是技术上做不到而是编排器在向上传递异常时做了简化处理——子 Agent 返回的详细错误被吞掉外层只保留一个失败状态。这就像一个传话游戏传到最后一环只剩一句“出事了”前面所有关键信息全丢了。2.2 什么是失败链一个 Agent 请求要经过的所有节点我所说的“失败链”是指一次 Agent 任务从用户请求到最终输出的完整链路上所有可能引发失败的环节。我根据自己的实战经验把一条典型的 Agent 执行链路拆成了九个节点请求接收网络层、鉴权、参数校验。意图解析理解用户到底要什么。任务规划把目标拆解成可执行的子任务。意图路由决定每个子任务派发给哪个子 Agent 或工具。工具选择与参数构造决定调用哪个外部 API、参数怎么填。外部调用网络请求、依赖服务、第三方 API。结果解析与上下文组装把工具返回的内容整理成模型能理解的上下文。模型生成LLM 基于上下文生成最终回答。输出校验与聚合检查输出是否符合预期把多个结果合并返回。任何一个节点出问题都可能让整个任务失败而且上游节点的问题会像多米诺骨牌一样向下传导。比如第 6 步外部调用超时第 7 步拿到空结果第 8 步模型产生幻觉或拒答第 9 步校验不通过最终对外抛出一个笼统的错误码。你看到的失败在最后一步真正的根因却可能在第一步或更早的系统环境里。2.3 模型为什么总是那个“背锅侠”既然失败链这么长为什么大家包括我第一时间都去怪模型我总结了三个原因。第一模型是链路上最“像决策者”的节点。其他节点比如工具调用、网络请求看起来都是机械性的而模型输出的内容语义丰富一旦它答得不对你天然觉得是它的问题。第二模型确实有随机性。同一个 prompt同一组参数上次成功这次失败这在大模型时代太常见了。这种不确定性让我们很习惯性地把“不稳定”和“模型有问题”画等号。第三也是最关键的系统设计上往往把模型放在链路的末端。前面所有节点的处理结果都被包装成模型的输入掩盖了中间发生的异常。比如工具返回空列表后如果不做显式标注模型根本不知道“搜索其实失败了”它只会认为“搜索成功了但世界上没有相关内容”。这种情况下模型给出的回答再奇怪也是情有可原的。所以把 Agent 失败都算给模型本质上是用“模型不可解释”来掩盖“链路不可观测”。想让结论可靠必须先让链路完整暴露在视野里。3. 实操复盘把一个粗错误码追成一条完整失败链3.1 第一步补可观测性让链路显形回到我实际的排查过程。我把全链路日志的框架搭起来之后第一步是在每个关键节点打点。当时我在代码里加了一个简单的埋点函数结构大概是下面这个样子import json import time import hashlib def trace_node(node_name, fn, request_id, **ctx): start_ts time.time() status ok error None try: result fn(**ctx) return result except Exception as e: status error error f{type(e).__name__}: {str(e)[:200]} raise finally: duration_ms round((time.time() - start_ts) * 1000, 2) io_hash hashlib.md5( f{node_name}:{status}:{duration_ms}:{error}.encode() ).hexdigest()[:8] log_entry { event: trace_node, request_id: request_id, node: node_name, status: status, duration_ms: duration_ms, io_hash: io_hash, error: error, } logger.info(json.dumps(log_entry, ensure_asciiFalse))实际项目里可能要用 OpenTelemetry 这类成熟的链路追踪框架但排查问题的临时场景下这种轻量打点反而更快见效。关键在于每个节点都要有唯一标识request_id 贯穿始终、有耗时、有状态、有错误摘要。我加了十几个这样的打点覆盖了请求接收、意图解析、任务规划、子 Agent 派发、工具调用、上下文组装、模型调用、结果聚合这几个核心环节。日志打出来之后我第一次看到了这次失败请求的完整生命轨迹。3.2 第二步从末端往上游逐环回溯拿到全链路日志后我的排查策略是从末端往上游推。这个思路很简单结果失败在最后但根因一定在更早的某一步所以从最后一步开始一步一步往前看每一步都问“这一步的输入是否正常”。我当时的排查记录大概是这样的第一环看主 Agent 的最后输出。失败码是AGENT_EXECUTION_TERMINATED触发位置在主 Agent 的聚合阶段——它在等待某个子 Agent 返回时超时了。第二环追那个超时的子 Agent。日志显示这个子 Agent 的模型调用根本没能正常结束finish_reason不是stop而是被上层中断了。看起来像是子 Agent 内部进入了一个死循环式的重试。第三环进入子 Agent 内部看工具调用。果然子 Agent 调用了外部搜索工具工具返回了一个结构异常的结果——不是预期的数据列表而是一个空数组且没有附带任何错误标记。子 Agent 把这个空数组直接放进了上下文。第四环看外部搜索工具的实现。工具内部封装了一个 HTTPS 请求异常处理逻辑写的是“捕获所有异常返回空数组”。也就是说无论网络失败、超时还是服务端返回 5xx工具层都统一转成了“空列表”。到了这一步我已经有了大致的方向但真正的“钉子”还没找到——为什么外部请求会失败。于是我用系统指令去查网络层的情况。这里插一句跟热词里提到的ssl_get_error错误码返回 1 相关。我查了工具调用日志里的异常详情里面赫然写着SSL error code 1。SSL_get_error返回 1 通常意味着SSL_ERROR_SYSCALL也就是 TLS 握手期间底层 socket 出现了 I/O 错误。这种错误最常见的诱因之一就是对端服务器主动断开了连接或者网络中间设备重置了连接。我开始怀疑是部署环境的网络问题。用ss -tnp查了服务到外部 API 的连接状态又用dmesg -T看了内核日志最后还用tcpdump抓了一下访问外部搜索服务的包。结论清晰了服务所在主机的出口网络在特定时间段内极不稳定多个外部请求都出现了 TCP 连接重置。这就是整条失败链真正的起点。3.3 第三步根因定位与失败链完整还原到这里整条失败链终于完整浮出水面。我用文字把它还原出来外部搜索 API 在 TLS 握手阶段发生连接重置SSL_get_error返回 1。工具层捕获异常后没有区分错误类型直接返回空列表。子 Agent 拿到空列表误判为“搜索成功但无相关内容”。汇总 Agent 在后续规划中基于这个错误判断继续行动最终超时。主 Agent 等待超时后抛出粗错误码任务终止。外部表现AGENT_EXECUTION_TERMINATED。注意看这条链上真正的技术根因在第 1 步而错误被“改头换面”发生在第 2 步等到第 5 步抛出错误码时信息已经丢了九成。更要命的是整个过程中模型几乎没做错任何事——它只是基于被污染的信息做了一个“合理但错误”的下一步决策。这恰恰印证了标题那句话别再把 Agent 失败都算给模型。顺便说一句这次排查让我意识到很多“模型答得不对”的 case追到后面其实都是链路污染问题。模型成了替罪羊。想避免这种情况与其天天调 prompt不如先把链路的可观测性做扎实。3.4 第四步修复方案与回归验证根因找到之后修复就不难了。我做了四件事第一重写工具层的错误处理逻辑。所有外部依赖的调用结果统一包装成结构化返回明确区分“成功”“失败”“超时”“重试”四种状态。失败时不再返回空数组而是返回一个带错误码和错误信息的对象。第二在子 Agent 的提示词里增加工具失败处理策略。当工具返回okfalse时要求模型明确输出“工具调用失败正在尝试替代方案”而不是沉默地把失败当成成功。第三调整编排器的异常传播逻辑。子 Agent 的失败信息不再被吞掉而是作为结构化结果继续向上传递最终由主 Agent 决定是否重试、降级或直接向用户返回可解释的错误。第四给外部调用层加上熔断和指数退避重试机制。针对 TLS 握手失败这类瞬时网络故障自动重试三次间隔分别是 1 秒、2 秒、4 秒避免一次偶发错误直接带崩整个任务。修复上线后我拿之前的失败请求做了回归。同一组用例修复前错误率大约 13%修复后降到了 1.2% 左右。剩下的零星失败也多与外部服务的独立故障有关但不会再因为一次瞬时网络抖动就让整条任务链路终止。这个结果让我非常感慨真正的修复工作和模型完全无关。4. 以后怎么少踩坑Agent 失败链排查的通用套路4.1 先把可观测性基线建起来一次排查让我彻底明白Agent 系统里最贵的不是算力而是“问题的可解释性”。不管你现在用的是哪个 Agent 框架我都建议你先把可观测性基线建起来至少要覆盖这些事件每个 LLM 调用的输入摘要、输出摘要、finish_reason、token 消耗。每个工具调用的入参、返回结构、耗时、异常类型。上下文组装后的 prompt 总长度、是否有截断、是否包含失败标记。每次重试的原因、重试次数、最终状态。编排器的状态转换记录派发了什么、聚合了什么、哪里触发了异常分支。请求级别的 trace_id保证所有日志可以通过同一个 ID 串起来。有了这些数据排查 Agent 失败就从“盲猜”变成了“看链”效率完全不是一个量级。我个人的经验是如果第一天就把 trace 做好后面能省下十倍的时间。有一点要提醒可观测性日志本身也可能造假。我之前踩过一个坑就是往日志里打了“工具调用成功”但成功标志是默认值根本没有反映真实状态。所以埋点的时候尽量直接记录原始返回值而不要记录“格式化后的友好信息”——原始值才能还原现场。4.2 设计一套不甩锅的错误码除了可观测性错误码本身也值得重新设计。我后来整理了一版错误码规范核心是“错误码必须能回答三个问题”在哪一环失败的、为什么失败的、能不能重试。具体来说一个合格的错误码或错误对象应该包含这些字段stage失败发生的阶段比如PLANNING、TOOL_CALL、LLM_GENERATION、AGGREGATION。node失败发生的具体节点比如某个子 Agent 的名字、某个工具的名字。root_cause_type根因类型比如TIMEOUT、EMPTY_RESULT、INVALID_SCHEMA、UPSTREAM_UNAVAILABLE、NETWORK。request_id/trace_id关联整条链路。retryable是否建议重试。original_error原始错误信息但要截断避免日志被写爆。这看起来是个小改动但价值很大。它把“一个布尔级的失败”升级成了“一段可追溯的索引”。我这次排查的教训就是如果错误码从一开始就带上stageTOOL_CALL、nodesearch_tool、root_cause_typeNETWORK可能十分钟就定位完了根本不用折腾半天。4.3 实用的系统排查指令清单Agent 服务跑在 Linux 主机上时排查看似在应用层很多时候根子却在系统层。下面这几个指令是我排查过程中的高频工具先列出来备用。先看服务自身的状态。journalctl是我最常用的服务日志工具排查 Agent 失败时我会这样用journalctl -u agent-service --since 10 min ago再看端口和连接状态。如果怀疑外部依赖或者内部服务之间的网络问题用ss查看连接状态比netstat更直接ss -tnp | grep -E agent|ESTAB|SYN-SENT要看系统层面的网络异常dmesg会有内核记录排查 TCP 重置、丢包、链路层问题很有用dmesg -T | tail -50如果确认了方向怀疑是外部 API 的握手问题可以用tcpdump抓包来看具体握手流程。生产环境抓包要注意脱敏并且只抓访问目标 IP 的流量不要把业务流量全抓下来tcpdump -i eth0 host 目标IP and port 443 -c 100这一套下来从应用日志到系统日志再到网络报文基本能把问题范围从“Agent 失败”缩小到“哪个网络环节、什么协议层面的异常”。再往上报的时候手里全是证据不是猜测。4.4 常见问题速查表最后把我这次排查中遇到过的典型问题整理成一个速查表方便大家按图索骥现象可能根因排查方向修复建议模型返回空内容上下文溢出、工具返回空、输出校验拦截检查 prompt 长度、工具返回结构、校验层逻辑拆分上下文、工具失败显式标记、放开合法输出工具调用超时外部 API 慢、网络抖动、DNS 解析慢看链路耗时分布、ss看连接、dig测 DNS增加超时熔断、指数退避重试、超时阈值调优外部 API 返回 422请求体 schema 不匹配、上游接口变更对比本地 schema 和线上 OpenAPI 文档用代码生成请求模型、定期跑契约测试子 Agent 执行被终止编排器超时、子 Agent 死循环、异常被吞查编排器超时配置、子 Agent 的循环次数日志设置最大重试次数和单任务总时长上限服务启动后 IP 冲突多实例部署时端口或 IP 配置重复ip addr查本机地址、ss -lntp查监听端口统一用容器网络分配、配置管理工具校验路由工具返回空列表但无报错异常被统一吞掉、错误分支只返回默认值检查工具层异常捕获逻辑、打印原始异常结构化返回、区分成功和失败分支这张表不可能覆盖所有场景但它给了你一个排查框架现象和根因之间永远隔着几个中间节点。不要停在现象表面往前多问几步答案就在链路上。我个人的体会是Agent 排查看似是个技术活本质上是个“还原现场”的活。你越是能完整还原一条请求从进入到退出的全过程越不容易被表象带偏。我也曾经把大量时间花在“咒骂模型不稳定”上后来发现那段时间全白费了。现在我再遇到 Agent 失败第一件事永远是打开链路追踪像读故事一样把每个节点的状态看一遍。第二件事是把错误码拆开看它的 stage、node、root_cause_type。如果错误码本身信息量不够我会去补埋点、补日志而不是急着重试或换模型。最后分享一个小习惯我会在 Agent 外层包一个“失败指纹”模块把每个节点的状态、错误类型、耗时拼成一个短字符串比如TOOL_CALL|search_tool|NETWORK|3002ms然后打印在日志首行。出现问题时直接看指纹十次里有八次一眼就能定位方向剩下的再进 tracing 系统慢慢查。这个习惯帮我省下了大量时间希望你也能用上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑