为什么需要 A/B 测试
Prompt 迭代的常见困境:
你改了 Prompt,加了"分点输出"
同事说"感觉变好了"
你信了,上线
一周后用户说"答案太碎了,不要分点"
→ 感觉错了,白改一周
这背后的三个陷阱:
- 感觉不可靠:人会被"新颖感"误导,新版本总是感觉好
- 没看全样本:试了 3 条就说好,样本太小不具代表性
- 没控制变量:同时改了 3 处,不知道哪处起作用
A/B 测试用控制变量 + 统计解决这三个陷阱,让 Prompt 迭代从"拍脑袋"变成"数据说"。
A/B 测试五步流程
第 1 步:明确假设
A/B 测试不是"随便试试",而是验证一个具体假设。
✗ "试试 B 版本好不好" → 太模糊
✓ "假设:B 版本加'先给结论再展开'结构,用户满意度提升"
假设三要素:
- 改了什么(变量)
- 预期什么变化(指标)
- 为什么这么想(依据)
第 2 步:控制变量
只改一个地方,其他全不变。
✗ A 版本:你是客服。问题:{{q}}
B 版本:你是资深客服。规则:礼貌专业。问题:{{q}}。输出分点。
→ 改了 3 处(角色/规则/格式),不知道哪个起作用
✓ A 版本:你是客服。问题:{{q}}
B 版本:你是客服。输出分点。问题:{{q}}
→ 只改格式,其他不变
想测多个改动?做 A/B/C/D 测试,每版本只改一处。这与 Prompt 评估 的"逐维评估"原则一致。
第 3 步:设计测试集
✗ 临时找 5 条测 → 不具代表性
✓ 准备 50-200 条覆盖典型场景的测试任务
测试集构成:
- 常见场景 60%(日常高频问题)
- 边界场景 20%(极端/异常输入)
- 困难场景 15%(复杂/多约束)
- 对抗场景 5%(诱导/越狱)
测试集要固化,每次迭代都用同一批,否则无法跨版本比较。
第 4 步:分流执行
200 条测试任务 → 随机分两半
A 组 100 条 → 跑版本 A
B 组 100 条 → 跑版本 B
注意:同一任务在 A、B 都要跑(配对设计)
→ 控制任务难度差异
第 5 步:统计判断
A 组平均分 4.2,B 组平均分 4.5
→ B 更好?不一定!
如果标准差大、样本小:
A: 4.2 ± 0.8
B: 4.5 ± 0.7
差距 0.3 可能在统计噪声内
→ 需统计显著性检验
四类评估指标
指标 1:客观正确性(最硬)
有标准答案的任务:
准确率 = 正确数 / 总数
例:信息抽取任务
A 版准确率 78%,B 版 85%
→ 差距 7 个点
适用:抽取、分类、计算等有对错的任务。
指标 2:格式合规率
要求 JSON 输出:
A 版 92% 合规,B 版 88%
→ A 更稳
要求分点:
A 版 70% 分点,B 版 95%
→ B 格式执行更好
适用:要求特定输出格式的任务。
指标 3:LLM 评分(无标准答案时)
让另一个 LLM 当裁判,1-5 分打分:
评分维度:相关性、准确性、完整性、清晰度
A 组:3.8 分
B 组:4.3 分
注意 LLM 评分有偏见(偏长答案、偏自身风格),需配合人工抽检校准。这与 Prompt 评估 的 LLM-as-Judge 方法一致。
指标 4:用户满意度
真实用户反馈:
A 版满意度 72%
B 版满意度 85%
最真实,但成本最高,样本最少
适用:上线后灰度收集。
样本量计算
样本太小,看不出显著性;太大,浪费成本。
经验值
| 效果大小 | 最少样本/组 | 总成本 |
|---|---|---|
| 大(差 15%+) | 30 | 60 次 |
| 中(差 5-15%) | 100 | 200 次 |
| 小(差 < 5%) | 500+ | 1000+ |
公式估算
import math
def sample_size(p_a, p_b, alpha=0.05, power=0.8):
"""计算 A/B 测试所需样本量
p_a: A 版转化率
p_b: B 版转化率(预期)
alpha: 显著性水平(0.05)
power: 统计功效(0.8)
"""
z_alpha = 1.96 # 95% 置信
z_beta = 0.84 # 80% 功效
p_avg = (p_a + p_b) / 2
n = ((z_alpha * math.sqrt(2 * p_avg * (1 - p_avg)) +
z_beta * math.sqrt(p_a * (1 - p_a) + p_b * (1 - p_b))) /
(p_b - p_a)) ** 2
return math.ceil(n)
# A 版准确率 80%,预期 B 版 85%
n = sample_size(0.80, 0.85)
print(f"每组需 {n} 样本") # 约 283
统计显著性判断
方法 1:卡方检验(分类任务)
from scipy.stats import chi2_contingency
# A: 80/100 正确,B: 85/100 正确
table = [[80, 20], [85, 15]] # [正确, 错误]
chi2, p, _, _ = chi2_contingency(table)
if p < 0.05:
print(f"显著(p={p:.3f}),B 更好")
else:
print(f"不显著(p={p:.3f}),差距可能是噪声")
方法 2:t 检验(评分任务)
from scipy.stats import ttest_ind
a_scores = [4.1, 3.9, 4.3, 4.0, 3.8, 4.2, 4.1, 3.9, 4.0, 4.2]
b_scores = [4.4, 4.5, 4.3, 4.6, 4.4, 4.5, 4.3, 4.4, 4.5, 4.4]
t, p = ttest_ind(b_scores, a_scores)
if p < 0.05 and t > 0:
print(f"显著(p={p:.3f}),B 评分更高")
方法 3:贝叶斯(小样本友好)
样本少时,频率派检验太保守,贝叶斯更灵活:
# 简化版贝叶斯 A/B
def beta_bayesian(success_a, total_a, success_b, total_b, n_sim=10000):
"""用 Beta 分布模拟后验"""
import numpy as np
a_samples = np.random.beta(success_a + 1, total_a - success_a + 1, n_sim)
b_samples = np.random.beta(success_b + 1, total_b - success_b + 1, n_sim)
prob_b_better = (b_samples > a_samples).mean()
return prob_b_better
# A: 8/10, B: 9/10
prob = beta_bayesian(8, 10, 9, 10)
print(f"B 比 A 好的概率: {prob:.1%}")
# 即使样本小也能给概率判断
A/B 测试框架
基础框架
import json
from typing import Any
class PromptABTest:
def __init__(self, prompt_a, prompt_b, test_cases):
self.prompt_a = prompt_a
self.prompt_b = prompt_b
self.test_cases = test_cases # 测试任务集
self.results = {"a": [], "b": []}
def run(self, llm_client):
"""对每个测试任务,A/B 都跑一遍"""
for case in self.test_cases:
# 版本 A
out_a = llm_client.complete(self.prompt_a.format(**case))
# 版本 B
out_b = llm_client.complete(self.prompt_b.format(**case))
self.results["a"].append({"input": case, "output": out_a})
self.results["b"].append({"input": case, "output": out_b})
return self.results
def evaluate(self, metric_fn):
"""用指标函数给每条输出打分"""
for version in ["a", "b"]:
for r in self.results[version]:
r["score"] = metric_fn(r["input"], r["output"])
def report(self):
import statistics
a_scores = [r["score"] for r in self.results["a"]]
b_scores = [r["score"] for r in self.results["b"]]
return {
"a_mean": statistics.mean(a_scores),
"b_mean": statistics.mean(b_scores),
"a_std": statistics.stdev(a_scores),
"b_std": statistics.stdev(b_scores),
"diff": statistics.mean(b_scores) - statistics.mean(a_scores),
}
# 使用
test = PromptABTest(
prompt_a="你是客服。问题:{q}",
prompt_b="你是客服。输出分点。问题:{q}",
test_cases=[{"q": "怎么退货?"}, {"q": "发票怎么开?"}, ...]
)
test.run(llm_client)
test.evaluate(metric_fn=lambda inp, out: 1 if "步骤" in out else 0)
print(test.report())
常见坑点
坑 1:过早停止
跑 20 条 B 领先 → 停了宣布 B 胜
→ 后 80 条 A 反超
→ 提前停止偏差
规则:达到预设样本量前不下结论
坑 2:多重比较
同时测 5 个版本 × 10 个指标 = 50 次比较
→ 总有一次"显著"是假阳性
修正:Bonferroni 校正,显著性阈值 ÷ 比较次数
坑 3:测试集污染
A/B 用了同一批测试任务
→ 知道某任务预期答案后,手动改 Prompt 应对
→ 成绩虚高,泛化差
规则:测试集与开发集分离
坑 4:指标错配
优化"格式合规率"→ B 分点输出 100% 合规
但用户满意度下降(答案太碎)
→ 指标没反映真实目标
规则:用与业务目标一致的指标
坑 5:忽略成本
B 版本效果好 2%,但 token 多 50%
→ 成本涨 50% 换 2% 效果,不划算
规则:效果指标 + 成本指标一起看
与其他技巧的关系
小结
A/B 测试的核心思想:让数据说话,不让感觉做决定。
- 控制变量 → 知道改了哪起作用
- 足够样本 → 排除统计噪声
- 显著性检验 → 不被假阳性骗
- 多指标联看 → 效果 + 成本 + 满意度
下次改 Prompt 前,先问自己:这个改动有数据支撑吗? 没有就先跑个 A/B 测试。配合 模板复用 把每个版本管理起来,你的 Prompt 迭代会从"手工艺"走向"工程化"。