← 返回资讯

多模型 A/B 测试怎么做:聚合层分流实践

2026-07-24

当你不确定 GPT-4o 还是 Claude 3.5 Sonnet 在你的业务场景中表现更好时,A/B 测试是最直接的答案——用真实流量在两个模型上分流,用数据说话,而不是凭感觉选模型。聚合层让这件事变得几乎零改动成本。

为什么要做多模型 A/B 测试

不同模型在不同任务上的优劣差异显著:

  • 代码生成:Claude 3.5 Sonnet 和 GPT-4o 各有优势,取决于任务类型
  • 中文创作:国内模型(通义、Kimi)在语感上有时优于境外模型
  • 长文档处理:Gemini 1.5 Pro 的长上下文窗口有明显优势
  • 成本:同等质量需求下,DeepSeek、Llama 等模型成本可能低 5–10 倍

没有测量就没有选型依据。A/B 测试让你在生产流量上低风险地评估候选模型。

举个真实的坑:不少团队选模型是靠”体验几个 prompt,感觉哪个答得好”。这种判断在客服场景里经常翻车——你手测的 20 个问题往往是常见问题,模型都答得不错,差异全藏在长尾里(用户输入错别字、追问三四轮之后、涉及你业务里的专有名词)。这些长尾恰恰是真实流量里占比不小的部分,只有把两个模型都摆到线上跑一段时间,才能看出差距。这也是为什么”聚合层分流 + 数据对比”比”主观试用”靠谱得多。

聚合层实现 A/B 分流的三种方式

方式一:权重路由(推荐)

在 LiteLLM 或兼容聚合层的配置中,为同一个对外名称配置多个上游,分配权重:

model_list:
  - model_name: chat-model      # 对业务代码暴露统一名称
    litellm_params:
      model: openai/gpt-4o
    weight: 50                  # 50% 流量

  - model_name: chat-model
    litellm_params:
      model: anthropic/claude-3-5-sonnet-20241022
    weight: 50                  # 50% 流量

业务代码只调用 model: chat-model,聚合层按权重自动分流。权重可随时调整,无需重启应用。

这里有几个容易被忽略的细节。第一,weight 是相对值,不是必须凑够 100——你写 weight: 3weight: 1,效果跟写 7525 是一样的,聚合层内部按比例做加权随机选择,每次请求独立抽样,不是按顺序轮询。第二,正因为是”每次请求独立抽样”,不要指望权重路由能帮你做”同一用户固定用同一模型”——它天然是无状态的,如果你的评估需要按用户维度做对比(比如统计某个用户这一周的整体满意度),就得配合方式二的哈希分组,权重路由只适合”总体流量对比”这种粗粒度场景。第三,改权重这件事本身要小心节奏:直接从 50:50 跳到 90:10 去验证新模型,一旦新模型有隐藏问题(比如某类 prompt 触发格式错乱),影响面会瞬间放大。更稳的做法是先给新模型 5%~10% 的权重跑一两天,日志里盯着出错率和延迟,没问题再逐步加到 30%、50%,这跟灰度发布是一个道理——A/B 测试本质上也是一种灰度,别把它当成开关一样一刀切。

方式二:应用层随机选择

在应用层根据请求 ID 或随机数选择模型,适合需要精确控制分组的场景:

import hashlib

def select_model(user_id: str) -> str:
    # 按用户 ID 稳定分组,同一用户始终用同一模型
    hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
    if hash_val % 2 == 0:
        return "gpt-4o"
    return "claude-3-5-sonnet"

model = select_model(current_user_id)
response = client.chat.completions.create(model=model, messages=[...])

这种方式的优点是分组稳定(同一用户始终使用同一模型),便于收集用户维度的反馈。但它有个反直觉的坑:当你想把分组比例从 50:50 调成 70:30 时,不能简单把 hash_val % 2 == 0 改成 hash_val % 10 < 7——两次取模用的是不同的划分方式,大量原本分在 A 组的用户会在调整后跳到 B 组,等于把之前积累的”同一用户持续观察”数据全部作废。正确做法是把哈希值映射到 0~99 的固定区间(hash_val % 100),A 组固定占 [0, 50),调整比例时只挪动区间边界(比如改成 [0, 70)),原来落在 [0, 50) 里的用户区间没变,不会被打乱,只有新落进 [50, 70) 的那部分用户会切到新分组,这才是平滑调整。另外用 MD5 对 user_id 做哈希是常见但不是唯一选择,如果 user_id 本身分布不均匀(比如新用户 ID 都是连续自增的整数),直接取模可能导致分组不均,哈希这一步的意义就是把不均匀的输入打散成近似均匀的分布。

方式三:Header 传参

部分聚合层支持通过请求头指定路由策略,适合在不修改业务逻辑的情况下动态切换:

response = client.chat.completions.create(
    model="chat-model",
    messages=[...],
    extra_headers={"X-Model-Group": "experiment-b"}
)

这种方式常用在两个场景:一是内部灰度,测试同学在自己的调试请求里带上特定 Header,强制走到实验分组,不影响真实用户流量;二是配合具体某个用户 ID 做定向复现,客服反馈某个用户体验差,你想知道他当时具体是哪个模型答的,靠 Header 可以手动指定分组走一遍。有个容易踩的坑是:如果你的请求链路中间经过了 CDN、企业网关或者某些反向代理,自定义 Header 有可能被中间层过滤掉(尤其是以 X- 开头但不在白名单里的头),聚合层最终收不到这个 Header,行为跟没传一样。上线前建议先用 curl 直接打一次聚合层地址,确认 Header 原样透传,再去验证业务代码里加的 Header 有没有生效,别一上来就在业务层排查半天。

评估指标体系

A/B 测试的关键是定义可测量的评估指标:

指标类型具体指标采集方式
质量指标用户满意度评分、点赞/踩前端埋点
任务完成率用户是否继续追问/放弃会话分析
延迟TTFT(首 token 时间)、总耗时聚合层日志
成本每次调用 token 费用聚合层计费
出错率4xx/5xx 比例、内容过滤率日志统计

建议同时收集自动指标(延迟、成本、出错率)和人工指标(质量评分),两者结合才能做出准确判断。

统计显著性与样本量

A/B 测试需要足够的样本量才有统计意义:

  • 小流量场景:每组至少积累 200–500 次请求再下结论
  • 置信度:通常要求 95% 置信度(p < 0.05)
  • 运行周期:至少覆盖一个完整业务周期(通常 7–14 天),避免时间效应干扰

样本量不足就下结论是 A/B 测试最常见的错误。举个具体的判断思路:假设 A 组 300 次请求里有 210 次拿到”满意”评价,B 组 300 次里有 225 次,满意率分别是 70% 和 75%,看起来 B 更好,但这 5 个百分点的差距,在这个样本量下很可能只是随机波动——你可以用两比例的显著性检验(two-proportion z-test)粗算一下,通常 5 个百分点、每组几百次请求这个量级,z 值大概率落在不显著的区间里。工程上不用真去手推公式,用 Python 的 statsmodels.stats.proportion.proportions_ztest 传入两组的成功次数和总数,几行代码就能拿到 p 值,p 大于 0.05 就说明现在下结论为时过早,得继续攒量或者延长观察周期,而不是看到哪个数字大就直接拍板切换。

还有一个经常被忽略的干扰项:时间效应。如果你的业务有明显的工作日/周末差异,或者早晚高峰用户群体不同(比如晚上更多是学生党,问题类型跟白天的企业用户不一样),只跑 3 天就下结论很容易被这种周期性波动带偏,这也是为什么建议至少覆盖一个完整周期——用一整周甚至两周的数据,才能把”时段差异”这个噪声磨平,剩下的差距才更接近模型本身的真实差异。

上线前自检:这几个现象你应该能复现

配置好权重路由之后,别急着导入真实流量,先花十分钟自测一遍,确认下面这几件事都符合预期:

  1. 分流比例大致对得上。 用同一个 model: chat-model 连续发 100 次请求(可以写个简单的循环脚本),统计聚合层日志里实际路由到每个上游的次数,50:50 的配置应该落在 40:60~60:40 这个区间内(100 次样本量本身有波动,别指望精确对半)。如果偏差特别离谱,比如跑出 90:10,先检查是不是权重配置漏了 reload,很多聚合层改配置文件后需要重启或触发 reload 才生效,改完文件没重启是最常见的低级错误。
  2. 日志里能区分”请求的模型名”和”实际路由的模型”。 这一点在方式一(权重路由)下特别重要,因为业务代码看到的永远是统一的 chat-model,如果日志只记了这个名字,你后面根本没法按模型拆分数据做评估。确认聚合层日志里有单独字段记录实际上游(比如 litellm_params.model 或者响应里带的真实 model 字段),这是整个 A/B 测试能不能做数据对比的前提。
  3. 故障场景下能看到真实报错,而不是被 Fallback 悄悄兜住。 聚合层配置了 Fallback 机制的话,某个上游超时或者返回 5xx 时会自动切到备用模型,业务侧完全无感知,这本来是好事,但会污染 A/B 数据——你以为这次是模型 B 答的,实际上因为 B 超时被切到了模型 A。排查方法是刻意让某个上游临时不可用(比如改错 API Key),观察日志里记的”实际使用模型”是否正确翻转成了 Fallback 目标,而不是还显示原来请求的模型名。

常见问题

A/B 测试期间如果某个模型出现故障怎么办? 聚合层的 Fallback 机制会自动将失败请求切换到备用模型,但这会污染 A/B 分组数据。建议在日志中标记实际使用的模型(而非请求的模型),评估时以实际模型为准。

需要对同一个问题同时请求两个模型做对比吗? 这是”影子测试”模式,成本加倍但数据最干净。更常见的做法是按时间或用户分组,成本更低,但需要保证两组流量的问题分布相似。

两个模型的输出格式不一样,怎么统一评估? 不同模型对同一个 prompt 的排版习惯确实有差异,比如有的模型更爱用列表,有的更爱输出大段文字。如果你的评估指标是”用户是否满意”这种主观打分,格式差异本身就是评估的一部分,不需要额外处理。但如果你要做结构化对比(比如都要求输出 JSON),建议在 prompt 里显式约束输出格式,并在聚合层之后加一层轻量的格式校验,格式不合规的直接记为失败样本,避免因为格式解析失败被误判成”模型能力不行”。

要不要给 A/B 测试单独开一个 API Key 来跟踪成本? 建议开。如果 A 组、B 组和你线上其他正常流量混用同一个 Key,账单里没法单独拆出这次实验消耗了多少 token、花了多少钱,复盘的时候只能凭日志时间范围去反推,容易算错也容易漏算被其他业务流量污染的部分。单独开 Key(或者聚合层支持的话,用独立的虚拟 Key/Team)跑实验,测完之后账单一目了然,成本对比这一项指标才立得住。

测试结束后如何平滑切换? 调整聚合层的权重配置即可,将流量比例从 50:50 改为 100:0,业务代码无需任何改动。这正是聚合层权重路由的核心优势。


延伸阅读:

想在真实流量上做多模型 A/B 测试?申请力达云聚合 API 内测,权重路由开箱即用。