说明本文讨论的是把外部模型 API 当成会故障的三方依赖来设计的多路接入与切换方案属于 AI 运维话题不涉及具体模型版本与价格。AI 领域版本迭代极快凡涉及版本号、价格、可用性请以你阅读时的官方页面为准。文中代码为结构示意请按自己的技术栈调整后再上生产。一、上游的可用性决定了你的可用性上限串联链路里最小的一环决定上限结论先给只要你的每次请求都必须穿过一次外部模型调用你的服务可用性上限就等于那段调用的可用性而且因为乘法关系只会更低。一次端到端请求的可用性是链路上每一段相乘。上游外部模型服务某个月跑到 99.9%你的接入层 99.9%业务服务 99.95%乘下来约 99.75%。你把自己那段提到 99.999%端到端也只挪到 99.85% 上下——乘法里最小的因子不在你手里。环节谁控制典型量级能动的空间外部模型 API上游厂商你只能观测换路、换厂商你的接入层自己可做到较高重试、多路接入都落在这里你的业务服务自己常规工程可控常规优化端到端相乘结果低于任一段上限被外部那一段压住它和普通三方依赖的差别只有三处把外部模型当依赖看绝大多数手段都是现成的超时、重试、熔断、降级、多路。需要单独对待的差异只有三处。第一错误分类更细。普通依赖是成功和失败两态外部模型还有第三态调用成功、返回正常、但结果不能用而这一态在 HTTP 层看不出来。第二延迟分布更宽。同一个接口短请求和长请求的耗时能差几个数量级同一输入的耗时也会波动超时值不能只按固定数字拍。第三返回内容是变长文本。结构化字段解析出错是显式的模型返回一段文本格式漂移是隐式的可能解析成功但字段落错位。你手里真正能动的只有三样东西外部那一段不可控能做的就是三件事缩短故障暴露时间早发现、准备可替换的路径多路接入、让不完美的结果仍然可用降级与兜底。三件事的优先级不是并列的。发现得慢换路再快也没用只有一条路发现再快也只能干看着。顺序上先补观测和错误分类再加第二路最后才是精细化的切换策略。二、三类失效形态要分开识别完全不可用信号最清楚误判也最少完全不可用的定义是连不上、超时、或者大面积返回 5xx。信号非常明确。但信号清楚不等于处置简单。最容易出的错是把两类东西混进同一个池子一类是上游真挂了另一类是你的出口、DNS、证书或者代理出了问题。区分办法很土但有效准备一个不经过业务逻辑的最小探测请求和业务链路的错误分开计数。最小探测也失败问题大概率在上游或链路最小探测正常而业务在报错就回头查是不是请求本身触发了上游的某种限制。部分劣化全是 200但全都是慢的部分劣化是最容易被漏掉的一类。接口没报错状态码正常只是延迟整体抬升、或者错误率小幅爬升。它之所以危险是因为只看错误率的告警完全看不见它。成功率 99% 看起来一切正常但 P99 从 800 毫秒涨到 8 秒体验已经崩了。而且它往往不是全量劣化只是某个子集变慢——某类长请求、某个区域的出口看整体平均值会被稀释掉。识别它的办法是把延迟按分位数看并且分组看P50、P95、P99 分三个看再按请求类型、按出口区域各看一遍。只有 P99 抬升说明小部分请求在变慢P50 一起抬说明整体在退化。行为变化接口是健康的输出变了第三类最隐蔽接口正常延迟正常错误率为零但输出分布变了。可能的表现是同类问题回答整体变短某个字段以前经常出现现在经常为空某类边界输入以前能处理现在稳定跑偏。这类变化在监控上没有任何异常只有拿固定问题集跑回归才会发现。所以对这类失效在线监控指标是没用的靠的是评测基线的定期回归。多路接入还会把「输出分布不同」变成常态基线要能接受多条路的差异。⚠️ 代码待验证fromenumimportEnumclassFailureMode(str,Enum):UNAVAILABLEunavailable# 连不上 / 大量 5xxDEGRADEDdegraded# 200 但延迟抬升 / 错误率小幅上升BEHAVIOR_CHANGEDbehavior# 接口健康输出分布变了defclassify(sample:dict)-FailureMode:# sample 来自一次聚合窗口而不是单次请求ifsample[error_rate]0.5orsample[connect_fail]0:returnFailureMode.UNAVAILABLE# 关键成功率正常时要靠延迟分位数而不是错误率来判断ifsample[p99_ms]sample[p99_baseline_ms]*3:returnFailureMode.DEGRADEDifsample[error_rate]0.01andsample[p99_ms]sample[p99_baseline_ms]*1.5:# 到这里三类显式信号都正常是否有行为变化要靠离线回归判定# 在线侧只能打一个待复核标记不能直接下结论returnFailureMode.BEHAVIOR_CHANGEDreturnFailureMode.DEGRADED最要紧的是最后那个分支在线信号全绿的时候你无法在链路上判定行为变化只能标记出来交给离线回归。指望在线监控抓行为漂移方向就错了。失效形态主要信号常见误判正确的第一动作完全不可用连接失败、5xx、超时把自己的网络故障算到上游头上触发换路同时看最小探测部分劣化延迟分位数抬升只盯错误率认为一切正常降权该路流量准备切换行为变化无在线异常回归集出现差异当成偶发逐条排查样本先切流量再回溯差异样本处置顺序不同是关键前两类要立刻动流量第三类要先动流量、再查原因。把三类混成一个「上游异常」告警切换一定慢。三、多路接入的三种形态同厂商多端点先做成本最低第一种是在同一个厂商内部接入多个区域或多个端点。优点是便宜、快、几乎不用改业务代码——协议一样只是地址不同。它的天花板也明显只解决「单点挂了」不解决「整个厂商挂了」。厂商级故障、账号级配额耗尽多端点全在同一个池子里一起受影响。这层值得先做但不能只有这层。跨厂商多模型抬得动上限代价在口径第二种是接多家厂商。这是唯一能把可用性上限真正抬起来的做法因为两个独立厂商同时全挂的概率远低于任何单点。代价从接入那一刻就开始付请求字段名不同、流式返回格式不同、错误码体系不同、结束原因的表达不同。更隐蔽的是能力与口径的差异两侧输出长度倾向不同、对同一条指令的服从程度不同。跨厂商不是纯网络层的动作它会往上穿透到提示、解析和评测。自建或端侧兜底拿能力换可用性第三种是自己部署一个较小的模型或者接一个端侧能力作为最末一层。它的定位不是「平替」而是保底所有外部路都不通时还能给出一个基本可用的回答而不是直接报错。要清楚它的代价。一是能力差距同一条提示在不同规模模型上的表现差异很大兜底路的输出需要更严格的校验二是它的可用性上限取决于你自己的基础设施——这也是它只能当最后一层、不能当主路的原因。⚠️ 代码待验证# 多路接入注册表每一条路都要显式登记自己的属性# 不要把这些差异藏在代码分支里routes:-id:primary-atier:primary# 主路scope:same-vendor# 同厂商不同端点endpoints:[endpoint-1,endpoint-2]weight:80timeout_ms:20000supports_stream:true-id:backup-btier:backup# 备用路scope:cross-vendor# 跨厂商口径与主路不同weight:20timeout_ms:30000supports_stream:trueprofile:profile-b# 关联提示与解析配置切换后要用它-id:local-fallbacktier:fallback# 保底路能力弱但可控scope:self-hostedweight:0# 常态不承接流量timeout_ms:60000supports_stream:false最值得注意的字段是profile。跨厂商那条路带了不同的提示与解析配置切换时必须一起切过去否则等于用 A 的提示去调 B。形态能解决什么主要代价建议位置同厂商多端点单端点故障挡不住厂商级故障第一层先做跨厂商多模型厂商级故障口径、能力、成本三重差异第二层主力自建 / 端侧兜底全外部不可用能力差距、自担运维最后一层保底四、切换的判据怎么定单一指标一定会骗你只看错误率会漏掉「全是 200 但都慢」只看延迟又会被另一种情况骗过——上游在快速返回错误响应时间是 20 毫秒延迟指标非常健康可用性其实已经没了。所以判据必须是错误率与延迟的组合条件错误率超过基线一定倍数且持续超过一个观察窗口或者延迟分位数超过基线若干倍且同样持续超窗。任意一个单独触发都只降权不切换。⚠️ 代码待验证defshould_switch(win:dict,cfg:dict)-bool:# win 是一个滑动窗口内的聚合不是单次请求err_badwin[error_rate]cfg[error_rate_threshold]slow_badwin[p95_ms]cfg[p95_baseline_ms]*cfg[slow_factor]# 关键一两种劣化都要有足够的样本量避免小流量下被噪声触发enoughwin[samples]cfg[min_samples]# 关键二必须持续够久抖动不算故障sustainedwin[unhealthy_seconds]cfg[min_unhealthy_seconds]returnenoughandsustainedand(err_badorslow_bad)两个容易被忽略的约束都在注释里样本量和持续时间。没有样本量下限低峰时几笔失败就能把整条路切掉没有持续时间下限一次网络抖动就会引发切换而切换本身是有成本的。迟滞与最小驻留让开关别抖切换之后最糟的状态是立刻又切回来。流量在两路之间反复横跳每跳一次都产生一批口径不一致的结果日志里全是切换记录没人看得懂发生了什么。两个机制能压住它。迟滞是指进入故障态和恢复正常的阈值不取同一个数——比如错误率涨到 20% 判故障要降到 5% 才算恢复中间这段谁也不动。最小驻留是指切换后无论信号好坏都强制保持一段时间不再切换。⚠️ 代码待验证switching:error_rate_threshold:0.20# 进入故障判断的阈值recovery_threshold:0.05# 恢复阈值更低形成迟滞带slow_factor:3# 延迟超过基线的倍数min_samples:200# 低于这个样本量不判定min_unhealthy_seconds:60# 持续多久才算真故障min_dwell_seconds:300# 切换后最小驻留时间禁止立刻切回max_switches_per_hour:3# 兜底一小时内切换次数上限最后一个字段是给自动化上的保险。前面所有阈值都可能被调错一小时切换次数上限能在最坏情况下把损失框住顺便让「一直在切」变成一条明确的告警。熔断与半开状态机落地熔断器的价值在于把「判断」和「执行」分开状态机负责什么时候切路由层负责切到哪。三态是最小可用的设计——闭合、断开、半开。闭合态正常放量连续满足故障条件后进断开态流量全走备路断开持续一段时间后进半开态放一小部分探测流量回主路成功回闭合失败退回断开并重新计时。要写死的两个参数是半开态的探测比例和判定所需样本量都不能靠默认值。一个常见坑熔断器是每路一个不是全局一个。全局熔断器在主路断开时会让整个路由层进入异常态备路也跟着受影响。五、切换之后要重新对齐什么切换的难点从来不在「切」切换这个动作本身很简单改一个权重就行。难点在切换之后不同路的输出分布不同同一套评测基线、同一套提示、同一套解析代码未必同时成立。最典型的翻车是主路切到备路接口通了任务也跑成功了但下游解析代码是按主路格式写的遇到备路输出的细微差别就解析失败或者解析没失败但字段落错位产出一批看起来正常、实际错误的业务数据。这类问题不触发告警只有业务侧发现数据不对才暴露。四样东西必须跟着路一起切要对齐的东西不对齐会怎样怎么算对齐了提示与系统约束服从度差异被放大输出结构跑偏每条路有独立的提示配置档解析与校验代码字段错位产出静默错误的数据断言失败率在可接受范围内评测基线拿主路的基线评判备路误判为质量下降基线按路记录比较同路前后成本与用量口径用量报表对不上容量与预算失真用量按路打标能分开也能合并四样里评测基线最容易做错。多路接入之后「同一个问题集跑出来的分数」不再可跨路比较因为备路本身就和主路有系统性差异。正确的用法是同一条路自己和自己比用基线监控每条路是否发生行为变化而不是判定「备路比主路差多少」——后者在设计多路时就已接受了。⚠️ 代码待验证{route_id:backup-b,profile:{prompt_profile:profile-b,parser_profile:profile-b,eval_baseline:baseline-backup-b,cost_tag:route:backup-b},switch_context:{switched_at:1760000000,trigger:p95_exceeded,from_route:primary-a,dwell_until:1760000300}}trigger字段不能省。切换发生后回头复盘第一个要回答的问题就是「当时为什么切」。没有这个字段日志里只有一串切换时间复盘只能靠猜。灰度切换不要一刀全切既然两条路的输出有差异就没有理由一次把流量全导过去。做法是按比例迁移先切 5%看解析失败率、业务指标、关键任务成功率没问题再逐级往上加最后才到全量。比例控制要放在路由层而不是散在各业务调用点。业务代码只说「我要调用模型」路由层决定走哪条路、按什么比例分切换和回滚就只是配置变更不需要发版。完整版资料清单本文用到的多路接入对照表与切换演练记录都整理在里面了扫码即可获取六、不能切换时的兜底三条兜底路径代价各不同有些情况切换根本走不通所有外部路同时不可用、备路能力不足以完成任务、或者任务对输出的要求高到备路不敢接。这时候只能降级。三条路径按代价从低到高排。排队代价最低请求先不失败进队列等上游恢复适合可以延后、用户能等完成的任务。降级到低配模型是中间一档外部全挂时走自建或端侧能力换可用性输出用更严的校验兜住。转人工是最后一档只适合少量高价值请求。兜底路径适用条件代价必须配套的东西排队等待任务可延后、用户能等延迟变长可能堆积队列深度上限、超时后置动作降级到低配模型所有外部路不可用质量下降更严格的输出校验转人工少量高价值请求人力成本、响应慢交接单据与状态回写三条路径的选择不能临时现判故障现场决定的策略大概率是错的。做法是按任务类型事先绑定哪类可以排队、哪类可以降级、哪类必须转人工写进配置故障时按配置执行。降级状态要显式传给下游降级之所以容易出问题是因为它经常被做成「静默的」。系统用低配模型顶着返回的响应结构和正常情况一模一样调用方看不出来——直到用户发现结果质量不对。要求就一条降级必须出现在响应里。响应体带一个表述当前服务模式的字段值至少区分正常、降级、兜底三档并带上降级原因和开始时间。调用方据此展示提示、把结果标记为待复核、或者直接拒绝使用。⚠️ 代码待验证{request_id:req_9c21,mode:degraded,degraded_reason:all_upstream_unavailable,degraded_since:1760000000,route_id:local-fallback,result:{text:……,quality_hint:reduced}}quality_hint经常被质疑多余反正已经有mode了。它的作用是给下游一个不用理解路由细节就能做判断的信号——下游不需要知道什么叫 local-fallback只需要知道这次结果的质量档位比平时低。三件不该假装的事第一不该把降级当正常返回。降级时也返回 200只是内容差一点调用链一层传一层到用户那里已经没人知道这次结果是降级的。第二不该把降级状态只写进日志。日志是给人查的下游系统读不到就没法做任何自动处置。第三不该在降级期间悄悄改口径。降级路能力弱有时会顺手把输出截断、把字段填默认值。不说明的话下游收到就是一份看起来完整、实际有缺的数据。七、演练与观测演练定期做且要挑低峰切换逻辑最怕「写了但从没触发过」。真故障发生时才发现判据不对、路由配错、备路没通过验证这时候已经晚了。所以切换演练要定期做而且要做成真正把流量切过去的形式而不是只调参数看一眼状态。三个固定动作在低峰期做切换会产生口径不一致的结果别落在业务高峰、保留当次全部记录切了多久、哪条路、有没有报错、演练后检查兜底路径验证排队和降级的配置是否还生效。频率上建议按季度做一次全量演练、按月做一次小比例切换。频率太低还有个坏处路由、提示、解析配置分属不同的人维护几个月不看就对不上了。每一次切换都要留档切换记录是排障时最有用的东西。要留的字段不多切换时间、触发原因、前后路、自动还是人工、驻留多久、切回时间。有这几项就能回答「当时为什么切」和「切了之后发生了什么」。⚠️ 代码待验证# 演练脚本骨架先在低峰窗口把主路权重降到 0观察一段时间再恢复# 注意以下命令是结构示意路由控制接口按自己的实现替换# 1. 记录演练开始route_ctl log--eventdrill_start--operator$USER--notequarterly failover drill# 2. 把主路权重降到 0流量全走备路route_ctl set-weight--routeprimary-a--weight0route_ctl set-weight--routebackup-b--weight100# 3. 观察窗口内检查三件事解析失败率、业务成功率、兜底是否被触发sleep900route_ctl stats--routebackup-b--window900s# 4. 恢复权重前确认备路输出与主路的差异是否在预期内route_ctl diff-baseline--routebackup-b# 5. 恢复主路route_ctl set-weight--routeprimary-a--weight80route_ctl set-weight--routebackup-b--weight20route_ctl log--eventdrill_end--operator$USER第 4 步是关键。多数演练只验证「切过去还能不能跑通」不验证「结果还对不对」。可用性恢复了、数据错了等于没恢复。三组指标一起看切换相关的观测最终收敛到三组指标要放在一起看单看任何一组都会误判。指标组具体看什么异常通常意味着上游健康错误率、延迟分位数、最小探测结果上游真在退化或在恢复正常路由状态当前生效路、权重、切换次数、驻留时长配置被改过或者正在抖动业务结果解析失败率、任务成功率、降级触发率切换后的口径没有对齐三组里最容易缺的是第三组。前两组接入监控系统就有第三组要业务侧埋点经常没人做。但没有它你只能知道「流量切了」不知道「用户拿到了什么」。完整版资料清单本文用到的多路接入对照表与切换演练记录都整理在里面了扫码即可获取附表 A关键取舍一览本文涉及的所有工程判断集中在这里方便按需回看。取舍本文结论判断依据位置三类失效形态分开识别、分开处置处置动作不同混一类会拖慢切换第二章部分劣化怎么发现延迟分位数 分组看成功率正常时错误率告警看不见劣化第二章行为变化怎么发现离线回归集不靠在线监控接口健康时在线信号全绿第二章切换判据错误率与延迟的组合条件单指标各有被欺骗的方式第四章判据的两个下限最小样本量 最小持续时间缺了会被噪声和抖动触发第四章防抖机制迟滞阈值 最小驻留时间反复切换会产生大量口径不一致结果第四章熔断器粒度每路一个不全局共用全局熔断会把备路一起拖下水第四章切换后要对齐什么提示、解析、评测基线、成本口径不对齐会产生静默的错误数据第五章评测基线怎么用同路自己比不跨路比分数跨路差异是多路设计的既定代价第五章兜底路径怎么选按任务类型事先绑定故障现场临时决定大概率是错的第六章降级要不要暴露必须写进响应体下游系统读不到日志无法自动处置第六章演练怎么做定期、低峰、真切流量、留档没触发过的切换逻辑等于没有第七章附表 B术语速查表术语含义多路接入为同一能力接入两条以上调用路径用于故障时切换同厂商多端点在同一厂商内接入多个区域或端点不解决厂商级故障跨厂商多模型接入不同厂商的模型是唯一能抬升可用性上限的做法自建兜底自行部署或使用端侧能力作为最末一层保底路径完全不可用连不上、超时或大面积 5xx 的失效形态部分劣化请求成功但延迟抬升或错误率小幅上升的失效形态行为变化接口与延迟均正常、输出分布发生变化的失效形态最小探测请求不经过业务逻辑、用于区分上游故障与自身链路问题的请求迟滞进入与退出故障态的阈值不取同一个数用于抑制抖动最小驻留时间切换后强制保持、禁止立即切回的时间窗口熔断器分三态控制某一路流量进出的状态机半开熔断后放少量探测流量回原路的中间状态路由比例各路承接流量的百分比按灰度策略逐步调整降级模式明确标注当前服务处于正常、降级还是兜底状态的响应字段评测基线用于监控单一路是否发生行为变化的固定问题集与结果记录写在最后这篇用到的资料写这篇文章时我把几个模型的官方文档、接口说明和切换记录都对了一遍顺手整理成几份配套的东西大模型学习路线图从 LLM 基础到 Agent 开发各阶段该学什么、用什么资料大模型全套教程按主题分好的视频与文档清单大模型实战好书24 本附每本适合的阶段资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「大模型」优先通过。拿到之后建议先看学习路线图那一份先定位自己在哪个阶段再决定学什么比一上来就啃框架效率高得多。