大模型业务的稳定性一直都是玄学。我接手那个内部代号为“23.8”的稳定性专项之前团队已经连着被线上事故折腾了一个多月模型偶尔返回空值、偶尔输出格式错乱、偶尔在关键金额字段上给你编一个数字出来传统监控里什么异常都查不到因为服务本身是200 OK。后来我们逐渐意识到大模型业务的不确定性异常不能用传统接口的故障模型去套得专门为它设计一套“防错机制”。这中间最核心的成果就是一套由路由重试、校验降级、复核机制组合而成的稳定性方案。这篇文章我就把这套方案从头到尾拆开讲一遍不聊虚的只聊我们实际怎么设计、怎么配参数、怎么踩坑又怎么填坑的希望给正在被大模型输出折磨的团队一个可以直接落地的参考。1. 大模型业务的不确定性到底从哪里来1.1 它和传统接口故障不是一回事以前我们做的常规业务系统稳定性问题基本可以归类为服务不可用、接口超时、网络抖动、数据库连接池耗尽。这些问题有一个共同特点——只要把服务拉起来、把请求重试几次、把容量扩上去故障基本就解决了。因为传统接口的输出是确定的只要你给我的输入合法我返回的结果就是稳定的错误最多是“这次没返回结果”。大模型业务完全不一样。它的输出是概率采样出来的同一个Prompt你调用十次可能得到十种不同的描述。这时候你会遇到一个特别恶心的情况服务一切正常、延迟正常、200 OK但返回给你的JSON里字段名是错的、类型是错的、或者干脆给你一个看似合理但业务上完全不合法的值。我们内部把这种问题叫做“不确定性地雷”。它最大的麻烦在于传统监控体系根本发现不了它因为从网络层、应用层看过去一切正常。只有到业务结果校验的时候你才发现问题。所以做稳定性第一步就是要接受一个现实大模型服务是一个带有随机性的外部依赖你必须在它外面再包一层“防护网”。1.2 不确定性异常的真实分布在专项推进过程中我们花了两周时间把历史事故做了一次归类分析梳理出四类典型异常。我把它们整理成了下面这个表这也直接决定了我后面整个稳定性方案的设计方向。异常层级典型表现高发原因传统方案是否有效请求层异常连接超时、5xx、限流拒绝、流式响应中断模型服务商负载波动、单点路由集中有效重试负载均衡就能解决结构层异常JSON解析失败、必填字段缺失、类型不匹配指令遵循不稳定、输出被截断部分有效重试有用但代价高语义层异常格式正确但内容答非所问、关键实体识别错误模型幻觉、上下文误导、Prompt歧义无效重试多少次都是错的业务规则层异常金额字段为负、用户ID不存在、违禁内容未拦截模型不懂业务约束、指令被降权无效必须外部强校验第一类异常我们交给了路由重试机制来处理第二类异常交给校验降级机制第三类和第四类则是复核机制的主战场。这三条防线分别对应标题里提到的三个机制也分别回答三个问题请求能不能正确发出去、发出去之后的结果合不合规、结果合规但语义上靠不靠谱。1.3 三条防线之间的协作逻辑这里我要强调一个容易被忽略的点这三条防线不是独立运行的而是按“成本从低到高、拦截从粗到细”的顺序串起来的。路由重试的成本最低只做网络层的容错不关心模型返回内容的对错校验降级的成本中等它拿到重试之后的输出结果做结构性和确定性规则的校验发现问题就触发降级策略复核机制的成本最高它处理的是那些“在校验层看来完全合法、但真实业务场景里不靠谱”的输出通过二次甚至三次检查来兜底。我把它们理解成一个漏斗路由重试挡住“发不出去”的异常校验降级挡住“长得不对”的异常复核机制挡住“看着对但其实是错的”异常。这样分层之后的好处很明显——每一层只需要关心自己该做的事逻辑不会纠缠在一起出问题也好排查。2. 路由重试先让请求抵达正确的出口2.1 把大模型调用当成“外部不可靠依赖”来设计我见过太多团队代码里直接用SDK调模型接口什么保护机制都不加结果线上稍微出点问题就手忙脚乱。他们的理由是“模型服务商承诺SLA很高不用做容错”。这话我一开始也信了直到某次晚上流量高峰期单一出口超时率飙到15%业务直接不可用我才意识到一个道理任何外部依赖只要不在你的进程里就不是100%可靠的。处理外部不可靠依赖的标准手法就是路由和重试。路由解决“往哪发”的问题——你有多个模型服务商或者同一服务商的多个入口时怎么把流量分配得合理重试解决“发失败了怎么办”的问题——第一次调用超时或报错要不要换个出口再试一次。2.2 多出口路由与动态健康检查我们的路由层最开始做得很简单就是配置了一个主出口和一个备出口主出口挂了切备出口。后来发现这种静态主备方案有两个问题一是主出口恢复后流量不会自动切回来二是某个出口虽然没挂但延迟已经劣化了你还傻傻地把流量往里面发。最后我们改成了加权轮询加动态健康检查。每个出口都有一个动态权重权重由三个指标决定最近5分钟的请求成功率、平均响应延迟、限流拒绝率。这三个指标会定期刷新成功率低的出口权重自动下调延迟高的出口减少分配流量。这样即使某个出口没有完全挂掉只是“半死不活”路由层也能感知到并把流量导走。这里给大家一个具体计算权重的参考公式我们实测下来效果不错weight base_weight * success_rate * latency_factor * rate_limit_factor # success_rate最近5分钟成功率低于0.9时开始衰减 # latency_factor如果P95延迟超过阈值则按比例下调 # rate_limit_factor出现限流时直接设为0.1进入“冷却”状态2.3 超时分级与重试参数的坑重试机制是稳定性方案里最容易被搞砸的一环。我见过有人把所有超时统一设成10秒重试三次退避策略随便写了一下结果一次上游波动直接触发了重试风暴。我们的做法是先把一次大模型调用拆成三个可独立测量的阶段连接阶段、首Token阶段、完整响应阶段。每个阶段设置独立的超时时间。比如连接阶段一般3秒就够了如果3秒还没建立连接大概率是网络问题直接切出口重试首Token阶段可能设10秒模型服务商在处理你的请求但还没开始输出这时候如果超时换个出口大概率能解决完整响应阶段根据业务要求来我们一般是30到60秒。重试次数和退避策略也有讲究。你不能无脑重试三次而是要看错误类型决定是否重试网络错误、连接断开、5xx错误可以重试这些是临时性问题。4xx错误说明请求本身有问题重试多少次都是白费直接放弃。模型返回“内容审核不通过”之类的业务错误更不能重试应该走降级策略。退避策略建议用指数退避加抖动避免所有重试请求在同一时刻打爆服务商。import random import time def retry_delay(attempt): # 基础退避1s, 2s, 4s加上随机抖动 base_delay 2 ** attempt jitter random.uniform(0, 0.5 * base_delay) return base_delay jitter2.4 流式请求与幂等性重试时最容易翻车的两个细节如果你们的业务只是简单的“发请求、等完整结果”前面说的基本够用。但如果你做的是流式响应比如打字机效果那重试就复杂了。流式响应的最大问题是请求已经发出去一半客户端已经收到一部分Token了这时候连接断了你重试一次客户端就会收到两段内容拼接在一起的诡异结果。我们的方案是在流式请求里加入request_id和offset机制。具体来说重试时不是从零开始重新请求而是带上“已接收到的最后位置”让模型服务商从指定位置继续输出。这个功能不是所有服务商都支持不支持的情况下就只能走“前端整体遮罩重试成功后替换全部内容”的策略宁可让用户看到一次卡顿也不能让他看到内容被拼错。另外还有一个极其重要、但经常被忽略的问题——重试的幂等性。大模型调用通常是按次计费的你重试三次如果每次都触发计费账就算不清了。而且如果是生成图片、生成视频这类消耗更大的任务重试带来的成本翻倍是不能接受的。解决思路是给每个业务请求生成一个全局唯一的trace_id带着这个ID去调用模型服务商服务商侧做去重同一个trace_id的重复请求只计费一次。即使服务商不支持你也要在自己的日志系统里把这个ID记下来方便对账和排查。3. 校验降级在错误结果进入业务之前拦住它3.1 校验层的职责边界只做机器能确定的事路由重试解决了“请求发不出去”的问题但发出去之后模型返回了内容这些内容合不合格就轮到校验层登场了。很多团队在这里犯的一个错误是把校验层设计得过于理想化——希望它能像人类一样理解内容、判断语义结果实现出来的东西要么过度复杂难以维护要么误杀率特别高。我们的经验是校验层只做机器能百分之百确定的事。机器能百分之百确定的是什么呢格式层面的东西。JSON能不能解析、必需字段在不在、字段类型对不对、枚举值是否合法、字符串长度有没有超限、数值是否在合理范围。这些规则是确定的、无歧义的校验结果不会出现“公说公有理”的情况。至于语义层面的判断比如“这段文案是否真的回答了用户的问题”“这个摘要是否准确地概括了原文”那不是校验层该干的活那是复核机制的职责。把这两个边界划清楚校验层的代码会非常简洁、可测试、可维护。3.2 校验器设计和降级链路的完整流程下面这个校验器设计是我们方案里最核心的一段代码我简化之后分享给大家看。它的核心思想是把校验规则定义成数据而不是写死在代码里。这样业务变了改配置就行不用改代码发版。class ResponseValidator: def __init__(self, schema): self.schema schema # 从JSON Schema配置加载 def validate(self, response_text: str): # 第一步解析JSON try: data json.loads(response_text) except json.JSONDecodeError as e: return ValidationResult(successFalse, errorJSON_PARSE_FAILED) # 第二步根据schema逐字段校验 for field, rules in self.schema.items(): if rules.get(required) and field not in data: return ValidationResult(successFalse, errorMISSING_FIELD, fieldfield) value data.get(field) if value is None: continue if type in rules and not isinstance(value, rules[type]): return ValidationResult(successFalse, errorTYPE_MISMATCH, fieldfield) if enum in rules and value not in rules[enum]: return ValidationResult(successFalse, errorINVALID_ENUM, fieldfield) if min_value in rules and value rules[min_value]: return ValidationResult(successFalse, errorOUT_OF_RANGE, fieldfield) if max_length in rules and len(str(value)) rules[max_length]: return ValidationResult(successFalse, errorTOO_LONG, fieldfield) # 第三步自定义扩展规则比如正则、黑名单词表 for rule_name, checker in self.custom_rules.items(): if not checker(data): return ValidationResult(successFalse, errorrule_name) return ValidationResult(successTrue, datadata)校验一旦不通过就进入了降级链路。降级不是直接把错误抛给用户而是按优先级走下面这条链路直接重试限定次数有些校验失败是模型随机性造成的比如偶尔一次字段输出不完整重试一次可能就好了。但注意重试次数要控制在1到2次多了成本受不了。提示词自动修复重试仍然失败时把上一次的错误信息塞回Prompt里让模型“知道哪里错了改一版出来”。实测这个方法对结构性问题非常有效修复成功率能到70%左右。静态兜底如果修复还不行就返回一个提前定义好的兜底结果。比如某类新闻摘要失败时直接返回“服务升级中请稍后查看”之类的静态文案至少保证用户看到一个正常页面而不是报错。标记复核把失败的请求标记为“降级处理”写入复核队列后续由复核机制决定是否需要人工修正或者重新生成。3.3 硬失败与软失败降级策略的分岔路口这里我要重点讲一下“硬失败”和“软失败”的区分这是校验降级方案里最关键的设计决策。硬失败指的是校验规则明确判定请求结果不可用必须走降级链路绝不能把原始结果返回给用户。比如金额字段是负数、用户ID不存在、输出包含违禁词这些必须拦下。软失败则是结果虽然有一些瑕疵但不影响用户正常使用可以返回给用户但打上标记。比如某个字段缺失但业务上并不关键、文本长度截断但核心信息完整。软失败的设计非常有用它能显著提高业务的整体可用率。你想一下如果一个请求只是少了非必需的点赞数字段你就把整个结果都作废走降级那可用率会很惨。正确的做法是记录一条“SOFT_FAIL”日志返回结果给用户同时在后台生成一条待复核记录。为了让大家对这个策略的取舍有更直观的理解我列了一张决策矩阵场景校验结果处理策略理由核心金额为负数硬失败重新生成-静态兜底绝对不能用错误金额非必需字段缺失软失败正常返回后台记录用户感知不到不影响核心体验JSON完全解析不了硬失败重新生成-静态兜底前端没法渲染文本长度超限且被截断软失败正常返回后台记录截断后的内容可能仍然可用输出疑似包含无关内容答非所问软失败返回但标记需复核语义判断不交给校验层断言4. 复核机制给关键业务装一道“人性化保险丝”4.1 实时拦截与异步复核的分工校验降级能拦住格式错误和确定性规则问题但它拦不住一类情况模型返回的内容格式完全正确、字段也都合法但内容本身是错的。最简单的例子用户问“今天上海天气怎么样”模型返回了一段结构完美、条理清晰的“北京天气介绍”。从校验层看这段内容格式没问题、字段齐全完全合法。但它就是错的。这类问题就是复核机制的地盘。我们先明确一个原则复核不等于二次调用大模型来验证第一次调用。如果你复核的时候又调一次模型那等于让同一个不稳定的裁判去复核自己不稳定的判决没有意义。我们把复核分成两层。第一层是实时拦截它需要极低的延迟所以只能跑一些确定性的跨字段一致性检查。比如“商品名称里提到了A品牌但品牌字段写的是B品牌”“订单金额超过1万元但用户等级是普通用户不符合优惠规则”这类规则不依赖模型是程序逻辑扫出来的发现问题直接拦截。第二层是异步复核它不阻塞主请求而是把结果投递到一个消息队列由专门的后台任务去分析。这一层可以用模型也可以用规则引擎甚至可以用人工。4.2 三级复核规则复核、模型自评、人工兜底我们最终把复核机制做成了三级结构每一级的成本和能力都不同适合不同风险的场景。第一级规则复核。这是速度最快、成本最低的一级。我们维护了一套跨字段一致性规则库比如一个订单的收货城市和收货人所在省份必须匹配、生成摘要时输入文本里必须包含摘要中出现的实体、金额计算必须满足“单价乘数量等于总价”之类的恒等式。这些规则用代码实现处理单个请求只需要几毫秒。第二级模型自评。这个自评不是让同一个模型再生成一遍而是用一个独立的验证器模型或者同一模型的不同配置来做质量评估。具体做法是给验证器一个打分模板让它从相关性、完整性、安全性几个维度给主模型的结果打分。只有打分低于阈值的才进入人工兜底。这里我要推荐一个我们摸索出来的设计——问“反问题”来验证一致性。比如主模型生成了一段新闻摘要验证器不直接问“这段摘要质量如何”而是问“这段摘要中提到的事实在原文中有依据吗”。让验证器逐条把摘要里的关键实体和原文做比对这样更容易暴露幻觉。第三级人工兜底。对于金额、法律条款、医疗建议这类高风险内容机器复核再靠谱我们也保留人工复核的能力。当然人工复核无法覆盖所有流量所以我们做了一个带优先级的抽样机制风险等级高的业务100%进人工队列风险等级低的业务按1%到5%的抽样率进入人工队列。人工复核的结果会反哺给规则库和模型形成迭代。4.3 复核队列的闭环指标监控与策略迭代复核机制不是搭完就完事了它必须形成闭环。我们为复核系统设计了四个核心指标每周复盘一次。第一个指标是复核拦截率进复核队列的内容里最终被判定为异常并打回的比例。如果这个比例长期偏高说明上游校验和降级策略可能有问题或者主模型服务商的质量下降了。第二个指标是降级率整体请求中走了降级链路的比例正常情况下应该控制在5%以内。第三个指标是误伤率被复核机制判定为异常、但人工确认其实是正确结果的比例。误伤率如果超过2%就要考虑是不是规则或者验证器太严格了。第四个指标是漏检率这个是抽样评估出来的每批人工兜底结果里再抽一部分由更资深的同事确认看复核机制有没有漏掉真正的问题。这四个指标定期复盘的意义在于把大模型业务的稳定性从“碰运气”变成“可度量、可迭代”。每次模型服务商升级、每次业务上线新场景我们都会先跑一轮回归样本集用这四个指标校准策略再全量放量。5. 踩坑实录这三个跟头我摔得最疼5.1 重试风暴引发的连锁故障这套方案上线之后的第一次大事故不是模型服务商挂了而是我们自己的重试策略引起的。当时我们把重试次数设得比较激进某个出口出现延迟劣化时第一批请求超时触发重试重试又因为路由权重没有及时更新继续打到同一个劣化出口再次超时再次重试。瞬间重试流量放大到普通流量的三倍多把一个还没完全挂掉的出口彻底压垮了。这次事故之后我们给重试机制加了一个核心约束——重试预算。以一分钟为窗口所有业务的重试次数总和不能超过一个预设值。预算用完了后面新进来的请求即使失败也不重试直接走降级。这样从系统层面兜住了重试风暴的底后续再没出现过类似问题。5.2 校验过度导致可用率暴跌还有一个坑是我印象最深的。当时为了追求“绝对可靠”我们给校验层加了很多“理论上应该成立”的规则结果上线当天可用率从99.2%直接跌到93%。排查了一整天才发现有一条规则要求“生成的文案必须包含用户输入的关键词”但有几个真实场景里模型用同义词替换了关键词语义完全正确却被校验规则打回了。从那以后我们立了一个规矩新增校验规则必须先灰度先在日志模式下观察误杀率再决定是否开启强制拦截。每条规则上线前都要过一遍“误杀评估”宁可不拦不能误杀因为误杀的代价是损失正常用户比漏放问题内容还难承受。5.3 模型升级后的判定漂移最后一个坑属于“外部因素变化”造成的。我们用的模型服务商某次更新了底层版本结果之前一直很稳定的校验和复核规则开始大量拦截正常内容。一开始我们还以为是自己的代码出问题了查了很久才定位到是模型行为变了。后来我们建立了一个回归样本集——把历史上出现过问题的请求、正常请求、边界请求都保存下来每次模型服务商升级、或者我们改了Promp或校验规则就先在回归样本集上全量跑一遍对比新旧结果。这个习惯帮我们避免了很多次“因为模型升级导致线上策略失效”的尴尬。写在最后的经验之谈做完这个专项我个人最大的体会是大模型业务的稳定性核心不是消灭不确定性而是把不确定性控制在业务可接受的范围内。路由重试解决“发不出去”的问题校验降级解决“格式不对”的问题复核机制解决“内容不对”的问题每一层都只管自己能管的那一段不越界、不背锅。如果让我给刚开始做这件事的团队一个建议我会说不要贪多求全先做校验降级这一层。它实现成本最低、见效最快能把最伤用户体验的格式错乱和字段异常挡住。等线上数据积累够了再逐步补上路由优化和复核闭环。稳定性建设永远是个持续迭代的过程你调好的每一个参数都只在“当前环境”下成立没有一劳永逸的方案只有不断跟不确定性博弈的手感。
阅读完成 · 觉得有帮助?