1. Agent Platform 整体设计与核心模块1.1 为什么需要自己搭一个 Agent Platform先交代一下背景。团队在做一个把大模型能力接入业务系统的中间层最初只是几个单点接口谁要调用模型就直接 HTTP 请求。但很快发现业务方要的不只是“问一句答一句”而是“让模型替用户把一件事完整跑完”查库存、对比价格、填单、回执确认这种多步骤操作如果全部写在业务代码里每个调用方都要自己维护一套复杂的状态流转和上下文拼接谁都扛不住。于是我们决定做一个统一的 Agent Platform把模型的调用、工具的执行、上下文的维护、任务的编排统一收口。这个平台对内暴露两类入口一类给实时对话场景用一类给任务型场景用。前者的核心诉求是响应快后者的核心诉求是稳。架构上我们没有一上来就堆微服务而是按照职责拆成四个核心模块接入网关层负责鉴权、限流、协议转换把外部请求统一转成平台内部的任务结构。编排调度层负责拆解用户意图规划执行步骤决定先调哪个工具、再问哪一轮模型。执行器层真正执行工具调用、模型调用、代码片段运行等逻辑。状态与记忆层负责保存任务快照、对话历史、工具返回结果按任务 ID 维度做隔离。每个模块之间通过内部消息通道传递上下文而不是直接同步调用。这个设计思路后来越来越被证明是对的但一开始我们并没有把“异步化”贯彻到入口层这也为后面那次超时故障埋下了雷。1.2 核心模块与数据流看数据流会更清楚一些。用户在前端发起一个请求比如“帮我把上个月的销售数据整理成报告并发送给相关同事”这个请求先到网关网关做完身份校验后立刻生成一个任务 ID然后把任务投递到编排队列。编排调度层拿到任务后开始工作它会先把这条自然语言请求转成一组结构化步骤比如“查询销售数据”、“整理成表格”、“生成分析总结”、“调用消息接口发送”。每执行一步系统都会把中间结果存进状态存储并更新任务状态机。这里我标注一下关键点每一步的执行都不是瞬时完成的。模型生成一段分析可能需要 2 到 5 秒工具查询可能又要几百毫秒整条链路算下来一个完整 Agent 任务的执行时间通常在 10 到 30 秒之间甚至更长。这个“长任务”特性是我们整个故障故事的最核心背景。如果我们做的只是一个普通 CRUD 接口每个请求 50 毫秒返回那后面所有的事情都不会发生。1.3 技术选型逻辑在做技术选型时我们当时处于“快速验证业务可行性”的阶段没有选择引入大量的新基础设施而是尽量复用团队已有的技术栈。选型对比大概是这样的模块方案选项我们最终的选择理由主语言Java / Go / PythonJava团队主力栈运维体系成熟排查问题经验丰富任务调度自研线程池 / 消息队列消息队列 Worker天然支持削峰、重试、故障恢复状态存储MySQL / RedisRedis MySQL 组合Redis 存热状态MySQL 做持久化归档模型调用直连模型 API / 统一网关自研模型网关统一处理鉴权、限流、超时和模型降级对外接口REST 同步 / REST 异步 / WebSocketREST 同步第一阶段快速上线客户端接入成本低现在回头看第一阶段的“REST 同步”选型属于典型的业务倒逼技术决策。当时业务方希望 API 调用方式越简单越好最好发一个请求就能拿到最终结果不想在客户端做任何异步逻辑。我们为了让平台尽快跑起来接受了这个妥协。但从这次故障之后我们彻底转向了“任务提交 轮询/推送”的异步模型。这个代价是客户端接入稍微复杂了一点但换来的却是平台整体的稳定性。凡是可能耗时超过 3 秒的接口在设计阶段就必须按异步来考虑——这句话现在已经是我们的内部设计红线了。2. 超时故障发生的背景与第一现场2.1 现象描述与告警那是一个普通的周二下午监控群突然连续弹出告警。告警内容并不陌生网关接口“任务执行接口”的 P99 延迟从平时的 1.2 秒飙到了 8.5 秒超时率从前一天的 0.1% 上升到了 23%。紧接着上游业务方开始在群里反馈说平台返回了大量 504 和 502。这里要说一下我们平台的超时配置。入口网关给“任务执行接口”配置的读超时是 5 秒写超时是 5 秒。而 Agent 任务动辄需要十几秒才能执行完成。按理说这两者天然是冲突的——同步接口根本不可能在 5 秒内返回一个完整执行结果。那为什么还能跑起来因为我们做了一层“伪同步”收到请求后同步执行编排逻辑但编排内部每一步之间其实是异步流转的如果整条链路能在 5 秒内跑完就正常返回如果跑不完……鬼知道会怎样。实际业务中很多轻量任务确实能在 5 秒内完成比如“查一下某个客户最近三笔订单的状态”这种简单工具调用模型一轮 工具一轮就完了。所以我们一直在这个临界状态下运行直到那天流量上来把所有问题一次性暴露了。2.2 初步排查与假设接到告警后我们第一反应是查下游模型服务的状态。这也是个很自然的排查方向。Agent 平台里最耗时、最不稳定的就是模型调用如果模型服务出现拥堵或限流我们的接口延迟会被直接拉高。当时我们看到模型网关的调用耗时确实有上升之前平均 800 毫秒到 1.5 秒的模型响应在故障期间变成了 3 到 6 秒。这更加坚定了我们“模型服务背锅”的判断。我们还去查了数据库慢查询。平台里的状态存储用了一部分 MySQLAgent 任务每流转一步都会更新状态如果某些任务上下文很长、子步骤很多那更新 SQL 可能会扫描较大的数据块。不过当时慢查询日志里并没有出现特别离谱的大查询数据库侧相对安静。资源监控也在看。每台物理机的 CPU、内存看起来都还算健康没有明显的突刺和内存溢出。因为问题不在算力层面而在“等待”层面——大量线程在等待外部 IO 返回CPU 根本跑不满。2.3 真正定位线程池被长任务占满排查持续了大概二十分钟真正让我们发现问题的是一个不太起眼的指标应用服务器的活动线程数。我们用的容器默认线程池是 200 个线程。平时这个池子的活跃线程通常在 30 到 60 之间故障期间直接打满到了 200之后新请求再也拿不到线程只能在队列里等待。而等待的请求又会占用客户端连接客户端等不到响应就开始超时。超时之后上游服务会做一次重试把同样的请求再打进来继续排队。这个循环一直转下去系统进入了典型的“线程饥饿”状态。其实这种问题在传统 Java 后端里非常经典理解起来不难。但为什么我们当时没有第一时间想到因为我们的服务不是普通后端它内部有不同的线程池在管理模型调用和工具调用。我们有专门给模型调用用的 HttpClient 线程池有给工具执行用的线程池还有给状态写入用的连接池。在主线程池打满之前我们已经观察到这些子池也在排队但当时的判断是“子池排队是结果不是原因”。真正的原因链条是这样的主线程池里有大量线程在处理 Agent 执行任务。每个线程会阻塞等待模型 API 返回这段等待时间少则 2 秒、多则 10 秒。因为所有上游请求都同步等待最终结果所以主线程池被这些“看起来活着、实际在等”的线程全部占住。新请求进不来入口超时率飙升。上游重试进来线程池彻底堵死。这里我还想强调一点线程池打满不等于资源不够而可能是线程被“卡”在外部依赖上。这是排查超时问题最容易误导人的地方。CPU 没跑满、内存没报警、数据库也不慢并不代表系统是健康的。对于大量依赖外部 IO 的服务POOL 的排队长度和 Web 容器活跃线程数反而更能说明问题。3. 根因深挖为什么“每个任务都很慢”只是表象3.1 Agent 任务的结构性耗时很多人在设计 Agent 平台时会对“慢”这件事估计不足。一个完整 Agent 任务的耗时构成大概是这样的第一轮模型调用把用户意图解析为执行计划耗时 1 到 3 秒。工具调用比如 SQL 查询、HTTP 请求耗时 0.2 到 2 秒。第二轮模型调用根据工具结果生成下一批动作或最终回复耗时 1 到 3 秒。多轮循环如果任务复杂可能循环 3 到 5 次。上下文整理与持久化0.1 到 0.5 秒。也就是说一个中等复杂度的 Agent 任务总耗时普遍在 8 到 20 秒之间。这还算是顺利的情况如果中间某次模型调用触发限流重试或者工具调用超时重试那 30 秒以上是很常见的。这个数据放到业务视角里去理解我们的平台是给内部业务系统做智能助手的用户点了按钮之后等 10 秒出结果体验上是“慢”但还在可接受范围。但服务端传统同步模型的容忍度只有几百毫秒。这两者之间的差距就是我们故障发生的结构性原因。3.2 共享资源竞争产生的雪崩效应除了任务本身慢还有一个更致命的东西所有任务都在抢同一批线程资源。Agent 平台承载的请求不只来自外部业务方还来自平台内部的定时任务、健康检查、配置刷新、缓存预热的线程。这些看起来无关紧要的小流量在正常情况下占比很低可一旦主线程池被长任务堵满它们的失败会引发一系列连带反应。举个例子负载均衡器会定期发送健康检查请求到每个应用实例。如果实例的健康检查接口因为线程池耗尽而迟迟不返回负载均衡器会把这个实例标记为不健康然后把流量切到其他实例。其他实例承受了更多流量也陷入一样的线程饥饿状态最终所有实例都被踢出流量调度。这就变成了一个极端的恶性循环。而且我们平台作为中间层它本身也在调用其他内部服务。线程池满后内部服务之间的调用也会排队下游服务看到上游长时间不返回就可能提前断开连接。一旦连接断开上游线程还要做异常处理、记录日志、决定是否重试这些动作同样消耗线程和 CPU。整个系统的吞吐量会断崖式下跌。3.3 重试风暴是放大器这次故障中重试机制扮演了放大器角色。我们的上游采用的是非常标准的重试策略默认重试一次超时时间为 3 秒。也就是说当平台入口线程池开始排队时上游发来的第一次请求会在客户端超时 3 秒后返回错误然后客户端立刻用同样的参数重发一次。这相当于每 1 个用户的真实操作在最坏情况下会给平台带来 2 到 3 倍的请求量。而 Agent 平台本身的业务特性决定了一次真实操作在内部可能已经创建了一个执行任务。如果这个任务在后台仍然在跑重试请求又会创建一个新的重复任务。两个任务同时跑同一段逻辑可能还会对业务数据造成重复处理。我们后来在平台的任务记录里确实发现了一批重复执行的任务它们的任务 ID 不同但业务参数一致排查了很久才确认是重试导致的重复调用。所以那次故障给我的第二个深刻教训是在引入超时和重试机制时必须同步考虑幂等性。平台侧要能在入口识别出“这是同一类请求的重试”并把多次提交合并成一次执行或者在任务执行前做好去重检查。4. 修复方案与架构改造落地4.1 第一时间的止血措施先把时间拨回故障发生当天。我们的核心目标是让服务尽快恢复不做激进的重构。当天我们做了三件事顺序很重要第一在入口网关把对模型调用和工具调用的全局超时改成一个更小的值避免单个任务无限期等待。这个动作能释放一部分线程但效果有限因为我们不能把任务超时压得太低否则业务根本做不成事。第二我们把上游服务的重试次数临时调整为 0并且联系上层调用方同步修改其超时配置。这是止血措施中最关键的一步。切断重试风暴之后线程池的排队情况立刻开始缓解大约十分钟后新请求的进入速度恢复正常。第三我们将入口网关对“任务执行类接口”的调用方式从同步等待改为异步提交。这一步是应急操作但方案是临时的客户端提交任务后立即返回任务 ID然后客户端每 1 秒轮询一次任务状态接口。这样至少让网关线程不被长任务卡住主线程池的压力迅速降下来了。不过我必须强调这一步当时属于“拆东墙补西墙”的临时办法。因为客户端切换轮询模式后并没有真正解决“后台任务并发执行数量不受控制”的问题。如果没有后续的队列改造等到流量再上来后台还是会崩溃只是从“线程池崩溃”变成了“执行器任务堆积”。4.2 根治方案任务队列化与异步化改造故障恢复后我们用两周时间把整个架构做了一个关键改造核心思路其实一句话把“同步等待结果”变成“异步流转状态”。改造后的流程是这样的客户端提交任务到网关网关生成唯一任务 ID将请求写入任务队列并立即返回。任务队列使用持久化消息存储确保任务在服务重启后不丢失。调度器从队列里拉取任务更新任务状态为“执行中”然后把任务派发给空闲的 Worker 执行。Worker 按内部编排逻辑执行模型调用、工具调用。每完成一步就把结果写回任务存储并更新任务状态。客户端通过轮询任务状态接口来感知任务进度等任务状态变为“成功”或“失败”后再拉取最终结果。为了不把客户端的接入成本提得太高我们还提供了另一种模式客户端在提交任务时可带上一个回调地址任务完成后平台主动推送结果。这样客户端连轮询都不用做只用一个 HTTP 接口收结果即可。改造后的任务状态机相对清晰状态含义流转方式PENDING已入队等待调度调度器拉取后变为 RUNNINGRUNNING正在执行编排逻辑执行完成后变为 SUCCESS 或 FAILEDFAILED整体执行失败可手动或自动触发重试回到 PENDINGSUCCESS全部步骤执行完毕结果数据保留一定时间供客户端拉取这里有个值得说的细节我们并没有把整个 Agent 执行过程做成原子性的事务。因为一次 Agent 执行涉及模型调用、外部工具调用、持久化等多个环节任何一步失败都可能导致全局回滚这在业务上是不现实的。我们采取的是“步骤快照”策略每个执行步骤的结果都先存储如果某一步失败系统可以从最近的快照点恢复重试而不需要从头再跑。这个设计思路也可以理解为“断点续跑”。它让“重试”从重来一次变成从失败点继续。实际效果是重试任务的耗时开销比原来降低了 60% 左右。4.3 监控与可观测性补强这次故障也暴露了我们在可观测性上的明显短板。当时我们有基础设施的监控但缺少“任务级”的视角。于是我们补了这么几类指标任务维度任务排队时间、执行时长、状态分布、步骤级耗时。线程池维度活跃线程数、队列深度、拒绝次数。依赖维度模型调用耗时分布、工具调用耗时分布、错误码占比。我们还在执行链路里埋了 Trace这样任意一个任务失败都能通过 Trace 看到具体哪一次模型调用超时、哪一个工具访问异常上下级调用关系一目了然。关于监控这件事我的实际经验是监控要建在链路的关键节点上而不是只盯着服务器资源。Agent 平台最核心的链路是“入口 → 队列 → 编排 → 工具/模型 → 持久化”每一跳都要有延迟指标和失败指标。有了这些下次再出问题定位时间能从小时级降到分钟级。4.4 稳定性治理的后续动作除了架构改造我们还做了几项稳定性治理这些动作当时看起来不起眼但实际作用非常大。限流方面我们在接入网关增加了基于用户和任务类型的双重限流。不同任务类型的成本差异其实是很大的简单问答任务只需要一次模型调用而复杂数据分析任务要做多轮调用和数据读取。如果两者混在一起共用一套限流策略简单任务会被复杂任务挤掉复杂任务又容易把系统拖垮。分开限流之后资源分配问题基本解决。熔断方面我们对模型调用和工具调用增加了熔断机制。以模型调用为例如果某一路模型服务的错误率连续 10 秒超过 50%平台上对应的调用会被快速降级返回给编排层一个可识别的错误码由编排层决定是转用备用模型还是终止任务。这能有效防止一个下游服务的故障拖垮整个平台。队列监控方面我们给任务队列增加了堆积告警。当队列积压超过阈值时系统会自动扩容 Worker 数量如果扩容还跟不上就触发入口限流拒绝新任务进入保护已有任务不饿死。5. 排查 Agent 平台超时故障的实战清单5.1 排查顺序建议如果你也维护一个 Agent 平台或者类似的异步长任务系统遇到线上接口超时问题时建议按下面的顺序排查可以少走弯路第一确认入口网关的超时配置和客户端的超时配置是否匹配。很多超时问题源于网关设了 5 秒超时而上游服务或业务方设的是 3 秒。这种配置冲突会导致客户端先超时重试请求又打进来系统在没有任何真实故障的情况下被压垮。我见过的线上故障里至少有三分之一属于这一类。第二看核心线程池和连接池的使用情况。不要只盯着 CPU 和内存。要看业务线程池的活跃线程数、等待队列长度、线程拒绝次数。如果有大量线程处于“等待”状态大概率是外部依赖返回太慢。第三逐个排查外部依赖的耗时分布。Agent 平台里最典型的外部依赖是模型 API、工具 API、数据库和缓存。要分别看它们的 P99 耗时和错误率。如果发现某个依赖从某个时间点开始 P99 明显升高那基本就是瓶颈所在。第四打开任务状态表看是否有大量任务卡在“执行中”状态持续超过正常时长。这种情况说明 Worker 可能已经崩溃或挂起或者某个执行步骤进入了死循环。第五回看最近是否有发布或配置变更。Agent 这种多依赖系统一次配置修改比如模型超时时间从 3 秒改成 8 秒都可能导致下游堆积。5.2 几个常见坑位有些坑我们踩过之后发现只要在设计阶段提前避免后面就能省下很大的精力。第一个坑是任务没有超时上限。模型调用有超时工具调用有超时但是整个任务没有超时上限导致一个卡死的任务永远占用着一个 Worker 资源。后来我们给所有任务都加了一个最大执行时长超时后自动标记为失败并释放资源。第二个坑是重试没有考虑幂等。很多平台在接 Agent 能力时都只设了一个简单的重试次数完全没有考虑任务重复提交的问题。这个在 Agent 场景尤其致命因为一次任务可能在重试请求前已经执行了一部分外部操作重复执行等于重复操作一套业务流程。最安全的做法是在任务提交接口强校验唯一键同键任务在后端幂等处理。第三个坑是异步化改造不彻底。只把入口改成异步提交但内部仍然用同步方式等待编排结果那问题只是被后移了并没有真正解决。异步改造必须覆盖全链路——入口异步、执行异步、状态持久化异步、通知异步。第四个坑是限流粒度太粗。按照总请求量来限流并不能区分“简单任务”和“复杂任务”的成本差异。更合理的做法是按资源消耗来估算经过计算的配额分配给不同任务类型。5.3 给 Agent 平台设计者的几点建议经历过这次故障我对 Agent 平台的建设有了一些非常个人的理解。Agent 平台本质上是一个“长任务编排系统”而不是一个“高性能接口系统”。它的设计目标不应该是在多少毫秒内响应请求而应该是在多少秒内可靠地完成一件事。所有围绕“可靠性”的设计——队列、重试、超时、幂等、监控、限流——都比吞吐量更优先。如果你想把这个平台做扎实我建议在最早期就引入任务列表和状态机而不是到用户量上来以后再补。任务列表不只是给用户看进度用的它还是我们排查问题、重置卡死任务、统计业务量的基础数据资产。举一个具体的设计细节每个任务都要记录“上次活跃时间”。在排查卡死任务时这个字段可以直接过滤出那些执行了半小时还没有进展的异常任务根本不需要去翻日志。最后一点建议是所有依赖外部 IO 的地方都要有备选方案。模型服务可能在高峰期不稳定工具服务可能眠了。平台层面可以做降级策略某个模型挂了自动切到备用模型某个工具挂了任务可以带着部分结果返回而不是整体失败。Agent 的容错能力本质上决定了平台的上限。6. 写到最后想分享的体会这次超时故障解决之后我有很长一段时间都在复盘一个问题为什么一个概念上这么简单的线程池问题我们花了近一个小时才定位我觉得最根本的原因是我们对 Agent 平台的认识还停留在“模型调用服务”这个层面而实际上它已经是复杂的任务编排系统了。用一套处理短事务的思路去设计长任务平台必然会在某一天被现实教育。另外技术上的修复其实没有那么难难的是在故障发生时做出正确的取舍。当时我们其实还可以选择继续加超时、加线程数来扛流量但那只是在延长系统的生命力并不会改变它必然崩溃的结局。真正让这个平台变得稳定的是那一次果断的异步化改造。如果你的团队也正在做一个 Agent 平台或者准备把现有服务改造成支持多步骤 Agent 任务的形态我真心建议提前看看自己的架构里有没有这些隐患同步等待模型调用没有任务队列没有幂等设计没有任务级别的监控。这些看不见的问题往往比看得见的代码缺陷更容易在线上给你致命一击。希望我这次的踩坑过程能让你少走几步弯路。