课程0基础Agent开发课 / 生产化部署 / AI应用灰度发布与AB测试-模型升级不停机
— 18 min read

AI应用灰度发布与AB测试-模型升级不停机

普通的 Web 服务发布,验收标准相对明确:接口响应时间在 200ms 以内,错误率低于 0.1%,就可以放量。AI 服务的验收标准复杂得多——新版本模型的代码一行没变,但它给用户的回答可能变好了,也可能在某些特定场景悄悄退化了。这种不确定性让 AI 服务的发布变成了一件需要精心设计的工程活动。

AI 应用灰度发布与 A/B 测试:模型升级不停机

普通的 Web 服务发布,验收标准相对明确:接口响应时间在 200ms 以内,错误率低于 0.1%,就可以放量。AI 服务的验收标准复杂得多——新版本模型的代码一行没变,但它给用户的回答可能变好了,也可能在某些特定场景悄悄退化了。这种不确定性让 AI 服务的发布变成了一件需要精心设计的工程活动。

本章介绍 AI 应用的灰度发布策略和 A/B 测试方法论,以及一套可以直接落地的实现方案。


1.1 为什么 AI 应用的发布比普通应用更需要谨慎

95%

5%

A/B 测试 50:50

A/B 测试 50:50

流量入口

流量分配器

旧版本 Stable

新版本 Canary

指标采集

指标对比
成功率 · 延迟 · 成本

是否达标?

逐步扩量
5% → 20% → 50% → 100%

自动回滚
切回旧版本

全量发布

版本A

版本B

指标对比
选择更优版本

版本B 更优?

灰度发布与A/B测试流程——流量分配、指标对比、逐步切流、全量发布及异常回滚机制

1.1.1 原因一:模型输出的不确定性

同一个模型,在不同的温度(Temperature,控制输出随机性的参数,越高输出越多样但越不稳定)、不同的 Prompt、不同的上下文长度下,行为可能截然不同。新版本模型(比如从 GPT-4o 升级到 GPT-4.1)在主流场景表现更好,但在某些边缘场景可能退化——这是 LLM 常见的"能力失衡"现象(Capability Regression)。

如果直接全量切换,等用户投诉才知道有问题,此时已经影响了所有用户。

1.1.2 原因二:用户体验敏感

AI 助手的回答质量直接影响用户信任度。一个用户曾经得到过高质量回答,再遇到明显退化的回答,会产生强烈的落差感。这种体验损失比普通功能 Bug 更难挽回。

1.1.3 原因三:回滚成本高

普通接口回滚:回滚代码,重新部署,5 分钟。
模型回滚:切换 API 调用的模型版本相对简单,但如果已经修改了 Prompt、调整了 RAG 检索策略或改了 Memory 格式,回滚意味着所有这些都要一起恢复。事先没有设计版本隔离机制,回滚就会变成噩梦。


1.2 灰度发布决策流程

错误率上升超阈值

人工 review 有质量退化

全部指标正常

异常

正常

异常

正常

计划升级新模型

测试环境全量验证
核心用例回归测试

测试通过?

修复问题后重新测试

生产环境 5% 流量
内部用户

观察期 24-48小时
监控核心指标

指标正常?

自动回滚
发送告警

手动回滚
分析原因

扩大到 20% 流量

再观察 24-48小时

指标正常?

50% 流量

再观察 12-24小时

指标正常?

100% 全量发布

关闭旧模型实例
保留旧版本配置30天


1.3 灰度发布策略

1.3.1 策略一:按流量比例

最常见的策略。在路由层按百分比把请求分配到新旧模型:

  • 阶段一:5% 流量 → 新模型,95% → 旧模型
  • 阶段二(24 小时后,若无异常):20% → 新模型
  • 阶段三(再 24 小时后):50% → 新模型
  • 阶段四:100% → 新模型,旧模型下线

优点:实现简单,适合大多数场景。
缺点:同一个用户在不同请求时可能遇到不同模型,体验不一致。

1.3.2 策略二:按用户分组

把用户分为若干组,不同组使用不同模型。同一个用户在整个灰度周期内只体验一个模型版本,避免"前一秒好,后一秒差"的体验割裂。

code
用户 ID 末两位 00-04(约 5%)→ 新模型
用户 ID 末两位 05-99(约 95%)→ 旧模型

优点:用户体验一致,反馈数据更可信(用户能准确判断"好"或"差")。
缺点:需要在会话层记录用户分组,实现稍复杂。

1.3.3 策略三:按请求类型

根据请求的复杂程度或场景分类,先在"低风险"场景用新模型:

  • 简单问答(问题短,答案明确)→ 用新模型
  • 复杂推理、代码生成 → 继续用旧模型(新模型先稳定后再切)
  • 特定业务场景(如法律、医疗)→ 单独验证后再切

适用场景:当新模型在简单任务上明显更好,但在复杂任务上表现不确定时。


1.4 A/B 测试设计

A/B 测试(将用户随机分成两组,一组用旧版,一组用新版,用数据对比效果的方法)是将用户随机分为 A 组(旧版本)和 B 组(新版本),通过统计方法比较两组的指标差异,得出客观结论。与灰度发布的区别在于:灰度发布侧重安全上线,A/B 测试侧重数据驱动决策。

1.4.1 指标选择

指标类型 具体指标 获取方式
自动化指标 RAGAS 评分、BLEU 分、模型调用成功率 自动计算
用户行为指标 回答被复制的比例、追问率、对话轮数 埋点统计
满意度指标 点赞率、点踩率、五星评分 用户明确操作
业务指标 任务完成率、用户留存、NPS 业务系统

关键原则:选择"北极星指标"(最重要的 1-2 个指标),而不是同时看 20 个指标。指标越多,越容易陷入数据海洋而无法决策。NPS(Net Promoter Score,净推荐值,通过"你会向朋友推荐这个产品吗"来衡量用户忠诚度的指标)是常用的业务层指标之一。

1.4.2 样本量计算

A/B 测试需要足够的样本量,才能得到统计显著(Statistically Significant)的结论——即,观察到的差异是真实的,而不是随机噪音。

计算样本量需要三个参数:

  • 基准指标值:当前旧模型的满意率,比如 75%
  • 最小可检测效果(MDE):你希望检测到多小的改善,比如 5%(从 75% 到 80%)
  • 置信水平:通常取 95%(即有 5% 概率把随机波动误判为真实效果)
python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

from scipy import stats
import math

def calculate_sample_size(
    baseline_rate: float,   # 基准指标,如 0.75 表示 75% 满意率
    mde: float,             # 最小可检测效果,如 0.05 表示提升 5 个百分点
    alpha: float = 0.05,    # 显著性水平(误判概率),通常取 0.05
    power: float = 0.80     # 统计功效(发现真实效果的概率),通常取 0.80
) -> int:
    """计算 A/B 测试所需的每组样本量"""
    effect_size = abs(mde) / math.sqrt(
        (baseline_rate * (1 - baseline_rate) +
         (baseline_rate + mde) * (1 - baseline_rate - mde)) / 2
    )
    z_alpha = stats.norm.ppf(1 - alpha / 2)   # 双尾检验
    z_beta = stats.norm.ppf(power)
    n = ((z_alpha + z_beta) / effect_size) ** 2
    return math.ceil(n)

# 示例:基准满意率 75%,希望检测到 5 个百分点的提升
n = calculate_sample_size(baseline_rate=0.75, mde=0.05)
print(f"每组需要约 {n} 个样本(共 {n*2} 个)")
# 输出:每组需要约 1264 个样本(共 2528 个)

# 如果每天有 500 次请求,至少需要 2528/500 ≈ 5 天才能完成测试

一个经验法则:如果每组少于 100 个样本,A/B 测试的结论基本不可信。

1.4.3 Feature Flag 实现模型 A/B 测试

Feature Flag(功能开关,通过修改配置而非重新部署代码来打开/关闭某个功能的机制)是一种通过配置控制功能开关的机制,无需重新发布代码就可以切换功能。它是 A/B 测试和灰度发布的技术基础。

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import hashlib
import time
from typing import Literal
from dataclasses import dataclass
import redis

@dataclass
class ABConfig:
    """A/B 测试配置,存储在 Redis 中,可动态修改"""
    experiment_id: str           # 实验 ID,如 "model_upgrade_v2"
    model_a: str                 # 控制组(旧模型)
    model_b: str                 # 实验组(新模型)
    traffic_to_b: float          # 分配给 B 组的流量比例,如 0.05 = 5%
    enabled: bool = True

class ModelABRouter:
    """
    模型 A/B 路由器
    核心逻辑:根据用户 ID 的哈希值决定分组,保证同一用户始终在同一组
    """

    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    def get_model_for_user(
        self,
        user_id: str,
        experiment_id: str = "model_upgrade"
    ) -> tuple[str, Literal["A", "B"]]:
        """
        根据用户 ID 返回应使用的模型
        返回:(模型名称, 分组标记)
        """
        config = self._get_config(experiment_id)
        if not config.enabled:
            return config.model_a, "A"

        # 用 user_id + experiment_id 计算哈希值
        # 这保证:同一用户在同一实验中始终被分配到同一组
        hash_input = f"{user_id}:{experiment_id}"
        hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)

        # 哈希值对 100 取模,得到 0-99 的均匀分布
        bucket = hash_value % 100

        if bucket < config.traffic_to_b * 100:
            return config.model_b, "B"
        else:
            return config.model_a, "A"

    def _get_config(self, experiment_id: str) -> ABConfig:
        """从 Redis 读取实验配置(支持运行时动态修改)"""
        key = f"ab:config:{experiment_id}"
        data = self.redis.hgetall(key)
        if not data:
            # 默认配置:100% 流量给旧模型
            return ABConfig(
                experiment_id=experiment_id,
                model_a="gpt-4o",
                model_b="gpt-4.1",
                traffic_to_b=0.0,
                enabled=False
            )
        return ABConfig(
            experiment_id=experiment_id,
            model_a=data[b"model_a"].decode(),
            model_b=data[b"model_b"].decode(),
            traffic_to_b=float(data[b"traffic_to_b"]),
            enabled=data[b"enabled"].decode() == "true"
        )

# 在 FastAPI 路由中使用
from fastapi import FastAPI, Depends
from openai import AsyncOpenAI

app = FastAPI()
openai_client = AsyncOpenAI()

@app.post("/chat")
async def chat(request: ChatRequest, router: ModelABRouter = Depends(get_router)):
    # 路由决策
    model_name, group = router.get_model_for_user(
        user_id=request.user_id,
        experiment_id="model_upgrade_v2"
    )

    # 记录分组信息,用于后续统计分析
    start_time = time.time()
    response = await openai_client.chat.completions.create(
        model=model_name,
        messages=request.messages
    )
    latency_ms = (time.time() - start_time) * 1000

    # 埋点:记录本次请求的实验数据
    await record_ab_event(
        user_id=request.user_id,
        experiment_id="model_upgrade_v2",
        group=group,
        model=model_name,
        latency_ms=latency_ms,
        prompt_tokens=response.usage.prompt_tokens,
        completion_tokens=response.usage.completion_tokens
    )

    return {"answer": response.choices[0].message.content, "group": group}

1.5 监控与决策

1.5.1 观察期:灰度多久才能做决策

原则:观察期至少覆盖 2 个完整的流量周期(对于 ToC 应用通常是 2 个自然周),且样本量达到统计显著的要求。

灰度阶段 建议观察时长 关键判断条件
5% 流量 24-48 小时 无明显错误率上升,无用户投诉
20% 流量 48-72 小时 自动化评估指标与旧模型持平或更好
50% 流量 24-48 小时 业务指标(满意率、任务完成率)显著不低于旧版本

1.5.2 自动回滚触发条件

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

from dataclasses import dataclass
import redis
import logging

@dataclass
class RollbackTrigger:
    """自动回滚触发条件配置"""
    error_rate_threshold: float = 0.05      # 错误率超过 5% 触发回滚
    latency_p99_threshold_ms: float = 30000  # P99 延迟超过 30 秒触发回滚
    satisfaction_drop_threshold: float = 0.1 # 满意率下降超过 10 个百分点

class AutoRollbackMonitor:
    """
    自动回滚监控器
    每分钟检查一次关键指标,触发阈值时自动回滚
    """

    def __init__(self, redis_client: redis.Redis, trigger: RollbackTrigger):
        self.redis = redis_client
        self.trigger = trigger

    async def check_and_rollback(self, experiment_id: str) -> bool:
        """
        返回 True 表示触发了回滚
        """
        stats_b = await self._get_stats("B", experiment_id)
        stats_a = await self._get_stats("A", experiment_id)

        # 检查 B 组的错误率
        if stats_b.error_rate > self.trigger.error_rate_threshold:
            await self._do_rollback(
                experiment_id,
                reason=f"B组错误率 {stats_b.error_rate:.1%} 超过阈值 {self.trigger.error_rate_threshold:.1%}"
            )
            return True

        # 检查 B 组的 P99 延迟
        if stats_b.latency_p99 > self.trigger.latency_p99_threshold_ms:
            await self._do_rollback(
                experiment_id,
                reason=f"B组P99延迟 {stats_b.latency_p99:.0f}ms 超过阈值"
            )
            return True

        # 检查满意率是否显著下降(与 A 组对比)
        satisfaction_drop = stats_a.satisfaction_rate - stats_b.satisfaction_rate
        if satisfaction_drop > self.trigger.satisfaction_drop_threshold:
            await self._do_rollback(
                experiment_id,
                reason=f"B组满意率比A组低 {satisfaction_drop:.1%}"
            )
            return True

        return False

    async def _do_rollback(self, experiment_id: str, reason: str):
        """执行回滚:将 B 组流量设为 0,发送告警"""
        key = f"ab:config:{experiment_id}"
        self.redis.hset(key, "traffic_to_b", 0)
        self.redis.hset(key, "enabled", "false")
        logging.critical(f"[AutoRollback] 实验 {experiment_id} 已回滚。原因: {reason}")
        # 触发告警通知(接入第11章的告警系统)
        await send_alert(
            level="critical",
            title=f"AI 模型灰度发布自动回滚",
            message=reason
        )

1.5.3 人工 Review 的关键指标看板

自动化指标只能捕捉定量异常。AI 回答的质量退化有时是定性的("回答变得啰嗦了"、"不再引用来源了"),需要人工抽样 review。建议流程:

  1. 每天抽取 B 组 50-100 条回答,人工打分
  2. 重点关注低分回答(评分低于 3/5)的共同特征
  3. 按话题分类统计:哪类问题上新模型退化了

1.6 实战:从 GPT-4o 升级到 GPT-4.1 的灰度方案

以下是一个端到端的升级方案,可以直接参考执行:

第一步:升级前准备(T-7 天)

  1. 用现有评估数据集(至少 200 条),在测试环境对比 GPT-4o 和 GPT-4.1 的自动化评分
  2. 重点测试业务高频场景:代码生成、知识问答、工具调用
  3. 记录 GPT-4o 的基准指标:满意率、平均延迟、P99 延迟、token 消耗

第二步:在 Redis 中初始化实验配置

bash
# 以下为命令行示例,非程序员可跳过,重点看说明
redis-cli HSET ab:config:gpt41_upgrade \
  model_a "gpt-4o" \
  model_b "gpt-4.1" \
  traffic_to_b 0.0 \
  enabled false

第三步:开放 5% 流量(T 天)

bash
redis-cli HSET ab:config:gpt41_upgrade traffic_to_b 0.05 enabled true

第四步:观察期看板

重点关注 B 组 vs A 组的以下指标:

指标 正常范围 触发回滚条件
API 错误率 < 1% > 5%
平均延迟 基准 ±20% > 基准 150%
满意率(点赞/点踩) 基准 ±5% 比 A 组低 10 个百分点以上
平均 token 消耗 基准 ±30% > 基准 200%(可能 Prompt 被误放大)

第五步:逐步放量并决策

每次放量后按计划观察,汇总数据。用统计检验(如本章的 calculate_sample_size 函数)确认样本量充足,再做最终决策。

第六步:全量后的清理

bash
# 全量发布后,保留旧配置 30 天用于紧急回滚
redis-cli HSET ab:config:gpt41_upgrade traffic_to_b 1.0
# 30 天后确认稳定,归档实验配置

1.7 与普通应用发布的对比

维度 普通 Web 服务发布 AI 模型升级
验收标准 接口成功率、响应时间 +回答质量、满意率、业务指标
灰度时长 数小时 数天到数周
回滚决策 自动(错误率阈值) 自动 + 人工质量 review
关键指标 HTTP 状态码、延迟 +LLM 评分、用户满意度、成本
版本隔离 代码版本 模型版本 + Prompt 版本 + 配置版本
数据记录 请求日志 +实验分组、评分、对话内容(脱敏)

1.8 小结

四件事要做对:

  • 灰度发布:按流量比例或用户分组,逐步验证新模型,别一刀切
  • A/B 测试:足够样本量 + 统计显著性检验,让数据说话而不是拍脑袋
  • Feature Flag:运行时动态切换,无需重新部署,回滚几秒钟
  • 自动回滚:定好量化触发条件,错误率超阈值立即回滚,不等人来盯

模型升级天然有不确定性。灰度的作用不是消除这种不确定性,而是把影响范围控制在可接受的范围内,出问题能快速止损。

本页目录