灰度与回滚:1% 灰度只是让少数人替你踩雷
实测——严重问题 1% 灰度只有 6% 概率发现;轻微退化几乎发现不了
AP5 的门禁挡住了评测集能测出来的退化。但有些问题只有真实用户能发现:真实的提问分布、真实的脏输入、真实的使用节奏。
所以要灰度。但灰度不是"先放 1% 试试"这么简单——
【新版本失败率从 2% 涨到 10%(严重问题)】
总流量 灰度比例 灰度组样本 能发现的概率
1000 1% 10 6%
1000 5% 50 90%
1000 20% 200 100%
总流量 1000、灰度 1% 的情况下,一个严重问题只有 6% 的概率被你发现。剩下 94% 的情况里,你会得出"灰度很稳,放量吧"的结论——然后全量翻车。
灰度太小,不是谨慎,是让少数人替你踩雷而已。
新配方的菜先给一桌客人试。一桌里只有 3 个人吃了,其中 1 个说"还行"——你能得出"新配方没问题"吗?
不能。你只是没听到抱怨,而不是证明了没问题。这两件事的差别,就是这一节的全部内容。
一、分流必须"粘住"用户
500 个用户 × 10 次访问,共 4500 次「相邻两次是否换了版本」
随机分流:翻转 817 次
哈希分流:翻转 0 次
随机分流(每次现掷骰子)是初学者最常写的一行代码:
if random.random() < 0.1: # ❌ 每次调用都重新决定
use_new_version()
后果有两个:
- 用户体验错乱:上一句还是新版风格,下一句变回旧版——用户会觉得"你们这个 AI 时好时坏";
- 数据被搅浑:同一个人的行为被同时算进两组,A/B 结论直接失效。
正确写法——按稳定的 key 哈希:
def bucket(user_id, pct, salt="exp_prompt_v3"):
h = hashlib.md5((salt + ":" + user_id).encode()).hexdigest()
return (int(h[:8], 16) % 100) < pct # ✅ 同一个用户永远落在同一边
用 user_id(没登录就用设备 id 或会话 id)。salt 的作用:换一次实验就换一次 salt,否则同一批"倒霉用户"会被你每一次灰度都选中。
二、灰度比例:多大才看得出问题
【新版本失败率 10%(严重)】 【新版本失败率 4%(轻微)】
1000 流量 1% 灰度 → 6% 1000 流量 1% 灰度 → 1%
1000 流量 5% 灰度 → 90% 1000 流量 5% 灰度 → 34%
1000 流量 20% 灰度 → 100% 1000 流量 20% 灰度 → 43%
10000 流量 1% 灰度 → 98% 10000 流量 1% 灰度 → 36%
10000 流量 5% 灰度 → 100% 10000 流量 5% 灰度 → 44%
10000 流量 20% 灰度 → 100% 10000 流量 20% 灰度 → 48%
三条实用结论:
① 决定发现能力的是"灰度组的绝对样本量",不是百分比。1% × 10000 = 100 个样本(98% 能发现),1% × 1000 = 10 个样本(6% 能发现)。同样是 1%,天差地别。所以流量小的产品应该用更大的灰度比例——20%、甚至 50%。
② 轻微退化极难发现。失败率 2% → 4% 这种变化,即使 20% 灰度、10000 流量,发现概率也只有 48%。这正是 AP5 门禁存在的理由:能在评测集上测出来的,就别指望线上灰度替你发现。
③ 灰度要配"最少观察量"。上线前先算:要发现 X 大小的退化,需要多少样本?样本不够就别放量,也别宣布成功。
# 一个够用的经验法则
最少观察量 = max(200, 基线失败次数 × 20) # 至少 200 个请求
最短观察时长 = 覆盖一个完整的使用周期 # 至少跨过一次早/晚高峰
"最短观察时长"经常被忽略:早上 10 点放灰度、11 点看着没事就全量,你完全没测到晚高峰的流量和另一批用户。
三、说好的 1%,实际是多少
设定 1% → 实际 1.15%(23/2000 用户)
设定 5% → 实际 4.75%(95/2000 用户)
设定 10% → 实际 9.45%(189/2000 用户)
设定 50% → 实际 50.05%(1001/2000 用户)
小比例灰度天然有偏差。上线后核对一次实际分流比例,别只信配置里写的数——如果实际是 3% 而你以为是 1%,你的风险敞口是三倍。
回滚:改配置,不发版
CONFIG = {
"prompt_version": "v3", # 提示词版本(ap2.4)
"model": "deepseek-chat", # 模型版本
"index_version": "kb_2026_08_01", # 检索索引版本(ap4.7)← 最容易忘记版本化的一个
"canary_pct": 10,
"canary_salt": "exp_prompt_v3",
"kill_switch": False, # 一键全量回退到稳定版
}
回滚 = 把 canary_pct 改成 0,或 kill_switch 改成 True——秒级生效,不用发版。
如果你的回滚方式是"改代码 → 提交 → 构建 → 部署",那么在最需要它的那几分钟里,你只能干等。
三样东西必须一起版本化:
| 版本 | 忘了会怎样 |
|---|---|
| 提示词 | 最容易记得的一个 |
| 模型 | 换了模型没换回来,行为完全不同 |
| 检索索引 | 最容易忘——只回滚提示词而索引还是新的,你回到的是一个从来没测过的组合 |
灰度期间该盯什么
第 ④ 步的关键是"分组对比"而不是"看绝对值":线上指标本来就在波动(ap2.5 的噪声地板),只有新旧两组在同一时间窗口内的对比才有意义。
🔧 动手做:给你的产品做一次灰度(20 分钟)
- 跑本节代码,看1% 灰度在小流量下只有 6% 的发现概率,和随机分流的 817 次体验翻转。
- 写你的
bucket()函数(哈希分流 + salt),并写一个测试确认同一 user_id 多次调用结果稳定。 - 把提示词/模型/索引三个版本号放进一份配置,配置能热更新(数据库/配置中心/环境变量 + 定时重载)。
- 加 kill_switch,并演练一次:从"决定回滚"到"线上生效"用了多久?超过 1 分钟就要改。
- 算你自己的最少观察量:按当前流量,发现一个"失败率翻倍"的问题需要多少样本、多长时间?
- 在日志里加
variant字段(control / canary),否则你没法做分组对比(ap7.4)。
✅ 做到这里你应该有:稳定哈希分流 + 三版本一体的热配置 + kill_switch(演练过)+ 最少观察量 + 日志里的 variant 字段。
为什么:AI 产品的改动比传统软件更难预测——改一句提示词,可能在你没测到的那类输入上完全崩掉(AP2 那三次"改了但没用"和 ap5.5 那次"更便宜但更差"都是例子)。灰度是你在门禁之后、全量之前的最后一道缓冲。
但这一节最该记住的是它的局限:灰度只能发现"明显的、大的"问题。轻微退化、长期效应、罕见场景——它都发现不了。别把灰度当成免测金牌。
一个常见错误:周一跑旧版收集数据,周二换新版收集数据,然后对比。
这不是 A/B,是"周一 vs 周二"——中间混进了流量结构变化、用户构成变化、上游模型的波动、甚至天气。ap2.5 实测过:同一个提示词跑 5 轮,分数本身就有 ±0.5 的波动;线上环境的波动只会更大。
正确做法:同一时间窗口内,两组并行,只比同期数据。
另外两条:① 别在灰度期间改别的东西——同时改了提示词和检索参数,出问题你分不清是谁干的;② 灰度组的用户要能反馈——ap5.6 的负反馈信号在灰度期尤其宝贵,它比指标更早告诉你不对劲。
我的 AI 产品日活【X】、日请求量【X】,当前失败率约【X%】。我要上线的改动是【描述:改提示词/换模型/换检索方案】。请帮我:1) 计算发现「失败率翻倍」和「失败率涨 50%」各需要多少灰度样本,据此给出灰度比例和最短观察时长建议;2) 写哈希分流函数(含 salt 管理);3) 列出灰度期间要分组对比的指标和判定标准(考虑噪声);4) 设计配置结构,让提示词/模型/索引三个版本能一起回滚;5) 一份放量节奏表(每一档的观察量和放行条件)。
灰度三句话:按 user_id 哈希分流(随机分流实测造成 817 次体验翻转)、决定发现能力的是绝对样本量不是百分比(小流量产品要用大比例)、回滚靠改配置不靠发版,且提示词/模型/索引三者要一起回。
还有一条要记住:灰度只能发现大问题——轻微退化即使 20% 灰度也只有 48% 的发现概率,那是 AP5 门禁的活。
下一节把东西真正放上公网:部署形态怎么选、并发怎么配、成本怎么控。
🔎 来源与核验· 3 条,点开核对
