
1. 项目背景Agent Platform 到底在做什么1.1 这个平台解决的核心问题先说背景。我接手的是一个面向企业客户的 Agent Platform简单来说就是把多个大模型 Agent 编排起来对外提供统一的对话与任务执行接口。业务方通过这个平台实现智能客服、工单自动分类、合同信息抽取、跨系统数据查询等场景。归纳下来就是一个中枢接收用户的自然语言请求拆解成子任务分发给不同的 Agent 去执行再把结果汇总返回给用户。听起来不复杂但真正把系统铺开后问题就来了。Agent 体系里的链路比普通 API 服务长得多——先是意图识别再是任务规划然后是工具调用中间可能还要多轮追问最后才是结果聚合。每一步都有一次模型调用一次调用可能是秒级多个步骤累积起来就是一个十几秒甚至几十秒的长尾请求。普通 HTTP 服务一般几百毫秒就该返回Agent 服务却天然是慢请求的集合体。我们上线初期把平台设计成同步调用模式原因很直接业务方要求简单一个接口传进来文本同步等结果返回。但同步模式对超时时间、任务编排、资源隔离的要求极高线上流量一旦上来哪一个环节抖动整条链路就会跟着遭殃。1.2 整体的技术架构与选型逻辑平台的技术栈是这样的底层用 Python 做 Agent 编排逻辑因为生态里工具类库和模型 SDK 最全对外用网关层做统一接入负责鉴权和限流任务执行走一个自研的轻量调度器维护 Agent 实例的启停和状态流转中间用消息队列解耦一些异步通知例如任务开始、结束、异常时的回调通知业务方。Agent 的编排核心是一个步骤引擎每个任务被定义为一个有向图结构节点有模型调用、工具调用、条件分支、子 Agent 调用这几类。步骤引擎按图顺序执行负责把每一步的输入、输出、状态快照都记录下来方便回溯和调试。选型时每一样都是反复权衡过的。异步化改造是后来才下决心做的初期为了快速上线复用了一整套同步 RPC 框架所有 Agent 任务在请求线程里直接跑。这个决定的代价就是后面要讲的超时故障本质上是在为同步长链路买单。2. 故障现象与排查现场2.1 线上告警先是一串超时报警然后是用户投诉故障发生在一个寻常的周三下午。先是监控系统弹出告警平台核心接口 P99 延迟从平时的 800ms 直接飙到 15 秒以上错误率从 0.1% 上升到 6%。紧接着客服那边就有用户反馈了说智能助手转圈圈转半天没反应还有人说提交了工单页面一直转圈最后提示失败。我们立刻拉取了网关层的日志发现大量请求最终返回了 HTTP 504。网关超时阈值是 30 秒也就是说有相当一部分 Agent 任务在 30 秒内没有跑完。而看过 Agent 编排日志后发现任务其实还在执行中并没有真正卡死或报错——只是太慢了。这种半死不活的状态最棘手。如果是直接报错说明某个环节崩了快速定位就能解决。但任务还在跑、只是跑不完说明瓶颈不在某一个点了而是整条链路上多个环节叠加延迟最终撑爆了超时上限。2.2 粗看链路负载不高但锁等待和慢调用引人注目第一反应是查资源。CPU、内存、磁盘 IO、数据库连接数全部看了一遍都很平稳没有明显瓶颈。这排除了扩容能解决的常规资源问题。然后看调用链追踪系统把一段时间内的慢请求拉出来逐个拆。拆完就开始有眉目了大量请求的时间都耗在了等待子 Agent 返回结果这个环节。进一步看我们的编排引擎在处理一个任务图时用的是深度优先遍历 同步等待。父 Agent 调用子 Agent就同步 block 在那边等结果回来。子 Agent 内部如果又有工具调用同样同步等。一个嵌套层级深的请求等的时间就是每一层的总和而这个总和很容易超过网关的 30 秒上限。对比数据很直观。正常请求的子 Agent 数平均 2 到 3 个单个子 Agent 耗时在 1 到 3 秒。故障时段的请求子 Agent 数平均到了 6 个以上单个子 Agent 因外部依赖变慢耗时膨胀到 5 到 10 秒。加起来一个请求不超时才怪。2.3 顺着锁等待再看一个隐蔽的状态竞争在背后发力慢但也应该有个上限吧为什么不是慢 10 秒、20 秒而是慢到 30 秒以上继续深挖时发现系统里还存在一个隐蔽的状态竞争问题。我们的 Agent 实例状态存储在 Redis 里多个子 Agent 完成后需要回调主任务更新主任务的状态和结果聚合字段。更新 Redis 时用的是一段比较轻量的 Lua 脚本保证原子性。问题出在一个细节上如果两个子 Agent 同时完成并回调其中一个回调要拿锁更新聚合状态另一个就得等。锁超时时间设置的是 5 秒若是极端情况下锁没有正常释放比如进程 GC 停顿或网络抖动后到的回调会阻塞在锁等待上而这个等待时间会叠加进任务总耗时。加上上面的同步嵌套等待一个任务的总耗时公式变成了各层级子 Agent 执行时间之和 多方回调的锁等待时间之和 外部工具调用的重试等待时间。这三项在高峰期互相放大直接把请求拖过了超时阈值。3. 根因分析为什么同步编排模式经不起真实流量考验3.1 同步等待 深度嵌套天生就是延迟放大器的复合体这次故障最本质的原因是我们的编排引擎选择了同步等待模型。做个简单推演。假设一个任务图包含 3 层顶层 Agent 依次调用 2 个中层 AgentA 和 B中层 Agent A 又调用 2 个底层 AgentA1 和 A2。如果每个底层 Agent 平均耗时 2 秒A 要等 A1、A2 全部完成后才能返回那 A 的耗时就是 4 秒假设 A1、A2 并行。顶层要依次等 A 和 B加起来就是 4 4 8 秒。也就是说光是最理想的情况这个任务就要 8 秒。如果在 A1 或 A2 的执行过程中外部工具 API 响应变慢比如重试了 3 次每次超时 3 秒A1 的耗时就从 2 秒变成了 2 9 11 秒。A 变成 11 秒顶层变成 15 秒。如果再叠加锁等待、GC 停顿、网络重传每项 2 到 3 秒一个 20 多秒的请求就这么产生了。真实用户请求分布一上来必然有尾部请求超过 30 秒。对比之下异步事件驱动模型就完全不一样每个 Agent 执行完就抛一个事件出去由事件总线通知下游整个链路是被事件推着走的不存在父等子的同步阻塞。任务总耗时的上限取决于最深的路径而不是所有路径的简单叠加更重要的是系统吞吐不会因为某个环节变慢而出现劣化扩散。3.2 外部依赖的抖动防护做得不够重试逻辑又放大了劣势第二个根因是外部依赖防护不足。Agent 平台里有些工具调用是访问内部业务系统的接口有些则是调用第三方 API例如前端部署的知识库检索服务、外部模型供应商的接口。开发的时候对每类外部依赖都配置了超时时间但这里有一个不太起眼却致命的问题超时时间值设得偏大而且重试策略是无脑重试。某个第三方接口的超时设成了 10 秒重试次数设成 3 次看起来挺合理。但故障时第三方 API 整体响应慢单次不是彻底超时而是 8 秒才返回。于是系统成功地执行了 3 次每次 8 秒总共 24 秒全部被这个工具调用吃掉了。设置外部调用超时和重试的铁律不是看单个调用本身的延迟而是要看这个调用在整个任务链路里能承受的预算。一个任务如果整体最多只能花 25 秒那单个工具调用的最大耗时就不能超过 5 秒重试次数最多 1 次。这需要在任务刚开始时就统一下发一份时间预算贯穿所有子 Agent 和执行步骤。3.3 监控粒度不够细慢请求迟迟没能引起警觉还有一个让我印象深刻的教训监控体系在故障前是有盲区的。我们平时的监控是看接口 P99、错误率、QPS这些指标在故障发生时确实异常了。但在故障发生前的半小时其实已经有苗头了——部分任务的执行时长从 8 秒慢慢爬到了 12 秒接口 P99 也从 800ms 升到了 2 秒左右。只是这些变化都还在正常波动的范围内没有被当成需要立刻处理的信号。如果当时有按任务类型拆分的耗时监控比如分包任务的平均耗时、涉及外部工具调用的任务耗时分布就会更早发现部分链路在劣化这个事实。通用的接口监控指标粒度太粗在 Agent 这种复杂链路中必须要按链路特征细分指标才能及时发现问题。4. 修复与优化怎样一步步把超时问题按下去4.1 短痛方案快速止血把同步调用改成有界等待故障当天我们做的第一件事不是重构而是先止血。止血方案有两个第一个是给所有外部工具调用和模型调用的超时设置一个全局硬上限。原先分散在各 Agent 逻辑里的 10 秒、15 秒超时统一收敛到单次调用不超过 5 秒重试最多 1 次。这个配置放在配置中心秒级生效改完立刻把最极端的超时请求压下来了。第二个是网关层的有界等待策略。网关不再被动等待内部任务 30 秒而是给任务设置一个 20 秒的内置截止时间。一旦超过网关直接返回失败给用户Agent 编排引擎则异步把任务标记为已取消并尽力中断还在运行的子 Agent。这套策略的意义在于不让失败请求继续消耗系统资源。以前 30 秒超时用户其实在第 30 秒才知道失败这 30 秒里系统一直在跑一个注定不能反馈给用户的任务大量线程、网络连接、Redis 连接都被占着。改成 20 秒先返回失败后线程立刻释放用户体验从转 30 秒圈变成了20 秒内给明确反馈虽然还是失败但至少反馈清晰。4.2 治本方案编排引擎的异步化改造止血之后我们花了三个版本迭代做编排引擎的异步化改造。这是治本的关键一步。改造的核心思想是所有 Agent 的执行不再是函数调用而是状态状态机 事件驱动。任务图里的每个节点被做成一个独立状态机节点从待执行到执行中再到已完成或失败状态变更时发出事件。编排引擎持有一张全局任务表记录每个任务当前的图状态、已完成节点、待续节点。具体实现上我们用了一个基于 Redis Stream 的轻量事件总线节点完成时向总线发布一条消息事件处理器收到消息后查看任务图将后续节点状态更新为待执行然后触发新一轮调度。整个过程没有任何线程阻塞等待。异步化之后效果是显著的父 Agent 不再等子 Agent而是发出调用子 Agent的指令后就结束了自己的执行周期子 Agent 完成后再用事件把结果带回给父节点。理想情况下一个三层任务图的总耗时从所有路径的总和变成了最深层路径的耗时 事件传递的少量开销可能有 30% 到 50% 的延迟下降。这里有一个实现上的坑必须说一下异步化改造时最容易出问题的是状态一致性和回调丢失。我们设计了一个本地消息表 定时补偿的机制所有节点状态变更先写数据库开启本地事务再投递事件到 Redis。如果 Redis 投递失败会有定时任务扫描超时未完成的任务主动推送重试执行事件。这是我强烈建议所有做异步编排的人都认真处理的地方否则会出现任务图挂在半路谁都不知道的诡异问题。4.3 重要参数配置清单直接抄作业结合这次实践我把一套经过验证的关键参数配置整理在下面供参考。可以根据自己的业务模型微调但思路是通用的参数项推荐值设置逻辑单次模型调用超时5-10 秒模型接口普遍有高延迟给足空间但不过度单次外部工具调用超时3-5 秒比模型调用更严格工具相对可预期外部工具重试次数0-1 次重试太多会占用任务整体预算任务级总超时20-30 秒结合业务容忍度和网关设置宁短勿长回调锁超时1-2 秒锁只应该保护极短的操作超过就是异常异步事件重投间隔30 秒掉线恢复或进程重启后快速补偿任务历史保留天数7-30 天用来排障和重放太久的就没必要了配置最重要的是统一管理。建议把所有外部调用的超时、重试参数放到配置中心按 Agent 类型和工具类型拆分不要在代码里到处写魔法数字。我们踩过的坑就是某个工具的超时时间散落在两个不同模块里一个改了一个没改排查时极为痛苦。5. 常见问题与排查技巧实录5.1 线上超时类问题的排查思路按顺序来排障顺序非常重要。我总结了一套五步走的思路经过这次故障验证确实能提高定位效率第一步确认超时现象的范围。是所有请求还是某一类请求是所有时段还是某个时段这种分类能快速缩小排查面。第二步查调用链。如果原有的监控系统没有按链路打点立刻在关键环节补上。我们需要能看到哪个环节耗了多少时间才能知道瓶颈在哪。这一步常常能定位到 60% 的问题。第三步查外部依赖。调用第三方 API 或者内部下游接口时把它们的 P99 耗时拉出来和本地对比。如果本地慢但下游正常问题在网络链路或序列化层如果下游本身就慢那就是下游的问题或重试策略的问题。第四步查锁和竞争。看系统有没有共享状态需要频繁互斥更新比如 Redis 锁、数据库行锁。当面流量高时锁的等待时间会迅速膨胀。第五步查资源异常。最后一步才看 CPU、内存、磁盘、线程池、连接池。因为资源问题通常只是表象真正原因是代码层面的某些操作放大了资源消耗。直接看资源容易被误导。5.2 实战排查工具和数据怎么有效利用故障排查过程中日志不是越多越好而是要能关联起来。建议至少在三个层面打点请求入口层记录用户请求 ID、业务场景、开始时间、结束时间、总共耗时时长。编排层记录任务图结构、每个节点的开始/结束时间及耗时。这个日志在排查到底是哪个 Agent 拖后腿时价值最大。外部调用层记录对下游 API 的实际请求、响应码、耗时、重试次数。这能准确判断外部依赖是否是瓶颈。配合一个简易的追踪 ID 贯穿三层排查时一个 ID 拉出全部日志按时间排序就能把整个链路在一个请求内的所有事件全部还原出来。这次故障里事件时间线回溯是我们最关键的排障手段。另外有个小技巧在 Agent 编排引擎里加了一个慢节点告警功能。每个节点执行时如果超过了当前任务剩余预算的一定比例我们设的是 30%就打印一条 WARN 日志并上报一个业务指标。这样下次再出现类似劣化时能直接定位到哪个节点超支而不是在全链路日志里大海捞针。5.3 后续部署时一定要提前避开的三个坑一是在异步化过程中容易出现事件风暴。改造初期我们所有节点完成都发事件一次任务光事件就发了几十条Redis 的 Stream 消费出现积压。后来加了事件聚合机制对同一任务图节点的完成事件做 500ms 窗口聚合一个窗口内只发一条推进事件事件量直接砍了 70%。二是任务图的并发度控制。异步化后如果图里可并行的节点数量很多调度器一次性把它们全丢出去执行下游系统的压力会瞬间过大。我的做法是为每个任务设置最大并发数例如 3调度器每次只推 3 个节点出去完成一个推进一个。这样下游不会出现瞬时尖刺。三是做好幂等。节点执行支持重复触发不产生副作用是关键。我们在每个节点上加了执行幂等标记重试或补偿时如果节点已经执行成功且结果已持久化直接跳过。否则分布式环境下很容易出现同一个节点被跑两次的脏数据问题。6. 一次真实故障带来的长期启发这次超时故障最后成了整个团队重构的催化剂。事后我们复盘时有一个感想Agent 平台的本质是长事务编排不该用短事务同步的方式去承载。每一步的异步化、有界等待、补偿机制都是为了让平台可以体面地处理慢依赖和失败场景。另一个我印象很深的是团队成员对预算思维的重视程度完全变了。现在设计任何 Agent 流程时第一件事是把这个任务允许的总耗时定下来然后按照链路层级做预算分配。预算一旦定好所有子任务的超时、重试、资源分配都有了一个明确的上限依据而不是代码里各自拍脑袋。最后分享一个排查时的小心得遇到超时问题先别急着查代码先把这个请求在每一个环节分别花了多少时间的数据拿到手。数据一旦完整链路里谁是那个拖延者一眼就能看出来。多数超时问题不是一个点出错而是多个点共同劣化后叠加超限只有把每个环节的时间量化出来才可能做针对性的优化。