资讯动态

Agent平台504故障复盘:超时叠加模型与四层治理方案

发布时间:2026/10/27 8:32:58 来源:尧图企业网站定制
1. 一次线上504把我从平台建设者打回排障工程师Agent Platform 这个项目我从零搭到上线前后大概花了三个月。平台的核心定位很明确把 LLM 智能体的编排、工具调用、会话记忆、任务调度这几件事做成一套可复用的底座让业务方不用每次都从零写一套 Agent 逻辑。技术栈上编排层用 Python 写模型侧接的是 DeepSeek 系列工具调用走标准的 function calling 协议前面挂了一层网关做鉴权和限流后面是 Redis 做会话状态、Postgres 存任务元数据。上线第一周风平浪静第二周开始出问题。某天下午两点多监控告警开始密集响起来——网关侧 504 比例从 0.3% 直接飙到 18%持续了大概七分钟。那七分钟里所有走 Agent Platform 的业务方都在报请求超时客服群里消息刷屏。我当时的第一反应是网关挂了但登上去一看网关进程活得好好的CPU 和内存都很正常问题不在网关本身。这次故障让我彻底明白一件事Agent 类系统的超时和传统 Web 服务的超时完全不是一个物种。传统服务超时你去看数据库慢查询、看下游接口 RT 基本就能定位但 Agent 系统的超时是一条链路上多个环节叠加的结果——LLM 推理本身耗时、工具调用可能卡住、多轮循环可能失控、流式响应可能中途断掉。任何一个环节出问题最终都会以 504 的形式暴露在用户面前但根因可能藏在完全不同的地方。这篇内容我想把这次故障的完整排查链路、根因定位过程、以及后续做的一整套超时治理方案讲清楚。适合正在做 Agent 平台、LLM 应用后端、或者任何涉及多环节串联调用系统的同学参考。不管你是刚接触 Agent 开发还是已经在生产环境跑了一段时间这里面的坑和思路应该都能对上号。2. 504 只是表象Agent 请求链路的超时叠加模型2.1 一条 Agent 请求到底经过了哪些环节要理解超时先得把一条请求的完整生命周期拆开。以我们平台上一个典型的带工具调用的多轮对话为例一次用户请求会经过这些环节网关接入层鉴权、限流、请求体解析正常耗时 5-20ms编排层预处理加载会话历史、拼装 system prompt、注入工具定义正常 20-50msLLM 首轮推理把 prompt 发给 DeepSeek等模型返回这一步耗时波动最大工具调用解析模型返回 function call 后解析参数、校验合法性正常 5-10ms工具实际执行可能是查数据库、调外部 API、读文件耗时完全取决于工具本身LLM 次轮推理把工具结果塞回上下文再让模型生成最终回复流式响应回传如果开了 streaming还要考虑 token 逐个吐出的时间如果 Agent 需要多轮工具调用比如先查天气、再查航班、再综合推荐那第 3 到第 6 步会循环多次。每一轮循环都是一次超时风险的叠加。2.2 为什么 Agent 的超时不能用单一阈值来卡传统 Web 服务设超时很简单下游接口 RT 是 200ms那我设 1s 超时绰绰有余。但 Agent 系统不行因为各环节的耗时分布差异极大环节正常耗时长尾耗时波动来源网关接入5-20ms100msGC、连接池争抢编排预处理20-50ms500ms会话历史过长、Redis 抖动LLM 推理1-8s60s模型负载、prompt 长度、输出长度工具执行10ms-2s30s外部依赖、网络、数据量流式回传与输出长度成正比受客户端影响客户端读取速度你看LLM 推理这一项的正常耗时就已经是网关的几百倍了。如果你用一个统一的超时阈值去卡整条链路要么卡太紧导致正常请求被误杀要么卡太松导致真正卡死的请求占用资源太久。我们当时犯的第一个错误就是网关层设了一个 30s 的全局超时编排层没有独立超时LLM 客户端用的是默认无超时。这个配置组合在低峰期没问题一旦 LLM 侧出现排队编排层就会一直等网关 30s 到了直接返回 504但编排层的协程还在后台挂着继续等 LLM 返回。结果就是用户看到的是 504但服务端的资源还在被这些僵尸请求占用越积越多形成恶性循环。2.3 超时叠加的数学本质这里有个很关键的认知串联链路的整体超时概率是各环节超时概率的累积。假设一条 Agent 请求经过 5 个环节每个环节单独看超时概率只有 1%那整条链路不超时的概率是 0.99^5 ≈ 95.1%也就是说整体超时概率变成了接近 5%。如果某个环节在高峰期超时概率升到 5%那整条链路超时概率就是 1 - 0.95 × 0.99^4 ≈ 8.8%。这还只是单轮的情况。如果 Agent 平均要循环 3 轮工具调用那环节数直接翻三倍超时概率会进一步放大。这就是为什么 Agent 系统的 504 比例往往比传统服务高一个数量级——不是某个环节特别差而是环节太多每个环节的小概率事件被累积放大了。理解了这一点后面的治理思路就清晰了要么减少环节数要么给每个环节设独立的、合理的超时要么让超时能够快速失败而不是傻等。3. 从监控大盘到根因那次故障的完整排查链路3.1 第一层排查确认 504 是网关产生还是上游产生故障发生时我做的第一件事是区分 504 的来源。网关返回的 504 有两种可能一是网关自己等上游超时了二是上游服务自己返回了 504 被网关透传。这两种情况的排查方向完全不同。我们网关用的是 Nginx 做反向代理proxy_read_timeout设的是 30s。查 Nginx 的 error log看到大量这样的记录upstream timed out (110: Connection timed out) while reading response header from upstream这说明是网关等编排层响应超时了不是编排层主动返回的 504。方向锁定问题在编排层或更下游。3.2 第二层排查编排层的请求都卡在哪一步编排层是 Python 写的用的是 FastAPI asyncio。我当时的做法是在编排层加了一个临时的耗时打点把每个环节的耗时都记到日志里。因为故障已经过去了我用的是事后补的埋点加上复现的方式——在测试环境用相同的并发量压了一遍。压测结果很说明问题网关到编排层的请求有 15% 左右在 30s 内没有返回这些卡住的请求绝大部分卡在等待 LLM 响应这一步进一步看卡住的请求有个共同特征prompt 特别长普遍在 8000 token 以上到这里基本能确定LLM 推理环节在长 prompt 场景下耗时暴涨超过了网关的 30s 阈值。3.3 第三层排查为什么长 prompt 会让 LLM 耗时暴涨这一步需要理解 LLM 推理的耗时构成。一次推理的耗时大致由两部分组成Prefill 阶段处理输入 prompt计算 KV cache。这个阶段的计算量和 prompt 长度成正比而且是一次性的大矩阵运算Decode 阶段逐个生成输出 token。这个阶段的耗时和输出长度成正比每个 token 都要做一次前向计算长 prompt 主要影响 Prefill 阶段。8000 token 的 promptPrefill 耗时可能是 2000 token 的 4 倍甚至更多因为注意力机制是 O(n²) 的复杂度。如果模型服务本身负载高Prefill 排队那耗时还会进一步拉长。我们平台的问题在于会话历史没有做截断和压缩。有些用户的会话跑了几十轮历史消息全量拼进 prompt导致 prompt 越来越长。平时可能 5s 能返回高峰期模型负载一上来直接飙到 40s。3.4 根因确认与故障时间线还原把上面的线索串起来故障的完整时间线是这样的下午两点左右某几个业务方的会话进入了长对话阶段prompt 长度普遍超过 8000 token同时段模型服务侧有一定负载具体原因不在我们可控范围Prefill 排队时间变长长 prompt 排队导致单次 LLM 推理耗时从正常的 3-5s 涨到 35-50s编排层没有独立超时一直傻等 LLM 返回网关 30s 超时先触发返回 504 给用户但编排层的协程还在等占用连接和内存新请求不断进来编排层协程池逐渐被占满正常请求也开始排队恶性循环504 比例飙升根因不是单一的而是三个问题叠加prompt 无截断、编排层无独立超时、网关超时与编排层超时不匹配。这里有个很重要的经验Agent 系统的故障往往是配置问题而非代码 bug。代码逻辑是对的但超时配置的组合方式错了照样出大事。4. 超时治理的四个层次从被动等待到主动控制4.1 第一层给每个环节设独立超时且要内紧外松故障之后我做的第一件事是把整条链路的超时重新设计了一遍。核心原则是内层超时必须比外层短这样内层超时后可以快速失败、释放资源而不是让外层等到超时。具体配置层级超时设置说明网关60s最外层给足余量编排层整体50s比网关短 10s留出回传时间LLM 单次推理25s单次推理的上限工具单次执行10s工具调用的上限Redis 操作500ms会话读写Postgres 查询2s任务元数据这样设计的好处是任何一层超时都会在更外层超时之前触发编排层有机会做清理释放协程、记录日志、返回友好错误而不是被网关一刀切。4.2 第二层LLM 推理的超时要能中断而不是放弃这里有个技术细节值得展开。Python 的 asyncio 里如果你用asyncio.wait_for给一个协程设超时超时后协程会被 cancel。但问题是HTTP 请求的 cancel 不一定能真正中断服务端的推理。我们用的是 httpx 做 LLM 调用。当wait_for超时触发 cancel 时httpx 会关闭连接但模型服务端可能还在继续推理因为它不知道客户端已经走了。这就导致服务端资源被浪费。更优雅的做法是在请求头里带上超时提示让服务端自己控制。不过这个取决于模型服务是否支持。我们后来的做法是双管齐下客户端设 25s 超时同时在 prompt 里控制输出长度通过 max_tokens 参数从源头减少推理时间。4.3 第三层会话历史的截断与压缩策略这是解决长 prompt 问题的根本手段。我们试过几种方案方案一滑动窗口截断。只保留最近 N 轮对话超出的直接丢弃。简单粗暴但会丢失早期上下文。方案二摘要压缩。把早期对话用 LLM 总结成一段摘要拼在 prompt 开头。效果好但增加了一次 LLM 调用反而可能加剧超时。方案三分层保留。最近的 3 轮保留完整内容更早的只保留用户消息和助手回复的第一句话再早的只保留角色标记。最终我们用的是方案三的变体配合一个 token 计数当 prompt 总 token 超过 6000 时自动触发压缩。压缩后的 prompt 控制在 4000 token 以内。实测下来Prefill 耗时从平均 8s 降到了 2s 左右。4.4 第四层熔断与降级让系统在压力下优雅地失败超时治理的最后一层是熔断。当 LLM 推理的超时率超过某个阈值我们设的是 5 分钟内超时率 20%自动触发熔断新请求直接返回服务繁忙请稍后重试不再排队已经进入编排层的请求如果还没开始 LLM 调用直接快速失败熔断期间后台持续探测 LLM 服务的健康度恢复后自动解除降级策略则是对于非核心的 Agent 功能比如推荐相关话题熔断期间直接跳过只保证核心对话功能可用。这套机制上线后再遇到类似的 LLM 侧波动系统不会再出现雪崩最多是部分请求快速失败整体可用性保住了。5. 那些只有踩过才知道的坑超时治理的实操心得5.1 坑一asyncio 的 cancel 不是万能的我一开始以为asyncio.wait_for超时后协程就干净地结束了。实际上如果协程内部有try/finally或者except块cancel 信号可能被吞掉。更麻烦的是如果协程正在执行同步阻塞操作比如某些 SDK 的内部调用cancel 根本不会生效协程会一直跑到阻塞操作结束。应对方法所有可能阻塞的操作都要用run_in_executor包起来并且给 executor 设超时。另外在协程里加asyncio.shield保护关键清理逻辑确保 cancel 时资源能正确释放。5.2 坑二流式响应的超时和普通请求不一样开了 streaming 之后超时的判断逻辑变了。普通请求是等完整响应流式请求是等第一个 token和等后续每个 token。如果第一个 token 迟迟不来那是 Prefill 慢如果 token 之间间隔太长那是 Decode 慢或者网络问题。我们后来给流式请求设了两个超时首 token 超时 15stoken 间隔超时 5s。任何一个触发就中断流并返回错误。这样能更精准地定位问题也避免用户对着一个卡住的流干等。5.3 坑三重试策略可能让超时雪上加霜故障初期我们配了自动重试LLM 调用失败就重试 2 次。结果发现在 LLM 侧负载高的时候重试反而加剧了拥堵——本来一次请求要等 30s重试三次就是 90s而且每次重试都在给已经过载的模型服务加压。正确做法重试要配合熔断和退避。我们后来的策略是只在特定错误码如连接失败时重试超时不重试重试间隔用指数退避且总重试时间不能超过整体超时预算。5.4 坑四监控埋点本身也会影响性能为了排查问题我在编排层加了很多耗时打点。结果发现高频打点每个环节都记一次在高并发下反而成了瓶颈日志写入的 IO 等待让请求耗时增加了 10% 左右。应对方法打点要采样不要全量。我们后来改成 1% 采样 慢请求全量记录超过 5s 的请求强制记录详细打点。这样既不影响性能又能抓到问题请求。6. 写给正在做 Agent 平台的同行几个可以抄的配置模板6.1 超时配置的参考模板如果你也在做类似的 Agent 平台下面这套配置可以直接参考数值需要根据你的实际 RT 调整# 编排层超时配置 TIMEOUT_CONFIG { gateway: 60, # 网关层 orchestrator_total: 50, # 编排层整体 llm_single_call: 25, # 单次 LLM 推理 llm_first_token: 15, # 流式首 token llm_token_interval: 5, # 流式 token 间隔 tool_execution: 10, # 单次工具执行 redis_op: 0.5, # Redis 操作 db_query: 2, # 数据库查询 max_agent_loops: 5, # 最大循环轮数 }关键点max_agent_loops 一定要设。我们一开始没设结果有个 Agent 陷入了工具调用循环一直调同一个工具直到网关超时才停。设了最大轮数后超过就直接返回任务过于复杂请拆分后重试。6.2 会话压缩的触发条件# 会话历史压缩策略 COMPRESSION_CONFIG { max_prompt_tokens: 6000, # 触发压缩的阈值 target_prompt_tokens: 4000, # 压缩后的目标 keep_recent_rounds: 3, # 保留完整内容的最近轮数 summary_max_tokens: 500, # 摘要的最大长度 }这个配置的核心逻辑是在 prompt 变长之前就主动压缩而不是等超时了再补救。6.3 熔断器的参数设置# 熔断器配置 CIRCUIT_BREAKER { window_seconds: 300, # 统计窗口 5 分钟 min_requests: 50, # 最少请求数才触发判断 error_rate_threshold: 0.2,# 错误率阈值 20% open_duration: 30, # 熔断持续时间 30s half_open_requests: 5, # 半开状态试探请求数 }熔断器的关键是窗口内最少请求数这个参数。如果不设低峰期偶尔一两个超时就会触发熔断反而影响可用性。6.4 一个容易被忽略的点客户端超时要和服务端对齐我们后来发现有些业务方的客户端设了 10s 超时但服务端整体预算是 50s。结果服务端还在正常处理客户端已经超时断开用户看到失败但服务端资源还在消耗。建议在 API 文档里明确写出服务端的超时预算让业务方把客户端超时设得比服务端略长比如服务端 50s客户端设 55s。这样能避免服务端还在干活客户端已经放弃的浪费。7. 故障之后我对 Agent 平台可靠性的一点个人体会这次故障之后我重新审视了整个平台的可靠性设计。最大的体会是Agent 系统的可靠性不取决于最慢的那个环节有多快而取决于最快的失败路径有多快。传统服务的思路是让每个环节都尽量快但 Agent 系统里 LLM 推理的耗时你控制不了模型服务的负载你也控制不了。你能控制的是当某个环节变慢时系统能不能快速感知、快速失败、快速恢复。所以后来我把重心从优化平均耗时转到了优化失败路径超时配置内紧外松、熔断快速触发、降级策略明确、监控采样精准。这些做完之后平台的 P99 耗时其实没怎么变但可用性从 99.5% 提到了 99.95%——因为那些原本会拖垮整个系统的长尾请求现在都被快速隔离了。如果你正在做类似的事情我的建议是先把超时和熔断做对再去优化性能。性能优化是锦上添花超时治理是雪中送炭。一次线上故障带来的教训比十篇架构文章都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑