AI 应用的 A/B 测试——怎么科学地评估 Prompt 改动是否有效
AI 应用的 A/B 测试——怎么科学地评估 Prompt 改动是否有效
适读人群:AI 产品工程师、负责 AI 应用质量的工程师 | 阅读时长:约 11 分钟 | 核心价值:完整的 AI A/B 测试设计框架,解决"我怎么知道这个 prompt 改动有没有用"的问题
有一次我把一个客服 AI 的 system prompt 改了,加了一段要求它"更主动询问用户的具体情况"。改完之后,我在自己测了十几条消息,觉得效果不错,就上线了。
结果一周后,用户投诉增多,说 AI 总是反问用户,很烦。
我那时候意识到一个问题:我是在用我自己的十几条测试消息,代表每天几万条真实对话来做判断。这个判断根本站不住脚。
从那之后,我开始认真设计 AI 应用的 A/B 测试体系。这篇文章是我踩了很多坑之后整理出来的。
AI A/B 测试和传统 A/B 测试的本质区别
传统 Web A/B 测试里,点击率、转化率这类指标是客观的,用户点了就是点了,没点就是没点,数据简单干净。
AI 应用的 A/B 测试难在几个地方:
1. 输出质量是主观的
"这个回答好不好",不同用户有不同标准,同一个用户在不同场景下也有不同期待。没有像点击率那样客观的质量指标。
2. 模型输出有随机性
同一个 prompt,同一个用户问题,LLM 可能给出不同的回答。这意味着你测到的"好结果"有一部分是运气,不是 prompt 改动带来的。
3. 用户信号噪音大
用户对 AI 的反应很复杂。"没有继续追问"可能意味着回答很好、也可能意味着用户放弃了。"对话变长"可能意味着用户满意、也可能意味着他在反复澄清。
4. 影响因素太多
A/B 测试期间,模型可能更新,用户群可能变化,业务场景可能变化。隔离单一变量很难。
实验设计:把问题想清楚再动手
在写任何代码之前,我会强迫自己回答这几个问题:
你想优化的核心指标是什么?
不能说"回答质量",这太模糊。要具体到可测量的信号:
- 用户在 AI 给出回答后的会话结束率(越高越好,说明问题被解决了)
- 用户追加追问的次数(越少可能越好,也可能是用户放弃了,要区分)
- 用户手动触发"转人工"的比例
- 显式的点赞/点踩反馈
你改了什么?
每次只改一个变量。prompt 改动 + 模型版本一起换,你不知道效果来自哪里。
你的样本量够吗?
AI 输出的方差很大,你需要比传统 A/B 测试更多的样本才能得出可靠结论。
from scipy import stats
import math
def calculate_sample_size(
baseline_rate: float,
expected_improvement: float,
alpha: float = 0.05, # 显著性水平
power: float = 0.8 # 检验效能
) -> int:
"""
计算检测给定效果量所需的最小样本量
baseline_rate: 当前的指标值(如会话解决率 0.65)
expected_improvement: 期望改进(如 0.05 表示提升到 0.70)
"""
effect_size = expected_improvement / math.sqrt(
baseline_rate * (1 - baseline_rate)
)
# 使用双样本比例检验
z_alpha = stats.norm.ppf(1 - alpha / 2)
z_beta = stats.norm.ppf(power)
n = ((z_alpha + z_beta) ** 2) / (effect_size ** 2)
return math.ceil(n)
# 示例:当前会话解决率 65%,想检测 5% 的改进是否真实
required_n = calculate_sample_size(0.65, 0.05)
print(f"每组需要至少 {required_n} 个样本")
# 输出:每组需要至少 约 620 个样本实验框架的工程实现
我用的是一个简单但够用的实验分配框架,核心是确保同一个用户在实验期间始终被分到同一个变体:
import hashlib
from enum import Enum
from dataclasses import dataclass
from typing import Optional
import json
import time
class Variant(Enum):
CONTROL = "control"
TREATMENT = "treatment"
@dataclass
class ExperimentConfig:
experiment_id: str
control_prompt: str
treatment_prompt: str
traffic_split: float = 0.5 # 50% 流量进入 treatment
start_time: float = 0
end_time: Optional[float] = None
class ABTestFramework:
def __init__(self, config: ExperimentConfig):
self.config = config
def get_variant(self, user_id: str) -> Variant:
"""
基于 user_id 的哈希分配变体
确保同一用户始终得到相同变体
"""
hash_input = f"{self.config.experiment_id}:{user_id}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
bucket = (hash_value % 100) / 100.0
if bucket < self.config.traffic_split:
return Variant.TREATMENT
return Variant.CONTROL
def get_prompt(self, user_id: str) -> tuple[str, Variant]:
"""返回该用户应该使用的 prompt 和对应的变体"""
variant = self.get_variant(user_id)
if variant == Variant.TREATMENT:
return self.config.treatment_prompt, variant
return self.config.control_prompt, variant
def log_event(self, user_id: str, variant: Variant, event_type: str,
metadata: dict = None):
"""记录实验事件"""
event = {
"experiment_id": self.config.experiment_id,
"user_id": user_id,
"variant": variant.value,
"event_type": event_type,
"timestamp": time.time(),
"metadata": metadata or {}
}
# 实际项目里写到数据库或消息队列
print(json.dumps(event, ensure_ascii=False))
return event
# 使用示例
config = ExperimentConfig(
experiment_id="prompt_v2_test_20250423",
control_prompt="你是一个客服助手,请简洁地回答用户问题。",
treatment_prompt="你是一个客服助手,请先理解用户的核心问题,然后给出具体可操作的解决方案。",
traffic_split=0.5
)
ab_test = ABTestFramework(config)
def handle_user_message(user_id: str, message: str) -> str:
prompt, variant = ab_test.get_prompt(user_id)
# 记录本次请求使用了哪个变体
ab_test.log_event(user_id, variant, "request_started", {
"message_length": len(message)
})
# 调用 LLM(省略具体实现)
response = call_llm(prompt, message)
return response
def record_session_end(user_id: str, session_resolved: bool, feedback: Optional[int] = None):
"""用户会话结束时记录关键指标"""
variant = ab_test.get_variant(user_id)
ab_test.log_event(user_id, variant, "session_ended", {
"resolved": session_resolved,
"user_feedback": feedback # 1=点赞, -1=点踩, None=未反馈
})收集有意义的反馈信号
这是最难的部分。用户很少主动给反馈,你必须设计隐式信号。
我实际用的信号体系(按可靠性排序):
高可靠性(直接反映用户满意度):
- 用户明确点赞/点踩
- 用户明确说"谢谢,解决了"这类话
- 问题解决后会话自然结束(vs 用户沉默离开)
中可靠性(需要配合其他信号解读):
- 会话轮数(轮数少可能是解决了,也可能是放弃了)
- 用户追问的是否是同一个问题(需要 LLM 判断)
- 会话结束后的再次访问时间(很快回来可能没解决)
低可靠性(有噪音,辅助参考):
- 回复长度(不是越长越好)
- 响应时间(用户感知延迟影响满意度,但不代表回答质量)对于 AI 输出质量的主观评估,我会用 LLM 作为"裁判"来大批量评分:
from openai import OpenAI
client = OpenAI()
def llm_judge_response_quality(
user_question: str,
ai_response: str,
context: str = ""
) -> dict:
"""
用 LLM 评估 AI 回答的质量
注意:这是有偏差的方法,不能完全替代人工评估,但可以用于大规模筛选
"""
judge_prompt = f"""请评估以下 AI 回答的质量。
用户问题:{user_question}
AI 回答:{ai_response}
{"背景信息:" + context if context else ""}
请从以下维度打分(1-5分)并给出简短理由:
1. 准确性:回答是否正确回应了用户的核心问题
2. 完整性:回答是否提供了足够的信息
3. 简洁性:回答是否避免了不必要的冗余
4. 可操作性:如果是解决问题的场景,给出的建议是否具体可执行
请用 JSON 格式返回:
{{"accuracy": X, "completeness": X, "conciseness": X, "actionability": X, "reasoning": "..."}}"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": judge_prompt}],
response_format={"type": "json_object"},
temperature=0 # 评估任务用 temperature=0,保持一致性
)
return json.loads(response.choices[0].message.content)结果分析
import pandas as pd
from scipy import stats
def analyze_experiment_results(events_df: pd.DataFrame) -> dict:
"""
分析 A/B 测试结果
events_df 包含: user_id, variant, event_type, metadata
"""
# 提取会话结束事件
session_ends = events_df[events_df["event_type"] == "session_ended"].copy()
session_ends["resolved"] = session_ends["metadata"].apply(lambda x: x.get("resolved", False))
control = session_ends[session_ends["variant"] == "control"]["resolved"]
treatment = session_ends[session_ends["variant"] == "treatment"]["resolved"]
control_rate = control.mean()
treatment_rate = treatment.mean()
# 卡方检验
contingency = [
[control.sum(), len(control) - control.sum()],
[treatment.sum(), len(treatment) - treatment.sum()]
]
chi2, p_value, _, _ = stats.chi2_contingency(contingency)
return {
"control_resolve_rate": f"{control_rate:.2%}",
"treatment_resolve_rate": f"{treatment_rate:.2%}",
"relative_improvement": f"{(treatment_rate - control_rate) / control_rate:.2%}",
"p_value": p_value,
"statistically_significant": p_value < 0.05,
"control_n": len(control),
"treatment_n": len(treatment),
"recommendation": "上线 treatment" if (p_value < 0.05 and treatment_rate > control_rate) else "保持 control 或继续收集数据"
}一个重要提醒: p < 0.05 只告诉你改变是真实的,不告诉你改变是否足够大值得上线。relative_improvement 是 0.5% 还是 5%,对业务的意义完全不同,要结合实际情况判断。
实验结束后的判断
我的判断标准:
- 统计显著(p < 0.05)且相对提升 > 3%:上线 treatment
- 统计显著但提升 < 1%:可能不值得维护两套 prompt 的复杂度
- 不显著:样本量不够就继续跑,样本量够了就说明这个改动没效果
最后一个建议:把实验结果记录下来,包括"无效实验"。两年后你会感谢自己,因为你有数据证明"这个方向我们试过,没用",不需要再重复试。
