为什么需要科学的模型评估

很多团队在微调完模型后,只做了几道手工测试题,感觉"效果好多了",就兴冲冲地部署上线。结果上线后发现:在某些场景确实变好了,但在另一些场景反而变差了;用户的整体满意度并没有提升;甚至因为模型行为的变化,导致一些老用户投诉。这就是缺乏科学评估体系导致的典型问题。

科学的模型评估体系包含两个层面:离线评估(在部署前用标准化测试集评估,回答"模型在受控环境下表现如何")和在线评估(部署后用A/B测试评估真实用户反馈,回答"模型在真实场景中表现如何")。两个层面缺一不可——离线评估好不代表线上好(存在分布偏移),线上表现好但不知道具体好在哪也不利于持续优化。

离线评估指标设计

离线评估的核心是构建高质量的测试集。测试集应该:与训练集完全独立(不能有任何重叠)、覆盖所有重要的业务场景、每个场景有足够的样本量(至少50-100条)、标注质量高(每条数据有标准答案或参考标准)。评估指标根据任务类型选择:分类任务(意图识别)→ Accuracy、F1、混淆矩阵;生成任务(对话、写作)→ BLEU/ROUGE(参考价值有限)→ 更推荐LLM-as-Judge评分;检索任务→ Recall@k、MRR、NDCG。

import json, random, numpy as np
from scipy import stats
from openai import OpenAI

client = OpenAI(api_key="your-deepseek-api-key", base_url="https://api.deepseek.com")

class ModelEvaluator:
    def __init__(self):
        self.test_set = []
        self.results = {"baseline":[], "candidate":[]}

    def load_test_set(self, path):
        """加载测试集"""
        with open(path, 'r', encoding='utf-8') as f:
            self.test_set = json.load(f)
        print(f"加载 {len(self.test_set)} 条测试数据")

    def llm_judge(self, question, answer_a, answer_b, criteria):
        """LLM-as-Judge:让DeepSeek评判两个回答的优劣"""
        prompt = f"""你是一个公正的评审专家。请比较以下两个回答。

问题:{question}

回答A:{answer_a}

回答B:{answer_b}

评审标准:{criteria}

请以JSON格式输出:
{{
  "winner": "A" | "B" | "tie",
  "score_a": 1-10,
  "score_b": 1-10,
  "reasoning": "简短说明"
}}"""
        response = client.chat.completions.create(
            model="deepseek-chat",
            messages=[{"role":"user","content":prompt}],
            temperature=0.1
        )
        return json.loads(response.choices[0].message.content)

    def evaluate_pairwise(self, baseline_model, candidate_model, num_samples=100):
        """成对评估:基座 vs 微调"""
        samples = random.sample(self.test_set, min(num_samples, len(self.test_set)))
        wins_a = wins_b = ties = 0
        for i, item in enumerate(samples):
            answer_a = baseline_model(item["instruction"], item.get("input",""))
            answer_b = candidate_model(item["instruction"], item.get("input",""))
            judgment = self.llm_judge(
                item["instruction"], answer_a, answer_b,
                item.get("criteria", "准确性、完整性、流畅性")
            )
            if judgment["winner"] == "A": wins_a += 1
            elif judgment["winner"] == "B": wins_b += 1
            else: ties += 1
            if (i+1) % 20 == 0:
                print(f"进度: {i+1}/{len(samples)}, A胜:{wins_a} B胜:{wins_b} 平:{ties}")
        return {"A_wins":wins_a, "B_wins":wins_b, "ties":ties, "win_rate_B":wins_b/(wins_a+wins_b+ties)}

class ABTestDesigner:
    def __init__(self):
        self.control_group = []
        self.treatment_group = []

    def calculate_sample_size(self, baseline_rate, expected_lift, alpha=0.05, power=0.8):
        """计算所需样本量"""
        from scipy.stats import norm
        z_alpha = norm.ppf(1 - alpha/2)
        z_beta = norm.ppf(power)
        p1 = baseline_rate
        p2 = baseline_rate * (1 + expected_lift)
        p_pooled = (p1 + p2) / 2
        n = (z_alpha * np.sqrt(2*p_pooled*(1-p_pooled)) + z_beta * np.sqrt(p1*(1-p1)+p2*(1-p2)))**2 / (p2-p1)**2
        return int(np.ceil(n))

    def check_significance(self, control_data, treatment_data, metric_name="satisfaction"):
        """检查统计显著性"""
        control = np.array(control_data)
        treatment = np.array(treatment_data)
        t_stat, p_value = stats.ttest_ind(treatment, control)
        effect = (treatment.mean() - control.mean()) / control.mean()
        ci = stats.t.interval(0.95, len(treatment)-1, loc=treatment.mean(), scale=stats.sem(treatment))
        return {
            "metric": metric_name,
            "control_mean": control.mean(),
            "treatment_mean": treatment.mean(),
            "relative_lift": f"{effect:.2%}",
            "p_value": p_value,
            "significant": p_value < 0.05,
            "ci_95": (ci[0], ci[1])
        }

    def run_ab_test(self, model_a, model_b, traffic_split=0.5, duration_days=7):
        """运行A/B测试"""
        print(f"A/B测试设计:")
        print(f"  对照组(A):{model_a}")
        print(f"  实验组(B):{model_b}")
        print(f"  流量分配:{traffic_split*100:.0f}%/{100-traffic_split*100:.0f}%")
        print(f"  建议运行时间:{duration_days} 天")
        n = self.calculate_sample_size(0.7, 0.05)
        print(f"  每组最少样本量:{n}")
        return {"status":"running", "expected_completion":f"{duration_days}天后"}

evaluator = ModelEvaluator()
evaluator.load_test_set("test_set.json")
# result = evaluator.evaluate_pairwise(baseline, finetuned, num_samples=100)

ab = ABTestDesigner()
n = ab.calculate_sample_size(baseline_rate=0.72, expected_lift=0.05)
print(f"需要每组 {n} 个样本才能检测到 5% 的提升")
# significance = ab.check_significance(control_scores, treatment_scores)

A/B测试的最佳实践

随机分流:使用用户ID的哈希值进行流量分配(如hash(user_id) % 100 < 50 → A组),确保两组用户特征分布一致。样本量计算:在开始实验前先计算所需样本量。如果预期提升只有3%-5%,通常需要每组5000-10000个样本才能得到统计显著的结论。样本量不足就贸然下结论是A/B测试最常见的错误。实验时长:至少运行一个完整的业务周期(通常1-2周),覆盖工作日和周末。太短的实验可能被"新奇效应"(用户对新模型感到好奇)或"星期效应"(周末用户行为与工作日不同)干扰。避免窥探:不要在实验中途频繁查看结果并提前终止。这会导致"窥探偏差"(peeking bias),使假阳性率从5%飙升到20%以上。设置实验时长后,到期再看结果。多指标综合判断:不要只看一个指标。如果你的微调模型在满意度上提升了3%但延迟增加了50%,你需要权衡这个trade-off。建议设置一个"护栏指标"(guardrail metrics)——如果核心业务指标(如转化率、留存率)出现显著下降,无论满意度如何都要暂停实验。

从评估到迭代

评估的最终目的不是给模型打分,而是找到下一步的优化方向。通过分析评估结果:哪些场景提升明显?(继续强化)哪些场景反而变差?(需要调整训练数据或策略)哪些场景没有变化?(数据可能不足或方法不对)将评估洞察转化为具体的优化任务,形成"微调→评估→分析→再微调"的持续改进循环。

A/B测试中的常见陷阱

A/B测试看似简单,实则充满陷阱。除了前面提到的窥探偏差和新奇效应,还有:样本污染问题——如果同一个用户可能在实验期间同时接触到A组和B组(比如在不同设备上),实验结论就会受污染。解决方法是使用稳定的用户标识(如账号ID而非设备ID)进行分流。时间效应干扰——如果在A/B测试期间发生了外部事件(如竞争对手推出新产品、行业政策变化),实验组和对照组可能受到同等影响,但难以从实验指标中排除。解决方法是使用A/A测试作为对照组。多重比较问题——如果你同时观察10个指标,即使没有实际效果,也会有一个指标"碰巧"达到统计显著。解决方法是对p值进行Bonferroni校正,或预先注册(preregister)你关心的主要指标。长期效果评估:大多数A/B测试只持续1-2周,只能观察到短期效果。但许多AI改进的效果需要更长时间才能显现——比如个性化能力的提升需要模型与用户多轮交互才能体现。建议对一些重要的模型变更设置"holdout组"——一小部分用户长期使用旧版本模型,用于评估长期效果。同时,建立"反向指标"监控——微调模型可能在短期内提升了某指标,但长期来看导致了用户倦怠或使用频率下降。AI产品的成功最终取决于用户留存和长期价值,而非短期的指标优化。

实验文化与管理

建立健康的实验文化比掌握实验技术更重要。核心原则:实验结论由数据驱动而非意见驱动("我觉得这个模型更好"不如"数据显示满意度提升3.2%,p=0.02");失败的实验也是成功(知道什么方法不work和有work同样有价值);鼓励提出大胆的假设并用严谨的实验验证;建立实验知识库——记录每次实验的假设、设计、结果和收获,避免重复踩坑。每月进行一次实验回顾(Experiment Review),分享上个月的实验成果和教训,逐步积累团队的实验智慧。

想亲手编排这个技能链?

在技能链中打开 →