想搞清楚大模型服务的稳定性光看监控指标没什么用真正要解决的是把那些“不确定性异常”当成系统工程来处理。这篇就借着一个实际做过的项目总结把路由、重试、校验降级、复核这一套机制完整拆开讲清楚每一层到底是干什么的、怎么设计、有哪些坑。大约半年前某公司的核心业务开始重度接入大模型结果上线没两周就暴露出一堆问题上游超时、响应格式飘忽不定、偶尔还能给你吐出一段完全不符合业务逻辑的内容。我们几个人轮流盯群夜里被报警叫醒好几次后来实在撑不住决定把稳定性的活儿系统做一次。这个项目代号就叫“23.8”意思是当时那个迭代版本号。整套方案做完之后线上因为模型端不可控因素导致的业务异常基本能做到分钟级自动恢复人工介入的次数也大幅下降。1. 整体设计与思路拆解先说结论大模型业务的稳定性不能靠“祈祷模型别出问题”也不能靠“人肉盯着日志救火”。我们最后落地的方案核心是三件事——路由、重试、校验降级外加一个兜底的复核机制。四层各管一段合起来覆盖了从“模型没接上”到“模型接了但瞎说”之间的所有主要异常。1.1 先搞清楚的几个底层问题动手之前我们先把问题分了一下类。大模型“不确定性”到底体现在哪就我们的场景来看主要是这四种可达性不确定服务忽快忽慢超时、连接中断、限流这些事经常发生。内容不确定同一个问题它今天这么答明天那么答甚至同一时刻不同副本给的结果都不一样。格式不确定我们要求输出JSON它偶尔给你带点markdown代码块偶尔在JSON后面多几句废话。业务逻辑不确定格式完全正确但内容本身是错的比如把用户A的订单信息说成用户B的。这四种问题里前两种传统手段还能应付但后两种尤其是格式和业务逻辑问题光靠链路优化解决不了必须在应用层做校验和兜底。这个认知是整个方案设计的出发点。1.2 为什么是这四层机制设计的时候我们其实讨论过要不要用更复杂的方案比如引入独立的模型网关组件或者直接上服务网格。后来评估下来团队维护成本和排障复杂度都会明显上升最后决定就把逻辑收敛在业务服务内部做一套足够扎实的SDK级方案。路由解决“找谁干活”的问题。多模型、多副本、多Region部署路由层负责把请求分给最合适的那个同时还要支持故障转移。重试解决“活儿没干成”的问题。超时、限流、瞬时抖动这些场景不值得直接报错给一次或者有限几次机会能吞掉很大比例的无谓失败。校验降级解决“活儿干得不对”的问题。拿到结果先过一遍格式校验、内容校验不对劲就走备选路径或者用预设兜底回复。复核解决“漏网之鱼”的问题。自动校验通过但业务上属于高风险的操作再叠加一道独立的判断逻辑哪怕多花点延迟也要保证不能错。这么说可能有点抽象打个比方。路由是前台接待帮你找对的窗口重试是窗口前面没人应答的时候你多叫两声校验降级是窗口给你递出来的东西有问题你当场让人重做或者换个窗口复核是东西拿到手之后你再找个第三方机构验一下因为这东西太贵重不能光听窗口说没问题。2. 核心机制拆解与实操要点这一节逐层拆开讲。每个部分都会把为什么这么做、怎么做的细节讲透。2.1 路由层多活是稳定性的地基先说说路由。当时我们的现状是有两家模型服务商这里不点名了各部署在两个Region每个Region后面挂着若干实例。最早的时候代码里写的是直连某个固定接入点结果那个接入点一抖动全站跟着抖。路由层要做的就是把“调用哪个模型”“调用哪个Region”“调用哪组实例”这几个决策独立出来变成一个可配置的规则引擎。我们实现的维度有四个模型优先级比如主用A模型的tier-1档配额不足或者失败率超标时切到B模型。Region亲和性默认优先本Region跨Region调用会增加延迟所以只有本Region整体故障时才切。负载均衡策略按实例的响应时间和错误率动态调权而不是简单的轮询。配置热更新路由规则要能动态下发随时调整某个模型的流量比例灰度切流。一个容易被忽略但很重要的问题是路由表的一致性。我们的架构里没有专门的配置中心最开始用本地配置文件改了要发版重启。后来引入了中间件的配置推送功能但是多实例之间更新存在短暂不一致会有那么几秒钟有的机器已经切换主模型了有的还没切。那几秒内流量会混合打向两个模型如果两个模型输出风格差异大用户感知就比较奇怪。所以路由规则变更的时候我们会先切少量验证流量确认没问题再全量推。路由层还有一个值得讲的点健康检查不能只看“通不通”要看“慢不慢”。我们踩过一个坑某个Region的实例一直能连通但响应时间从200ms退化到了2s基于“连通性”的健康检查根本发现不了流量还一直往里打整体成功率被拖得很惨。后来我们把健康检查阈值改成了分位数判断比如过去5分钟P95响应时间超过设定阈值就自动摘除该实例直到恢复。2.2 重试层把抖动和超时拦在业务外路由解决了“往哪打”重试解决的是“打了没反应”怎么办。早期我们犯过的错误是重试太激进——只要超时就重试重试次数设得还特别大结果就是把已经快扛不住的模型服务又压上一根稻草雪上加霜。重试策略最后收敛成这几个要点重试次数上限默认1次最多不超过2次。别问为什么不是3次4次重试本质是对冲瞬间抖动不是解决持续故障。持续故障要靠路由切换不能靠重试去赌。超时时间分级连接超时和读超时分开设置连接超时设短比如1.5秒读超时根据业务容忍度设成5秒到15秒不等。读超时如果设得太短模型刚要生成到一半就被掐断反而浪费了一次有效调用。退避策略重试之间要有间隔。我们用的是指数退避加抖动基础间隔200ms翻倍然后加一个0到50ms的随机量。这样做是为了避免重试请求在模型服务端恢复的那一瞬间同时打进来形成惊群。幂等判断这一点特别重要。重试之前必须先确认业务请求是幂等的。大模型生成本身没有副作用但你的业务在调用模型之前可能已经写了一些上下文日志、扣了用户额度。我们在重试前会检查请求里有没有携带幂等键有的话直接用原幂等键重试没有的话会生成一个唯一标识传下去确保即使同一个请求被发送多次下游能识别出来不至于产生重复扣费或者重复记账的问题。重试层有一个很容易被忽视的细节就是重试时机的选择。我们最初的实现是只要抛异常就进重试逻辑后来发现模型服务返回的业务错误码比如“内容安全策略命中了”“输入预处理失败”这类4xx错误重试一百次也没用该拒绝还是拒绝。所以重试必须建立在“错误类型可重试”的判断上。我们的规则是连接错误、超时、5xx、限流429这四类才重试其他的直接走降级或者报错。提示重试不是万能的。如果你发现一个服务经常需要靠重试才能成功那说明上游的健康状态已经不健康了优先去查路由层和模型服务本身的状况而不是继续加大重试力度。2.3 校验层不能光看“通不通”还得看“对不对”路由和重试解决的是“模型服务通了没”校验层解决的是“模型说的话能不能用”。这一层是我们投入精力最多、收益也是最明显的地方。校验的核心逻辑分三段格式校验、内容校验、业务规则校验。格式校验最简单也最基础。我们要求模型输出必须是合法的JSON且包含预期字段。实现的时候写了一个通用解析器能处理几种常见的不规范情况内容里夹杂着markdown代码块标记json这种提取出真正的JSON部分。JSON尾随多余字符比如结束括号后面还有解释性文字自动截断。字段类型不符比如数字字段返回了字符串做一次宽松的类型转换。格式校验我们还做了一个“回去问一下”的策略。如果解析失败但能明显看出模型是有能力修正的我们就精简一下上下文去掉一些无关的对话历史把要求再明确一遍重新请求一次。这不算重试因为请求内容变了它更像“换一种问法”。实测下来“重新问”的成功率比盲目重试高很多。内容校验针对的是“格式正确但内容明显有问题”的情况。我们实现的几种检测器包括空内容检测返回的是一段占位符或者空白文本这种直接判失败。重复内容检测同一句话反复出现典型的模型退化现象。关键词偏离检测根据业务预设的话题范围检查输出里是否出现了越界词汇或概念。业务规则校验是最重的。因为不同业务的合规要求、下游系统约束不一样。比如我们有一个场景需要模型生成一段SQL查询条件那就要校验生成的SQL字段是否在允许的字段白名单里表名是否合法Join条件是否完整。如果模型生成的SQL里有白名单之外的字段直接拒绝并走降级。校验不通过时的降级路径我们设计了三条按优先级走单次重问去掉多余上下文明确输出要求再试一次。这能解决大多数“模型走神”的问题。切换次级模型换一个同等级但可能风格不同的模型再试一次。兜底话术如果上面都不行直接返回预设的答案。比如“当前无法获取该信息请稍后再试”。对B端业务来说一个明确的兜底回复远好过把一句胡话返回给下游系统。2.4 复核机制高风险场景的“二次确认”校验降级解决了绝大多数“模型结果质量差”的问题但还有一个盲区校验逻辑本身也可能判断失误。尤其是语义层面的偏差有时候完全符合格式、内容也通顺但事实就是错的。这种问题无法在自动校验里绝对斩尽杀绝于是我们设计了复核机制。它不是要求对每一个结果都做二次处理而是针对“高风险操作”才启用。复核分两个层面规则复核和人工复核。规则复核是自动的。我们在业务系统里预设了一套规则比如涉及金额变更、订单状态流转、用户权限调整这类的操作模型生成的结果必须经过一个独立的规则引擎再验证一遍确认操作参数在合法范围。结果中存在数值类字段时需要跟调用方传入的原始参数做一致性比对比如模型输出的金额必须等于输入金额减去优惠金额不等就拦截。涉及跨用户数据查询的会用另一个独立的判断逻辑检查查询条件里有没有包含和当前用户身份不匹配的过滤条件。人工复核是兜底的兜底。规则复核无法覆盖的场景会进入一个待确认队列由运营人员在管理后台处理。这里有一个心得人工复核队列必须设置超时策略。如果一条待复核的请求一直等在那没人处理会把整个业务链路卡死。我们设置的是超过30秒没有处理自动按拒绝处理并返回兜底话术同时给运营人员发送异常提醒。复核机制的设计里最容易被忽视的是复核逻辑不能复用校验逻辑。如果判定“模型输出是否正确”的第一层校验和第二层复核用的是同一套代码、同一个函数库那第一层漏掉的错第二层大概率也会漏。我们的做法是校验层用的是可灵活配置的宽松规则复核层用的是纯逻辑比对。比如校验层允许一定程度的近似文本匹配复核层就必须做精确匹配两边实现互相独立。3. 实操过程与关键环节实现这一节把落地过程中的关键实现细节、参数选择和代码层面的注意点写出来方便直接参照。3.1 路由与重试的配置化落地路由规则我们最终用YAML配置存到中间件的配置管理里代码里只保留解析逻辑。配置文件的示例结构如下router: default_group: primary groups: primary: - model: provider_a_tier1 region: cn-east-1 weight: 70 timeout_ms: 8000 max_retries: 1 - model: provider_b_premium region: cn-east-1 weight: 30 timeout_ms: 10000 max_retries: 1 fallback: - model: provider_a_tier2 region: cn-north-1 weight: 50 timeout_ms: 12000 max_retries: 2 - model: provider_b_standard region: cn-north-1 weight: 50 timeout_ms: 12000 max_retries: 2这里有一个选型上的细节主组和兜底组的超时时间、重试次数为什么不一样因为兜底组本来就是应急用的可以容忍更大的延迟和更多的重试。主组追求的是快兜底组追求的是成。重试逻辑的实现建议封装成独立的函数不要散落在调用代码里。伪代码如下def call_with_retry(func, max_retries1, base_delay0.2, is_retryableNone): attempt 0 while True: try: return {ok: True, data: func()} except Exception as e: if attempt max_retries or (is_retryable and not is_retryable(e)): raise delay base_delay * (2 ** attempt) random.uniform(0, 0.05) time.sleep(delay) attempt 1调用层要注意is_retryable这个回调必须由上层业务传入它判断的不仅仅是异常类型还包括错误码。我们实际用的判断逻辑是def is_retryable_error(e): if isinstance(e, (TimeoutError, ConnectionError)): return True err_code getattr(e, code, None) if err_code in (429, 500, 502, 503, 504): return True return False3.2 校验与降级的执行顺序设计校验这块要做到“先轻后重”——成本越低的校验越要先执行成本高的放在后面。因为如果轻量级的校验就失败了根本没必要跑后面那些重量级的逻辑。执行顺序我们定为格式解析纯本地逻辑成本最低先做。JSON字段完整性校验本地查字典次低。关键词/话题范围检测本地规则匹配。业务规则校验可能涉及查库或者调用其他服务成本最高放最后。这个顺序带来的一个隐性收益是能快速识别模型服务端的问题。比如连续多个请求在格式解析这一关就挂了说明模型服务大概率在异常状态比如上下文被污染了这时候可以直接触发路由切换而不是傻傻地一条条继续调。降级的路径选择我们用的是一套“状态计数”机制。不是单个请求失败就立刻降级而是按滑动窗口统计过去1分钟内的失败率超过5%才开始降级低于2%就恢复。这个阈值的设置需要根据业务容忍度调整。如果业务链路本身对延迟敏感阈值可以放宽到10%。滑动窗口的实现需要小心。我们最初用简单计数器有两个显著问题一是“5分钟内失败5次”这种简单阈值容易被突发流量打穿二是窗口边界的请求归属判断容易出错。后来改成分桶计数比如以10秒为一个桶存最近6个桶的数据统计时把6个桶加起来既能精确滑动又不用存太多的历史状态。3.3 轻量复核与异步补偿规则复核如果做成同步调用延迟会叠加在上面三层之上。所以我们的原则是能够异步的复核不要同步。在用户操作类场景中我们用同步复核拦截高风险错误在纯粹的生成类场景比如生成一段文案我们采用异步复核加日志记录。等复核发现有问题时再通过消息队列通知到业务方由业务方决定是撤回内容还是修正展示。人工复核这块我们做了一个管理后台的队列功能。运营人员可以查看待复核的请求详情包括原始输入、模型输出、校验结果、命中规则等。需要一个可以一键“通过”或“拒绝”的操作同时记录处理人和处理时间便于事后追溯。心得复核机制不是越高频越好。如果一条请求已经被前面的格式校验、内容校验都过了而且本身不是高风险操作就没必要再过一遍复核。复核对性能有损耗用太多会拖垮整体吞吐。我们最终设置了“复核抽查比例”默认对10%的低风险请求和100%的高风险请求做复核。4. 常见问题与排查技巧实录这一节整理了我们在整个项目迭代过程中遇到的典型问题以及最终沉淀下来的排查思路。4.1 模型一直超时重试也不管用现象某段时间主模型P99延迟飙到20秒超时重试全部失败连带整个业务的成功率下降。排查过程第一步是看路由层的健康检查报告发现主模型服务端持续响应缓慢不是瞬时抖动。第二步检查重试策略发现重试请求也会被限流拒掉返回429。第三步确认兜底组路由配置当时是生效的但流量依然集中打在主模型上因为我们的健康检查阈值没触发“摘除”动作只是告警。最终修复把健康检查阈值从“平均响应时间”改成“P95响应时间”并且把“持续超过阈值1分钟”作为摘除标准。摘除之后自动切兜底组等P95恢复稳定超过5分钟再重新引入流量。4.2 格式校验总是误伤现象格式校验的拒绝率异常高但人工看那些被拒绝的内容大部分其实都还能用。排查过程日志显示拒绝原因集中在“JSON解析失败”但打开原始响应看很多是带了markdown标记或者前后有补充说明。确定是解析器太死板只认标准的JSON格式。修复方式解析器里增加预处理逻辑做三件事——去掉外层markdown代码块标记、找到第一个“{”和最后一个“}”进行截取、对非法转义字符做修正。同时加了一条规则预处理之后还是解析失败才走重问。4.3 复核队列堆积阻塞了整个流程现象某天业务高峰期人工复核队列积压了上千条请求后面调用方一直在等待整体流程被拖住。排查过程复核队列没有设置超时策略这本身就是一个设计缺陷。运营人员当时在忙别的事没有及时看到待办提醒。修复方式加上了超时强制处理逻辑30秒未处理自动拒绝并返回兜底话术。同时增加了一个规则当队列积压超过200条时自动把复核请求的优先级调低先保障新的用户请求不被阻塞。4.4 兜底话术不够“业务”用户直接投诉现象降级之后返回的兜底话术太生硬用户反馈体验极差。排查过程最初的兜底话术是统一的“系统繁忙请稍后再试”。对于C端用户这还行但我们的业务是B端对接下游系统拿到这句话不知道怎么处理。修复方式兜底话术改成可配置的分类模板。比如查询类是“查询超时请检查输入参数后重试”操作类是“操作未完成未产生扣费请确认后重试”。同时向下游透传一个标志位标明本次结果是降级返回让下游可以决定要不要特殊处理。4.5 常见问题速查表现象优先排查方向解决建议模型调用全部超时路由健康检查、模型服务商状态触发路由摘除切备用模型单条调用超时稳定复现请求参数大小、上下文长度精简上下文调整超时参数输出JSON频繁解析失败格式预处理逻辑加强预处理必要时改提示语内容通过校验但业务方不认可业务规则校验范围补充规则校验项增加人工复核降级触发太频繁校验阈值设置调整降级触发百分比优化主模型路由重试放大上游压力重试策略减少重试次数延长退避间隔5. 项目上线后的数据表现与优化空间整套机制上线之后的第八周我们拉了一次数据对比。不说多华丽但能说明这套方案的长期价值。大模型调用整体失败率包括超时、限流、内部错误从最高的12.7%降到约2.1%进入稳定期后基本在这个水平上下浮动。业务层数据异常率通过了基础校验但业务规则有问题下降了约68%。这主要靠业务规则校验和复核机制的补充。人工介入事件从高峰期每周10次以上降到平均每周0到1次而且大部分介入是排查新上线业务场景的规则覆盖问题不是处理线上故障。单次调用平均耗时P95从9秒多降到7秒左右。这里有一部分是重试减少带来的影响另一部分是路由健康检查优化后请求不容易卡在已经退化的实例上。上线初期还发现一个问题校验规则如果设置太严会间接提高拒绝率太松又会漏掉真正的错误。这个度只能靠持续调整和线上数据反馈来逼近。我们目前的做法是新业务接入时先以宽松模式跑一段时间积累日志样本人工标注一批“正常”和“异常”结果再去调整规则阈值。后面的优化方向也明确。一是把现在的规则引擎升级成可配置化的策略编排让业务方能在页面上调整校验逻辑不用每次改代码发版二是尝试引入一个独立的“语义校验模型”做复核虽然成本会增加但能覆盖更多难以通过硬规则发现的语义错误三是做更细粒度的分级降级让不同的失败场景返回更贴合业务语义的结果。最后说一个我体会最深的事稳定性的问题越靠上层解决成本越高越靠底层解决效果越好。路由和重试是在“不让异常发生”校验是在“发现异常”复核是在“补偿异常”。如果你的业务还在被大模型输出的不稳定折磨不妨照着这个层次自查一遍看看自己到底缺的是哪一层。很多团队的第一反应是加校验但实际可能问题出在路由策略太单一或者重试把上游打挂了。先理清楚层级再动手优化别一上来就堆规则。