Agent Platform 线上 504 故障复盘:LLM 调用链超时治理与全局时间预算实践

发布时间:2026/10/25 9:31:07
Agent Platform 线上 504 故障复盘:LLM 调用链超时治理与全局时间预算实践 1. 一次凌晨两点的504把我从平台思维拽回了工程现实Agent Platform 这个词这两年几乎被说烂了。不管是大厂的中台团队还是三五个人的小作坊只要沾上 LLM都想搭一个智能体平台——统一管理 Prompt、统一调度模型、统一编排工具链、统一做可观测。听起来很美我当初也是这么想的于是花了大概两个月把一套自认为还算能打的 Agent Platform 从零搭了起来前端一个可视化编排面板后端一套 Agent Runtime底下挂 DeepSeek 作为主力推理模型再配一层工具调用网关和会话记忆存储。上线第一周风平浪静第二周开始零星有用户反馈偶尔转圈很久然后报错。我当时没太当回事以为是前端 loading 状态没处理好。直到某个凌晨两点监控告警炸了——接口 504 大面积出现持续了将近四十分钟值班的同学在群里疯狂 我。那一刻我才意识到我搭的不是一个平台而是一个随时可能因为一个环节卡住就整体雪崩的脆弱系统。这篇东西不打算写成一份平台架构说明书那种文章网上太多了。我想做的是把这次真实的线上超时故障从头到尾拆开504 到底是怎么冒出来的、为什么我一开始的排查方向全错了、LLM 调用链里的超时和传统后端超时有什么本质区别、最后我是怎么定位并修掉的、以及修完之后我在 Agent Platform 的超时治理上做了哪些体系化的改造。如果你也在做 LLM 相关的平台或者智能体应用尤其是用 DeepSeek 这类大模型做推理后端的这篇应该能帮你少走一段我走过的弯路。需要先说明一点下面涉及的具体参数、阈值、代码片段都是基于我这次实际项目场景的合理还原和常见工程实践补充不同业务量级下数值需要你自己压测后调整不要照抄。2. 504 不是服务器挂了而是链路里有人等太久2.1 先搞清楚 504 到底是谁抛出来的很多人一看到 504 就下意识觉得是后端服务崩了其实不是。504 Gateway Timeout 的本质是网关或反向代理在约定的时间内没有从上游拿到完整响应于是主动掐断连接并返回 504。也就是说504 往往不是最终那个服务报的而是站在最前面的那个代理报的。在我的架构里请求链路大概是这样用户浏览器 - Nginx网关超时 60s - Agent Platform API业务后端 - Agent Runtime编排引擎 - LLM 网关 - DeepSeek 推理服务 - 工具调用网关 - 外部 API / 数据库 - 会话记忆存储Redis / 向量库Nginx 配的proxy_read_timeout是 60 秒。只要上游 60 秒内没吐完数据Nginx 就返回 504。所以 504 只是一个症状真正的问题在于这条链路上某个环节的耗时超过了 60 秒而且没有被任何一层提前拦截。2.2 为什么 LLM 场景的超时和传统后端完全不是一回事传统后端接口的超时通常是数据库慢查询、下游 RPC 卡住、锁竞争这几类耗时分布相对集中P99 一般也就几百毫秒到几秒。但 LLM 场景完全是另一个物种单次推理耗时天然就长。DeepSeek 这类模型一次完整生成动辄几秒到几十秒输出 token 越多越久而且这个耗时是正常的不是异常。耗时方差极大。同样一个 Prompt输入长度不同、输出长度不同、当前推理集群负载不同耗时可能从 3 秒跳到 40 秒。流式和非流式是两种完全不同的超时模型。流式SSE只要首 token 出来了后面是持续吐字网关的 read timeout 会被不断刷新非流式则要等整个响应生成完一次性返回超时风险高得多。Agent 场景是多次 LLM 调用的串联。一个 Agent 完成任务可能要调用 5 次甚至 10 次模型每次几秒到几十秒累加起来轻松突破任何单次超时阈值。我当初犯的最大错误就是用传统后端接口的超时思维去设计这套系统——给每个环节都设了一个看起来足够大的超时但没有考虑串联放大效应。单看每个环节都不超时串起来就爆了。2.3 那次故障的真实触发路径事后复盘那次 504 的触发路径是这样的某个用户提交了一个相对复杂的 Agent 任务需要多轮工具调用。Agent Runtime 第一轮调用 DeepSeek正常返回耗时 8 秒。第二轮需要调用一个外部工具 API那个 API 当时响应很慢但我的工具调用网关没有设超时于是一直等。等工具返回后第三轮又调用 DeepSeek这次因为推理集群负载高单次耗时 35 秒。此时距离请求发起已经过去了 50 多秒Nginx 的 60 秒大限将至。第四轮还没来得及发起Nginx 直接掐断返回 504。问题的核心不是某一环特别慢而是没有任何一层做全局的时间预算管理。每一层都只管自己那一小段没人对整个请求还剩多少时间负责。3. 排查方向错了三次才摸到真正的病灶3.1 第一次误判以为是 DeepSeek 服务不稳定故障刚发生时我第一反应是模型服务的问题。因为 504 集中出现在调用 DeepSeek 的链路上而且那段时间确实推理集群负载偏高。我做的第一件事是去翻 LLM 网关的日志看是不是大量请求卡在模型调用上。结果发现卡在模型调用上的请求确实有但不是全部。有相当一部分 504 的请求日志显示模型调用早就返回了卡住的是后面的工具调用环节。这就排除了单纯是模型慢这个假设。这里有个经验排查超时故障第一步永远是看时间花在哪了而不是猜谁最可能慢。我当时的日志粒度太粗只记录了每个阶段的开始和结束没有记录每个阶段的耗时导致我花了大半天才把时间分布拼出来。后来我强制要求所有关键节点都打上带耗时的结构化日志这是后话。3.2 第二次误判以为是并发太高把连接池打满了排除了模型问题后我转向怀疑并发。因为故障发生在凌晨理论上流量不高但监控显示那一刻确实有一波请求堆积。我怀疑是数据库连接池或者 HTTP 连接池被打满导致请求排队。查下来发现连接池使用率确实偏高但没到打满的程度。而且如果是连接池问题表现应该是请求排队等待而不是部分请求直接 504。更关键的是连接池等待是有上限的等不到会抛异常而不是静默地一直等。所以这个方向也不对。3.3 第三次才找对工具调用网关根本没有超时真正让我定位到问题的是我把那次故障期间所有 504 请求的完整链路日志拉出来按时间轴一条条对齐。对齐之后发现一个共同特征所有 504 的请求都在工具调用环节停留了异常长的时间有的甚至超过 40 秒。我去看工具调用网关的代码傻眼了——那个 HTTP 客户端创建的时候压根没设 timeout。用的是默认配置而某些 HTTP 库的默认超时是无限等待。也就是说只要外部工具 API 不主动断开连接我的网关就会一直等下去直到 Nginx 把整个请求掐掉。这就是典型的沉默的等待没有报错、没有异常、没有日志就是静静地卡在那里把整个请求的时间预算全部吃掉。3.4 一个被忽略的细节流式响应的假活跃排查过程中还发现一个很隐蔽的坑。有一部分请求走的是流式SSE返回理论上只要首 token 出来Nginx 的 read timeout 就会被不断刷新不应该 504。但实际上这些请求也 504 了。原因在于流式响应中间如果出现长时间没有数据吐出的空窗期网关的 read timeout 照样会触发。比如 Agent 在两次工具调用之间需要思考这段时间没有 token 输出如果空窗期超过 Nginx 的proxy_read_timeout连接照样被掐。这个坑特别阴因为你在应用层看日志请求明明还在正常处理但网关那边已经判定你死了。解决办法是在流式响应里定期发送心跳比如注释行或空事件保持连接活跃。这个后面细说。4. 修复方案从各管一段到全局时间预算4.1 核心思路给每个请求发一张时间预算表定位到问题后我的修复思路不是简单地把超时调大而是引入**全局时间预算Time Budget**的概念。具体做法是请求进入 Agent Platform API 的那一刻就根据请求类型分配一个总预算比如普通对话 30 秒、复杂 Agent 任务 90 秒。然后这个预算随着请求在链路中传递每一层在发起下游调用前都要先算一下我还剩多少时间并据此设置自己的超时。这样做的价值在于任何一层都不会无限制地等待因为它的等待上限是由全局剩余预算决定的而不是一个孤立的固定值。4.2 工具调用网关的超时改造最直接的修复是给工具调用网关加上超时。但这里有个细节不能简单设一个固定值而应该用剩余预算动态计算。import time import httpx class ToolGateway: def __init__(self, total_budget: float): # total_budget 是整个请求的总时间预算秒 self.deadline time.monotonic() total_budget def remaining(self) - float: return max(0.0, self.deadline - time.monotonic()) async def call_tool(self, tool_name: str, payload: dict): remaining self.remaining() if remaining 0: raise TimeoutError(f请求总预算已耗尽无法调用工具 {tool_name}) # 给单个工具调用分配剩余预算的一部分留出后续 LLM 调用的时间 tool_timeout min(remaining * 0.5, 15.0) async with httpx.AsyncClient(timeouttool_timeout) as client: resp await client.post( fhttps://tool-gateway/{tool_name}, jsonpayload, ) return resp.json()这段代码的关键点有三个deadline 用time.monotonic()而不是time.time()。monotonic 不受系统时钟调整影响做超时计算更可靠。单个工具调用只拿剩余预算的一部分这里取 50%上限 15 秒给后面的 LLM 调用留出空间。如果工具把预算全吃光后面模型就没时间了。预算耗尽时直接抛错而不是继续尝试。快速失败比慢慢等死要好得多至少用户能立刻收到明确错误。4.3 LLM 调用的超时与重试策略LLM 调用这块要复杂一些因为模型推理本身耗时波动大而且有时候慢是正常的。我的策略是场景首 token 超时整体超时重试策略普通对话非流式不适用剩余预算的 80%不重试直接降级流式对话10 秒剩余预算的 90%首 token 超时可重试 1 次Agent 单步推理15 秒剩余预算的 60%不重试走降级模型Agent 工具后推理15 秒剩余预算的 70%不重试这里有个反直觉的点LLM 调用我基本不重试。原因是模型推理本身耗时就不稳定重试一次意味着时间翻倍很容易把预算吃光。与其重试不如直接降级——比如从 DeepSeek 的大模型切到小模型或者返回一个当前繁忙请稍后重试的友好提示。如果确实要重试一定要用指数退避 抖动并且严格受剩余预算约束import asyncio import random async def call_llm_with_retry(prompt, remaining_budget, max_retries1): for attempt in range(max_retries 1): try: timeout min(remaining_budget * 0.6, 20.0) return await llm_client.generate(prompt, timeouttimeout) except TimeoutError: if attempt max_retries: raise # 指数退避 抖动避免重试风暴 backoff min(2 ** attempt random.uniform(0, 0.5), remaining_budget * 0.2) await asyncio.sleep(backoff)4.4 流式响应的心跳保活针对前面提到的流式空窗期被网关掐断问题解决办法是在流式响应中定期发送心跳。以 SSE 为例可以在 Agent 思考、调用工具的间隙每隔几秒发一个注释行async def stream_with_heartbeat(agent_task, interval5.0): # 启动心跳任务 async def heartbeat(): while True: await asyncio.sleep(interval) yield : heartbeat\n\n # SSE 注释行客户端会忽略 # 实际业务流和心跳流合并 async for chunk in merge_streams(agent_task(), heartbeat()): yield chunk心跳间隔要小于网关的 read timeout。比如 Nginx 配的是 60 秒那心跳设 5 到 10 秒就很安全。这个改动上线后流式请求的 504 基本消失了。5. 修完之后我在平台层面做的超时治理5.1 全链路超时配置的三层对齐单点修复只能解决眼前问题要防止复发得在平台层面把超时配置体系化。我总结了一个三层对齐原则网关层Nginx 的proxy_read_timeout是最后一道防线设得最大比如 120 秒。应用层Agent Platform API 的总预算要小于网关超时比如 90 秒留出网络传输和序列化的余量。下游层LLM 调用、工具调用、存储访问各自的超时加起来不能超过应用层预算。这三层必须满足下游各环节超时之和 应用层总预算 网关超时。任何一层配错都会导致上层以为还在处理下层其实已经放弃的错位。我见过太多项目Nginx 配 60 秒应用层某个下游调用配 120 秒结果就是下游还在慢慢跑网关早就把连接掐了日志里全是请求处理成功但客户端收到 504的诡异现象。5.2 超时可观测让每一次超时都有迹可循故障排查最痛苦的就是不知道时间花在哪。所以我在平台里加了一套超时可观测机制每个关键节点记录耗时用结构化日志字段包括trace_id、stage、duration_ms、remaining_budget_ms。超时事件单独打点包括超时发生在哪个阶段、当时剩余预算多少、下游是谁。按 trace_id 聚合能一键拉出某个请求的完整时间轴。有了这套东西下次再出超时我不用再花大半天拼日志直接看时间轴就知道卡在哪。5.3 降级与熔断超时不是终点而是分支超时发生后怎么办我的原则是超时不是异常而是一个需要处理的分支。具体做了几件事模型降级DeepSeek 大模型超时自动切到更小更快的模型保证有结果返回。工具降级外部工具超时返回缓存结果或默认值而不是让整个 Agent 任务失败。熔断某个下游连续超时达到阈值暂时熔断避免请求持续堆积。这里要特别注意降级要有明确的用户提示。用户能接受结果稍微差一点但不能接受莫名其妙拿到一个错误的结果还不知道为什么。所以降级发生时我会在响应里带上一个标记前端据此展示当前为简化结果之类的提示。5.4 压测把超时问题在线上之前就逼出来最后也是最重要的一点超时问题必须靠压测暴露不能靠线上运气。我后来搭了一套压测方案专门模拟各种极端场景模拟 LLM 推理慢注入延迟看链路是否按预期超时和降级。模拟工具 API 不响应挂起连接看网关是否会无限等待。模拟高并发看连接池、线程池是否成为瓶颈。模拟流式响应空窗看心跳机制是否生效。压测的价值在于它能在可控环境里把那些平时不出现、一出事就是大事的问题提前逼出来。那次故障之后我把压测纳入了每次发版的必做项再也没出过类似的线上超时事故。6. 几个我踩过、也希望你别踩的坑聊完体系化的东西最后分享几个具体的、血泪换来的经验点都是那种文档里不会写、但实际做 Agent Platform 一定会遇到的。第一个坑不要用固定超时值要用动态预算。我一开始给每个下游都设了固定超时比如工具调用 10 秒、LLM 调用 30 秒。看起来合理但 Agent 场景下请求复杂度差异极大简单请求 5 秒就完了复杂请求要 80 秒。固定值要么对简单请求太宽松浪费要么对复杂请求太严格误杀。动态预算才是正解。第二个坑超时时间要区分连接超时和读取超时。很多 HTTP 客户端只设一个 timeout其实应该分开。连接超时connect timeout通常设短一点比如 3 到 5 秒因为连不上就是连不上等再久也没用读取超时read timeout才是根据业务设的。我当初就是没区分导致连接阶段的问题被读取超时掩盖了。第三个坑日志里一定要有 trace_id而且要贯穿全链路。没有 trace_id排查超时就是大海捞针。我现在的做法是请求入口生成 trace_id通过上下文一路透传到所有下游调用和日志任何一个环节出问题都能串起来看。第四个坑小心重试放大。超时后重试是本能反应但在 Agent 场景下重试可能让本来只是慢的请求变成雪崩。因为重试意味着更多请求涌入下游更慢更多超时更多重试。所以重试一定要有上限、有退避、有熔断而且要在全局预算约束下进行。第五个坑别忽略流式响应的空窗期。这个前面说过了但值得再强调一次。流式不等于永远不会超时中间的空窗期照样会被网关判定为超时。心跳保活是必须的。第六个坑超时阈值要和业务方对齐不能自己拍脑袋。我当初设 60 秒网关超时纯粹是觉得60 秒够长了。但后来和产品聊才知道用户对 Agent 任务的耐心其实只有 30 秒左右超过就会觉得卡死了。所以超时阈值不只是技术问题更是体验问题得和业务一起定。这次线上超时故障前后折腾了我大概一周才彻底修干净。回过头看技术上的修复其实不难难的是思维方式的转变——从给每个环节设个超时到为整个请求管理时间预算从超时是异常到超时是必须处理的分支。Agent Platform 这类系统本质上是一个由多个不确定耗时环节串联起来的链路任何一环的失控都会传导到整体。把时间当成一种需要全局调度的资源来管理是我这次最大的收获。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询