学习路径的 A/B 实验设计:怎么证明 AI 推荐比固定路线更有效
学习路径的 A/B 实验设计:怎么证明 AI 推荐比固定路线更有效
一、"我觉得 AI 推荐有效"不叫验证
构建了一个 AI 驱动的个性化学习路径推荐系统。当别人问你"它比固定路径好多少?"时,你的回答不能是"感觉有效"。工程验证需要定量实验——A/B 测试是最直接的方案。
但教育领域的 A/B 测试有其特殊性:学习效果的反馈周期长(几周甚至几个月)、用户群体的异质性大、以及存在伦理问题(不能让对照组用户接受明显更差的学习方案)。
二、A/B 实验设计框架
flowchart TD A[实验设计] --> B[随机分流] B --> C[实验组: AI 推荐路径] B --> D[对照组: 固定难度阶梯] C --> E[观察期: 4 周] D --> E E --> F[指标采集] F --> F1[主要指标: 正确率提升] F --> F2[次要指标: 完成题目数] F --> F3[体验指标: 放弃率/满意度] F --> F4[长期指标: 间隔后正确率] F1 --> G[统计分析] F2 --> G F3 --> G F4 --> G G --> H{显著性检验} H -->|p < 0.05| I[AI 推荐显著优于固定路径] H -->|p ≥ 0.05| J[无显著差异或需要更多数据]三、实现:A/B 实验引擎
import random import hashlib import math from dataclasses import dataclass, field from datetime import datetime, timedelta from typing import Optional from scipy import stats @dataclass class ExperimentConfig: """A/B 实验配置""" experiment_id: str description: str # 分流配置 traffic_split: float = 0.5 # 实验组流量占比 # 实验周期 duration_days: int = 28 # 实验观察期 # 评估指标 primary_metric: str = "correctness_improvement" secondary_metrics: list[str] = field( default_factory=lambda: [ "problems_completed", "dropout_rate", "retention_accuracy", ] ) @dataclass class UserGroup: """用户分组信息""" user_id: str group: str # "control" | "treatment" assigned_at: datetime class ABTestEngine: """A/B 实验引擎 负责:分流、指标采集、统计分析。 """ def __init__(self, config: ExperimentConfig): self.config = config self._user_assignments: dict[str, UserGroup] = {} def assign_group(self, user_id: str) -> UserGroup: """为用户分配实验组 使用哈希分流保证同一用户始终在同一组。 哈希分流的优势: 1. 确定性:同一用户多次请求结果一致 2. 不需要维护用户-分组映射表(但这里仍维护用于监控) """ if user_id in self._user_assignments: return self._user_assignments[user_id] # 使用哈希 + 取模实现分流 hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16) bucket = hash_val % 1000 # 1000 个桶 # 前 50% 桶为实验组 if bucket < int(self.config.traffic_split * 1000): group = "treatment" else: group = "control" assignment = UserGroup( user_id=user_id, group=group, assigned_at=datetime.now(), ) self._user_assignments[user_id] = assignment return assignment def analyze_results( self, control_metrics: list[float], treatment_metrics: list[float], metric_name: str, ) -> dict: """统计分析:比较两组指标是否有显著差异 使用 Welch's t-test(不假设方差相等)。 """ # 描述性统计 control_mean = sum(control_metrics) / len(control_metrics) if control_metrics else 0 treatment_mean = sum(treatment_metrics) / len(treatment_metrics) if treatment_metrics else 0 relative_lift = ( (treatment_mean - control_mean) / control_mean * 100 if control_mean > 0 else 0 ) # Welch's t-test t_stat, p_value = stats.ttest_ind( treatment_metrics, control_metrics, equal_var=False ) # 效应量(Cohen's d):衡量差异的实际幅度 cohens_d = self._cohens_d(treatment_metrics, control_metrics) # 置信区间(95%) ci_lower, ci_upper = self._confidence_interval( treatment_metrics, control_metrics ) significant = p_value < 0.05 return { "metric": metric_name, "control_size": len(control_metrics), "treatment_size": len(treatment_metrics), "control_mean": round(control_mean, 4), "treatment_mean": round(treatment_mean, 4), "relative_lift_pct": round(relative_lift, 2), "cohens_d": round(cohens_d, 3), "p_value": round(p_value, 4), "significant": significant, "ci_95": (round(ci_lower, 4), round(ci_upper, 4)), "verdict": ( f"AI 推荐显著{'优于' if relative_lift > 0 else '不如'}固定路径" f"(提升 {abs(relative_lift):.1f}%,p={p_value:.3f})" if significant else "两组无显著差异" ), } @staticmethod def _cohens_d(a: list[float], b: list[float]) -> float: """计算 Cohen's d""" n1, n2 = len(a), len(b) if n1 < 2 or n2 < 2: return 0.0 mean1, mean2 = sum(a) / n1, sum(b) / n2 var1 = sum((x - mean1) ** 2 for x in a) / (n1 - 1) var2 = sum((x - mean2) ** 2 for x in b) / (n2 - 1) # 合并标准差 pooled_std = math.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2)) if pooled_std == 0: return 0.0 return (mean1 - mean2) / pooled_std @staticmethod def _confidence_interval(a: list[float], b: list[float]) -> tuple[float, float]: """计算两组均值差的 95% 置信区间""" n1, n2 = len(a), len(b) if n1 < 2 or n2 < 2: return (float("-inf"), float("inf")) mean1, mean2 = sum(a) / n1, sum(b) / n2 var1 = sum((x - mean1) ** 2 for x in a) / (n1 - 1) var2 = sum((x - mean2) ** 2 for x in b) / (n2 - 1) diff = mean1 - mean2 se = math.sqrt(var1 / n1 + var2 / n2) # 使用正态近似(大样本时 t 分布趋近正态) z_95 = 1.96 return (diff - z_95 * se, diff + z_95 * se) # ---- 实验示例 ---- def simulate_experiment(): """模拟一次 A/B 实验""" config = ExperimentConfig( experiment_id="ai_path_vs_fixed_v1", description="验证 AI 推荐路径 vs 固定难度阶梯", traffic_split=0.5, duration_days=28, ) engine = ABTestEngine(config) # 模拟 200 个用户的数据 random.seed(42) control_data = [ random.gauss(0.10, 0.15) # 对照组正确率提升均值 10% for _ in range(100) ] treatment_data = [ random.gauss(0.18, 0.15) # 实验组正确率提升均值 18% for _ in range(100) ] result = engine.analyze_results( control_data, treatment_data, "correctness_improvement" ) print("A/B 实验结果:") for key, value in result.items(): print(f" {key}: {value}") # simulate_experiment()四、实验设计的工程考量
4.1 样本量估算
在实验开始前,需要估算需要多少样本量才能检测到预期的效应。规则是:
- 效应越小,需要的样本越多
- 标准差越大,需要的样本越多
- 可以使用功效分析(Power Analysis)来估算最小样本量
4.2 辛普森悖论
按总体统计,AI 推荐组优于固定路径组。但按难度分组后,可能每个难度子集内 AI 推荐都不如固定路径。这就是辛普森悖论——聚合统计可能欺骗你。补救方案是分层分析:对不同用户群(新手/进阶/高级)分别评估。
4.3 新奇效应
新用户可能因为"新鲜感"而在短期内表现更好,随着新鲜感消退回归真实水平。实验周期应该足够长(至少 4 周)来排除新奇效应。
五、总结
A/B 实验不是简单地比较两组的平均值。它涉及:分流机制的设计(哈希分流保证一致性)、统计显著性检验(t-test + 置信区间)、效应量评估(Cohen's d)、以及避免实验偏差(辛普森悖论、新奇效应)。在教育类产品的实验设计中,还需要额外关注伦理边界——不能让任何一组用户接受明显有害的学习方案。
