尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

大模型请求超时后,重试机制如何不拖垮服务

大模型请求超时后,重试机制如何不拖垮服务 大模型请求超时后重试机制如何不拖垮服务文中的事故链路和数值均为说明性场景不对应特定线上事件上线标准应按实际压测和业务约束确定。大模型 API 的调用体验与传统微服务有着本质区别极高的延迟、不稳定的首 Token 时间TTFT以及频繁发生的 upstream 504 Gateway Timeout。当上游供应商的服务发生小规模网络抖动时绝大多数工程师的第一反应是在 SDK 外层加一个while retry_count 3的循环重试。这个看似理所当然的操作在真实的高并发生产环境中往往是毁灭性的。成百上千个微服务实例同时发起指数级增加的重试请求会将上游短暂的网络抖动瞬间放大为持续数小时的雪崩故障。把 LLM 调用的重试机制做对关键在于建立带重试预算Retry Budget与 Token 裁剪的工程拦截器。flowchart TD A[应用发起 LLM 请求] -- B{第一道防线重试预算 Token 桶} B -- 预算耗尽 -- C[直接抛出 429 / 返回降级兜底] B -- 预算充足 -- D[调用 LLM API] D -- E{响应状态评估} E -- 200 OK -- F[解析输出并返回] E -- 5xx / Timeout / 429 -- G{已达 Max 重试次数?} G -- 是 -- H[触发系统级熔断] G -- 否 -- I[第二道防线指数退避 Jitter 随机抖动] I -- J[第三道防线Prompt 自动压缩剪枝] J -- B周三下午 3 点的请求雪崩盲目重试导致 LLM API 线程池彻底锁死在一个用户量巨大的智能客服系统里上游模型供应商因为节点切换出现了持续 5 秒的响应延迟毛刺。原先的网关代码设置了简单的固定 1 秒超时重试。当首批 500 个请求超时后客户端立刻发起第二次重试而此时新一秒的 500 个并发请求又涌了进来。1 秒钟内后端线程池堆积的连接数从 500 激增到 1500 个。由于没有任何退避与抖动机制这 1500 个连接在同一时刻再次发送重试请求。供应商的 API Gateway 瞬间收到几倍于平时峰值的流量冲击直接激活限流规则返回 429 Too Many Requests。系统在不到 30 秒的时间内彻底锁死所有应用节点的 Asyncio 事件循环被密集的超时任务卡掉最终导致整条业务线瘫痪。指数退避加随机抖动避免成千上万重试请求同时冲垮 Gateway解决重试流量惊群效应Thundering Herd Problem的标准工程手段是结合 Full Jitter 的指数退避算法Exponential Backoff with Full Jitter。传统的固定间隔重试如每次隔 1 秒或者纯指数退避每次隔 1s, 2s, 4s, 8s虽然延长了等待时间但所有挂起请求的重试时间点依然是同步对齐的。注入 Full Jitter 之后第 $n$ 次重试的实际休眠时间 $T_{sleep}$ 计算公式为$$T_{sleep} \text{random}(0, , \min(T_{max}, , T_{base} \times 2^n))$$通过引入随机数原本在同一微秒发生的重试请求被均匀稀释到了一个连续的时间段内上游 API 网关接收到的流量脉冲被瞬间平滑化。Token 级断路器在重试时精简 Prompt 降低延迟与成本对于大模型请求而言重试不仅消耗 CPU 和连接句柄更在源源不断地消耗 Token 预算。如果上游延迟过高是因为 Prompt 包含过长上下文如 30,000 Token导致的 Prefill 阶段过慢你原封不动地重试 30,000 Token 的 Prompt 只会继续引发超时。聪明的重试拦截器应当具备Prompt 动态剪枝能力首次请求携带全量历史对话上下文包含最近 10 轮对话。第一次重试触发剪枝丢弃最早的 5 轮历史对话将 Prompt 压缩至 10,000 Token 内。第二次重试仅保留 System Prompt 与用户当前最后一轮提问将 Context 强制压到极限。Prompt 的体积减小了模型的 Prefill 计算量成倍下降重试成功的概率显著提升。import asyncio import random import time import logging from typing import List, Dict, Any, Optional logging.basicConfig(levellogging.INFO) logger logging.getLogger(ResilientLLMClient) class ResilientLLMClient: 面向生产环境的抗压 LLM 客户端 集成指数退避、Full Jitter 随机抖动、重试预算以及 Prompt 动态剪枝 def __init__( self, base_delay_sec: float 0.5, max_delay_sec: float 8.0, max_retries: int 3, retry_budget_ratio: float 0.2 ): self.base_delay_sec base_delay_sec self.max_delay_sec max_delay_sec self.max_retries max_retries # 重试预算计数器重试次数不能超过总请求次数的 20% self.total_requests 0 self.total_retries 0 self.retry_budget_ratio retry_budget_ratio def _allow_retry(self) - bool: 检查重试预算防止重试风暴引发系统级坍塌 if self.total_requests 10: return True current_ratio self.total_retries / self.total_requests return current_ratio self.retry_budget_ratio def _prune_messages(self, messages: List[Dict[str, str]], attempt: int) - List[Dict[str, str]]: Prompt 动态剪枝防线重试次数越多丢弃的历史上下文越多 if attempt 0 or len(messages) 2: return messages system_msg [m for m in messages if m[role] system] user_latest [messages[-1]] middle_history [m for m in messages[len(system_msg):-1]] # 根据重试次数按比例丢弃中间历史对话 keep_ratio 0.5 ** attempt keep_count int(len(middle_history) * keep_ratio) pruned_history middle_history[-keep_count:] if keep_count 0 else [] logger.info(f[Prompt Pruning] 第 {attempt} 次重试历史消息数由 {len(middle_history)} 剪枝至 {len(pruned_history)}) return system_msg pruned_history user_latest async def execute_chat_completion( self, mock_api_func, messages: List[Dict[str, str]] ) - Dict[str, Any]: 执行带保护的异步请求 self.total_requests 1 current_messages messages for attempt in range(self.max_retries 1): try: # 针对重试请求执行 Prompt 剪枝 if attempt 0: current_messages self._prune_messages(messages, attempt) # 发起真实 API 调用 start_time time.time() response await mock_api_func(current_messages) return response except (asyncio.TimeoutError, Exception) as exc: if attempt self.max_retries: logger.error(f[LLM Failure] 达到最大重试上限 ({self.max_retries})抛出异常) raise exc # 检查重试预算 if not self._allow_retry(): logger.warning([Retry Budget Exceeded] 重试预算已耗尽直接干预熔断禁止继续重试) raise RuntimeError(LLM Retry Budget Exceeded) from exc self.total_retries 1 # 计算带 Full Jitter 的退避等待时间 max_backoff min(self.max_delay_sec, self.base_delay_sec * (2 ** attempt)) sleep_sec random.uniform(0, max_backoff) logger.warning( f[LLM Retry Guard] 请求异常: {str(exc)} | f第 {attempt 1} 次重试将在 {sleep_sec:.2f}s 后发起... ) await asyncio.sleep(sleep_sec)幂等 Key 与请求去重防止重试导致后端生成重复 Task对于大模型执行写操作或复杂 Task 的场景例如 Agent 调用工具去下单或发送邮件重试极易引发重复执行问题。必须在请求头中注入Idempotency-Key幂等键。幂等键的推荐计算规则$$\text{Idempotency-Key} \text{SHA256}(\text{User_ID} \text{Session_ID} \text{Raw_Prompt})$$当客户端因网络超时重试时服务端拦截器通过 Redis 校验该Idempotency-Key。如果发现该请求已经在执行池中服务端不会启动二次推理而是挂起当前重试连接直接等待先前的计算任务返回结果实现绝对的幂等保护。线上闸门配置重试预算Retry Budget在拦截器中的落地重试绝不能是无限度的。工程上应当在系统全局设立重试预算Retry Budget。例如系统在过去 1 分钟内的重试请求数最多不得超过正常请求总数的 10%。一旦整个系统的重试占比超过 10%说明上游 LLM 供应商已经发生了大面积瘫痪。此时重试预算熔断器激活后续所有失败请求立即被拒绝直接回退到兜底的规则卡片。不盲目放大故障懂得在危机时刻优雅止损这才是一个成熟工程架构该有的克制与严谨。
返回列表