
做 Agent Platform 的人估计都体会过这种场景线上一切平稳突然某个下午告警群开始刷屏用户说任务提交了半天没反应你打开监控面板发现 P99 已经从平时的 300ms 直接飙到了 12 秒。我这次踩的就是典型的一例。表面上看是一个工具接口变慢了实际上牵出了平台设计上的三个深层问题缺少端到端超时预算、线程池没有隔离、Agent 重试机制在放大故障。这篇文章把整个排查过程、根因分析和修复方案完整复盘一遍希望能给同样在做 Agent 编排平台、智能体框架或者复杂异步任务系统的朋友一些参考。文章不会太长篇大论讲原理重点放在我当时看到了什么、怎么定位、怎么改的上。1. 架构布局超时故障为什么会发生在这里1.1 平台的基本组成先交代一下我们这套 Agent Platform 的现状。它不是单体的机器人服务而是一个多层编排系统专门负责把用户的自然语言请求拆解成可执行的子任务再交给不同的 Agent 去完成。核心分这么几层层级职责关键组件网关层对外 API 入口、鉴权、限流、任务 ID 生成API Gateway编排层任务拆解、子任务状态流转、依赖关系管理Orchestrator执行层运行 Agent 主循环调用 LLM 和各类工具Runner / Worker工具层封装内部服务调用、HTTP 接口、数据库操作Tool Registry记忆层会话记录、向量检索、KV 状态存储Memory Store执行层是整个平台最忙的地方。一个 Agent 不是简单做一次请求-响应它会进入一个循环推理 → 决定调哪个工具 → 调用工具 → 观察结果 → 继续推理直到任务完成或达到上限。这个循环每一轮都可能涉及到一次或多次 LLM 调用加上一次或多次工具调用。所以一次用户请求在系统内部往往会被放大成十几次甚至几十次内部调用。这个特点为后面的超时故障埋下了很重要的伏笔。1.2 一次正常请求的调用链一个正常的任务大概长这样用户在网关层提交请求分析这几个数据源生成一份周报。编排层把任务拆分先查数据 A、再查数据 B、再调模型生成摘要、最后调报表工具渲染。执行层启动一个 Runner按顺序执行子任务。每执行一个工具调用Runner 会等待结果返回。工具层把内部 HTTP/RPC 请求发出去拿到数据后交回给 Runner。Agent 可能还会根据返回结果决定是否需要补充查询于是再进入下一轮循环。这个链条里每一层都有一段等待时间。正常情况下模型推理平均 1~2 秒工具调用平均 50~200ms整体 P99 能维持在 300ms 左右用户无感。但如果任何一个环节出现延迟会直接导致 Agent 循环的某一轮卡住而且因为 Agent 有反思和纠错机制一旦某次调用失败或超时它可能不会立刻退出而是重新规划再试一次。这个再试一次就是超时放大效应的起点。1.3 架构上的隐患复盘之后我认为当时架构里有三个隐患属于平时没问题一出事就是大事的类型每个环节有独立超时但全局没有端到端超时预算。网关层设置了整体超时 15s编排层每个子任务 10s工具层每个调用 5s模型层 10s。表面上每层都有兜底但这些时间是叠加的不是取最大。一次用户请求如果经历模型 5s 工具 5s 模型 5s 工具 5s单看每一层都没超时用户侧却可能已经等了 20 多秒。执行层线程池是共享的工具连接池也是共享的。所有工具调用共用一个 HttpClient 连接池所有 Runner 线程共用一个线程池。这种设计在依赖全部健康时挺高效但只要有一个工具变慢它占住的连接和线程就会挤占其他工具的份额造成一根筷子坏了一桌饭。重试机制没有感知剩余预算。Agent 的反思机制会在工具调用失败时触发重试重试次数有上限3 次但重试发生时不会去检查用户请求还剩多少时间。结果就是一次本来可以在 3 秒内失败返回的任务因为重试被拖到了 20 秒以上。这三个隐患大部分 Agent 平台早期都会有。如果你正在做类似的系统我建议先对照检查一下别等到出故障再回头改。2. 故障现场监控告警与用户感知2.1 故障现象描述那天下午 14:20 左右监控开始报警。我先看到的不是平台自己的监控而是网关层报了一堆 504 Gateway Timeout。紧接着用户群里陆续有人反馈提交任务后一直没有结果像是在转圈转了很久最后报错了。我调出监控面板几个关键数字触目惊心指标正常值故障时网关层 P99 耗时300ms12s网关层 5xx 错误率0.1%4.8%执行层活跃线程数15~30200接近上限工具调用平均 RT50~200ms6~8s模型服务调用 RT1~2s1.5s正常用户集中的吐槽场景都指向同一个能力一个会调用内部数据查询接口的分析类 Agent。这个 Agent 的逻辑是用户输入诉求后它会自动判断需要查询哪些指标然后调用一个数据查询工具去拉数最后再让模型生成结论。2.2 第一轮排查网关、编排层、模型服务最开始我按顺序做排除法第一步看网关日志。大量超时日志都标注了downstream timeout说明请求不是被网关自身限流或拦截的而是下游没在规定时间内返回。第二步看编排层。CPU 40% 左右内存正常GC 也没有明显停顿。看起来编排层自身负载并不高问题应该不在任务拆分逻辑上。第三步看模型服务。这是我最担心的一环因为如果 LLM 推理服务抖动整个平台都会变慢。但当时模型服务的 RT 和错误率都在正常范围内没有出现大规模波动。所以问题几乎可以确定不在模型层而在工具链的某个环节。然后我在工具监控里看到了那个数据查询接口它的 P95 耗时从平时的 200ms 猛增到了 8 秒左右成功率倒是没怎么下降但请求队列肉眼可见地越来越长。2.3 最误导人的第一印象当时我的第一反应也是很多人的第一反应肯定是数据库慢查询赶紧让 DBA 去看。 运维同学查了一下确实在慢查询日志里找到了一条耗时很长的聚合查询。这条查询平时几十毫秒完成当天因为一个用户导入了一批大表数据统计信息过期执行计划选错索引导致跑了几秒。我们一度以为定位到了根因让 DBA 把查询优化了一下索引加上慢 SQL 消失。但诡异的是工具接口的 RT 并没有恢复正常错误率甚至还在缓慢爬升。这时候我才意识到我们把慢和堵混为一谈了。慢是下游服务本身处理能力下降堵是上游资源被占用、请求根本发不出去。那条慢 SQL 只是点燃了引线真正让故障持续扩大的是连接池和线程池被占满之后的排队效应。当所有连接都被慢请求占据时其他工具的正常请求也得排队等连接整个平台的 RT 自然全线飙升。3. 顺着调用链挖根因重试放大与线程阻塞3.1 链路追踪还原事实我们平台在研发阶段就接入了全链路追踪每个请求都有 traceId。故障复盘时我在链路追踪系统里拉了一条最典型的失败链路终于看清了完整画面。链路显示用户的一个简单请求查一下上个月某指标趋势进入编排层后被拆成了 4 个子任务。第一个子任务调用数据查询工具正常情况下应该 200ms 返回。但那段时间工具接口 RT 已经飙到 8s第一次调用直接吃到了单步超时上限——我们当时给工具层设置的超时是 5s。第一次调用失败后Agent 的反思机制触发了重试。重试不是立刻开始而是经过了一轮模型重新规划这一轮模型推理花了 1.5s然后重新发起工具调用又等了 5s失败。然后 Agent 再次规划再次重试。整个过程叠加下来第一次工具调用5s超时第一次模型反思1.5s第二次工具调用5s超时第二次模型反思1.5s第三次工具调用5s超时最终返回失败总耗时约 18s而网关层超时设置是 15s所以用户在 15s 时就已经看到了 504。但平台内部的 Runner 线程还在傻傻地跑着几轮无效的重试继续占着线程和连接资源不肯释放。这就是我们线上故障持续时间那么长的直接原因——不是每个请求都这样但只要流量稍微上来资源就会被这些已经注定要失败但还在重试的请求占满。3.2 线程 Dump 与连接池真相链路追踪把行为讲清楚了但还没有回答一个核心问题为什么那个工具接口变慢之后整个平台全部变慢别的工具也没惹谁。我抓了几次线程 Dump。线程 Dump 显示执行层有 200 多个线程处在 WAITING 状态堆栈全部指向同一个位置httpclient连接池的leaseConnection。也就是说大量 Runner 线程并不是在调用工具而是卡在等待获取连接这一步连接还没拿到更别提发请求了。再去查 HttpClient 连接池配置真相大白整个执行层共用一个 HttpClient 实例连接池maxTotal是 200maxPerRoute是 200。那会儿数据查询工具因为慢 SQL 导致 RT 变长连接被一个个占住不放。由于它是平台里使用频率最高的工具很快 200 个连接几乎全部被它占据。其他所有工具——包括模型调用、记忆存储、报表渲染——全都排起了长队。线程池也被波及执行层的线程池 max 是 256当线程都在等待连接时新的任务进来没有空闲线程开始在队列里排队。用户侧的感受就是提交任务一直接不了。总结一下我当时看到的因果链大概是这样的阶段表现原因1某个工具接口 RT 从 200ms 涨到 8s下游数据库慢查询2工具调用开始超时单步超时 5s但整体无预算控制3Agent 反思触发重试放大调用次数重试不知晓剩余时间4连接池被慢请求占满所有工具共用一个连接池5线程池被排队线程占满所有 Runner 线程共用一个线程池6非相关工具也全部变慢资源竞争导致全链路拥堵3.3 根因定性故障结束后我们把问题归成两类触发条件和放大条件。触发条件比较明确下游数据服务因为执行计划误判出现了秒级慢查询。这个属于外部依赖偶发抖动单靠优化 SQL 能解决眼前但拦不住下一次。放大条件才是真正需要改造的也是 Agent Platform 这种多跳架构所特有的整体没有端到端超时预算。各层超时是串联叠加的关系而不是整体约束。Agent 重试和反思机制没有时间感知。平台赋予了 Agent自动反思、尝试补救的能力却没有告诉它你已经没有时间了立刻失败返回。线程和连接资源没做隔离。一个慢工具能占满全部共享资源影响所有其他工具。没有熔断降级。当一个工具已经明显不健康时平台没有主动切断流量还在不断把新请求打进这个故障点。一句话总结就是外部依赖不健康是导火索平台自身的四个缺陷把一个小抖动放大成了全站事故。3.4 为什么之前的压测没暴露事后有人问我你们不是做了压测吗为什么没测出来 这个问题值得好好想一下。我们当时确实做了压测而且压测报告还挺漂亮。但复盘后发现压测场景和真实场景有几个关键差异压测时所有依赖都是健康的。我们没有注入过某个下游服务偶发变慢 10 倍的故障场景。压测时 Agent 的调用模式太单一。我们主要模拟了单次工具调用和模型直连的场景但真实用户请求会触发多轮 Agent 循环、反思、重试。这种调用放大在压测里根本没有被建模。压测时没有关注长尾连接占用。我们看的是平均 RT 和 QPS没有专门去观察连接池在高延迟场景下的占用情况和线程排队情况。后来我们补了一课Agent 平台这类多跳系统压测一定要加入故障注入主动打断某个依赖看系统能不能扛住。否则压测只能证明健康情况下能跑多快证明不了依赖不健康时会不会雪崩。4. 修复方案超时预算、隔离与熔断三板斧4.1 建立端到端超时预算第一件事是所有修改里优先级最高的给每个进入平台的请求建立端到端 deadline并且让 deadline 贯穿整条调用链。我们的做法是在网关层接受请求时根据用户可接受的响应时间比如 8 秒生成一个AgentContext里面保存着 deadline 时间戳。这个 context 会随着任务分解、Agent 循环、工具调用一直往下传。每次真正发起外部调用前都要先用剩余时间计算这次调用最多还能等多久。伪代码大概长这样// 进入网关时创建上下文 AgentContext ctx new AgentContext(); ctx.setDeadlineNanos(System.nanoTime() TimeUnit.SECONDS.toNanos(8)); // 在任何外部调用点先算剩余预算 long remainingNanos ctx.remainingNanos(); if (remainingNanos 0) { throw new DeadlineExceededException(全局预算已耗尽拒绝本次调用); } // 实际超时 min(单步超时, 剩余预算) long callTimeoutNanos Math.min(tool.defaultTimeoutNanos, remainingNanos); // 用这个值去设置 HTTP 客户端的 read timeout / connect timeout tool.invoke(request, callTimeoutNanos);关键点在于这里的超时是动态的不是写死的。如果前面已经花了 6 秒后面任何一步调用都不可能再等 5 秒最多只能等剩余的那 2 秒。这样无论调用链多长用户侧的总耗时永远不会超过 8 秒。模型调用同样要走这个逻辑。LLM 的推理时间不可控但预算前必须先问一句剩余预算还够不够让模型做一次推理不够就直接失败返回绝不进入下一轮。4.2 线程池与连接池隔离第二件事是对资源做隔离避免再次发生一个慢工具堵死所有工具的情况。我们按工具的重要性和调用特征把它们分成了几组每一组分配独立的线程池和独立的 HttpClient 连接池工具组典型场景线程池核心/最大队列长度连接池 maxPerRoute核心链路组模型调用、会话读取、主任务编排64 / 12825664数据查询组报表查询、指标拉取、DB 访问32 / 6412832辅助功能组邮件通知、日志归档、低优先级操作8 / 166416高风险外部组调用第三方服务、不可控供应商 API8 / 8328这么一拆之后就算数据查询组被打满核心链路组仍然有独立的线程和连接可以使用模型调用和任务编排不会跟着遭殃。用户收到的最坏结果也只会是分析类功能变慢而不是整个平台卡死。这里有一个实际调整中的细节线程池拆开之后要注意队列积压时的丢弃策略。我们给高风险外部组的队列设置了有界队列满了之后直接拒绝配合下面的熔断逻辑让失败的请求快速失败而不是无限排队。4.3 重试策略修正第三件事是修正 Agent 的重试机制。这是 Agent Platform 和普通 Web 服务最不一样的地方。普通 Web 服务可以在超时后安全重试但 Agent 平台的重试必须回答三个问题这个工具是幂等的吗非幂等操作比如创建订单扣减库存一律禁止自动重试只能让用户手动重新提交。幂等操作如查询数据获取状态可以重试。剩余预算够不够如果剩余预算小于重试一次的成本通常是单步超时 模型反思时间直接放弃重试。我们内部定了一个规则剩余预算小于 1.5 倍单步超时时禁止重试。重试之间要不要退避一定要。我们加了指数退避加上随机抖动第一次重试等待 200ms 随机 0~100ms第二次 400ms 随机 0~200ms。不能因为刚好超时导致所有请求在同一时刻疯狂重试形成重试风暴。同时对 Agent 循环本身也加了预算约束。每个 Agent 循环开始前会问一次剩余预算还有多少低于阈值时直接跳过反思和重新规划走快速失败分支。不管你这次推理能得出什么结论用户已经等不起了先把错误交出去比什么都强。4.4 熔断与降级第四件事是给工具层加熔断降级。这个在普通微服务里是标配但在 Agent 平台里配置起来需要一点针对性。我们给每个工具组配了一个熔断器采用滑动窗口统计。规则大致是最近 10 秒内错误率超过 20% 或者平均 RT 超过 2 秒熔断器打开后续请求直接走降级路径不再真实发起调用。熔断打开后持续 30 秒然后进入半开状态允许少量请求透传探活。熔断打开时Agent 的降级路径有几种选择如果有缓存数据直接用缓存结果并在最后汇报里标明数据可能不是最新。如果不需要精确结果返回一个默认的兜底值或部分结果。如果任务强依赖这个工具就直接终止该子任务并把失败原因写入 trace让用户知道是哪一步出了问题。这里要给做 Agent 平台的朋友提个醒熔断器和 Agent 的反思机制是有冲突的。如果 Agent 在调用一个已经熔断的工具时还在反复思考要不要换个参数再试一次熔断就失去了意义。所以熔断状态必须暴露给编排层编排层看到工具处于熔断状态时要跳过这个子任务或直接走降级而不是把它当成一个普通失败丢给 Agent 去反思。4.5 上线验证与灰度这些改动涉及执行层的核心路径不能一次性全量上。我们的上线步骤大概是第一轮先把超时预算逻辑在灰度环境跑通用录制回放的方式验证所有外部调用的超时值都变成 min(单步, 剩余预算)确认没有出现预算耗尽后连接不释放的 bug。第二轮开放到内部用户流量观察 P99 和线程池活跃数是否回落。这阶段我们发现一个之前没预料到的问题有些工具本身需要较长的执行时间比如导出大文件在预算紧张时会被快速失败。所以后面又调整了预算分配策略对长耗时工具单独允许更长预算但全局仍然设有硬上限。第三轮做了一次故障演练强制把下游数据服务限速到原来的 1/10观察平台会不会再次雪崩。结果熔断在 10 秒内生效数据查询组请求被快速拒绝其他核心链路正常整体 P99 维持在 900ms 以内比故障前的 8 秒好了一个数量级。回切之后我们连续观察了一周。故障那天的场景没有再现P99 稳定在 400~500ms 之间错误率回到 0.2% 以下。5. 复盘清单Agent 平台超时治理还能怎么做5.1 核心配置基准这次踩坑之后我梳理了一份我们平台现在的超时和资源治理配置清单可以当作一份基础模板。如果你也在做类似系统照着这个思路校准未必完全适用但方向大概率是对的配置项我们当前的值设计思路端到端用户超时预算8s取产品可接受时间上限的 80%留 20% 给网络传输和网关排队模型单次调用超时10s实际受剩余预算约束模型推理属于高价值调用预算充足时可以多等核心工具单次超时5s大部分内部工具 200ms 内返回5s 已经是很宽松的兜底辅助工具单次超时3s辅助功能可以接受失败不必等太久幂等工具重试次数最多 1 次超过 1 次几乎都是徒劳只会放大压力连接池分配按工具组拆分核心/数据/辅助/外部四个池互不干扰熔断阈值10s 窗口错误率 20% 或平均 RT 2s宁可误伤一个工具也不让它拖垮平台熔断恢复时间30s 后半开给下游留出恢复窗口5.2 几点实操体会再分享几个不一定写进架构设计文档、但实际排查中特别有用的点。第一日志里一定要带上 deadline 的剩余值。光记录超时了没用我要知道是单步超时触发还是全局预算触发。现在我们的每条失败日志都会附带remainingMs字段一眼就能看出是不是上游预算消耗太多导致的连锁反应。第二排查超时要分清楚慢和堵。慢是下游服务响应慢堵是上游资源不够。这两个问题的现象都可能表现为你的服务 RT 变高但处理方法完全不同。看线程 Dump 是最直接的鉴别方式如果线程都卡在等待连接说明堵如果线程都在等下游的 IO 响应说明慢。第三重试一定要加上限并且重试前先检查剩余预算。我看到很多系统犯同一个错误重试次数设置了 3 次但第 2、3 次重试发生时用户的请求其实早就应该超时了。重试不是越挫越勇而是要在有限成本内解决问题预算不足时快速失败才是对用户最大的尊重。5.3 后续可以优化的方向这次故障解决的从 8 秒回到 500ms但超时治理这件事其实还没结束。我目前在尝试的几个方向也顺便提一下一是预算感知调度。如果要拆的子任务很多而剩余预算只够完成一半编排层应该主动降低任务优先级先做最重要的那部分低价值的补充分析额外验证直接跳过。这个规则可以做成平台的通用策略而不是让每个 Agent 自己决定。二是把熔断信息反馈给模型。现在的降级路径是预先写好的但更灵活的做法是当某个工具熔断时把该工具当前不可用请尝试其他路径或直接作答写进模型推理的提示词里让模型自己决定是换工具还是放弃。这个在内部实验过确实能让 Agent 表现得更智能但要注意控制 token 消耗和推理延迟。三是动态超时自适应。单步超时可不可以根据最近一段时间的工具 RT 分布动态调整比如某个工具连续 10 分钟都维持在 50ms超时降至 1s一旦它开始抖动超时自动放宽到 3s。这样既能保护用户体验又能给工具留出喘息空间目前还在灰度测试阶段。最后说一句个人体会。这次故障让我把 Agent Platform 的超时问题从HTTP 层的问题重新理解成编排层的问题。Agent 平台里一次用户请求背后可能藏着十几次内部调用任何一层慢都会被循环和重试机制放大。做这类系统单步超时再严谨也不够必须把端到端预算、资源隔离、熔断降级当成平台级能力来做。后来我们每次做任务编排设计第一条评审规则就是trace 里能不能看到剩余预算排不上队的时候能不能快速失败。记住普通的服务可以被容忍慢但 Agent 平台不能因为某个工具慢了就让所有人的任务都卡死。希望这篇复盘对你有用。