大概是半年前我负责的一个客服工单分类服务在晚高峰出了次线上事故。模型服务持续超时客户端还在无脑重试请求量像滚雪球一样翻了三倍消息队列开始积压工单迟迟出不了分类结果。等模型服务缓过来之后问题反而更恶心——它返回的JSON格式悄悄变了我们这边解析全部失败线上分类结果一夜之间变成了空白。事后复盘大家争论得最凶的问题不是为什么会超时而是为什么一个看起来没有技术难度的调用会在这么多层环节上同时崩掉。答案其实很朴素我们把大模型当成了普通接口在用。对它预设了确定性没有防住不确定性。后来我们花了很长时间逐步把稳定性体系补齐核心就是三层机制——路由重试、校验降级、复核机制。这套方案迭代到今天这个版本在线上经受住了好几轮高峰流量和多次模型服务故障的考验整体稳定了很多。这篇文章我就把这三层机制的设计思路、落地细节和踩过的坑完整拆开讲清楚每一层到底在解决什么问题、怎么搭最稳、有哪些参数可以抄作业以及哪些地方按我的经验是需要避开的。1. 大模型业务的不确定性到底来自哪里1.1 一次线上事故把大模型当普通接口用的后果先说说那次事故的具体过程。工单分类这个业务很简单用户提交一条文本工单系统调用模型判断属于退款、投诉、咨询还是其他再路由给对应处理小组。上线初期我们把模型调用封装成了一个函数设置了超时3秒重试1次拿到文本结果就直接透传给业务流程。听起来没什么问题对吧但大模型场景和普通RPC有一个本质区别普通接口的返回内容是契约明确的状态码、字段、错误信息都是约定好的而大模型返回的是自然语言它可能超时、可能截断、可能输出格式不合法、可能语义完全跑偏。这些都不归超时和重试管。那晚的故障链是这样的供应商那边某个实例负载升高响应从正常的800毫秒恶化到5秒以上我们的客户端连续超时之后触发重试重试流量叠加在原本就拥堵的链路上直接把服务压垮。等模型服务恢复后输出格式已经被服务端变更过我们的解析逻辑不认识新字段名于是所有分类结果全部判空。这一连串问题没有一层是在调用前或调用后防住的。1.2 三类不确定性和三层防御的关系我后来把所有遇到过的异常归成了三类每类对应不同的防御方式第一类是请求链路的不确定性。网络抖动、实例重启、限流、负载升高这些会导致请求超时、连接中断、返回5xx。这类问题需要靠路由、重试、熔断这类基础稳定性手段来解决属于最外层防御。第二类是模型输出的不确定性。同一段输入今天返回这个格式明天可能返回另一个格式换了个prompt里看似无关的表达输出语义就可能跑偏甚至一个模型版本升级后原本能分类对的case开始分错。这类问题靠校验层来兜住输出必须通过格式和语义校验才能进入业务。第三类是业务决策的不确定性。模型输出格式没问题、语义也合规但结论本身是错的——把投诉工单误判成普通咨询导致用户投诉被延迟处理。这类问题靠复核机制兜底在一些影响面大的场景里做二次确认。三类不确定性、三层防御正好是标题里说的路由重试、校验降级、复核机制。下面我按处理顺序把每层展开讲从请求发出前一直到业务落库后。2. 路由重试把“打出去”这一步做稳2.1 路由不只是选择一个模型而是动态分发的决策问题很多团队接入大模型时路由就是写死一个endpoint。稳定跑起来没啥一旦模型服务升级、成本调整或者某个供应商出问题就要改代码发版周期长还容易出事故。我建议把路由设计成一个独立的决策模块核心职责是根据当前请求的特征和各个后端实例的健康状态动态选择一个最合适的服务地址。这里有一个很反直觉的点路由不应该只看延迟和可用性还要看模型版本。版本路由的价值不仅在故障转移更重要的是灰度发布。我们的上线流程是新模型版本先接5%的生产流量观察几天语义漂移情况和错误率一切正常再逐步放量到20%、50%、100%。如果新版本对某些类型的输入表现明显变差就把这类请求动态切回旧版本。这样做的好处是模型行为变化不再是上线后才发现而是上线过程中就能发现。给路由模块加健康状态的逻辑也很重要。我建议为每个候选实例维护一个滑动窗口窗口里记录最近一分钟的成功率、平均延迟、P99延迟和排队深度然后给这些指标加权算出一个健康分。请求进入时按健康分做加权随机选择。哪个实例在降级、哪个实例在限流都会被健康分自然拉低流量会自动流向更健康的实例这就是动态路由和静态负载均衡最大的区别。一个简化的路由配置可以长这样routes: - name: primary-model weight: 80 selector: version: v2 - name: backup-model weight: 20 selector: version: v1 fallback: enabled: true min_success_rate: 0.7 timeout_ms: 2000这里的关键是fallback这一层当主模型连续失败超过阈值或者响应耗时超过预设请求会主动切到备用模型。这个切换最好在路由层自动完成而不是由业务代码感知后手动处理。2.2 重试策略什么时候该重试什么时候该直接放弃重试是整个稳定性体系里最容易被写坏的地方。很多团队直接把SDK默认重试次数开到3次甚至更多觉得多试几次总能成功。但大模型场景的重试代价比其他接口高得多一次模型调用可能消耗大量token预算同时还会把排队延迟进一步放大。我现在的重试策略是区分错误类型只对值得重试的错误重试。可重试网络超时、连接中断、5xx服务端错误、限流如果是429且响应里带Retry-After。不可重试4xx参数错误、认证失败以及模型返回内容本身不合法。这里要特别强调最后一种情况模型返回的JSON解析失败很多人第一反应是重试一次看看运气。我的建议是不要重试而是走降级。原因很简单模型输出格式错误往往不是偶发的网络问题而是模型服务端变更了输出规格或者上下文被污染了重试大概率还是同样的错误反而白白消耗算力和时间。重试参数我推荐一组经过线上验证的配置首超时1500到2000毫秒最大重试2次采用指数退避退避基数1.5同时加±20%的随机抖动。加抖动这一步非常关键假设有100个请求同时超时如果没有抖动它们会在几乎同一个时间点发起重试形成一次新的流量尖峰加了抖动之后重试流量会被打散下游的恢复压力会小很多。另外重试必须配合幂等设计。大模型服务本身是无状态的超时并不代表请求没被处理很可能处理结果已经在返回路上了。如果客户端无脑重试同一笔业务请求就会被模型服务处理多次。解决方法是引入request_id每次业务请求生成一个唯一ID透传到模型调用层和服务端。服务端看到相同request_id就复用之前的结果这样即使重发也不会造成重复处理。2.3 熔断与限流防止重试变成二次事故重试风暴是稳定性事故中很典型的放大器。你设了重试2次下游故障期间原本1000个请求会变成最多3000个。如果所有客户端同时重试、同时退避结束下游瞬间就会被击穿。熔断器在这里起的作用是当某个实例的错误率超过阈值直接快速失败不再浪费时间和流量去试。我的熔断参数是这样的时间窗口10秒内错误率超过50%且调用量大于50次熔断器打开。熔断打开后后续请求直接走降级路径不再进入模型服务熔断器进入半开状态后放5个探测请求过去如果都成功就恢复全量流量如果还有失败就继续保持熔断。这样设计的好处是下游在恢复过程中不会被瞬间而来的大量请求再次打死。限流也很重要。限流的对象不是用户的业务流量而是对模型服务的调用量。我们在模型调用层做了一个信号量并发控制最多允许同时20个在途请求超过的请求进入排队或者直接降级。这个数字需要压测确定但思路是宁可让一部分请求走兜底方案也不能让所有请求都阻塞在模型调用上把整个业务线拖死。3. 校验降级模型输出不能直接当业务结果3.1 分层校验格式、结构、语义三步走校验层是我认为整个方案里投入产出比最高的一环。它做的不是复杂的事情就是把模型的输出挡在业务逻辑外面检查一遍检查不过关的绝不进入下游。校验要分层做从低级到高级一层层收紧格式层输出能不能被正常解析。要求模型按JSON输出时至少要做到JSON可解析、字段存在、类型正确。结构层字段值是否在合法范围内。比如分类结果必须是指定枚举值日期字段必须符合格式金额必须在合理区间。语义层输出内容是否满足业务规则。关键词白名单、黑名单、相似度阈值、业务规则引擎都算这一层。分层校验的意义在于不同层次的失败对应不同的处理策略。格式层失败往往需要走降级甚至重试结构层失败说明模型的输出规范和业务定义不一致需要调整prompt或做映射语义层失败是模型能力问题需要靠复核或者规则引擎兜底。举一个具体的例子。我们的工单分类服务要求模型输出这样的JSON{ category: refund, confidence: 0.92, summary: 用户反馈商品质量问题申请退款 }格式层检查的是这条内容能不能被JSON.parse解析结构层检查category是否在[refund, complaint, consult, other]枚举中confidence是否在0到1之间语义层检查summary是否太短、是否包含明显的敏感词、是否和category描述的对象一致。3.2 降级梯度从模型到兜底方案的分级切换校验不通过之后怎么办这就是降级机制要回答的问题。降级不能是挂了就报错而应该是一套有梯度的预案按严重程度逐级下沉。我做了一套L0到L3的降级定义L0正常调用主模型使用标准temperature。L1切换到备用模型、降低temperature、增加few-shot示例尝试消除输出格式漂移。L2完全不依赖模型改走规则引擎。关键词匹配、模板匹配、预设分类规则把请求降级成可预期的传统处理。L3异步化降级先把请求放进队列承诺延迟处理避免在峰值时期阻塞主链路。触发条件也要设计清楚。以我们的配置为例主模型连续3次校验失败触发L1L1连续失败5次或者L1响应超时直接触发L2当模型服务的错误率超过某个全局阈值所有请求直接往L2走。注意一个细节降级一定要提前把降级后吃什么准备好。规则引擎不是临时写的我们提前把常见工单的关键词库、正则规则、分类模板都维护好甚至在日常流量里做小比例抽样对比看规则分类和模型分类的差异率。这样真正降级时业务方知道当前方案能覆盖多少场景而不是降级到一个空壳。3.3 校验失败的三种去向静默、兜底、告警校验失败的结果不能一刀切。我们按业务影响面分了三种处理方式第一种是静默降级。适用于低风险场景比如内容打标、非关键字段填充。校验失败就直接用规则引擎的兜底结果不报错、不打扰用户。第二种是显式兜底。适用于用户可见的场景比如客服回复生成。校验失败时返回预定义的安全话术比如抱歉我暂时无法处理这个问题正在转接人工。这样至少不会让用户收到一段模型胡编的回复。第三种是告警加阻断。适用于高风险的场景比如金融类决策、内容合规判断。校验不通过就拒绝本次操作同时发起告警由人工介入检查原因。这类场景宁可损失部分可用性也不能让未经验证的模型结论直接进入业务。每种失败都要记录一条日志包含模型原始输出、校验失败原因、降级策略和最终返回内容。这些日志不仅是排查问题的依据也是后续优化prompt和阈值的数据底座。没有这些数据你就只能靠感觉判断到底是模型变笨了还是校验太严了。4. 复核机制关键场景的二次确认与闭环4.1 先判断“是否需要复核”影响面决定投入并不是所有大模型业务都需要复核机制。复核是有成本的它会引入延迟、需要人工参与、还可能在产品上呈现未确认的状态。所以先想清楚一个原则只有当模型结论进入业务后会带来不可逆后果或者用户体感极差时才值得做复核。我常用的判断维度有三个。第一是影响面这个模型结论会影响多少用户、多大金额第二是不可逆性结论出错后能不能轻松改回来第三是置信度模型自评的置信度是否足够高。比如工单分类分类错了用户可能被多等一会儿这类场景做抽样复核就够了但如果是自动退款判断金额一旦打出去就很难追回必须做高比例甚至全量复核。所以复核机制的密度不是一个固定值而是一个按风险评估动态调节的系数。低风险场景抽查1%高风险场景100%复核中风险场景按置信度阈值动态决定是否进入人工队列。这个比例要定期复盘根据线上复核差异率来调整。4.2 置信度评估与复核队列怎么配合置信度评估是复核队列的前置环节。如果模型返回的confidence明确可信复核就可以少做一些如果置信度很低或者根本没有confidence字段就需要从其他角度估算可信程度。有一次我们发现某个场景的模型返回的confidence普遍虚高都是0.9以上但复核差异率居高不下。后来才明白模型自评的置信度分布偶尔会整体偏移不能当作唯一依据。解决思路有两个一是从模型服务端拿到logits或者约束采样的概率分布做更细粒度的可信度评估二是用白名单规则对关键conflict字段做二次检查比如判断类别为投诉但摘要内容明显是夸赞这类矛盾。复核队列设计要注重几个参数优先级、超时时间、容量水位。我把复核请求分成高、中、低三个优先级对应不同的处理SLA。高优先级要求15分钟内人工处理中优先级2小时低优先级24小时。如果队列积压超过水位就会自动触发降级审批批量兜底策略避免人工队列成为新的瓶颈。4.3 双路交叉验证让模型之间互相纠错人工复核很贵所以在人工介入之前还可以先加一道机器复核也就是双模型交叉验证。具体做法是主模型给出结果后如果属于中高风险场景把同样的输入发给另一个模型作为第二路判断。两个结果一致且都达到置信度阈值直接通过结果不一致或者其中一路置信度偏低就进入人工复核队列。双路验证的工程实现要关注异步化。主结果可以先返回给业务第二路验证在后台跑等第二路结果出来后判断差异。如果主结果正确第二路验证只是日志记录不打断主链路如果发现差异再触发修正机制。我用过一个经验值第二路验证的模型参数可以更保守宁可误判不一致也不要放过可疑case。复核结果不能白白流失。每一笔被人工纠正的记录都要回流到样本池用于定期评估当前模型、阈值和校验规则是否合理。我们每两周会对复核差异率做一次回归分析如果某个分类的差异率持续上升就会去查是不是prompt语义漂移了或者该分类本身就特别难判需要单独调低置信度阈值。5. 落地复盘这套23.8版方案里几个最实际的教训5.1 重试引发下游重复操作幂等和去重不能只靠嘴上说说第一次做重试改造时我们只考虑了重试多少次、退避多久完全没考虑到重试会导致下游重复操作。线上就出现过工单被重复创建的故障一次请求超时模型服务其实已经处理成功客户端重试后又发了一次结果同一个工单被系统创建了两遍。这个问题靠事后去重很难救必须在链路设计阶段就埋好幂等。现在我们的做法是三管齐下客户端生成request_id透传全链路、数据库对业务唯一键加唯一索引、消费端做状态机校验幂等键已经存在就直接返回旧结果。这三个缺一个都有风险。5.2 超时、重试次数和降级阈值之间的参数博弈参数配置是看起来简单但实际上最考验压测功底的部分。刚开始我把首超时设成了10秒理由是模型生成需要时间结果模型服务出问题的那次所有请求都阻塞在超时窗口里业务线程全被占满整条链路直接瘫痪。后来改成流式场景分两段超时首token等待2秒整体生成上限5秒。重试从1次改成最多2次退避基数1.5加抖动。降级触发阈值从错误率5%且持续60秒调整成错误率2%且持续30秒。这些参数不是拍脑袋定的每一组都跑过全链路压测压出问题再回调。测试时有个重要的思路故障注入要分级别从小流量超时到全量熔断都要演练不能只测最好状态。5.3 校验过严导致的大面积误拦截校验规则也不是越严越好。有一版我们把白名单关键词和正则规则写得特别全想着宁可错杀也不放过结果上线后大量合法的工单被判成分类未知用户体验直线下降。后来我们想明白了一个原则校验层应该先做粗筛再逐步加精。第一版只做格式层和结构层的必要校验语义层先用黑名单兜住明显的问题跑一段时间积累了足够的误拦截样本后再把白名单规则一条条加回来。同时用拦截率和误杀率两个指标监控拦截率太高说明校验过严误杀率高说明规则设计有问题。这两个指标要一起看缺一个都会失衡。5.4 人工复核不能成为同步关键路径复核机制最容易犯的错是把人工复核放在同步主链路上等于每次请求都要等一个真人看完才返回。这在数据量小的时候没问题流量一旦上来人工队列积压就成了新的瓶颈。我的解法是先返回结果、后台复核、有差异再修正。用户在交互界面上看到的是模型生成的初步结果同时带一个待复核标记。如果后台复核发现问题系统会通过消息通道推送修正结果用户也可以发起申诉。这个模式牺牲了一点即时准确性换来了主链路的高可用和复核的充分时间。实际跑下来修正率稳定在可接受范围内用户体感反而比一直等待好得多。这套23.8版方案并不是一次设计完成的而是在多次事故和复盘里一点点补出来的。到现在我依然觉得大模型业务的稳定性没有终点三层机制只是把不确定性从不可控变成了可观测、可干预。下一步我打算把复核差异率、降级覆盖率和模型输出格式违规率这些指标做成日常SLO看板让稳定性从事后补救真正走向事前预警。