第 13 章 · A/B 测试与对照实验
本章目标:
- 理解科学验证的重要性
- 掌握实验设计方法
- 学会样本量计算和统计显著性检验
- 建立回归测试集管理机制
13.1 为什么需要 A/B 测试
Agent 系统的改进往往难以量化:
- Prompt 优化效果?
- 新工具是否提升准确率?
- 多步 vs 单步架构哪个更好?
原则:没有测量的改进只是观点。
13.2 实验设计
python
import random
from dataclasses import dataclass
from typing import List, Callable, Any
@dataclass
class ExperimentConfig:
name: str
baseline: Callable[[str], str] # 对照组函数
variant: Callable[[str], str] # 实验组函数
metric: Callable[[str, str], float] # 评估函数
class ABTest:
def __init__(self, config: ExperimentConfig):
self.config = config
self.results = {"A": [], "B": []}
def run(self, test_cases: List[str], n_iterations: int = 3):
"""运行 A/B 测试
Args:
test_cases: 测试用例列表
n_iterations: 每个用例运行次数(减少随机性)
"""
for case in test_cases:
for _ in range(n_iterations):
# 随机分配
group = random.choice(["A", "B"])
# 执行对应版本
if group == "A":
result = self.config.baseline(case)
else:
result = self.config.variant(case)
# 评估
score = self.config.metric(case, result)
self.results[group].append(score)
def analyze(self) -> dict:
"""分析结果"""
import statistics
from scipy import stats
a_scores = self.results["A"]
b_scores = self.results["B"]
# 描述性统计
stats_a = {
"mean": statistics.mean(a_scores),
"median": statistics.median(a_scores),
"stdev": statistics.stdev(a_scores) if len(a_scores) > 1 else 0
}
stats_b = {
"mean": statistics.mean(b_scores),
"median": statistics.median(b_scores),
"stdev": statistics.stdev(b_scores) if len(b_scores) > 1 else 0
}
# 显著性检验(t 检验)
t_stat, p_value = stats.ttest_ind(a_scores, b_scores)
return {
"baseline": stats_a,
"variant": stats_b,
"p_value": p_value,
"significant": p_value < 0.05,
"recommendation": "_variant" if stats_b["mean"] > stats_a["mean"] else "baseline"
}13.3 样本量计算
python
import math
from scipy.stats import norm
def calculate_sample_size(
effect_size: float, # 预期提升幅度
alpha: float = 0.05, # 显著性水平
power: float = 0.8, # 统计功效
baseline_rate: float = 0.5 # 基准准确率
) -> int:
"""计算所需样本量
Args:
effect_size: 预期的相对提升(如 0.1 表示 10% 提升)
alpha: 第一类错误概率
power: 统计功效(1 - 第二类错误)
baseline_rate: 对照组基准率
Returns:
每组所需最小样本量
"""
z_alpha = norm.ppf(1 - alpha/2)
z_beta = norm.ppf(power)
# 两组比较的样本量公式
p1 = baseline_rate
p2 = baseline_rate * (1 + effect_size)
n = ((z_alpha + z_beta) ** 2 *
(p1 * (1 - p1) + p2 * (1 - p2))) / (p2 - p1) ** 2
return max(int(math.ceil(n)), 30) # 至少 30 个样本
# 示例:检测 10% 提升,需要多少样本?
n = calculate_sample_size(effect_size=0.1, baseline_rate=0.7)
print(f"每组需要 {n} 个样本") # 约 623 个13.4 评估口径设计
python
def evaluate_rag_answer(
question: str,
answer: str,
ground_truth: str
) -> float:
"""评估 RAG 回答质量"""
from ai import createGateway
gateway = createGateway({"apiKey": process.env.AI_GATEWAY_API_KEY})
judge = gateway("openai/gpt-4o")
prompt = f"""评估以下 RAG 回答的质量。
问题:{question}
参考答案:{ground_truth}
模型回答:{answer}
请从以下维度评分(1-5分):
1. 准确性:回答是否与参考答案一致
2. 完整性:是否涵盖了关键点
3. 相关性:是否与问题相关
只输出一个综合分数(1-5整数)。"""
result = judge.generate([{"role": "user", "content": prompt}])
return int(result.content.strip())
# 批量评估
async def batch_evaluate(test_set: list) -> dict:
scores = []
for item in test_set:
answer = await call_agent(item["question"])
score = evaluate_rag_answer(
item["question"],
answer,
item["ground_truth"]
)
scores.append(score)
return {
"mean_score": sum(scores) / len(scores),
"distribution": {i: scores.count(i) for i in range(1, 6)}
}13.5 回归测试集管理
python
import json
from pathlib import Path
from datetime import datetime
class RegressionSet:
def __init__(self, path: str = "regression_tests.json"):
self.path = Path(path)
self.tests = self._load()
def _load(self) -> list:
if self.path.exists():
return json.loads(self.path.read_text())
return []
def add(self, question: str, expected: str, category: str = "general"):
"""添加测试用例"""
self.tests.append({
"id": len(self.tests) + 1,
"question": question,
"expected": expected,
"category": category,
"added_at": datetime.now().isoformat()
})
self._save()
def _save(self):
self.path.write_text(json.dumps(self.tests, indent=2, ensure_ascii=False))
def run_all(self, agent_fn) -> dict:
"""运行全部回归测试"""
results = []
for test in self.tests:
answer = agent_fn(test["question"])
passed = answer.strip() == test["expected"].strip()
results.append({
"id": test["id"],
"passed": passed,
"actual": answer
})
passed_count = sum(1 for r in results if r["passed"])
return {
"total": len(results),
"passed": passed_count,
"failed": len(results) - passed_count,
"pass_rate": passed_count / len(results) if results else 0,
"details": results
}
# 使用示例
reg = RegressionSet()
reg.add("Python 的异步关键字是什么?", "async", "programming")
reg.add("解释 Transformer 架构", "transformer", "ml_concepts")
results = reg.run_all(my_agent)
print(f"通过率: {results['pass_rate']:.1%}")本章小结
- A/B 测试需要明确的假设和可测量的指标
- 样本量由效应大小、alpha 和 power 共同决定
- LLM-as-judge 可自动化评估流程
- 回归测试集是质量保证的基础
🛠️ 动手实践
- 设计并运行一个 Prompt A/B 测试,测量准确率提升
- 实现样本量计算器,输入预期提升自动算出所需样本
- 建立回归测试集,每次发布前自动运行