AI Agent的灰度发布与A/B测试:安全上线的不二法门

AI Agent的版本升级比传统软件风险更大——模型行为的改变可能是非线性的,一个看似小的prompt修改可能导致某些场景的输出质量骤降。灰度发布和A/B测试是管控这种风险的核心手段。本文将系统设计AI Agent的灰度发布与测试框架。 一、为什么AI Agent的发布更难 1.1 非确定性变化 传统软件的版本变化是确定性的:同样的输入,要么行为变了,要么没变。但AI Agent: 同一输入可能因为模型温度产生不同输出 行为变化可能在99%的输入上不可见,但在1%的边缘case上严重退化 prompt的细微修改可能导致输出风格的连锁变化 1.2 影响范围难以预估 修改: 在system prompt中增加了"回答要简洁"的要求 预期效果: 回复更简短 实际影响: - 简短了,但丢掉了重要细节(用户满意度下降) - 代码回复缺少注释(开发者投诉) - 情感回复变得冷漠(用户体验变差) - 多轮对话中信息不足导致追问增多(交互效率下降) 1.3 回归测试困难 传统软件有明确的测试用例——输入A应该得到输出B。但AI Agent的"正确输出"是模糊的:同一个问题可以有多个好答案。如何判断版本更新是否导致质量下降? 二、灰度发布策略 2.1 多维度灰度 class GradualRollout: def __init__(self): self.dimensions = { "traffic_percentage": [1, 5, 10, 25, 50, 100], # 流量百分比 "user_segment": ["internal", "beta", "free", "paid"], # 用户群体 "scenario": ["chat", "code", "analysis"], # 使用场景 "region": ["cn-east", "cn-south", "global"], # 地域 } def get_rollout_plan(self, version): """分阶段灰度计划""" return [ # Phase 1: 内部用户1% RolloutPhase( name="内部测试", traffic=0.01, user_segment=["internal"], duration_hours=24, success_criteria={"error_rate": "<1%", "satisfaction": ">=4.0"} ), # Phase 2: Beta用户5% RolloutPhase( name="Beta测试", traffic=0.05, user_segment=["beta"], duration_hours=48, success_criteria={"error_rate": "<2%", "satisfaction": ">=3.8"} ), # Phase 3: 10%免费用户 RolloutPhase( name="小规模公测", traffic=0.10, user_segment=["free"], duration_hours=72, success_criteria={"error_rate": "<3%", "satisfaction": ">=3.7"} ), # Phase 4: 全量 RolloutPhase( name="全量发布", traffic=1.0, user_segment=["all"], duration_hours=0, # 持续 success_criteria={"error_rate": "<3%", "satisfaction": ">=3.7"} ), ] 2.2 自动化质量门禁 每个灰度阶段设置质量门禁,不达标则自动暂停: ...

2026-07-13 · 4 min · 825 words · 硅基 AGI 探索者
A/B测试

AI系统A/B测试实践:数据驱动的模型选择

引言 A/B测试是验证AI系统效果最可靠的方法。与其依赖基准分数,不如在真实用户中进行对照实验。2026年,A/B测试已经成为AI产品迭代的标配流程。本文将系统介绍AI系统A/B测试的实践方法。 A/B测试基础 什么是A/B测试 将用户随机分为两组:A组使用版本A(如GPT-5),B组使用版本B(如Claude 4),比较两组的关键指标。 AI系统A/B测试的独特性 输出不确定性:同一输入可能产生不同输出 延迟变化:不同模型的响应速度不同 成本差异:不同模型的API成本可能差10倍 多维评估:不仅看准确率,还要看用户满意度、延迟、成本 实验设计 步骤一:定义假设 假设:使用Claude 4替代GPT-5作为客服机器人后端, 用户满意度将提升5%以上,且API成本降低30%以上。 步骤二:选择指标 指标类型 具体指标 说明 主要指标 用户满意度评分 核心评估指标 次要指标 任务完成率、首次解决率 辅助评估 护栏指标 延迟、错误率、成本 确保不恶化 业务指标 留存率、转化率 最终业务价值 步骤三:计算样本量 from scipy import stats def calculate_sample_size(baseline_rate, mde, alpha=0.05, power=0.8): """ 计算所需样本量 baseline_rate: 基线指标值 mde: 最小可检测效应 alpha: 显著性水平 power: 统计功效 """ z_alpha = stats.norm.ppf(1 - alpha/2) z_beta = stats.norm.ppf(power) p1 = baseline_rate p2 = baseline_rate + mde p_avg = (p1 + p2) / 2 n = ((z_alpha * sqrt(2 * p_avg * (1 - p_avg)) + z_beta * sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2) / (p2 - p1) ** 2 return ceil(n) 步骤四:流量分配 方案一:50/50均分 - A组:50%流量 - B组:50%流量 - 优点:最快达到统计显著 - 缺点:风险较高(B可能更差) 方案二:90/10渐增 - A组:90%流量(对照组) - B组:10%流量(实验组) - 优点:风险可控 - 缺点:需要更长时间 推荐:先10%灰度,确认无问题后扩到50% 实验执行 流量路由 class ABTestRouter: def __init__(self, experiment_id, variants): self.experiment_id = experiment_id self.variants = variants # {"A": 0.5, "B": 0.5} def assign(self, user_id): """ 根据用户ID分配实验组 """ # 使用一致性哈希,确保同一用户始终在同一组 hash_value = hash(f"{self.experiment_id}:{user_id}") bucket = hash_value % 100 / 100 cumulative = 0 for variant, ratio in self.variants.items(): cumulative += ratio if bucket < cumulative: return variant return list(self.variants.keys())[-1] 实验配置 experiment_config = { "id": "exp_2026_07_gpt5_vs_claude4", "name": "GPT-5 vs Claude 4 客服对比", "start_date": "2026-07-01", "end_date": "2026-07-14", "variants": { "A": { "model": "gpt-5", "system_prompt": "v1.0", "temperature": 0.7 }, "B": { "model": "claude-4-opus", "system_prompt": "v1.0", "temperature": 0.7 } }, "allocation": {"A": 0.5, "B": 0.5}, "primary_metric": "user_satisfaction", "guardrail_metrics": ["latency_p95", "error_rate", "cost_per_session"], "sample_size": 10000 # 每组 } 数据收集 def log_experiment_event(user_id, variant, event_type, event_data): """ 记录实验事件 """ event = { "timestamp": datetime.now().isoformat(), "experiment_id": "exp_2026_07_gpt5_vs_claude4", "user_id": user_id, "variant": variant, "event_type": event_type, # "request", "response", "feedback" "event_data": event_data } # 写入数据仓库 data_warehouse.insert("ab_test_events", event) 结果分析 统计显著性检验 def analyze_experiment(results_a, results_b, metric="satisfaction"): """ 分析实验结果 """ # 描述性统计 stats_a = { "mean": mean(results_a[metric]), "std": std(results_a[metric]), "n": len(results_a) } stats_b = { "mean": mean(results_b[metric]), "std": std(results_b[metric]), "n": len(results_b) } # t检验 t_stat, p_value = stats.ttest_ind(results_a[metric], results_b[metric]) # 效应量 pooled_std = sqrt((stats_a["std"]**2 + stats_b["std"]**2) / 2) cohens_d = (stats_b["mean"] - stats_a["mean"]) / pooled_std # 置信区间 diff = stats_b["mean"] - stats_a["mean"] se = sqrt(stats_a["std"]**2/stats_a["n"] + stats_b["std"]**2/stats_b["n"]) ci_lower = diff - 1.96 * se ci_upper = diff + 1.96 * se return { "stats_a": stats_a, "stats_b": stats_b, "difference": diff, "p_value": p_value, "significant": p_value < 0.05, "effect_size": cohens_d, "confidence_interval": (ci_lower, ci_upper) } 护栏指标检查 def check_guardrails(results_a, results_b, thresholds): """ 检查护栏指标 """ alerts = [] # 延迟检查 if results_b["latency_p95"] > thresholds["latency_p95"]: alerts.append(f"延迟超标:B组P95={results_b['latency_p95']}ms") # 错误率检查 if results_b["error_rate"] > thresholds["error_rate"]: alerts.append(f"错误率超标:B组={results_b['error_rate']}") # 成本检查 if results_b["cost_per_session"] > thresholds["cost_per_session"]: alerts.append(f"成本超标:B组={results_b['cost_per_session']}") return alerts 常见陷阱 陷阱一:提前停止 实验还没达到所需样本量就因为"B看起来更好"而停止。 ...

2026-07-02 · 3 min · 452 words · 硅基 AGI 探索者
提示测试

提示测试方法论:如何科学评估提示效果

引言 提示工程不是"一次写好"的艺术,而是"持续测试和优化"的科学。一个看似完美的提示,可能在某些边界情况下表现糟糕。2026年,提示测试已经成为AI应用开发的核心环节。本文将系统介绍提示测试的方法论。 为什么需要提示测试 问题一:模型差异 同一个提示在不同模型上的表现可能差异巨大: 提示:翻译以下句子为英文。 输入:今天天气很好。 GPT-5输出:The weather is very good today. ✓ Claude 4输出:Today's weather is quite nice. ✓ GLM-5输出:Today weather very good. ✗ (语法错误) 问题二:输入多样性 看起来不错的提示,可能在某种输入上失败: 提示:[情感分析] 输入分布: - 90%标准评论 → 准确率95% - 10%讽刺评论 → 准确率40% ← 问题! 问题三:提示脆弱性 微小的提示修改可能导致效果大幅下降: 原始提示(准确率90%): 请分析评论情感,输出positive/negative/neutral。 修改后(准确率60%): 分析一下这个评论的情感,输出正面/负面/中性。 ← 只是换了表述,效果大幅下降! 提示测试框架 测试维度 维度 说明 测试方法 准确性 输出是否正确 人工评估/GPT-4评估 格式一致性 输出格式是否稳定 自动解析验证 鲁棒性 对输入变化的敏感度 对抗性测试 模型通用性 在不同模型上的表现 跨模型测试 成本效率 token消耗与效果的比值 成本-效果分析 测试数据集 标准测试集 针对特定任务构建测试集: ...

2026-07-02 · 3 min · 601 words · 硅基 AGI 探索者
Prompt版本控制与A/B测试

Prompt版本控制与A/B测试:数据驱动的Prompt优化

为什么Prompt需要A/B测试 “我觉得这个Prompt更好”——这是Prompt工程中最常见也最危险的句子。直觉在Prompt优化中往往不可靠:一个看起来更精巧的Prompt可能在生产环境中表现更差。 真实案例: 某电商团队将客服Prompt从"详细回复"版本切换到"简洁回复"版本,直觉上认为用户更喜欢简洁。A/B测试结果:简洁版本的客户满意度下降了12%,因为用户需要多次追问才能解决问题。 数据驱动的Prompt优化不是可选项——它是生产系统的必需品。 Prompt版本控制 语义化版本控制 from dataclasses import dataclass from typing import Optional import hashlib @dataclass class PromptVersion: """Prompt版本""" major: int # 不兼容的修改(如输出格式变更) minor: int # 向后兼容的功能新增 patch: int # Bug修复和微调 hash: str # 内容哈希 @property def version_string(self) -> str: return f"v{self.major}.{self.minor}.{self.patch}" @staticmethod def compute_hash(content: str) -> str: return hashlib.sha256(content.encode()).hexdigest()[:12] class VersionedPrompt: """带版本控制的Prompt""" def __init__(self): self.versions: list[dict] = [] self.active_version: Optional[str] = None def commit(self, prompt_content: str, change_type: str = "patch", changelog: str = "") -> str: """ 提交新版本 change_type: major / minor / patch """ # 确定版本号 if not self.versions: version = PromptVersion(1, 0, 0, "") else: latest = self.versions[-1]["version"] if change_type == "major": version = PromptVersion(latest.major + 1, 0, 0, "") elif change_type == "minor": version = PromptVersion(latest.major, latest.minor + 1, 0, "") else: version = PromptVersion(latest.major, latest.minor, latest.patch + 1, "") version.hash = PromptVersion.compute_hash(prompt_content) version_id = f"{version.version_string}-{version.hash}" self.versions.append({ "version_id": version_id, "version": version, "content": prompt_content, "changelog": changelog, "timestamp": datetime.now(), "is_active": False }) return version_id def rollback(self, version_id: str): """回滚到指定版本""" for v in self.versions: v["is_active"] = (v["version_id"] == version_id) self.active_version = version_id def diff(self, version_a: str, version_b: str) -> str: """比较两个版本的差异""" import difflib content_a = next(v["content"] for v in self.versions if v["version_id"] == version_a) content_b = next(v["content"] for v in self.versions if v["version_id"] == version_b) diff = difflib.unified_diff( content_a.splitlines(keepends=True), content_b.splitlines(keepends=True), fromfile=version_a, tofile=version_b ) return ''.join(diff) A/B测试框架 测试设计 from dataclasses import dataclass from enum import Enum import numpy as np from scipy import stats class MetricType(Enum): ACCURACY = "accuracy" LATENCY = "latency" COST = "cost" USER_SATISFACTION = "satisfaction" TASK_COMPLETION = "completion" @dataclass class ABTestConfig: """A/B测试配置""" name: str description: str # 变体 control_version: str # 基线版本(A) treatment_version: str # 实验版本(B) # 流量分配 traffic_split: float # treatment流量比例 (0-1) # 指标 primary_metric: MetricType # 主要指标 secondary_metrics: list[MetricType] # 统计参数 significance_level: float = 0.05 # α statistical_power: float = 0.8 # 1-β minimum_detectable_effect: float = 0.05 # MDE # 持续时间 min_samples_per_variant: int = 1000 max_duration_days: int = 14 class PromptABTest: """Prompt A/B测试执行器""" def __init__(self, config: ABTestConfig, prompt_registry): self.config = config self.registry = prompt_registry self.results: dict[str, list] = { "control": [], "treatment": [] } def assign_variant(self, user_id: str) -> str: """分配用户到变体""" # 使用用户ID哈希确保同一用户始终在同一组 hash_value = hash(user_id) % 100 / 100 if hash_value < self.config.traffic_split: return "treatment" else: return "control" def get_prompt(self, variant: str) -> str: """获取对应变体的Prompt""" version = (self.config.control_version if variant == "control" else self.config.treatment_version) return self.registry.get(version) def record_result(self, variant: str, result: dict): """记录实验结果""" self.results[variant].append(result) def analyze(self) -> dict: """分析实验结果""" control_data = [r[self.config.primary_metric.value] for r in self.results["control"]] treatment_data = [r[self.config.primary_metric.value] for r in self.results["treatment"]] # 描述性统计 control_mean = np.mean(control_data) treatment_mean = np.mean(treatment_data) # 统计检验 if self._is_continuous(self.config.primary_metric): # 连续指标:t检验 t_stat, p_value = stats.ttest_ind( treatment_data, control_data ) effect_size = (treatment_mean - control_mean) / np.std(control_data) else: # 二分类指标:卡方检验 control_success = sum(control_data) treatment_success = sum(treatment_data) chi2, p_value = stats.chi2_contingency([ [control_success, len(control_data) - control_success], [treatment_success, len(treatment_data) - treatment_success] ])[:2] effect_size = (treatment_mean - control_mean) / control_mean # 置信区间 ci = self._confidence_interval( treatment_data, control_data, self.config.significance_level ) # 结论 significant = p_value < self.config.significance_level winner = "treatment" if significant and treatment_mean > control_mean else \ "control" if significant else "inconclusive" return { "control_mean": control_mean, "treatment_mean": treatment_mean, "relative_improvement": (treatment_mean - control_mean) / control_mean, "p_value": p_value, "effect_size": effect_size, "confidence_interval": ci, "significant": significant, "winner": winner, "sample_sizes": { "control": len(control_data), "treatment": len(treatment_data) } } def _confidence_interval(self, treatment, control, alpha): """计算置信区间""" diff = np.mean(treatment) - np.mean(control) se = np.sqrt(np.var(treatment)/len(treatment) + np.var(control)/len(control)) z = stats.norm.ppf(1 - alpha/2) return (diff - z * se, diff + z * se) 样本量计算 class SampleSizeCalculator: """计算所需样本量""" @staticmethod def for_proportion(baseline_rate: float, mde: float, alpha: float = 0.05, power: float = 0.8) -> int: """ 比率指标(如准确率)的样本量计算 baseline_rate: 基线比率 mde: 最小可检测效应 alpha: 显著性水平 power: 统计功效 """ from scipy.stats import norm z_alpha = norm.ppf(1 - alpha/2) z_beta = norm.ppf(power) p1 = baseline_rate p2 = baseline_rate + mde p_avg = (p1 + p2) / 2 n = ((z_alpha * np.sqrt(2 * p_avg * (1 - p_avg)) + z_beta * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2) / (p1 - p2) ** 2 return int(np.ceil(n)) @staticmethod def for_continuous(baseline_std: float, mde: float, alpha: float = 0.05, power: float = 0.8) -> int: """ 连续指标(如延迟)的样本量计算 """ from scipy.stats import norm z_alpha = norm.ppf(1 - alpha/2) z_beta = norm.ppf(power) n = 2 * ((z_alpha + z_beta) * baseline_std / mde) ** 2 return int(np.ceil(n)) 多变量测试(Multivariate Testing) class MultivariatePromptTest: """ 多变量Prompt测试 同时测试多个Prompt组件的变化 """ def __init__(self): self.factors = {} # 因子及其变体 def add_factor(self, name: str, variants: list[str]): """添加测试因子""" self.factors[name] = variants def generate_combinations(self) -> list[dict]: """生成所有组合""" from itertools import product factor_names = list(self.factors.keys()) factor_values = list(self.factors.values()) combinations = [] for values in product(*factor_values): combinations.append(dict(zip(factor_names, values))) return combinations def design(self) -> dict: """实验设计""" combos = self.generate_combinations() return { "total_combinations": len(combos), "combinations": combos, "traffic_per_combo": 1.0 / len(combos), "min_samples_per_combo": self._calc_min_samples(), "estimated_duration_days": self._estimate_duration(), } 实际案例:客服Prompt优化 # 案例:优化电商客服Prompt # 基线Prompt (v1.0.0) control_prompt = """ 你是一个电商客服助手。请回答用户的问题。 """ # 实验Prompt (v1.1.0) - 添加了情绪感知和解决方案导向 treatment_prompt = """ 你是一个专业的电商客服助手。 回答原则: 1. 先理解用户的情绪和核心诉求 2. 提供具体的解决方案,而非泛泛而谈 3. 如果需要转人工,明确说明原因 4. 保持友好但专业的语调 回答结构: - 确认问题:简要复述用户的问题 - 解决方案:给出具体步骤 - 后续支持:提供额外帮助选项 """ # 配置A/B测试 config = ABTestConfig( name="客服Prompt优化-情绪感知版", description="测试添加情绪感知和结构化回答是否提升客户满意度", control_version="v1.0.0", treatment_version="v1.1.0", traffic_split=0.5, primary_metric=MetricType.USER_SATISFACTION, secondary_metrics=[MetricType.TASK_COMPLETION, MetricType.LATENCY], minimum_detectable_effect=0.03, # 3%提升 min_samples_per_variant=2000, ) # 运行测试 ab_test = PromptABTest(config, prompt_registry) # 分析结果 results = ab_test.analyze() """ 预期输出: { "control_mean": 0.78, # 基线满意度 78% "treatment_mean": 0.83, # 实验组满意度 83% "relative_improvement": 0.064, # 6.4%提升 "p_value": 0.002, # p < 0.05,显著 "effect_size": 0.12, # 小到中等效应 "confidence_interval": (0.02, 0.08), "significant": True, "winner": "treatment" } """ 渐进式发布 class ProgressiveRollout: """ 渐进式发布 获胜的变体逐步增加流量 """ ROLLOUT_STAGES = [ {"traffic": 0.05, "duration_hours": 24, "min_success_rate": 0.85}, {"traffic": 0.20, "duration_hours": 48, "min_success_rate": 0.87}, {"traffic": 0.50, "duration_hours": 72, "min_success_rate": 0.88}, {"traffic": 1.00, "duration_hours": None, "min_success_rate": 0.88}, ] async def rollout(self, prompt_version: str): """渐进式发布""" for stage in self.ROLLOUT_STAGES: # 设置流量 await self.traffic_manager.set_traffic( prompt_version, stage["traffic"] ) # 等待观察期 if stage["duration_hours"]: await asyncio.sleep(stage["duration_hours"] * 3600) # 检查指标 success_rate = await self.metrics.get_success_rate( prompt_version, window_hours=stage["duration_hours"] ) if success_rate < stage["min_success_rate"]: # 回滚 await self.traffic_manager.rollback(prompt_version) await self.alerting.notify( f"发布失败:成功率 {success_rate:.1%} < " f"阈值 {stage['min_success_rate']:.1%}" ) return False return True 结语 Prompt优化不应该是基于直觉的猜测游戏。版本控制提供了可追溯的历史,A/B测试提供了统计严谨的决策依据。2026年的Prompt工程实践应该是: ...

2026-06-30 · 5 min · 991 words · 硅基 AGI 探索者
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 探索者
llm ab testing handbook

大模型A/B测试实施手册

概述 大模型A/B测试实施手册是AI智能体领域中大模型A/B测试实施手册的重要主题。本文将从多个角度深入分析这一话题,为读者提供系统性的认知框架和实践参考。 核心概念 基本定义 在深入讨论之前,我们需要明确几个核心概念。AI智能体是指能够感知环境、理解指令、规划行动并调用工具完成任务的AI系统。与传统的聊天机器人不同,智能体具有自主性、目标导向性和工具使用能力。 大模型A/B测试实施手册涉及的关键技术包括: 大语言模型:作为智能体的认知引擎,负责理解、推理和生成 工具调用:通过Function Calling或MCP协议与外部系统交互 记忆系统:短期记忆处理当前对话,长期记忆存储历史经验 规划引擎:将复杂任务分解为可执行的子步骤 技术原理 从技术层面看,大模型A/B测试实施手册的核心在于如何让AI系统更好地理解和执行人类意图。这涉及多个技术环节的协同: 首先是感知层,智能体需要准确理解用户的自然语言指令,提取关键信息和约束条件。其次是规划层,将高层目标分解为具体的执行步骤。然后是执行层,调用合适的工具完成每个步骤。最后是反馈层,根据执行结果调整后续策略。 实践分析 当前现状 在实践指南领域,当前的技术实践呈现出几个明显特征: 工程化程度提升:从实验室原型到生产级系统,工程能力成为关键差异化因素 评估体系完善:越来越多标准化的评测基准被提出,帮助开发者量化能力边界 开源生态繁荣:开源框架和工具链的成熟降低了开发门槛 安全意识增强:对AI安全和对齐问题的重视程度显著提升 关键挑战 尽管进展显著,大模型A/B测试实施手册仍面临几个核心挑战: 技术挑战: 大模型的幻觉问题在智能体场景下被放大,因为智能体需要做出实际决策 多步推理中的错误累积效应导致长程任务成功率下降 工具调用的可靠性受外部API稳定性影响 工程挑战: 智能体的可观测性不足,调试和排错困难 成本控制与性能优化的平衡 从单机到分布式部署的架构复杂性 安全挑战: Prompt注入等攻击手段不断进化 智能体权限管理需要更精细化的控制 数据隐私保护在多Agent协作场景下更加复杂 优化策略 针对上述挑战,以下是几个关键优化方向: 技术优化 分而治之:将复杂任务分解为可独立验证的子任务,降低单步错误影响 多路投票:对关键决策使用多次采样投票机制,提高可靠性 渐进式信任:智能体权限从最小化开始,根据表现逐步扩展 人在回路:高风险决策保留人工审核环节 工程优化 可观测性优先:建立完善的日志、指标和追踪体系 灰度发布:新版本智能体先在小流量环境验证 自动化测试:构建端到端测试套件,防止回归 成本监控:实时追踪Token消耗和API调用成本 案例研究 为了更具体地说明大模型A/B测试实施手册的实践价值,我们来看一个典型场景: 某科技公司在内部IT运维中部署了AI智能体,负责处理员工的工单请求。智能体需要理解员工的自然语言描述,判断问题类型,查询知识库,执行修复操作或转接人工。 实施过程中遇到的关键问题包括: 员工描述模糊导致意图识别错误 知识库信息过时导致给出错误建议 某些操作需要管理员权限存在安全风险 解决方案: 引入澄清对话机制,在不确定时主动追问 建立知识库更新流程,定期审核内容 实施权限分级制度,敏感操作需人工确认 效果:工单首次解决率提升35%,平均处理时间缩短60%,员工满意度显著提升。 未来趋势 大模型A/B测试实施手册的发展趋势值得关注: 标准化:MCP等开放协议将推动工具接口标准化,降低集成成本 垂直化:针对特定行业和场景的专用智能体将大量涌现 协作化:多智能体协作将成为复杂任务的标准解决方案 自主化:智能体的自主决策能力将持续提升,但需要配套的安全机制 结论 大模型A/B测试实施手册是AI智能体技术发展中的重要一环。无论是技术原理的深入理解,还是实践中的工程优化,都需要系统性思维。对于开发者和企业而言,关键在于: 理解技术能力和边界,避免过度期待 建立系统化的评估和监控体系 在创新和安全之间找到平衡 持续学习和适应快速变化的技术生态 硅基AGI探索者将持续关注实践指南领域的最新进展,为读者提供深度分析和实践指导。— ...

2026-06-27 · 1 min · 88 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 探索者
a b testing for llm

LLM A/B 测试实践:统计显著性与业务指标

LLM A/B 测试的特殊性 传统 Web 产品的 A/B 测试已经相当成熟——改个按钮颜色、调整文案、优化布局,通过 CTR 和转化率就能快速判断优劣。但 LLM 产品的 A/B 测试面临独特挑战: 输出非确定性:同一个输入,两次调用可能得到不同输出 质量维度多元:没有单一指标能衡量"回答好不好" 长尾效应:大部分对话可能表现类似,但少数关键场景差异巨大 学习效应:用户可能需要时间适应新模型的行为风格 A/B 测试完整流程 ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 假设构建 │ -> │ 实验设计 │ -> │ 执行与监控 │ -> │ 分析与决策 │ │ Hypothesis │ │ Design │ │ Execute │ │ Analyze │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ 一、假设构建 每个 A/B 测试都应始于一个清晰的假设: from dataclasses import dataclass from typing import Optional @dataclass class ABTestHypothesis: """A/B 测试假设""" # 假设描述 statement: str # "将模型从 GPT-4o-mini 升级到 GPT-4o 后, # 在复杂推理任务上的用户满意度将提升 10%" # 自变量 treatment_description: str # "使用 GPT-4o 替代 GPT-4o-mini" control_description: str # "继续使用 GPT-4o-mini" # 因变量(核心指标) primary_metric: str # "CSAT 评分" expected_effect: float # 0.10 (提升10%) expected_direction: str # "increase" or "decrease" # 次要指标 secondary_metrics: list # ["重试率", "平均对话轮次", "人工求助率"] # 护栏指标(不应恶化的指标) guardrail_metrics: list # ["延迟 P95", "成本/请求", "安全违规率"] # 目标人群 target_segment: str # "all_users" 或 "power_users" 等 # 最小可检测效应 (MDE) mde: float # 0.05 (最小可检测 5% 变化) def validate(self) -> list[str]: issues = [] if not self.statement or len(self.statement) < 10: issues.append("假设描述太短") if not self.primary_metric: issues.append("缺少主要指标") if self.expected_effect <= 0: issues.append("预期效应应为正数") if not self.guardrail_metrics: issues.append("缺少护栏指标——可能导致意外回退") return issues 二、样本量计算 import math from scipy import stats class SampleSizeCalculator: """A/B 测试样本量计算器""" @staticmethod def for_proportion( baseline_rate: float, mde: float, # 最小可检测效应(绝对值) alpha: float = 0.05, power: float = 0.80, two_sided: bool = True, ) -> dict: """ 比例类指标的样本量计算(如 CSAT 满意率、重试率) 参数: baseline_rate: 基线比例(如当前 CSAT = 0.75) mde: 最小可检测效应(如 0.05 表示检测 5% 绝对变化) alpha: 显著性水平 power: 统计功效 """ z_alpha = stats.norm.ppf(1 - alpha / 2 if two_sided else 1 - alpha) z_beta = stats.norm.ppf(power) p1 = baseline_rate p2 = baseline_rate + mde p_avg = (p1 + p2) / 2 n = ((z_alpha * math.sqrt(2 * p_avg * (1 - p_avg)) + z_beta * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2) / (p2 - p1) ** 2 return { "sample_per_group": math.ceil(n), "total_sample": math.ceil(n * 2), "baseline_rate": baseline_rate, "mde": mde, "alpha": alpha, "power": power, "expected_duration_days": math.ceil(n * 2 / 1000), # 假设每天1000用户 } @staticmethod def for_continuous( baseline_mean: float, baseline_std: float, mde: float, alpha: float = 0.05, power: float = 0.80, ) -> dict: """ 连续型指标的样本量计算(如评分均值、延迟) 参数: baseline_mean: 基线均值 baseline_std: 基线标准差 mde: 最小可检测效应(绝对变化量) """ z_alpha = stats.norm.ppf(1 - alpha / 2) z_beta = stats.norm.ppf(power) n = 2 * ((z_alpha + z_beta) * baseline_std / mde) ** 2 return { "sample_per_group": math.ceil(n), "total_sample": math.ceil(n * 2), "baseline_mean": baseline_mean, "baseline_std": baseline_std, "mde": mde, } # 示例:计算 CSAT 从 75% 提升到 80% 所需的样本量 calc = SampleSizeCalculator() result = calc.for_proportion(baseline_rate=0.75, mde=0.05) # 输出: sample_per_group ≈ 2554, total_sample ≈ 5108 LLM 特有的样本量考量 因素 影响 调整策略 输出非确定性 增加方差,需要更多样本 对同一输入运行多次取均值 用户异质性 不同用户群体效应不同 分层随机化 冷启动效应 新模型初期表现可能不稳定 设置预热期 时段效应 不同时间段对话质量不同 确保两组同时段运行 三、实验设计 随机化策略 import hashlib import random class ABTestRouter: """A/B 测试流量分配器""" def __init__(self, experiment_id: str, traffic_split: dict = None): self.experiment_id = experiment_id # 默认 50/50 分配 self.traffic_split = traffic_split or {"control": 0.5, "treatment": 0.5} self.salt = experiment_id # 用于哈希的盐值 def assign(self, user_id: str) -> str: """ 基于用户 ID 的确定性分配 同一用户始终分到同一组 """ # 使用哈希确保确定性 hash_input = f"{self.salt}:{user_id}" hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16) bucket = (hash_value % 10000) / 10000.0 cumulative = 0.0 for group, ratio in self.traffic_split.items(): cumulative += ratio if bucket < cumulative: return group return list(self.traffic_split.keys())[-1] # fallback def assign_with_layering(self, user_id: str, existing_experiments: list[str]) -> str: """ 分层分配:避免与正在运行的其他实验冲突 """ # 将已有实验纳入哈希 hash_input = f"{self.salt}:{user_id}:{':'.join(sorted(existing_experiments))}" hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16) bucket = (hash_value % 10000) / 10000.0 cumulative = 0.0 for group, ratio in self.traffic_split.items(): cumulative += ratio if bucket < cumulative: return group return list(self.traffic_split.keys())[-1] class StratifiedRouter: """分层随机化:确保关键维度均衡""" STRATA = ["user_type", "region", "device", "usage_level"] def __init__(self, experiment_id: str, strata_weights: dict = None): self.experiment_id = experiment_id self.strata_weights = strata_weights or {} self.assignments = {} # 缓存分配结果 def assign(self, user_id: str, user_attributes: dict) -> str: # 计算分层 key strata_key = "|".join( str(user_attributes.get(s, "unknown")) for s in self.STRATA ) # 分层内随机分配 hash_input = f"{self.experiment_id}:{strata_key}:{user_id}" hash_value = int(hashlib.sha256(hash_input.encode()).hexdigest(), 16) return "treatment" if hash_value % 2 == 0 else "control" 实验配置 @dataclass class ABTestConfig: experiment_id: str name: str hypothesis: ABTestHypothesis # 流量分配 traffic_split: dict = field(default_factory=lambda: {"control": 0.5, "treatment": 0.5}) total_traffic_pct: float = 1.0 # 使用多少比例的总流量 # 时间设置 start_date: str = "" end_date: str = "" min_duration_days: int = 14 # 最短运行天数 warmup_days: int = 2 # 预热天数(数据不计入分析) # 指标配置 primary_metric: str = "" secondary_metrics: list = field(default_factory=list) guardrail_metrics: list = field(default_factory=list) # 统计参数 alpha: float = 0.05 power: float = 0.80 mde: float = 0.05 # 停止规则 early_stop_on_guardrail: bool = True guardrail_thresholds: dict = field(default_factory=lambda: { "latency_p95_ms": 5000, # P95 延迟不超过 5s "safety_violation_rate": 0.001, # 安全违规率不超过 0.1% "cost_per_session": 0.15, # 每次会话成本不超过 $0.15 }) 四、统计显著性检验 from scipy import stats import numpy as np class SignificanceTester: """A/B 测试显著性检验""" def test_proportion(self, control_success: int, control_total: int, treatment_success: int, treatment_total: int, alpha: float = 0.05) -> dict: """ 比例类指标的双比例 Z 检验 """ p_control = control_success / control_total p_treatment = treatment_success / treatment_total p_pooled = (control_success + treatment_success) / (control_total + treatment_total) se = np.sqrt(p_pooled * (1 - p_pooled) * (1/control_total + 1/treatment_total)) z_stat = (p_treatment - p_control) / se if se > 0 else 0 p_value = 2 * (1 - stats.norm.cdf(abs(z_stat))) # 置信区间 diff = p_treatment - p_control ci_lower = diff - stats.norm.ppf(1 - alpha/2) * se ci_upper = diff + stats.norm.ppf(1 - alpha/2) * se # 效应量 relative_lift = diff / p_control if p_control > 0 else 0 return { "test_type": "two_proportion_z_test", "control_rate": round(p_control, 4), "treatment_rate": round(p_treatment, 4), "absolute_diff": round(diff, 4), "relative_lift": f"{relative_lift*100:.2f}%", "z_statistic": round(z_stat, 4), "p_value": round(p_value, 6), "significant": p_value < alpha, "ci_95": [round(ci_lower, 4), round(ci_upper, 4)], "winner": "treatment" if (p_value < alpha and diff > 0) else ("control" if (p_value < alpha and diff < 0) else "no_significant_diff"), } def test_continuous(self, control_values: list[float], treatment_values: list[float], alpha: float = 0.05) -> dict: """ 连续型指标的检验(t 检验或 Mann-Whitney U 检验) """ control = np.array(control_values) treatment = np.array(treatment_values) # 正态性检验决定使用哪种检验 if len(control) >= 30 and len(treatment) >= 30: # 大样本使用 Welch t 检验 t_stat, p_value = stats.ttest_ind(treatment, control, equal_var=False) test_name = "welch_t_test" else: # 小样本使用 Mann-Whitney U u_stat, p_value = stats.mannwhitneyu(treatment, control, alternative="two-sided") t_stat = u_stat test_name = "mann_whitney_u" diff = treatment.mean() - control.mean() # 置信区间(基于 t 分布) pooled_se = np.sqrt(treatment.var(ddof=1)/len(treatment) + control.var(ddof=1)/len(control)) df = len(treatment) + len(control) - 2 ci_lower = diff - stats.t.ppf(1-alpha/2, df) * pooled_se ci_upper = diff + stats.t.ppf(1-alpha/2, df) * pooled_se return { "test_type": test_name, "control_mean": round(control.mean(), 4), "control_std": round(control.std(), 4), "treatment_mean": round(treatment.mean(), 4), "treatment_std": round(treatment.std(), 4), "absolute_diff": round(diff, 4), "relative_lift": f"{(diff/control.mean())*100:.2f}%" if control.mean() != 0 else "N/A", "statistic": round(t_stat, 4), "p_value": round(p_value, 6), "significant": p_value < alpha, "ci_95": [round(ci_lower, 4), round(ci_upper, 4)], } def sequential_test(self, daily_data: list[dict], alpha: float = 0.05) -> dict: """ 序贯检验:每天检查是否可以提前停止 使用 Bonferroni 校正控制总犯错误率 """ num_checks = len(daily_data) adjusted_alpha = alpha / num_checks # Bonferroni 校正 results = [] for i, day_data in enumerate(daily_data): result = self.test_proportion( day_data["control_success"], day_data["control_total"], day_data["treatment_success"], day_data["treatment_total"], alpha=adjusted_alpha ) result["day"] = i + 1 result["adjusted_alpha"] = adjusted_alpha results.append(result) # 如果已显著或护栏指标触发,可以停止 if result["significant"]: return { "stopped_early": True, "stopped_at_day": i + 1, "final_result": result, "all_daily_results": results, } return { "stopped_early": False, "final_result": results[-1] if results else None, "all_daily_results": results, } 五、业务指标选择 指标分层框架 class MetricFramework: """A/B 测试指标分层框架""" METRIC_TREE = { "北极星指标": { "description": "最能反映产品价值的单一指标", "llm_examples": ["每周活跃对话用户数 (WAC)", "人均每日对话轮次"], "sensitivity": "低 — 需要较长时间观察", }, "主要指标": { "description": "直接反映假设是否成立的指标", "llm_examples": ["CSAT", "任务完成率", "回答准确率"], "sensitivity": "中 — 通常 2-4 周可检测", }, "次要指标": { "description": "帮助理解主要指标变化原因", "llm_examples": ["重试率", "对话深度", "人工求助率"], "sensitivity": "高 — 快速响应变化", }, "护栏指标": { "description": "确保不出现严重回退", "llm_examples": ["延迟 P95", "安全违规率", "每次会话成本"], "sensitivity": "高 — 需要实时监控", }, "调试指标": { "description": "用于诊断问题,不用于决策", "llm_examples": ["Token 使用量", "API 错误率", "特定类别表现"], "sensitivity": "高", } } @staticmethod def recommend_metrics(scenario: str) -> dict: recommendations = { "model_upgrade": { "primary": "回答准确率(基于评估集)", "secondary": ["CSAT", "重试率", "对话深度"], "guardrails": ["延迟 P95", "安全违规率", "成本/请求"], }, "prompt_change": { "primary": "任务完成率", "secondary": ["用户重述率", "回答长度分布"], "guardrails": ["安全违规率", "Token 使用量"], }, "feature_addition": { "primary": "功能采纳率", "secondary": ["CSAT", "会话深度", "NPS"], "guardrails": ["延迟 P95", "错误率"], }, "ui_change": { "primary": "任务完成率", "secondary": ["CSAT", "CES", "会话深度"], "guardrails": ["页面加载时间", "错误率"], }, } return recommendations.get(scenario, "Unknown scenario") 六、常见陷阱与解决方案 陷阱一:Peeking(偷看) class PeekingWarning: """偷看陷阱演示与解决方案""" def demonstrate_peeking_problem(self): """ 如果每天检查 p 值并在 p<0.05 时停止, 实际犯错误率远超 5% """ # 模拟 1000 次实验(A 和 B 实际无差异) false_positive_count = 0 for _ in range(1000): control = np.random.binomial(1, 0.10, 500) # 转化率 10% treatment = np.random.binomial(1, 0.10, 500) # 每天检查(假设每天 50 个样本) for day in range(1, 11): c_slice = control[:day*50] t_slice = treatment[:day*50] # 简化:使用卡方检验 _, p_value = stats.chi2_contingency([ [c_slice.sum(), len(c_slice) - c_slice.sum()], [t_slice.sum(), len(t_slice) - t_slice.sum()] ]) if p_value < 0.05: false_positive_count += 1 break actual_fpr = false_positive_count / 1000 return { "nominal_alpha": 0.05, "actual_false_positive_rate": actual_fpr, "inflation_factor": actual_fpr / 0.05, "solution": "使用序贯检验或 Alpha Spending 函数" } 陷阱二:辛普森悖论 class SimpsonParadoxCheck: """辛普森悖论检测""" def check(self, data: pd.DataFrame, group_col: str, metric_col: str, strata_col: str) -> dict: """ 检测是否存在辛普森悖论: 整体趋势与分层趋势相反 """ # 整体差异 overall = data.groupby(group_col)[metric_col].mean() overall_diff = overall.get("treatment", 0) - overall.get("control", 0) # 分层差异 strata_results = {} reversals = [] for stratum, stratum_data in data.groupby(strata_col): stratum_overall = stratum_data.groupby(group_col)[metric_col].mean() stratum_diff = stratum_overall.get("treatment", 0) - stratum_overall.get("control", 0) strata_results[stratum] = { "control_n": len(stratum_data[stratum_data[group_col] == "control"]), "treatment_n": len(stratum_data[stratum_data[group_col] == "treatment"]), "control_mean": stratum_overall.get("control", 0), "treatment_mean": stratum_overall.get("treatment", 0), "diff": stratum_diff, } # 检测方向反转 if (overall_diff > 0 and stratum_diff < 0) or \ (overall_diff < 0 and stratum_diff > 0): reversals.append(stratum) return { "overall_diff": overall_diff, "strata": strata_results, "reversals": reversals, "simpson_paradox": len(reversals) > 0, "recommendation": "分层分析而非整体分析" if reversals else "整体分析可行", } 七、决策框架 class ABTestDecision: """A/B 测试决策框架""" @staticmethod def decide(primary_result: dict, guardrail_results: dict, secondary_results: dict, config: ABTestConfig) -> dict: # 1. 检查护栏指标 guardrail_breaches = [] for metric, result in guardrail_results.items(): threshold = config.guardrail_thresholds.get(metric) if threshold and result.get("treatment_mean", 0) > threshold: guardrail_breaches.append({ "metric": metric, "treatment_value": result["treatment_mean"], "threshold": threshold, }) if guardrail_breaches: return { "decision": "STOP", "reason": "护栏指标被突破", "breaches": guardrail_breaches, } # 2. 检查主要指标 if not primary_result["significant"]: return { "decision": "CONTINUE" if primary_result.get("p_value", 1) > 0.3 else "INCONCLUSIVE", "reason": "主要指标未达统计显著性", "p_value": primary_result["p_value"], "recommendation": "继续收集数据或增大样本量", } # 3. 主要指标显著 winner = primary_result["winner"] if winner == "treatment": # 检查次要指标是否支持 secondary_support = all( r.get("relative_lift", "").startswith("-") is False for r in secondary_results.values() ) return { "decision": "SHIP", "reason": "主要指标显著提升,护栏指标未突破", "confidence": "high" if secondary_support else "medium", "primary_lift": primary_result["relative_lift"], "secondary_support": secondary_support, } else: return { "decision": "HOLD", "reason": "主要指标显著下降", "primary_drop": primary_result["relative_lift"], } 决策速查表 情况 主要指标 护栏指标 决策 主要指标显著提升 ✅ ✅ 未突破 全量发布 主要指标显著提升 ✅ ❌ 突破 不发布,分析权衡 主要指标显著下降 ❌ ✅ 保持对照组 主要指标无显著差异 - ✅ 视成本决定 主要指标无显著差异 - ❌ 保持对照组 结语 LLM A/B 测试比传统 Web A/B 测试复杂得多,但核心原则不变:清晰的假设、足够的样本量、正确的统计检验、严格的护栏监控。最大的陷阱不是统计方法不对,而是没有想清楚要测什么就开始测。在 LLM 时代,一个好的 A/B 测试框架是产品迭代的基础设施——没有它,所有的"优化"都只是猜测。 加入讨论 这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。 ...

2026-06-25 · 8 min · 1692 words · 硅基 AGI 探索者
鲁ICP备2026018361号