ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型调用稳定性工程:重试、限流、降级与成本控制实践

大模型调用稳定性工程:重试、限流、降级与成本控制实践 接手过几个把大模型接进生产环境的项目之后我才真正理解一件事大模型调用这件事从“能通”到“能扛得住”中间差的不是模型选得多好而是一整套围绕稳定性、成本和体验的工程兜底缺一个环节都可能出事故。我第一次在日请求量几万次的系统里做 AI 客服问答时线上动不动就冒出超时告警用户反馈最多的是“问着问着就卡住”“答到一半断了”。后来我花了大半个季度去补课核心就是把大模型调用的重试、限流、降级与成本控制这四件事拆开、想透、落地。这篇文章就是我当时踩坑之后的完整复盘包含原理、代码、参数和很多只有实际跑过才会注意到的细节希望能帮正准备把大模型真正用起来的团队少走点弯路。1. 整体设计思路这四件事为什么必须一起考虑1.1 一次普通的大模型调用里藏着哪些失败模式很多人初次对接大模型 API 时会默认它“和普通 HTTP 接口一样”发一个请求等一个返回最多 catch 一下异常。但实际跑起来你会发现失败的方式五花八门。以我维护过的对话服务为例一条链路从业务后端发起到上游模型返回结果中间至少隔着四层业务服务到网关的网络路径、网关到模型服务的调度、模型实例的排队与推理、最后 tokens 的返回。任何一层抖动前端看到的就是一句“模型请求失败请稍后重试”。常见的错误场景可以归纳成几类瞬时网络问题连接超时、半包、DNS 解析抖动特征是不重试就恢复得很快。上游限流HTTP 429说明你在某个时间窗口内请求太多被排队或拒绝。模型服务不可用/过载5xx 类错误或者网关返回 503/504。这种情况并不意味着你的请求内容有问题而是服务端暂时扛不住。参数或鉴权错误400、401、403属于“重试一万次也没用”的类型必须修代码而不是重试。部分流式响应中断SSE 流在传输中途断开用户已经看到了前半段文字丢掉上下文会造成体验问题处理起来比重试普通接口更讲究。这些失败模式对应到工程上就是重试要去覆盖“重试能救回来”的部分限流去控制“对上游的访问频率”降级去兜底“完全不可用时用户的体验”成本控制则管住“这一切都在预算内”。四者不是四个独立组件而是一条链路上的四道闸门。1.2 只做其中一个会出什么问题有人会问我直接把重试做好是不是就够了不够。我早期犯过一个典型错误给调用层加了无脑重试超时就重试 3 次结果上游限流的时候重试加剧了流量压力原本只是部分请求失败最后变成整个服务被限流反而影响了更多正常请求这就是“重试风暴”。反过来只做限流而不做降级也不行。限流本质上是把超出预算的请求挡在外面但如果被挡住的请求没有一个兜底逻辑用户体验就是从“偶尔卡顿”变成“大面积不可用”。我以前上线过一个功能设置每分钟最多调用模型 200 次量一旦超了就直接抛异常前端显示“服务繁忙”结果活动期间的流量稍微一冲用户就开始集中投诉。成本控制也一样不做成本控制模型调用费用会以非常隐蔽的方式上涨。别以为只有每天几百万请求的大厂才会遇到这个问题一个小团队跑一个 7×24 小时的自动化脚本prompt 写得不严谨上下文越长 token 消耗越大月底账单翻倍的现象非常常见。所以这四个机制的正确关系是限流守住上游和预算红线重试消化瞬时抖动降级兜住极端故障成本控制让整条链路跑得长久。它是一个组合拳而不是单选题。1.3 一条建议的设计原则先保住体验再追求最优我自己做完这几轮改造后总结了一条核心原则调用链路的设计优先级应该是“用户可感知的体验”高于“单次调用的最优结果”。什么意思呢就是当大模型不好用的时候用户并不关心你到底调了哪个模型、用了多长的上下文他们只关心有没有得到响应、响应快不快、答案对不对。因此降级方案的核心冲击点不是让用户看到报错而是让用户得到一个替代答案哪怕是命中缓存里的相似问题答案都比让用户干等超时好。这个原则还会指导你决定降级的顺序、缓存的设计和超时的设置。后文里我反复用到的思路都是这个不要把“调用大模型”当成一次性动作而要把“响应用户”当成真正的目标。2. 重试策略怎么重试才不吃大亏2.1 第一步是区分哪些错误值得重试很多重试实现最大的问题就是“一视同仁”。不管什么异常都要重试三次等于把代码剪在一团毛线里。正确做法是先做错误分类。我通常维护一张理性映射表在代码里直接查表决定当前错误对该请求的处理方式。错误类型HTTP 状态码是否可以重试处理建议参数错误/鉴权失败400 / 401 / 403否记录日志、告警、修改配置或提示用户超出上下文长度400部分服务返回具体 code否截断或压缩上下文后重新请求请求速率超限429可以但要退避使用指数退避并考虑全局限流服务端内部错误500 / 502可以有限次数重试配合总超时预算服务不可用/过载503 / 504可以退避重试并触发熔断降级逻辑网络超时 / 连接中断由客户端判定可以注意幂等性与流式状态的判断你可以直接把这个表抄进自己的调用封装里。我的实现方式是写一个is_retryable()方法先看异常类型再看状态码最后看错误码三个条件都通过了才进入重试逻辑。2.2 指数退避和抖动重试间隔不是拍脑袋定的“失败后立即重试”为什么不行因为如果你的请求是因为上游服务过载而失败的那么立刻接着再打一次只会加重过载。正确做法是让重试间隔随时间指数增长第一次失败后等 0.5 秒第二次等 1 秒第三次等 2 秒以此类推。基础公式是delay min(初始间隔 * 2^尝试次数, 最大间隔)这个公式已经很好了但在高并发场景下还缺一个关键因素抖动jitter。没有抖动时同一时间失败的一批请求会同时进入退避窗口退避结束后它们又会同时发起下一次重试再次形成对上游的流量尖峰叫做“惊群效应”。加入抖动后重试时间会被拆散核心公式变成import random def compute_backoff(attempt, base_delay0.5, max_delay30): exponential min(base_delay * (2 ** attempt), max_delay) # 全抖动在 [0, exponential] 之间随机取一个值 return random.uniform(0, exponential)这段代码的意思是把退避间隔随机化到从 0 到指数上限之间。这样即使 1000 个请求同时失败它们的重试间隔也会分散开上游才有机会恢复。我实测在并发压力测试下加不加抖动对上游限流触发率的影响可以达到 30% 以上。2.3 重试次数和总超时预算必须锁死重试次数不是越多越好。我建议给“单次请求的完整生命周期”定一个总预算比如 5 秒、10 秒或 20 秒超过这个时间还没成功就直接返回降级结果而不是继续重试。具体来说一次性给所有重试次数加上一个总超时预算import time timeout_budget 10 # 总预算 10 秒 attempt 0 max_attempts 4 start time.time() while attempt max_attempts: remaining timeout_budget - (time.time() - start) if remaining 0: raise TimeoutError(重试总预算耗尽) try: return call_llm(payload, timeoutmin(3, remaining)) except RetryableError as e: attempt 1 sleep_time compute_backoff(attempt) time.sleep(min(sleep_time, remaining))注意这里有一个细节每次真正调用模型时的单次超时也要根据剩余预算动态调整避免最后一次重试还没开始就把预算耗光了。这些看起来是小优化但在生产环境里非常关键。2.4 重试时的幂等性别把用户消息发两次大模型接口本身大多不保证幂等尤其是一些非标准接入通道。如果你的用户提交了一条指令第一次请求超时了重试时如果无脑重发同一个请求理论上用户可能收到两次扣除费用或者系统里产生两条重复记录。我的处理方式是在业务层生产一个全局唯一的 request_id每次调用前把 request_id 带到请求头或请求体里同时记录下来第一次请求和重试请求之间的关联。很多大模型网关服务包括私有化部署的网关支持按 request_id 做去重。如果没有这个能力至少你的业务侧要保证写入、记账这类操作是幂等的用同样的唯一键判断“这个请求已经处理过了”就不要再处理一次。另外流式请求的重试更特殊。如果用户已经收到了部分流式响应链路断了这时候重发整个请求可能让用户看到两遍前一半的内容。我的做法是如果已经返回了超过 N 个 token就不再重试而是走降级逻辑给用户一个“回答可能不完整请重试一次”的提示。如果还没返回任何 token再按正常重试处理。2.5 实战心得重试参数到底怎么定我后来沉淀下来的默认参数是这样的初始退避 0.5 秒重试次数最多 3 到 4 次最大退避上限 30 秒单次总预算 10 到 15 秒用指数退避加全抖动。这个组合在多数对话场景里都能把瞬时错误恢复到 99% 以上。但要强调一点这些参数不是通用的。如果是内部工具链里的批处理任务可以放宽到重试 5 次、总预算 60 秒如果是用户实时对话总预算超过 8 秒用户就已经明显感知到“卡”所以预算要更激进。建议你自己拿历史错误日志回放一遍把超时分布跑出来再决定参数。3. 限流保护上游和钱包3.1 限流要解决的问题不只是“被上游限制”很多人理解限流就是“防止接口被调用太多次”这个理解没有错但到上层面是不够的。对大模型调用来说限流有两个层次。第一层是遵守上游服务的速率限制。OpenAI 和各家大模型服务普遍按 RPM每分钟请求数和 TPM每分钟 token 数来限流你超过了就返回 429。这一层的目的是让你的调用不会把上游打挂也能避免自己总是被限流。第二层是控制自己的成本预算。大模型调用是稀缺计算资源每一次调用都要花真金白银。没有限流一个写歪了的定时任务可能在半夜调用几千次模型月底账单出来才发现。限流在这里起的作用是“预算闸门”即使是凌晨的批处理任务也必须遵守全局的每分钟/每小时/每天配额。3.2 限流算法怎么选令牌桶最实用限流算法有几个常见的选择我结合自己用过的情况简单说说。固定窗口计数器每分钟计数一次超过 100 次就拒绝。实现最简单但存在“边界尖峰”问题比如第 59 秒到第 60 秒之间两个窗口各计了 100 次实际相隔 1 秒内可能通过 200 次请求。滑动窗口/滑动日志用时间戳列表记录请求精确度更高但内存开销大对于大流量接口不友好。令牌桶以固定速率向桶里放令牌请求需要拿到令牌才能通过。优点是允许短时突发流量同时限制长期平均速率非常适合大模型场景——因为大模型场景本身是稀疏但单次耗时长的突发一点点没有关系。我实际用的基本都是令牌桶。在代码层面可以直接使用现成的限流库。Java 生态里 Guava 的RateLimiter就是令牌桶的实现Python 生态可用的库也非常多比如limits库支持 memory、redis 存储。3.3 令牌桶实现示例一个非常基础的令牌桶实现思路是维护tokens和last_refill_time每次请求前先按时间补令牌import time class TokenBucket: def __init__(self, capacity, refill_rate): self.capacity capacity self.refill_rate refill_rate self.tokens capacity self.last_time time.monotonic() def try_acquire(self, count1): now time.monotonic() elapsed now - self.last_time self.tokens min(self.capacity, self.tokens elapsed * self.refill_rate) self.last_time now if self.tokens count: self.tokens - count return True return False生产环境建议把令牌桶放到 Redis 里做全局限流尤其是多实例部署的时候。单机限流在多副本跑同一个任务时很容易失效每台机器都允许 100 次三台机器就是 300 次直接打爆上游。用 Redis 实现时可以基于.lua脚本保证原子性也可以用现成的开源库。重点不是实现算法而是“全局共享”这一层的设计。3.4 并发层的限流信号量控制连接占用除了 QPS 限流大模型调用还必须做并发数限制。原因是大模型单次请求耗时很长几秒到几十秒如果并发冲到 100即使每秒只发 10 个请求也会同时有 100 条请求悬挂在等待中内存、连接池全部被占用。我这里说一个踩过的坑之前做一个批量翻译功能一次提交 1000 个句子我用了ThreadPoolExecutor(max_workers10)本来以为并发 10 很安全。结果每个 worker 里处理一个句子就要调用一次模型模型返回慢时整个线程池都被占满其他实时对话请求全被排队在等待池里整体响应时间从 2 秒变成 30 秒。后来我拆成了两个线程池一个给实时对话用一个给批处理任务用并各自设置独立的最大并发数比如实时 5、批处理 4。同时在调用模型的最外层加了一个信号量保证整条链路的并发总占用不超过某一个硬上限import threading semaphore threading.Semaphore(8) def call_with_limiter(payload): with semaphore: return call_llm_with_retry(payload)这个信号量看起来很简单但它是整个限流体系里最可靠的一层——不管重试怎么退避、降级怎么切换只要并发数超过设定值新请求就必须等待或者直接走降级。3.5 上游限流和自身限流要联动不要互相打架有一种常见错误是上游要求“每分钟 100 次”你在客户端自己限流“每分钟 120 次”结果依然会出现零星 429。根本原因是上游的限流口径往往是多维度的RPM TPM 并发数你自己单维度限流自然是挡不住的。正确做法是把客户端限流阈值设到上游阈值的 70% 到 80%留出余量。同时要对 429 做专门的规则处理遇到 429 不仅是重试还要把当前的令牌桶速率临时调低。用大白话说就是——上游告诉你“你太快了”你就该真的慢下来而不是堵着重试完全靠运气。4. 降级当模型被堵死时系统仍然要能撑住4.1 降级的本质用次级策略换基本的可用性降级这个概念在分布式系统里被讲了很久但在大模型场景里有一个特别之处大模型调用通常涉及“生成内容”这种非确定性输出没有简单的、完全等价的备用实现。这就意味着降级不可能做到“效果一模一样”只能做到“用户仍然能完成流程”。我给你一个判断标准**降级的目标是让用户在这个交互节点不空手而归。**它可以是第二个模型给出的较粗糙回答可以是缓存中相似问题的历史答案也可以是一句明确的“服务暂时不可用请稍后再问”。我在一个 FAQ 场景里打的降级策略大概是这样的层次优先级降级方案适用条件预期效果1调用主模型高性能模型正常情况最优效果2调用备用模型低成本/轻量模型主模型超时/限流/失败响应更快效果稍差3命中语义缓存请求与历史问题相似秒回效果可接受4本地规则引擎/关键词匹配以上全部失败兜底保证有内容返回5固定话术提示重新提问都失败且无法匹配体验最差但不至于白屏4.2 熔断机制不要把故障模型打到死降级方案的触发条件不能是“每次失败都立刻切换”否则主模型只是抖动一下就被降级打穿了用户侧体验反而不稳定。需要引入熔断器模式。熔断器有三种状态关闭Closed正常放行所有请求到主模型。打开Open失败率达到阈值熔断器打开所有请求直接走降级方案不再访问主模型持续一段时间。半开Half-Open熔断一段时间后放少量试探请求到主模型如果成功就认为服务恢复熔断器关闭如果失败继续保持打开状态。用 Java 生态的 Resilience4j 或 Sentinel 都可以直接配置这个状态机。我自己用 Resilience4j 时的配置经验是失败率阈值设为 50%滑动窗口大小设为 20 次调用熔断打开持续 30 秒。这个组合在多数场景下不会误伤轻微抖动又能及时拦住持续故障。4.3 缓存是最被低估的降级手段大家聊降级时都盯着“备选模型”却容易忽略一个性价比非常高的方案语义缓存。大模型应用的相当一部分流量其实是重复问题比如客服系统里“怎么退货”“运费怎么算”这类问题每天被问几百次。如果每次都调用模型重新生成一次回答成本高、延迟也高。我在系统里加了一层基于向量相似度的语义缓存先把用户问题做 embedding然后在缓存里查找相似度超过阈值的旧答案命中就直接返回不经过模型推理。语义缓存有两个好处成本几乎为零命中后不再产生模型调用费用。天然是降级方案当模型故障时缓存命中率大幅提升能接住一部分请求。要注意的坑是时效性。FAQ 类的答案会随时间变化比如价格政策调整缓存里存了昨天的答案今天就不正确了。我的做法是给缓存加 TTL并且把高时效性内容拆到另一条未缓存链路里同时业务侧对这类内容做主动失效。4.4 降级链路一定要做超时控制降级方案虽然通常比主链路更快但也不是瞬间完成的。比如备用模型也可能慢本地规则引擎也可能因为数据量大而变慢。所以降级链路同样要有自己的超时预算不能主链路超时了降级链路又跑 20 秒用户还是觉得卡。我设的经验值是主模型链路总预算 10 秒熔断后降级链路总预算 5 秒。如果降级链路也超时就直接返回固定话术。永远保证最坏情况下用户能在 10 秒内得到明确反馈。4.5 实战顺带说一句别把“降级后返回空结果”写进代码有个特别容易犯的错降级里 catch 到所有异常后return null或者return 。表面上程序没崩但业务侧拿到空值照样会出问题比如前端渲染空白、客服系统判定为回答失败。降级的兜底结果应该是一个有意义的、用户可读的串哪怕只是“系统正忙请稍后再试”。空结果比失败更隐蔽出了问题更难定位。5. 成本控制把 token 消耗算成一本明白账5.1 大模型账单的基本盘算公式大模型服务的计费方式普遍是按 token 数计算的而且输入prompt和输出completion的单价往往不同。要控制成本先得把账算明白。单次调用成本的基本公式单次调用成本 输入 token 数 × 输入单价 输出 token 数 × 输出单价比如某个模型中输入 0.005 元 / 千 token、输出 0.015 元 / 千 token一次输入 2000 token、输出 500 token 的调用成本大概是2000 / 1000 × 0.005 500 / 1000 × 0.015 0.01 0.0075 0.0175 元单次几分钱听起来不多但日活 10 万、每人平均对话 3 轮的系统一天就是几百万次调用成本立刻变成几千甚至上万元一天。这也是为什么成本控制必须做在链路设计里而不是等账单出来才拍大腿。5.2 上下文越长钱包越瘦1M 上下文是把双刃剑最近各家模型都在宣传长上下文有 128K、200K甚至有宣称 1M 上下文全量可用的。能处理的上下文长是好事但成本上往往是被忽略的。长上下文的第一个坑是输入 token 直接翻倍。你每轮对话把历史消息都塞进 prompt之前 10 轮对话加起来几千 token看起来不多但如果你在长上下文窗口里塞了几百页文档每次调用都会重新计算全部输入 token。Prompt 里有 500K token 的输入单次调用的输入费用就非常夸张。第二个坑是长上下文的计算代价。即使是按“输入输出”计费服务端处理长上下文的资源开销也要远高于短文本。很多平台对长上下文的计费单价会更高或者在长上下文模式下给输出限速。如果只是测试长上下文体验很好但若在生产系统里默认把所有链路都配置成最大上下文成本可能直接翻数倍。我的策略是“按需分配上下文长度”简单问答只用最近 2 到 3 轮对话需要分析长文档的专用接口才开启长上下文。模型参数量大不大不重要给什么上下文才决定成本。5.3 降低 token 消耗的三条主线我在项目里把 token 消耗优化拆成了三条线每次过一遍都能砍掉不少费用。第一prompt 瘦身。很多人的 prompt 里藏着大量每轮都不变的系统指令、履历、格式要求。把这些固定内容提前压缩去掉冗余词汇尽量精简为结构清晰的指令。同样一个任务写得好和写得啰嗦的 prompttoken 消耗可以差 30% 以上。第二缓存高频内容。这里的缓存有两类一类是对重复请求的语义缓存命中后完全不产生 token 费用另一类是提示词缓存不少平台对相同的输入前缀有缓存折扣把固定系统 prompt 放在请求的最前面并保持稳定不变就能吃到折扣。这个方法被很多团队忽略了但实实在在能省钱。第三用轻量模型处理简单任务。不是所有请求都需要最强大的模型。意图识别、关键词抽取、情感判断这类任务用轻量模型就够了。我在链路里加了模型路由根据任务难度做分流简单任务走便宜模型。同一个业务量成本下降 40% 是很正常的。5.4 给调用链加上硬预算成本控制的最后一环是给每个用户、每个功能模块、每天/每月分别设置预算上限。超过就不再调用付费模型转而走降级方案。我举一个真实的配置例子在某个内容总结功能里我设置了“每个用户每天最多 30 次模型调用”同时在服务端维护了一个 Redis 计数器每调一次就往上加 1超过 30 次返回“今日次数已用完”。另外全局设置“每日模型调用费用上限 500 元”统计服务每小时汇总一次成本一旦超过就自动把所有非核心链路切换到缓存和轻量模型。这两种硬预算配合起来效果是即使业务方改了需求、出了 bug也不会出现“一晚上烧掉一个月预算”的灾难。6. 工程落地一条完整的调用链路长什么样6.1 调用链路的整体文字框架把中断的内容串起来一条生产可用的调用链路大概是这样的文字框架入口请求 → 业务参数校验 → 全局限流器和并发信号量 → 语义缓存查询 → 结果命中则直接返回 → 未命中则进入模型调用层 → 模型调用层按优先级排列主模型/备用模型 → 每次调用都套用重试、退避与超时预算 → 失败率触发熔断器后自动切换备用模型或降级方案 → 返回结果后异步统计 token 消耗和成本 → 写入日志监控。这个顺序很重要我简单解释一下为什么限流必须放在最前面因为如果流量已经超过预算后面的所有环节都该省下不应该用大模型去扛尖峰。缓存查询放在限流之后、模型调用之前是为了用最便宜的路径先接住尽可能多的请求。重试只能包裹在模型调用这一层不能把业务层整个包进去重试不然可能重复执行业务逻辑。降级层的触发依赖熔断状态设计上要放在主模型调用失败之后而不是靠业务方每个调用方各自判断。6.2 Java 生态落地Spring Boot Resilience4j Sentinel如果你用的是 Java 技术栈落地会比较顺手。Spring Boot 体系里Resilience4j 提供熔断、重试、限流等组件配合 Spring AI 或自己封装的 HTTP client 都能用。Sentinel 则更适合做治理规则动态下发、控制台可视化。两者可以混用。我给出一个极简的 Resilience4j 配置思路resilience4j.retry: instances: llmCall: maxAttempts: 3 waitDuration: 500ms retryExceptions: - org.springframework.web.client.HttpServerErrorException - java.net.SocketTimeoutException ignoreExceptions: - org.springframework.web.client.HttpClientErrorException$BadRequest enableExponentialBackoff: true exponentialBackoffMultiplier: 2这个配置里值得注意的点是retryExceptions和ignoreExceptions一定要明确列出哪些异常才重试。Spring 默认会重试所有异常那很危险。熔断配置可以这样resilience4j.circuitbreaker: instances: llmService: slidingWindowSize: 20 failureRateThreshold: 50 waitDurationInOpenState: 30s permittedNumberOfCallsInHalfOpenState: 3配合 Sentinel 可以做到动态限流基于 QPS 或线程数。我的经验是不管用什么组件关键是要把“哪些异常算失败”定义清楚这是踩坑最多的地方。6.3 Python 生态落地自己封装一个调用网关在 Python 项目里我没用太重的框架而是封装了一个LLMGateway类集中处理限流、重试和降级。核心结构如下class LLMGateway: def __init__(self): self.rate_limiter TokenBucket(capacity50, refill_rate10) self.semaphore threading.Semaphore(6) self.circuit_breaker CircuitBreaker() def call(self, prompt, task_levelnormal): if not self.rate_limiter.try_acquire(): return self.fallback(prompt) with self.semaphore: if not self.circuit_breaker.allow_request(): return self.fallback(prompt) try: result self.call_with_retry(prompt) self.circuit_breaker.record_success() return result except Exception: self.circuit_breaker.record_failure() return self.fallback(prompt)这个封装让我们业务方不用关心底层到底是调哪个模型、有没有限流、要不要降级他们只需要面对一个统一的call(prompt)方法。对一个中大型团队来说这种“网关化”的思路非常重要可以避免每个业务线都自己重写一套重试降级逻辑到最后无法统一治理。6.4 监控指标你看得清才控制得住没有监控上述所有机制都等于盲跑。我按照重要到次要排几个监控指标请求量和成功/失败量最基本的健康度。重试率超过 10% 就说明当前网络或服务不稳定。429 出现次数判断限流策略是否生效。熔断器状态一段时间内频繁进入 Open 状态说明上游问题比预期严重。降级率降级占比太高说明主链路质量在衰退。平均每请求 token 数用于判断 prompt 是否过度膨胀。每日成本曲线顶上预算低于预算下限时告警。P95/P99 延迟用户真实体感的关键指标。告警阈值我建议先定宽一点跑一两周拿到基线再逐步收紧。一开始就设很窄的阈值会产生大量误报团队很快就会对告警麻木。7. 常见问题与排查技巧实录7.1 重试风暴真的会发生吗会而且是在你最不希望看到的时候。我记得有一次模型服务商出了一个半小时的故障我们的调用方全都看着超时然后疯狂重试等于在故障期间给上游打了几倍的流量。后来排查日志发现重试请求占到所有请求的 60%上游服务雪上加霜。排查重试风暴先看两个指标单位时间内的重试请求量、重试请求来源分布。来源分布如果是集中的多半是某个批处理任务无脑重试如果是全链路都涨大概率是大家在同一个退避窗口结束后同时发起重试这种时候就要靠抖动来解决。7.2 429 和 5xx 的处理姿势完全不同很多团队把 429 当成普通异常来处理一见到就重试这不对。429 是你的请求量超过配额如果直接重试大概率还是 429。正确姿势是立即停止当前请求的重试先做退避等待全局限流器的速率临时下调如果持续出现 429立刻触发降级把流量切换到备用模型或缓存。而 5xx 是服务端的问题可以正常按指数退避重试预期是大概率重试一次就能成功。7.3 降级了但用户还是体验很差问题出在哪我遇到过一种很隐蔽的问题降级逻辑本身没错但降级响应时间很长用户照样体验到“卡死”。原因是在降级链路里调用备用模型时没有设置独立的超时预算备用模型超时了又去查缓存缓存也慢缓存服务本身过载最后兜底话术迟迟没返回。排查时要沿着降级链路逐步打点计时每一层花了多少时间哪一层吃掉了预算。降级链路里每一层都必须设置自己的超时宁可提前返回一个一般答案也不要拖到用户超时。7.4 成本突然翻倍怎么定位成本突增的原因通常集中在这几个点prompt 被无意中改长有人在优化提示词时把完整文档塞进 system prompttoken 直接暴涨。带上下文的历史消息越积越多多轮对话场景里每次把之前所有消息都发出去时间一长 token 数量滚雪球。模型路由失效简单任务也走了付费的高性能模型单价比预期高一截。缓存失效语义缓存的阈值被调高或误删导致大量重复问题重新打到模型。排查方法是按“功能模块 × 单次平均 token 数 × 调用量 × 单价”拆开拉一个表一般十分钟就能锁定到底哪块异常。7.5 流式接口断流后的处理经验流式场景下的错误处理和普通调用差别很大。普通调用要么成功要么失败流式调用有可能“成功了一半”才失败用户已经收到了部分文字。这时候如果直接重试用户会看到内容重复还会造成成本浪费之前的输出 token 已经计费。我的策略是流式场景主要用断点续传式的重试但这需要模型服务端支持特定的续传参数如果不支持就改为“从业务层重新构造 request_id 并给用户提示‘回答可能不完整请再次提问’”同时把已返回的部分内容缓存起来。核心原则是绝不盲目在用户已经看到一半答案时重发整个请求。结尾想说的话做了这几个项目的成本核算和维护之后我最大的体会是大模型接进业务并没有想象中那么“魔法”它更像是一台需要认真照看的生产机器。重试、限流、降级和成本控制听起来都是老生常谈的工程手段但在大模型这个全新的不确定性场景里它们的组合方式、参数选择、触发顺序都需要重新打磨。最后再分享一个小技巧**所有方案上线前一定先用故障注入chaos testing模拟一次比如让模型服务端返回 429、断掉网络、把响应时间人为拖长到 30 秒看你的链路是否按照预设的优先级走。**这个验证做完你心里才有底无论哪一天模型服务商抖一下至少你的系统还能撑住。
返回列表