模型部署不同于代码部署,失败是静默的、分布式的,需同时设质量门和健康门,并讨论了蓝绿部署的高GPU成本问题。
三个特性打破了常规假设。
失败是静默的、分布式的。提取准确率下降 2% 不会产生任何错误、延迟变化或告警。它会在三周后以工单的形式出现。
输出是非确定性的。你无法通过比较两个响应得出任何结论。比较必须是统计性的,基于一个样本。
容量是策略的约束。同时运行蓝绿部署意味着要持有两套 GPU。对于大型模型,这会在部署期间使预算中最昂贵的这一行翻倍,这是倾向于小规模金丝雀而非蓝绿部署的真正原因。
再补充一个,适用于在托管模型之间切换而非自有权重的情形:提供商在稳定名称下更新模型。Silent model updates 覆盖了这种情况,防御手段与部署时应用的评估门控相同,只是在 schedule 上而非 deploy 上触发。
它们可以组合使用,合理的默认值是组合使用:先用影子捕获真实输入上的崩溃和严重回归,然后用金丝雀捕获影子看不到的问题,最后晋升。LLM 的影子流量涵盖了复制机制,包括一个有副作用的请求被影子化的陷阱。
拆分必须按用户粘性,而非按请求。一个会话在两个模型版本之间交替的用户会得到一个不一致的助手,你在会话上计算的任何指标都会被污染。将一个稳定标识符 hash 到桶中:
import hashlib
def variant(user_id: str, canary_percent: int, salt: str = "chat-model-2026-08") -> str:
"""Stable assignment: the same user always lands in the same bucket
for a given salt. Change the salt to re-randomise a later experiment."""
h = hashlib.sha256(f"{salt}:{user_id}".encode()).digest()
bucket = int.from_bytes(h[:4], "big") % 100 # 0..99
return "canary" if bucket < canary_percent else "stable"
两个细节。salt 意味着第二次发布不会重用同样倒霉的用户,否则会使你的实验产生关联性。使用 sha256 而不是 Python 内置的 hash,因为内置的 hash 是按进程随机的,会在重启时重新分配所有用户。
在基础设施层,同一分片可以通过一个 Service 后面的两个 Deployments 以及成比例的副本数来表达,或者通过服务网格或入口的加权路由来表达。副本数方法是最简陋的——10% 的金丝雀意味着十个副本中有一个,所以你无法在没有十个副本的情况下表达 1%——但它不需要安装任何东西。无论哪一层做分片,都要将 variant 记录在请求上,这样每条日志、追踪和评估都可以按它分组。没有这个标签,金丝雀就产生不了任何证据。
是一组在发布前决定的阈值,自动检查,在凌晨 2 点不需要人工判断。三个层级,按顺序评估:
离线评估,在任何流量之前。对候选版本运行冻结的评估集。绝对门控:低于阈值,发布不启动。这是唯一一个失败零成本的层级。
运营指标,从第一批金丝雀请求开始。错误率、p95 延迟、首个 token 的时间、每秒 token 数、截断率、拒绝率、每请求成本。这些指标变化快,能在几分钟内捕获严重故障。
质量指标,在金丝雀窗口内。无论你在实时流量上对正确性有什么代理指标:schema 验证通过率、工具调用成功率、检索锚定的引用检查、点踩率、样本人工审核。这些很慢,但它们决定模型是否真的更好。
完全自动化第一层和第二层。第三层是样本量问题咬人的地方,也是大多数团队基于一个不可能有意义的数字晋升的地方。
假设你的质量代理是通过率——schema 有效、工具调用成功、裁判说可接受。你想检测该率的大小为 Δ 的回归。标准的两比例样本量,所有假设都标注了:
n per variant ≈ 2 × p(1−p) × (z_α/2 + z_β)² / Δ²
p baseline pass rate
Δ the smallest regression you want to detect, in absolute terms
z normal quantiles; for 95% confidence and 80% power,
z_α/2 = 1.96 and z_β = 0.84, so (1.96 + 0.84)² = 7.85
Worked, with p = 0.90:
Δ = 0.05 (detect a drop from 90% to 85%):
n = 2 × 0.09 × 7.85 / 0.0025 = 565 requests per variant
Δ = 0.02 (detect 90% → 88%):
n = 2 × 0.09 × 7.85 / 0.0004 = 3,533 per variant
Δ = 0.01 (detect 90% → 89%):
n = 2 × 0.09 × 7.85 / 0.0001 = 14,130 per variant
Note the 1/Δ² : halving the effect you want to catch quadruples the sample.
现在将其转换为金丝雀持续时间,这才是你真正需要的数字:
total traffic ......... 20 requests/second = 1.73 M/day
canary share .......... 5% = 86,400 canary requests/day
requests with a usable quality label ... 30% (not every request has one)
= 25,920 labelled/day
To detect Δ = 0.02, needing 3,533 labelled canary requests:
3,533 / 25,920 ≈ 0.14 days ≈ 3.3 hours
At a 1% canary instead:
canary labelled/day = 5,184 → 3,533 / 5,184 ≈ 16 hours
这就是将"跑一会儿金丝雀"变成一个决策的算术。如果答案比你愿意等待的时间长,你的选择是诚实的:提高金丝雀百分比、接受更大的可检测 Δ、标记更多请求、或在离线评估上更用力(在那里你控制样本)。LLM 评估的统计数据会进一步处理通过同时观察六个指标你所创建的多重比较问题。
实时流量不是随机实验,除非你让它成为。粘性分桶给你随机分配;但如果分片与地理位置、租户或一天中的时间相关,它不能阻止金丝雀群体漂移。在相信结果差异之前,检查两个变体是否看到了可比的输入分布。
在发布开始前将中止条件写入发布计划,形式为"如果 X 持续 Y 分钟,则回滚"。写下来的意义在于,在凌晨 2 点,看着一个数字波动时,没有人需要决定什么算作坏。
# rollout.yaml — the plan, reviewed with the change
steps: [1, 5, 25, 50, 100] # percent of traffic
dwell: [1h, 4h, 12h, 12h, -] # minimum time at each step
abort_if:
- metric: error_rate_5xx op: ">" value: 0.5% for: 5m
- metric: p95_latency_ms op: ">" value: 1.25 relative_to: stable for: 10m
- metric: schema_valid_rate op: "<" value: 0.98 relative_to: stable for: 30m
- metric: cost_per_request op: ">" value: 1.15 relative_to: stable for: 1h
- metric: thumbs_down_rate op: ">" value: 1.30 relative_to: stable for: 4h
on_abort:
- set canary traffic to 0 # seconds; the stable pods never went away
- keep canary pods running # for diagnosis, not for traffic
- page the on-call, do not auto-promote again without a human
最后两行是人们常漏掉的。在中止后保持金丝雀 pods 运行可以保留证据——日志、追踪、一个可供检查的活进程——以及将流量设置为零而不是删除部署,使回滚在几秒内完成而非一个调度周期。接下来做什么在 on-call runbook 里,那里面有一页专门讲这个症状。