
10月15号下午我盯着监控面板看了将近十分钟所有曲线都是漂亮的绿色——请求成功率99.8%HTTP错误率几乎为零网关层面没有任何告警。但我心里清楚出事了。因为从下午三点开始业务方陆续有人反映“摘要质量突然变差”而我拉出最近500条请求的明细时看到那个熟悉的免费线路端点返回的正文输出中位数从平时的540个tokens掉到了128个。这就是我们十月份遇到的最典型的一次“隐形翻车”主线路上所有可观测指标全部正常但模型实际输出已经劣化到没法用的程度。这篇文章把整个过程拆开讲一遍包括Hermes这套自动容灾系统当时做了什么、为什么它一开始没能拦住、以及后来我们做了哪些紧急调优让它能在下一次类似故障时提前把人叫醒。如果你也在跑多模型网关或者你的业务依赖免费额度、低成本通道来承接批量流量这篇文章应该能帮你少踩几个坑。1. 先交代背景为什么网关里会有一条“免费线路”1.1 Hermes 在整套链路里的位置我们内部维护了一个多模型网关统一对外提供推理服务。上层业务不直接面对各家模型供应商而是通过网关按请求路由到不同的上游端点。Hermes 是这个网关配套的调度代理组件内部代号一直叫它 Hermes。它不拦截业务请求而是旁路计算每个上游端点的健康画像然后通过动态调整网关侧的路由权重来实现容灾。权重高流量就多权重低流量就被分走权重归零端点进入隔离状态。这套设计的好处是调度动作不影响正常请求路径即使 Hermes 本身挂掉网关依然按最后一次下发的权重工作。1.2 免费线路到底承担了什么流量所谓“免费线路”指的是某个开发平台每月赠送的免费调用额度端点。它不花钱但稳定性和付费端点完全不是一个量级。我们当时把三类流量放在免费线路上批量摘要任务、离线分类任务、低优先级的实验请求。这些流量的共同特征是对延迟不敏感、对成本极敏感、容忍一定程度的失败重试。交互式对话和核心业务推理则全部走付费主力端点不敢碰免费通道。这种流量分层思路本身没问题问题出在“免费线路承担了过多批量流量”之后它的劣化信号被噪音淹没了。1.3 为什么当时放心让它跑了三个月坦白说免费线路在最初三个月的表现确实不错。P99 延迟稳定在 1.8 秒左右输出 token 数中位数在 500 以上重试率不到 2%。对比付费端点质量差距在可接受范围内而成本差距是几十倍。于是我们逐渐把更多批量任务调度过去。现在回头看这里面有个隐性问题依赖一个免费端点本质上是在依赖别人的资源策略。上游随时可能调整配额、限流策略、甚至模型版本你没有任何协议保障。而我们的监控体系当时还停留在“能用/不能用”的层面这为后来的“隐形翻车”埋了伏笔。2. “隐形翻车”实录所有指标全绿但模型已经废了2.1 第一波症状不是从监控发现的而是从用户反馈开始的10月15号下午 15:20 左右业务方在群里反馈批量摘要任务开始出现大量“答非所问”的结果。比如给一段电商评论返回的摘要只有一句话或者干脆是重复原文的开头部分。我是从用户反馈开始排查的而不是监控告警。当时监控面板上没有任何红色告警这本身就是个危险信号——说明我们只监控了“请求执行情况”没有监控“响应质量情况”。十五分钟后我拉出了免费线路最近 2 小时的所有请求明细发现了一个非常典型的劣化特征输出长度分布出现了明显的双峰。一部分请求正常返回 500 到 800 tokens另一部分请求只返回 50 到 200 tokens而且后者的占比在持续上升。2.2 排查链路从网关日志到上游响应体排查过程大致如下第一层看网关错误日志。结果很意外几乎没有 5xx 错误。网关侧超时率从正常 0.2% 涨到了 4.7%但因为这个绝对数值不高没有触发告警阈值。第二层看 HTTP 状态码分布。全部是 200 OK。这也是最迷惑人的地方——如果大量 429 或者 503我们早就能定位了。第三层看响应体本身的规律。我随机抽取了劣化请求和正常请求做对比发现劣化请求的响应体普遍短得很整齐有些甚至明显是在句子中间被切断的。配合延迟数据一看P50 从正常的 1.2 秒涨到了 4.5 秒P99 从 1.8 秒飙到了 7.8 秒。到这一步基本可以确认免费线路的上游端点要么做了内部排队限流要么开始对部分请求执行“降级返回”策略。2.3 根因上游限流降级但是用的是“静默截断”而不是错误码我们和平台方确认后根因浮出水面这个免费端点从月初开始就调整了配额策略一旦用量接近当日阈值后续请求会进入一个内部排队队列。排队时间过长时上游不会返回 429而是直接返回一个“简化处理”的结果——只处理开头部分省掉中间过程。这就是为什么响应体呈现出整齐的截断形态而且状态码依然是 200。我把这类故障统称为“静默翻车”协议层面一切正常但服务能力明显劣化。它不像直接宕机那样来得猛烈却会在不知不觉中拉低整个业务的输出质量。2.4 为什么传统健康检查根本拦不住它传统健康检查通常做两件事探活ping 端点是否可连通和检查状态码是否有 4xx/5xx。这套逻辑能防住“端点挂了”但防不住“端点还活着但已经废了”。免费线路当时的情况是探活正常、状态码正常、响应体存在。唯一异常的是响应长度的分布、延迟的形态以及重试率缓慢上升。这些都是“质量指标”不是“可用性指标”。如果我们没有在这次事故后把质量信号纳入监控下次它翻车我依然发现不了。3. Hermes 的容灾改造从“探活”升级到“健康画像”3.1 丢掉单点探活换成多维信号融合事故之后我们第一时间就给 Hermes 增加了一系列质量信号采集。现在它评估一个端点是否健康看的不是“能不能连通”而是一组健康画像信号维度采集方式正常参考区间劣化参考区间P50 延迟网关侧响应耗时统计1 分钟窗口1.2s 以下4.5s 以上P99 延迟网关侧慢请求统计1 分钟窗口2.0s 以下6.0s 以上HTTP 错误率上游返回 5xx 的占比1% 以下5% 以上网关重试率因为超时或异常而触发重试的占比2% 以下10% 以上输出 token 中位数网关对每个响应做长度统计500 以上200 以下疑似截断比例响应体在固定位置异常切断的比例5% 以下30% 以上这里额外解释一下“疑似截断比例”怎么判定。我们没有做复杂的语义分析而是利用了几个便宜有效的特征响应是否以句号/换行符结尾、是否包含明显的不完整 JSON 结构、以及输出长度是否落在正常分布的长尾之外。这三点组合起来足以在免费线路出问题时尽早暴露。3.2 Hermes 的异常评分逻辑有了多维信号下一步就是把它们融合成一个可操作的评分。我给出一个简化版的核心逻辑保留了我们在生产环境里实际使用的主要思路# Hermes 健康评分核心逻辑简化版 def score_endpoint(endpoint, window300): p50 delay_percentile(endpoint, 0.50, window) p99 delay_percentile(endpoint, 0.99, window) err error_rate(endpoint, window) # HTTP 5xx / 超时 retry retry_rate(endpoint, window) # 网关侧重试占比 out_tokens median_output_tokens(endpoint, window) trunc truncated_ratio(endpoint, window) # 疑似截断占比 # 每个维度做 min-max 归一化数值越高质量越差 score 0.0 score 0.25 * _score(p99, [2.0, 6.0]) score 0.20 * _score(err, [0.01, 0.05]) score 0.20 * _score(retry, [0.02, 0.10]) score 0.20 * _score(1.0 - trunc, [0.95, 0.60]) score 0.15 * _score(out_tokens, [500, 150]) return score_score 函数很简单输入当前值、正常边界、劣化边界线性映射到 0 到 1 之间。0 表示完全健康1 表示彻底翻车。各维度的权重是我们根据这次事故的数据反推后调出来的——延迟和相关信号合起来占了 65%输出质量类信号占了 35%。事故前我们把重心几乎全放在错误率上这是一个教训。3.3 从“评分”到“调度”Hermes 的状态机评分只是输入真正执行动作的是一个简单状态机。每个端点始终处于以下四个状态之一HEALTHY正常处理全量流量评分低于 0.45。AT_RISK评分在 0.45 到 0.65 之间降低该端点权重到正常值的 30%只保留低风险批量任务。DRAINED评分超过 0.65且连续两个窗口确认该端点全部流量切走进入隔离观察。OBSERVING端点被隔离后每 30 秒用探针请求做质量验证若连续三次评分回到 0.4 以下才允许以小比例流量试探性回切。这套状态机的关键点在于“不要反应太快也不能反应太慢”。反应太快容易因为一次毛刺就把好端点降权反应太慢又会酿成我们在 10 月 15 号遇到的局面。所以我们要求“连续两个窗口都超阈值才行动”每个窗口是 5 分钟加上确认时间最迟 10 分钟左右能完成隔离。4. 自动容灾触发之后我们又做了一轮紧急调优4.1 第一次自动切换拦住了故障却带来了成本冲击10 月 20 号距离我们上线新版 Hermes 四天之后免费线路第二次翻车。这一次 Hermes 在 11 分钟内就把评分从 0.38 拉到了 0.82连续两个窗口确认后自动将所有流量切到了缓冲用的付费端点。从结果看自动容灾是生效了。但副作用很快出现了原本免费线路承接的批量任务全部涌向付费端点当天下午的推理成本直接翻了接近三倍。运维同学发文来问“是不是计费口径出错了”我如实回复不是计费错了是我们把几十万次批量请求一次性倒进了付费池子。4.2 分级熔断与冷却时间把“一刀切”改成“逐步降权”这次成本冲击让我们认识到全量切走是最安全的但未必是最合理的。于是我们把动作改成了分级熔断第一级评分 0.45-0.65 之间执行“降权到 30%”。也就是把交互式流量和一部分核心批量任务切走只留少量低风险任务继续尝试。第二级评分超过 0.65 且确认窗口通过执行“全量切走”。端点进入隔离状态不再接收任何业务请求。同时我们加入了 120 秒冷却时间端点从 DRAINED 状态恢复到 OBSERVING 状态后至少观察 10 分钟期间只允许探针流量不允许业务流量直接涌入。这主要是为了防止“上游刚好抖动了一下我们就把所有流量切走抖动恢复后又全部切回来”的乒乓效应。4.3 质量回归探针用固定问题持续“钓”端点为了在端点恢复阶段更可靠地判断“它真的好了吗”我们还加了一个质量回归探针。探针思路不复杂每 30 秒向免费端点发送 3 个固定格式的问题要求模型按固定 schema 返回 JSON。一旦响应不符合 schema 结构、或者输出长度明显偏短探针立即产生负向信号。为什么选固定问题而不是随机问题因为随机问题的输出长度天然波动大很难设阈值。固定问题配合固定格式你可以在每一次响应上做严格校验任何劣化都会立刻暴露出来。4.4 调优后的最终参数配置这是我们 10 月底最终落地的参数配置可以直接作为参考{ endpoint: free-tier-default, quality_scoring: { window_seconds: 300, trigger_score_at_risk: 0.45, trigger_score_drained: 0.65, confirm_windows: 2, cool_down_seconds: 120 }, graded_actions: [ { level: 1, score_range: [0.45, 0.65], action: reduce_weight_to_0.3, note: 只保留低风险批量任务 }, { level: 2, score_range: [0.65, 1.0], action: drain_all, note: 全部流量切走端点进入隔离观察 } ], recovery: { probe_interval_seconds: 30, probe_pass_required: 3, back_ratio: 0.1, step_up_ratio: 0.2, observation_minutes: 10 } }调优后第一周Hermes 又成功识别了一次免费的社区端点劣化这次不是全量翻车只是延迟缓慢爬升。因为评分系统提前介入流量在 AT_RISK 阶段就被降权了没有对业务造成任何可见影响也没有引发成本冲击。5. 这套容灾方案的边界什么能防什么依然防不了5.1 免费线路为什么会反复翻车三点共性经历了两轮事故之后我总结出免费端点翻车的三个共性原因。第一共享额度池。免费端点大多挂在共享配额下面同一批用户一起消耗额度。一旦某个大用户跑起大量任务所有共享者的延迟和成功率都会受影响。这种劣化是渐进式的很难用固定阈值捕捉。第二上游降级策略不可见。上游把“限流”实现为“排队”而不是“报错”把“降价”实现为“截断返回”而不是“降权提示”。你作为下游几乎没有任何协议层面的感知手段。第三免费端点没有 SLA 保障。它不会因为质量劣化而给你补偿也不会提前通知你策略变更。对依赖它的人来说唯一的应对方式就是默认它随时可能翻车并且在架构上做好自动切换的准备。5.2 Hermes 现在依然会漏掉的几种场景诚实地说目前的健康画像方案还有一些盲区。缓慢劣化最难防。如果输出质量以每周 1% 的速度下降评分曲线会非常平滑可能在触发阈值之前业务已经默默接受了很差的结果。对这种场景单纯靠评分不够还需要定期的人工质量抽检或语义评估。另一种是“模型效果变差但输出格式正常”。例如上游悄悄切换了模型版本新模型的输出长度和延迟指标都很正常但实际语义质量明显下降了。Hermes 目前只能感知结构和性能信号无法做真正的语义判断。我们在考虑接入固定语义测试集但这需要额外成本暂时还没有落地。5.3 面向 11 月的计划成本感知调度与按业务分级容灾这次调优带来的最大改变不是把评分阈值调好了而是我们第一次意识到容灾不应该是“全有或全无”而应该结合成本和业务重要性来做分级。下一步我们计划给 Hermes 增加“成本感知调度”能力。简单来说当免费端点评分下降时不是直接切走全部流量而是让调度器根据当前付费端点的容量冗余、实时单价以及业务请求本身的优先级动态决定切走多少流量。低优先级批量任务可以留着继续和免费端点“耗”高优先级交互流量则优先转移到付费端点。这个方向能不能在 11 月底前跑通还得看我们有多少时间填调度策略的坑。至少从 10 月的经验看方向是对的免费资源可以用但一定要在它背后放一个随时能接手的影子并且提前想好代价。最后再分享一个我自己的体会模型监控这件事真正难的从来不是“挂了通知我”而是“快挂了提前告诉我”。传统的探活和状态码检查只能覆盖前者覆盖不了后者。如果你也在搭类似的监控体系希望这篇文章里的健康画像思路能给你一些参考。先把那些“看起来正常、实际上已经在劣化”的指标纳入监控你就能在用户发现之前先发现一次隐形翻车。