1. 一次线上504把我从“平台幻觉”里拽了出来
Agent Platform 这个词,过去一年在技术圈里被反复咀嚼。你随便打开一个技术社区,满屏都是“智能体编排”“多工具调用”“自主规划”“LLM 工作流”。但真正把一套 Agent Platform 推到线上、扛住真实流量的人,心里都清楚一件事:Demo 跑通只是入场券,线上超时才是真正的考试。
我这次踩的坑,就是一个非常典型的线上超时故障。表面上看是网关返回了 504,实际上背后牵扯到 LLM 调用链路、工具执行耗时、流式响应策略、连接池配置、超时传递机制等一整条链路。关键词里出现的 Agent Platform、504、超时、LLM、deepseek,恰好构成了这次故障的完整画像。
这篇文章适合谁看?如果你正在做 Agent 平台、正在接大模型 API、正在设计多步骤工具调用链路,或者你已经被“偶发超时”折磨过,那这篇内容应该能帮你少走一些弯路。我会把这次故障从现象、定位、根因、修复到复盘完整讲一遍,中间穿插参数计算、配置示例和实操心得。不是教科书,是一份带着现场灰尘的排障记录。
先说结论:Agent Platform 的超时问题,90% 不是单一环节慢,而是超时预算没有在整条链路上做统一分配。你给网关设了 60 秒,给 LLM 客户端设了 120 秒,给工具调用设了 30 秒,看起来每个都合理,但串起来就是灾难。下面我一步步拆。
2. 故障现场:504 不是突然出现的,是攒出来的
2.1 现象描述与第一反应
故障发生在一次常规版本发布后的第二天上午。监控大盘上,Agent Platform 的 P99 延迟从平时的 8 秒左右缓慢爬升到 40 秒,然后网关层开始零星出现 504。最开始是每 10 分钟几条,两个小时后变成每分钟几十条,用户侧表现为“智能体卡住不动,最后报错”。
我第一反应是去看 LLM 供应商的状态页。关键词里提到的 deepseek 是我们当时主要接入的模型之一,状态页显示正常。再看自己的服务指标:CPU 不高、内存稳定、GC 正常。这就排除了“机器扛不住”这种最粗暴的原因。
接着看日志,发现大量请求卡在tool_call阶段。具体来说,Agent 在规划完步骤后,会调用一个内部 HTTP 工具去查数据,这个工具平时 200 毫秒返回,故障期间偶尔要 20 秒以上。而 LLM 客户端那边,因为等工具结果,整个请求就一直挂着,直到网关的 60 秒超时触发,返回 504。
注意:504 是网关侧的超时信号,不代表后端一定死了。很多时候后端还在跑,只是网关等不及了。这时候如果你只盯着网关调大超时,只会把问题往后推,不会解决。
2.2 为什么“偶发”最可怕
这次故障最恶心的地方在于它的偶发性。不是所有请求都超时,大概 5% 到 8% 的请求会触发。这就导致几个问题:第一,压测很难复现,因为压测流量太“干净”;第二,日志分散,单看一条请求看不出问题;第三,团队里会有人觉得“是不是网络抖动,重试一下就好了”。
我当时的判断是:偶发超时背后一定有资源竞争或者长尾依赖。后来证明这个判断是对的。那个内部工具在特定参数下会触发一次全表扫描,平时数据量小没事,那天刚好有一批历史数据归档任务在跑,锁竞争导致查询变慢。Agent 的 LLM 调用又恰好没有设置合理的“工具调用超时”,于是一个慢查询拖垮了一整条 Agent 链路。
这里有个经验:Agent Platform 和传统 Web 服务最大的区别是,它的单次请求耗时是多个异构环节的叠加。LLM 推理可能 2 秒,工具调用可能 200 毫秒,但如果工具调用变成 20 秒,LLM 那边还在傻等,整个请求就废了。传统服务里你给数据库设个 3 秒超时就行,Agent 里你得给每个工具、每次 LLM 调用、每个规划步骤都设超时,还要保证它们能正确传播。
2.3 初步定位:从网关日志到链路追踪
我们用的是标准的链路追踪体系,每个请求有 trace_id。我拉了一批 504 请求的 trace,发现一个共同模式:请求在agent_plan阶段正常,进入tool_execute后开始变慢,然后llm_generate阶段一直不返回,最后网关超时。
具体时间线大概是这样的:
| 阶段 | 正常耗时 | 故障耗时 |
|---|---|---|
| 请求接入与鉴权 | 50ms | 50ms |
| Agent 规划(LLM) | 1.5s | 1.8s |
| 工具调用 | 200ms | 15s~25s |
| 结果汇总(LLM) | 2s | 一直等待 |
| 网关总耗时 | 约 4s | 超过 60s |
从表里能看出来,工具调用从 200 毫秒变成 20 秒,是第一个异常点。但真正致命的是第四阶段:LLM 客户端在等工具结果时,没有设置独立的超时,它继承的是整个请求的超时预算。而网关的 60 秒一到,直接切断连接,后端其实还在跑,资源没释放。
这就引出一个关键设计问题:超时应该分层设置,并且每一层都要有独立的熔断和降级策略。下面我会详细讲我们后来怎么改的。
3. 根因拆解:超时预算、连接池与 LLM 客户端的三角债
3.1 超时预算没有统一分配
先解释一个概念:超时预算(Timeout Budget)。你可以把它理解成“这个请求总共允许花多少钱”,每个环节花掉一部分,剩下的留给后面。如果前面花超了,后面就没钱可花,必须快速失败。
我们当时的配置是这样的:
- 网关超时:60 秒
- Agent 服务整体超时:无(依赖网关)
- LLM 客户端超时:120 秒
- 工具调用超时:无(依赖 HTTP 客户端默认)
- HTTP 客户端默认超时:无限制
你看出来问题了吗?网关 60 秒,LLM 客户端 120 秒,这意味着 LLM 客户端觉得自己可以等 120 秒,但网关 60 秒就把它砍了。工具调用没有超时,意味着它可以无限等。这就是典型的“各管各的”,没有全局视角。
正确的做法是反过来:从最外层往里分配。比如网关给 60 秒,那 Agent 服务最多用 55 秒,LLM 调用最多用 30 秒,工具调用最多用 10 秒,每个环节都要留出缓冲。这样即使某个环节慢了,也能快速失败并返回有意义的错误,而不是让网关一刀切。
我们后来改成了这样一套预算:
# 超时预算配置示例(伪代码,表达逻辑) gateway: timeout: 60s agent_service: total_budget: 55s planning_llm: timeout: 15s tool_execution: timeout: 8s retry: 1 summary_llm: timeout: 20s buffer: 12s # 留给序列化、网络抖动等这个 buffer 很重要。很多人算预算时算得刚刚好,结果序列化花了 2 秒,直接超。留 20% 左右的缓冲是经验值。
3.2 连接池耗尽引发的连锁反应
工具调用变慢之后,第二个问题出现了:连接池耗尽。我们的 Agent 服务用的是 HTTP 连接池,默认最大连接数 200。平时够用,但工具调用从 200 毫秒变成 20 秒后,每个连接被占用的时间变成原来的 100 倍。原来 200 个连接每秒能处理 1000 个请求,现在只能处理 10 个。请求开始排队,排队又导致更多请求超时,形成正反馈。
这其实是经典的“慢依赖拖垮调用方”问题。解决办法有两个方向:一是给工具调用设置更严格的超时和熔断,二是把连接池按依赖隔离,不要让一个慢工具影响其他工具。
我们后来做了连接池隔离:
# 按工具类型隔离连接池(示意) tool_pools = { "fast_query": Pool(max_connections=100, timeout=2), "slow_report": Pool(max_connections=20, timeout=10), "external_api": Pool(max_connections=50, timeout=5), }这样即使slow_report变慢,也只影响它自己的 20 个连接,不会把整个服务的连接池吃光。这个改动上线后,类似故障的影响面缩小了 80% 以上。
提示:连接池隔离不是银弹,它只是限制了爆炸半径。真正的解法还是让慢依赖快速失败,或者用异步非阻塞的方式调用。
3.3 LLM 客户端的“傻等”问题
第三个根因在 LLM 客户端。我们用的是 deepseek 的 API,客户端库默认没有设置工具调用阶段的超时。当 Agent 决定调用工具时,LLM 客户端会发起一个 HTTP 请求到工具服务,然后一直等响应。如果工具服务 20 秒才返回,LLM 客户端就等 20 秒。这本身没问题,问题是它等的时候,整个请求的其他部分都停着。
更麻烦的是,LLM 的流式响应和工具调用混在一起时,超时逻辑会更复杂。比如你开了流式输出,LLM 一边生成 token 一边决定调用工具,这时候如果工具慢,流就断了,但客户端可能还在等。
我们后来的做法是:把工具调用从 LLM 客户端里拆出来,做成独立的异步任务。Agent 规划完步骤后,不直接让 LLM 客户端去调工具,而是把工具调用请求发到消息队列,由独立的 worker 执行,执行完再回调。这样 LLM 客户端只负责生成,不负责等工具,超时逻辑就清晰了。
这个改动比较大,但效果很明显:LLM 客户端的 P99 从 40 秒降到 6 秒,工具调用的超时也能独立控制。
4. 修复实操:从配置到代码的完整落地
4.1 第一步:统一超时配置中心
我们做的第一件事,是把所有超时配置集中到一个配置中心,而不是散落在各个服务的代码里。这样改一处,全链路生效。
配置结构大概是这样:
{ "timeout_budget": { "gateway": 60000, "agent_total": 55000, "planning_llm": 15000, "tool_execution": 8000, "summary_llm": 20000, "buffer": 12000 }, "tool_specific": { "fast_query": 2000, "slow_report": 10000, "external_api": 5000 } }然后在代码里,每个环节启动时都去读这个配置,并且做校验:所有子超时之和不能超过父超时。这个校验很关键,我们后来加了一个启动检查,如果配置不合法直接拒绝启动,避免带病上线。
4.2 第二步:给工具调用加熔断和降级
光有超时还不够,因为超时是“等它慢”,熔断是“发现它慢就不等了”。我们给每个工具加了熔断器,规则是:10 秒内失败 5 次,熔断 30 秒。熔断期间,工具调用直接返回降级结果,比如“当前查询繁忙,请稍后重试”。
熔断器的参数需要根据业务调整。我们的经验是:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 统计窗口 | 10s | 太短容易误判,太长反应慢 |
| 失败阈值 | 5 次 | 根据 QPS 调整,QPS 高可以适当提高 |
| 熔断时长 | 30s | 给依赖恢复的时间 |
| 半开试探 | 3 次 | 熔断结束后放少量请求试探 |
降级策略也很重要。不是所有工具都能降级,比如支付类工具不能随便降级。我们的做法是给每个工具标记“是否可降级”,可降级的返回缓存或默认值,不可降级的直接快速失败并告警。
4.3 第三步:LLM 调用的重试与退避
LLM 调用本身也会超时,尤其是 deepseek 这种外部 API。我们的策略是:只对幂等的、非流式的调用做重试,流式调用不重试。因为流式调用重试会导致重复输出,用户体验很差。
重试参数:
retry_config = { "max_retries": 2, "backoff_factor": 0.5, "max_backoff": 5, "retry_on": ["timeout", "connection_error", "rate_limit"], "no_retry_on": ["invalid_request", "auth_error"] }退避算法用的是指数退避加抖动:第一次等 0.5 秒,第二次等 1 秒,加上随机抖动避免惊群。实测下来,重试能挽回大约 30% 的偶发超时,但前提是超时时间要设得合理。如果超时设成 30 秒,重试两次就是 90 秒,网关早挂了。所以重试必须和超时预算配合。
4.4 第四步:异步化工具调用
前面提到把工具调用拆出来,具体实现是这样的:
- Agent 规划完成后,生成一个工具调用任务,包含 trace_id、工具名、参数。
- 任务发到消息队列(我们用的是 Redis Stream,轻量够用)。
- 独立的 worker 消费任务,执行工具调用,设置自己的超时。
- 执行完成后,把结果写回 Redis,并通知 Agent 服务。
- Agent 服务收到通知后,继续后续的 LLM 汇总。
这个架构的好处是:工具调用的超时和重试完全独立,不会阻塞 LLM 客户端。坏处是增加了复杂度,需要处理任务丢失、重复消费等问题。我们加了幂等键和死信队列来兜底。
代码示意:
# 工具调用 worker 的核心逻辑 async def execute_tool_task(task): tool = get_tool(task.tool_name) try: result = await asyncio.wait_for( tool.execute(task.params), timeout=tool.timeout ) await redis.set(f"tool_result:{task.id}", result, ex=300) await redis.publish(f"tool_done:{task.trace_id}", task.id) except asyncio.TimeoutError: await redis.set(f"tool_result:{task.id}", {"error": "timeout"}, ex=300) await redis.publish(f"tool_done:{task.trace_id}", task.id)这个改动上线后,Agent 服务的平均响应时间从 8 秒降到 4 秒,P99 从 40 秒降到 12 秒。504 基本消失。
5. 常见问题与排查技巧实录
5.1 超时问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 网关 504,后端日志正常 | 网关超时小于后端耗时 | 对比网关和后端超时配置 | 统一超时预算 |
| 偶发超时,压测不复现 | 长尾依赖或资源竞争 | 看 P99 和慢查询日志 | 熔断+降级 |
| 工具调用越来越慢 | 连接池耗尽 | 看连接池活跃数和等待数 | 连接池隔离 |
| LLM 调用超时 | 外部 API 限流或网络抖动 | 看重试日志和供应商状态 | 重试+退避 |
| 流式响应中断 | 工具调用阻塞流 | 看流式日志的时间戳 | 异步化工具调用 |
5.2 几个容易忽略的坑
第一个坑:超时时间设成整数秒。很多框架默认超时是整数秒,但实际网络抖动可能是毫秒级。我们后来把所有超时改成毫秒,精度更高。
第二个坑:重试没有上限。有一次一个工具挂了,重试了 10 次,每次 5 秒,直接把请求拖到 50 秒。后来加了最大重试次数和总超时。
第三个坑:日志里没有记录超时时的上下文。超时发生后,你只能看到“timeout”,看不到当时请求的参数、工具名、trace_id。我们后来在超时异常里强制带上这些信息,排查效率提升很多。
第四个坑:只监控平均延迟,不监控 P99。平均延迟 2 秒看起来很好,但 P99 可能 30 秒。Agent 场景下,P99 才是用户体验的真实反映。
提示:建议给每个工具和 LLM 调用都单独打点,记录耗时分布。这样出问题时能快速定位是哪个环节的长尾。
5.3 一个真实的排查案例
故障当天,我拉了一个 trace,发现请求在tool_execute阶段卡了 22 秒。但工具服务的日志显示,这个查询只花了 200 毫秒。这就矛盾了。
后来发现,问题出在 Agent 服务和工具服务之间的网络代理上。代理层有一个连接池,默认最大连接数 50,故障期间被慢请求占满,新请求排队。工具服务本身很快,但请求根本没到工具服务。
这个案例告诉我们:排查超时问题时,不要只看两端,中间的任何一层都可能是瓶颈。代理、网关、服务网格、DNS,都可能引入额外延迟。我们后来在每一层都加了耗时打点,形成完整的链路视图。
6. 复盘:Agent Platform 的稳定性设计原则
6.1 超时是设计出来的,不是调出来的
很多人遇到超时就调大参数,60 秒不够就 120 秒,120 秒不够就 300 秒。这是最危险的做法,因为超时调大只是把问题延后,资源占用时间变长,系统更容易雪崩。
正确的做法是:先定用户体验目标,再倒推超时预算。比如用户能接受的最长等待是 30 秒,那网关就设 30 秒,然后往里分配。每个环节都要有超时,每个超时都要有降级。
6.2 异步化是 Agent 平台的必经之路
Agent 的天然属性是“多步骤、长链路、异构依赖”。同步串行执行所有步骤,在 Demo 阶段没问题,线上一定出问题。异步化不是可选项,是必选项。
异步化之后,你需要处理的新问题包括:任务状态管理、结果回调、幂等、死信队列、分布式追踪。这些复杂度是值得的,因为它换来了更好的隔离性和可观测性。
6.3 监控要覆盖“等待时间”而不只是“执行时间”
传统监控看的是服务执行时间,但 Agent 场景下,大量时间花在“等待”上:等 LLM 返回、等工具返回、等队列。这些等待时间如果不监控,你就不知道瓶颈在哪。
我们后来加了一个指标叫wait_time,专门记录每个环节的等待耗时。这个指标帮我们发现了连接池排队、队列积压等问题。
6.4 故障演练要常态化
这次故障之后,我们做了一件事:定期注入工具延迟,观察系统行为。比如把某个工具的响应时间人为调成 10 秒,看熔断是否触发、降级是否生效、告警是否及时。
第一次演练就发现了问题:熔断器配置了,但降级逻辑没写,熔断后直接抛异常,用户体验更差。后来补上了降级,再演练就顺畅了。
7. 最后分享几个实操小技巧
第一个技巧:给每个 LLM 调用加一个“最大 token 数”限制。有时候 LLM 生成失控,一直输出,导致请求超时。限制 token 数能有效控制单次调用时长。
第二个技巧:用asyncio.wait_for而不是asyncio.wait。wait_for会在超时后取消任务,wait不会。Agent 场景下,超时后必须取消任务,否则资源泄漏。
第三个技巧:在网关层加一个“快速失败”开关。当后端整体延迟升高时,网关可以主动拒绝部分请求,返回 503 而不是 504。503 比 504 更友好,因为 503 表示“服务暂时不可用”,504 表示“网关超时”,后者更容易让用户以为是自己网络问题。
第四个技巧:记录超时时的完整请求快照。包括请求参数、工具名、LLM 输入、trace_id。这些信息在排查时价值极高,但很多团队只在异常时记录堆栈,不记录业务上下文。
第五个技巧:定期 review 超时配置。业务在变,依赖在变,超时配置也要跟着变。我们后来定了一个规矩:每个季度 review 一次所有超时配置,确保它们仍然合理。
这次故障让我对 Agent Platform 的稳定性有了更清醒的认识。它不是把 LLM 和工具拼起来就完事,而是要在超时、熔断、降级、异步化、可观测性上做大量细致的工作。希望这篇记录能帮到正在踩类似坑的你。如果你也在做 Agent 平台,欢迎交流,尤其是那些“偶发但致命”的问题,往往最有价值。