agent ab testing platform

Agent A/B 测试平台搭建:从实验设计到统计显著性

引言 Agent 的非确定性使得"感觉更好"不能作为决策依据。一个 Prompt 的微调可能提升某类任务的表现,却悄悄损害了另一类。2026年,A/B 测试已成为 Agent 优化的科学方法——用数据说话,用统计检验做决策。 一、Agent A/B 测试的特殊性 与传统 Web A/B 测试不同,Agent A/B 测试面临独特挑战: 维度 传统 A/B 测试 Agent A/B 测试 指标 点击率、转化率 输出质量、任务完成率、用户满意度 测量 确定性(点击=1/不点击=0) 非确定性(同一输入可能不同输出) 变量 UI 元素 Prompt、模型、工具、温度 噪声 低 高(LLM 输出方差大) 样本量 百万级 千级(成本限制) 指标延迟 即时 分钟级(需要完整执行) 二、实验设计框架 2.1 假设构建 @dataclass class ExperimentHypothesis: """实验假设""" name: str description: str independent_variable: str # 自变量(如 temperature) control_value: any # 对照组值(如 0.3) treatment_value: any # 实验组值(如 0.5) dependent_variables: list[str] # 因变量(如 task_completion_rate) expected_effect: str # 预期效果 min_detectable_effect: float # 最小可检测效应 (MDE) statistical_power: float # 统计功效 (通常 0.8) significance_level: float # 显著性水平 (通常 0.05) def required_sample_size(self) -> int: """计算所需样本量""" # 基于双比例检验的样本量计算 p1 = self.baseline_rate # 基线成功率 p2 = p1 + self.min_detectable_effect # 预期成功率 z_alpha = 1.96 # α=0.05 z_beta = 0.84 # power=0.8 n = ( (z_alpha * (2*p1*(1-p1))**0.5 + z_beta * (p1*(1-p1) + p2*(1-p2))**0.5) ** 2 ) / (p2 - p1) ** 2 return int(n) + 1 # 示例 hypothesis = ExperimentHypothesis( name="temperature_optimization", description="将 temperature 从 0.3 调至 0.5,预期能提升创意写作任务的用户满意度", independent_variable="temperature", control_value=0.3, treatment_value=0.5, dependent_variables=["user_satisfaction", "task_completion_rate"], expected_effect="满意度提升 5%", min_detectable_effect=0.05, statistical_power=0.8, significance_level=0.05, baseline_rate=0.75 # 当前满意度 75% ) # 所需样本量 ≈ 2,435 per group 2.2 实验配置 @dataclass class ExperimentConfig: experiment_id: str name: str hypothesis: ExperimentHypothesis traffic_allocation: float # 实验占总流量比例 (0-1) control_split: float # 对照组在实验流量中的比例 (通常 0.5) targeting_rules: list[Rule] # 目标用户筛选 duration_days: int # 预计运行天数 metrics: list[Metric] # 追踪指标 guardrail_metrics: list[Metric] # 护栏指标(不可恶化) early_stop_rules: list[Rule] # 提前停止规则 cost_budget: float # 实验成本预算 # 护栏指标示例 GUARDRAIL_METRICS = [ Metric(name="error_rate", type="counter", max_threshold=0.05), Metric(name="p95_latency", type="histogram", max_threshold_ms=10000), Metric(name="cost_per_request", type="histogram", max_threshold=0.15), Metric(name="toxic_output_rate", type="counter", max_threshold=0.01), ] 三、流量分配系统 class ExperimentRouter: """实验流量路由""" def __init__(self, redis_client): self.redis = redis_client async def assign( self, user_id: str, agent_name: str ) -> VariantAssignment: """为用户分配实验变体""" # 1. 获取活跃实验 experiments = await self._get_active_experiments(agent_name) for exp in experiments: # 2. 检查目标规则 if not self._matches_targeting(user_id, exp.targeting_rules): continue # 3. 检查是否已分配 existing = await self._get_assignment(user_id, exp.experiment_id) if existing: return existing # 保持一致性 # 4. 一致性哈希分配 bucket = self._hash_bucket(user_id, exp.experiment_id) # 5. 决定是否进入实验 if bucket < exp.traffic_allocation: # 在实验内部分配对照组/实验组 inner_bucket = self._hash_bucket( f"{user_id}:{exp.experiment_id}", "inner" ) if inner_bucket < exp.control_split: variant = "control" else: variant = "treatment" else: variant = "excluded" # 不参与实验 assignment = VariantAssignment( experiment_id=exp.experiment_id, user_id=user_id, variant=variant, config=exp.get_variant_config(variant), assigned_at=datetime.now() ) await self._save_assignment(assignment) return assignment # 没有匹配的实验 return VariantAssignment(variant="default", config={}) def _hash_bucket(self, key: str, salt: str = "") -> float: """一致性哈希,返回 0-1 之间的值""" h = hashlib.sha256(f"{key}:{salt}".encode()).hexdigest() return int(h[:8], 16) / 0xFFFFFFFF 四、指标收集与统计检验 4.1 指标收集器 class ExperimentMetricsCollector: """实验指标收集器""" async def record( self, experiment_id: str, user_id: str, variant: str, metrics: dict ): """记录单次实验观测""" event = { "experiment_id": experiment_id, "user_id": user_id, "variant": variant, "timestamp": time.time(), **metrics # task_completed, satisfaction_score, latency_ms, tokens_used, cost } # 写入时序数据库 await self.influxdb.write( measurement="experiment_events", tags={"experiment_id": experiment_id, "variant": variant}, fields=metrics, timestamp=event["timestamp"] ) async def aggregate( self, experiment_id: str, metric_name: str ) -> dict: """聚合实验指标""" return { "control": await self._compute_stats(experiment_id, "control", metric_name), "treatment": await self._compute_stats(experiment_id, "treatment", metric_name) } async def _compute_stats( self, exp_id: str, variant: str, metric: str ) -> MetricStats: values = await self.influxdb.query( f'SELECT "{metric}" FROM "experiment_events" ' f'WHERE "experiment_id" = \'{exp_id}\' ' f'AND "variant" = \'{variant}\'' ) return MetricStats( n=len(values), mean=statistics.mean(values), std=statistics.stdev(values) if len(values) > 1 else 0, median=statistics.median(values), p25=np.percentile(values, 25), p75=np.percentile(values, 75), p95=np.percentile(values, 95), ) 4.2 统计检验 from scipy import stats import numpy as np class StatisticalTester: """统计显著性检验""" def test_proportion( self, control_successes: int, control_total: int, treatment_successes: int, treatment_total: int, alpha: float = 0.05 ) -> TestResult: """比例检验(用于完成率等二值指标)""" # 卡方检验 contingency = [ [control_successes, control_total - control_successes], [treatment_successes, treatment_total - treatment_successes] ] chi2, p_value, _, _ = stats.chi2_contingency(contingency) # 效应量 p_control = control_successes / control_total p_treatment = treatment_successes / treatment_total effect_size = p_treatment - p_control # 置信区间 se = np.sqrt(p_control*(1-p_control)/control_total + p_treatment*(1-p_treatment)/treatment_total) ci_lower = effect_size - 1.96 * se ci_upper = effect_size + 1.96 * se return TestResult( test="chi_square", p_value=p_value, significant=p_value < alpha, effect_size=effect_size, confidence_interval=(ci_lower, ci_upper), control_rate=p_control, treatment_rate=p_treatment, interpretation=self._interpret( p_value, alpha, effect_size, p_control, p_treatment ) ) def test_continuous( self, control_values: list[float], treatment_values: list[float], alpha: float = 0.05 ) -> TestResult: """连续值检验(用于满意度分数、延迟等)""" # 正态性检验 _, p_normal_ctrl = stats.shapiro(control_values) _, p_normal_treat = stats.shapiro(treatment_values) if p_normal_ctrl > 0.05 and p_normal_treat > 0.05: # 正态分布:使用 t 检验 statistic, p_value = stats.ttest_ind( control_values, treatment_values, equal_var=False # Welch's t-test ) test_name = "welch_t_test" else: # 非正态:使用 Mann-Whitney U 检验 statistic, p_value = stats.mannwhitneyu( control_values, treatment_values, alternative='two-sided' ) test_name = "mann_whitney_u" # 效应量 (Cohen's d) pooled_std = np.sqrt( ((len(control_values)-1) * np.var(control_values, ddof=1) + (len(treatment_values)-1) * np.var(treatment_values, ddof=1)) / (len(control_values) + len(treatment_values) - 2) ) cohens_d = (np.mean(treatment_values) - np.mean(control_values)) / pooled_std return TestResult( test=test_name, p_value=p_value, significant=p_value < alpha, effect_size=cohens_d, control_mean=np.mean(control_values), treatment_mean=np.mean(treatment_values), interpretation=self._interpret_continuous( p_value, alpha, cohens_d, np.mean(control_values), np.mean(treatment_values) ) ) def _interpret(self, p_value, alpha, effect, p_ctrl, p_treat): if p_value >= alpha: return f"无统计显著差异 (p={p_value:.4f} ≥ {alpha})。建议继续收集数据或增大样本量。" direction = "提升" if effect > 0 else "下降" return ( f"统计显著 (p={p_value:.4f} < {alpha})。" f"实验组{direction}了{abs(effect)*100:.1f}个百分点" f"({p_ctrl:.1%} → {p_treat:.1%})。" ) 4.3 序贯检验(支持提前停止) class SequentialTester: """序贯检验:允许在实验过程中提前判断""" def __init__(self, alpha: float = 0.05, power: float = 0.8, num_looks: int = 5): # Bonferroni 校正 self.adjusted_alpha = alpha / num_looks self.looks = num_looks self.current_look = 0 def should_stop_early( self, control_data: list, treatment_data: list, sample_size_ratio: float # 当前样本量 / 计划样本量 ) -> EarlyStopDecision: """检查是否可以提前停止""" self.current_look = int(sample_size_ratio * self.looks) result = StatisticalTester().test_continuous( control_data, treatment_data, self.adjusted_alpha ) # 护栏指标检查 guardrail_ok = self._check_guardrails(control_data, treatment_data) if not guardrail_ok: return EarlyStopDecision( should_stop=True, reason="护栏指标恶化,建议立即停止实验", winner="control" ) if result.significant: if result.effect_size > 0: return EarlyStopDecision( should_stop=True, reason=f"实验组显著优于对照组 (p={result.p_value:.4f})", winner="treatment" ) else: return EarlyStopDecision( should_stop=True, reason=f"实验组显著劣于对照组 (p={result.p_value:.4f})", winner="control" ) # 计算当前功效 current_power = self._compute_power( len(control_data), result.effect_size ) if current_power > 0.8 and not result.significant: return EarlyStopDecision( should_stop=True, reason=f"功效充足({current_power:.1%})但无显著差异,停止实验", winner="tie" ) return EarlyStopDecision(should_stop=False) 五、LLM 特有的 A/B 测试方法 5.1 LLM-as-Judge A/B 测试 class LLMJudgeABTest: """使用 LLM 作为评判者的 A/B 测试""" async def judge_pair( self, prompt: str, response_a: str, response_b: str, criteria: list[str] ) -> JudgmentResult: """让 LLM 判断哪个回答更好""" judge_prompt = f"""You are an impartial judge. Compare two responses to the same prompt. Prompt: {prompt} Response A: {response_a} Response B: {response_b} Criteria: {', '.join(criteria)} Evaluate which response is better. Consider: 1. Accuracy and correctness 2. Completeness 3. Clarity and structure 4. Adherence to instructions Respond in JSON: {{ "winner": "A" | "B" | "tie", "confidence": 0.0-1.0, "reasoning": "explanation", "scores": {{"A": float, "B": float}} }}""" response = await self.judge_llm.invoke(judge_prompt, temperature=0.0) return JudgmentResult(**json.loads(response.content)) async def run_experiment( self, test_cases: list[TestCase], control_agent: Agent, treatment_agent: Agent, num_judges: int = 3 # 多评判者取平均 ) -> ExperimentResult: results = [] for case in test_cases: # 生成两组回答 response_ctrl = await control_agent.run(case.input) response_treat = await treatment_agent.run(case.input) # 多评判者投票 judgments = [] for i in range(num_judges): judge = self.judges[i] judgment = await judge.judge_pair( case.input, response_ctrl, response_treat, case.criteria ) judgments.append(judgment) # 多数投票 winner = self._majority_vote(judgments) results.append({ "test_id": case.id, "winner": winner, "confidence": np.mean([j.confidence for j in judgments]), }) # 统计分析 wins_treatment = sum(1 for r in results if r["winner"] == "treatment") wins_control = sum(1 for r in results if r["winner"] == "control") ties = sum(1 for r in results if r["winner"] == "tie") # Bradley-Terry 模型检验 bt_stat = self._bradley_terry_test(wins_treatment, wins_control, ties) return ExperimentResult( wins_treatment=wins_treatment, wins_control=wins_control, ties=ties, p_value=bt_stat.p_value, significant=bt_stat.p_value < 0.05, avg_confidence=np.mean([r["confidence"] for r in results]) ) 六、实验报告自动化 class ExperimentReporter: """自动化实验报告生成""" async def generate_report( self, experiment_id: str ) -> ExperimentReport: exp = await self.repo.get(experiment_id) metrics = await self.collector.aggregate_all(experiment_id) test_results = {} for metric_name, data in metrics.items(): if metric_name in ["task_completed", "user_thumbs_up"]: # 比例检验 result = self.tester.test_proportion( data["control"].successes, data["control"].total, data["treatment"].successes, data["treatment"].total ) else: # 连续值检验 result = self.tester.test_continuous( data["control"].values, data["treatment"].values ) test_results[metric_name] = result # 护栏指标检查 guardrail_status = self._check_guardrails(metrics, exp.guardrail_metrics) # 生成决策建议 recommendation = self._generate_recommendation( test_results, guardrail_status, exp.hypothesis ) return ExperimentReport( experiment=exp, sample_sizes={ "control": metrics["task_completed"]["control"].total, "treatment": metrics["task_completed"]["treatment"].total, }, results=test_results, guardrail_status=guardrail_status, recommendation=recommendation, summary=self._generate_summary(test_results, recommendation), generated_at=datetime.now() ) def _generate_recommendation(self, results, guardrails, hypothesis): primary = results.get(hypothesis.dependent_variables[0]) if not primary.significant: return Recommendation( action="continue_or_stop", reason=f"主指标无显著差异 (p={primary.p_value:.4f})。" f"建议:若已达到计划样本量则停止;否则继续收集数据。" ) if primary.effect_size > 0 and guardrails.all_passed: return Recommendation( action="ship", reason=f"主指标显著提升 (p={primary.p_value:.4f}, " f"效应量={primary.effect_size:.3f})。" f"护栏指标全部通过。建议全量发布。" ) if primary.effect_size < 0: return Recommendation( action="do_not_ship", reason=f"主指标显著下降 (p={primary.p_value:.4f})。不建议发布。" ) if not guardrails.all_passed: return Recommendation( action="do_not_ship", reason=f"主指标虽提升但护栏指标恶化:{guardrails.violated}。不建议发布。" ) 七、A/B 测试 Checklist □ 实验假设明确(自变量、因变量、预期效果) □ 样本量计算完成(MDE、power、alpha) □ 流量分配使用一致性哈希(同一用户体验一致) □ 护栏指标已定义并监控 □ 统计检验方法匹配指标类型(比例/连续) □ 序贯检验支持提前停止 □ LLM-as-Judge 评判使用多评判者 □ 实验报告自动生成 □ 决策建议基于数据而非直觉 □ 实验结果归档可追溯 结语 A/B 测试是 Agent 优化的科学基石。在 LLM 的非确定性世界里,直觉是不可靠的,只有统计检验才能区分真实效果和随机噪声。投资 A/B 测试平台不是开销,而是回报率最高的基础设施投资。让每一次 Prompt 修改、每一次模型升级都有数据支撑,这就是 Agent 工程的成熟标志。 加入讨论 这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。 ...

2026-06-28 · 7 min · 1319 words · 硅基 AGI 探索者
agent a b testing framework

智能体 A/B 测试框架设计与实现

为什么智能体需要专属的 A/B 测试框架 传统的 A/B 测试方法论起源于 Web 产品优化领域——按钮颜色、页面布局、推荐策略的对比。但 AI 智能体(Agent)与传统软件产品有着本质区别:智能体的输出具有非确定性、多步骤推理和工具调用链路等特征。一个智能体在相同输入下可能产生截然不同的行为路径,这使得经典的"单次曝光-单次转化"测试模型不再适用。 智能体 A/B 测试框架需要解决三个核心挑战: 输出空间高维化:智能体的回复不仅是文本,还包含工具调用序列、中间推理步骤和最终决策,评估维度极其丰富。 非确定性重复:同一版本的智能体对同一输入可能给出不同答案,需要多次重复实验才能估计真实分布。 长期效果评估:智能体在多轮对话中的策略累积效应(如记忆机制、上下文管理)使得短期指标可能误导长期判断。 框架整体架构 一个完整的智能体 A/B 测试框架由以下五层组成: 第一层:实验管理层 实验管理层负责实验的全生命周期管理,包括实验创建、流量分配、版本控制和实验终止逻辑。 ExperimentManager ├── ExperimentConfig │ ├── variants: [Variant A (control), Variant B (treatment)] │ ├── traffic_split: {A: 50%, B: 50%} │ ├── min_sample_size: 500 │ ├── significance_level: 0.05 │ └── max_duration: 14d ├── TrafficAllocator (一致性哈希) └── ExperimentRegistry (实验元数据存储) 流量分配采用一致性哈希策略,确保同一用户在实验期间始终被分配到同一变体,避免交叉污染。对于智能体场景,还需要考虑会话级别的分配——同一用户的不同对话 session 可能需要独立分配,以支持对话内的策略迭代。 第二层:数据采集层 数据采集层是框架的感知系统,负责捕获智能体运行过程中的全链路数据。与传统 A/B 测试不同,智能体测试需要记录的不仅是输入和输出,还包括: 推理轨迹:每一步思考的内容、使用的提示模板、温度参数 工具调用日志:调用了哪些工具、调用顺序、参数、返回结果、耗时 中间状态快照:上下文窗口的演变、记忆检索结果、规划树的中间节点 环境交互记录:智能体与外部环境的每次交互及其后果 class AgentTraceRecorder: def __init__(self): self.trace_schema = { "input": str, # 用户输入 "variant_id": str, # 实验变体 "reasoning_steps": list[dict], # 推理步骤序列 "tool_calls": list[dict], # 工具调用序列 "intermediate_states": list[dict], # 中间状态 "final_output": str, # 最终输出 "latency_ms": int, # 端到端延迟 "token_usage": dict, # Token 消耗 "error_info": dict | None # 错误信息 } 第三层:评估指标层 这是框架最关键的部分。智能体的评估指标体系分为四个层次: 第一层:结果质量指标 任务完成率:智能体是否正确完成了用户请求(二值指标,需人工或 LLM-as-Judge 标注) 输出准确率:对于有标准答案的任务,计算精确匹配或语义相似度 用户满意度:显式反馈(点赞/点踩)和隐式信号(是否继续追问、会话长度) 第二层:过程质量指标 ...

2026-06-26 · 2 min · 310 words · 硅基 AGI 探索者
鲁ICP备2026018361号