课程0基础Agent开发课 / 模型微调 / 微调效果评估与部署
— 19 min read

微调效果评估与部署

微调完了,怎么知道效果好不好?怎么部署到生产?

微调效果评估与部署

微调完了,怎么知道效果好不好?怎么部署到生产?


1. 为什么评估至关重要:不评估就上线是最危险的做法

先说一个真实的反面教材类型的场景:

你花了一周时间收集数据、准备环境、跑完训练,loss 曲线看起来不错,随手测了几个问题,感觉比基础模型好了很多,兴奋地上线了。

三天后,你收到用户反馈:某类问题的回答质量变差了,有时候格式乱掉,有时候答非所问。你去查,发现是微调后的模型在训练数据没有覆盖到的边界情况上表现很差——这是过拟合,模型对训练样本"死记硬背"了,遇到新问题就出错。

更糟糕的是,因为微调用的是领域专有数据,模型还"遗忘"了一部分原来的通用能力——这叫灾难性遗忘(Catastrophic Forgetting)。用户说"自从上次更新,这个 AI 感觉变笨了",他们说的是真的。

没有系统评估,你完全不知道:

  • 模型在哪些问题上还是会出错(覆盖盲区)
  • 微调后有没有遗忘原来的通用能力
  • 新模型比基础模型到底好多少(可能只是心理安慰)
  • 上线后会不会让某些用户的体验变差

评估的目标是:用数据说话,而不是靠感觉。

2. 为什么评估比训练更重要

很多人微调完模型,跑几个例子感觉不错就上线了。这是最危险的做法。

没有系统评估,你不知道:

  • 模型在哪些问题上还是会出错
  • 微调后有没有"遗忘"原来的能力
  • 新模型比基础模型到底好多少(可能只是心理安慰)
  • 上线后用户体验会不会更差

评估的目的是:用数据说话,而不是靠感觉


3. 第一步:建立评估集

微调前就要准备好评估集,而不是训练完了再想。

评估集的要求:

  • 数量:至少50条,覆盖目标任务的各种情况
  • 多样性:包含简单、中等、困难三个难度
  • 代表性:覆盖真实用户会问的问题类型
  • 黄金答案:每条都有人工标注的参考答案
python
# 评估集格式示例
eval_dataset = [
    {
        "id": "eval_001",
        "question": "我们公司的年假政策是什么?",
        "reference_answer": "根据公司政策,工作满1年享有5天年假,满3年10天,满5年15天。",
        "category": "HR政策",
        "difficulty": "easy"
    },
    {
        "id": "eval_002",
        "question": "请帮我写一封拒绝供应商报价的邮件,语气要委婉",
        "reference_answer": "感谢贵方提供的报价方案...",  # 参考答案
        "category": "邮件写作",
        "difficulty": "medium"
    },
    # ... 至少50条
]

4. 第二步:三层评估体系

微调效果评估框架图
微调效果三层评估体系 — 人工评估、LLM 自动评估与 A/B 测试

4.1. 层级1:人工评估(最可靠,最费时)

适用场景:第一次评估新模型,快速判断方向是否正确。

操作步骤:

  1. 准备20-30个测试问题(覆盖主要使用场景)
  2. 对比基础模型和微调后模型的输出
  3. 用1-5分打分,记录具体理由
python
# 批量生成对比输出,方便人工评估
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
import json

def generate_comparison(questions, base_model_name, finetuned_model_path):
    """生成基础模型和微调模型的对比输出"""
    results = []

    # 加载两个模型
    base_tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    base_model = AutoModelForCausalLM.from_pretrained(
        base_model_name, torch_dtype=torch.float16, device_map="auto"
    )

    ft_tokenizer = AutoTokenizer.from_pretrained(finetuned_model_path)
    ft_model = AutoModelForCausalLM.from_pretrained(
        finetuned_model_path, torch_dtype=torch.float16, device_map="auto"
    )

    for q in questions:
        # 基础模型输出
        base_input = base_tokenizer(q, return_tensors="pt").to(base_model.device)
        base_output = base_model.generate(**base_input, max_new_tokens=200, temperature=0.1)
        base_response = base_tokenizer.decode(base_output[0], skip_special_tokens=True)

        # 微调模型输出
        ft_input = ft_tokenizer(q, return_tensors="pt").to(ft_model.device)
        ft_output = ft_model.generate(**ft_input, max_new_tokens=200, temperature=0.1)
        ft_response = ft_tokenizer.decode(ft_output[0], skip_special_tokens=True)

        results.append({
            "question": q,
            "base_model": base_response,
            "finetuned_model": ft_response,
            "human_score_base": None,    # 人工填写
            "human_score_ft": None,      # 人工填写
            "notes": ""
        })

    # 保存为JSON,方便人工评分
    with open("comparison_results.json", "w", encoding="utf-8") as f:
        json.dump(results, f, ensure_ascii=False, indent=2)

    print(f"对比结果已保存到 comparison_results.json,请人工打分")
    return results

人工评分标准:

分数 含义
5分 完全正确,表述清晰,超出预期
4分 基本正确,有小瑕疵
3分 方向对但有明显不足
2分 部分正确,有较大错误
1分 完全错误或答非所问

判断标准:微调后平均分比基础模型高0.5分以上,才算有实质性提升。


4.2. 层级2:LLM自动评估(效率高,适合大批量)

用 GPT-4o 或 Claude 作为"评委",自动评估模型输出质量。

为什么 LLM 评估比 ROUGE 更适合中文?

ROUGE(Recall-Oriented Understudy for Gisting Evaluation)是为英文设计的指标,通过词重叠率衡量相似度。但它有两个问题:

  1. 中文需要分词:不分词直接用字符级别计算,准确率很低
  2. 语义不敏感:两个意思相同但用词不同的答案,ROUGE 分可能很低

LLM 评估能理解语义,更适合开放式问答的评估。

python
# pip install openai
from openai import OpenAI
import json
from typing import List, Dict

client = OpenAI()

def llm_evaluate_batch(eval_cases: List[Dict]) -> List[Dict]:
    """
    批量用 LLM 评估模型输出

    eval_cases 格式:
    [{"question": "...", "reference": "...", "response": "..."}, ...]
    """
    results = []

    for case in eval_cases:
        prompt = f"""你是一个专业的AI输出质量评估员。

请评估以下AI助手的回答质量。

【用户问题】
{case['question']}

【参考答案(标准答案)】
{case['reference']}

【AI回答(待评估)】
{case['response']}

请从以下维度评分(每项1-10分):
1. 准确性:回答是否与参考答案一致,有无事实错误
2. 完整性:是否覆盖了参考答案的关键信息
3. 表达质量:语言是否清晰、流畅、专业

以JSON格式返回,不要有其他文字:
{{"accuracy": 8, "completeness": 7, "quality": 9, "overall": 8, "reason": "简要说明评分理由"}}"""

        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"},
            temperature=0  # 评估要稳定,用temperature=0
        )

        scores = json.loads(resp.choices[0].message.content)
        results.append({
            **case,
            "scores": scores
        })

    return results


def summarize_evaluation(results: List[Dict]) -> Dict:
    """汇总评估结果,计算平均分"""
    if not results:
        return {}

    metrics = ["accuracy", "completeness", "quality", "overall"]
    summary = {}

    for metric in metrics:
        scores = [r["scores"][metric] for r in results if metric in r.get("scores", {})]
        if scores:
            summary[f"avg_{metric}"] = sum(scores) / len(scores)

    summary["total_cases"] = len(results)
    summary["pass_rate"] = sum(1 for r in results if r.get("scores", {}).get("overall", 0) >= 7) / len(results)

    return summary


# 使用示例
eval_cases = [
    {
        "question": "我们公司的年假政策是什么?",
        "reference": "工作满1年5天,满3年10天,满5年15天",
        "response": "根据公司规定,入职满一年后可享有5天带薪年假..."
    },
    # ... 更多测试案例
]

results = llm_evaluate_batch(eval_cases)
summary = summarize_evaluation(results)
print(f"平均综合分:{summary['avg_overall']:.1f}/10")
print(f"通过率(7分以上):{summary['pass_rate']:.1%}")

评分解读指南:

平均综合分 含义 建议
8.5分以上 优秀,超过预期 可以上线
7.0-8.5分 良好,满足需求 可以上线,持续迭代
5.5-7.0分 一般,有明显不足 需要补充训练数据再微调
5.5分以下 较差,不符合预期 检查数据质量和训练配置

4.3. 层级3:ROUGE 指标(适合摘要、翻译类任务)

对于摘要、翻译等有标准答案的任务,ROUGE 仍有参考价值,但中文需要先分词。

python
# pip install rouge-score jieba
import jieba
from rouge_score import rouge_scorer

def evaluate_rouge_chinese(predictions: list, references: list) -> dict:
    """
    中文 ROUGE 评估

    注意:必须先用 jieba 分词,否则结果不准确
    ROUGE 是为英文设计的,中文需要先分词才能正确计算词重叠率
    """
    scorer = rouge_scorer.RougeScorer(
        ['rouge1', 'rouge2', 'rougeL'],
        tokenizer=None  # 我们自己处理分词
    )

    all_scores = {"rouge1": [], "rouge2": [], "rougeL": []}

    for pred, ref in zip(predictions, references):
        # 用 jieba 分词,用空格连接(让 rouge_score 按空格切分)
        pred_tokenized = " ".join(jieba.cut(pred))
        ref_tokenized = " ".join(jieba.cut(ref))

        score = scorer.score(ref_tokenized, pred_tokenized)
        all_scores["rouge1"].append(score["rouge1"].fmeasure)
        all_scores["rouge2"].append(score["rouge2"].fmeasure)
        all_scores["rougeL"].append(score["rougeL"].fmeasure)

    return {
        "rouge1": sum(all_scores["rouge1"]) / len(all_scores["rouge1"]),
        "rouge2": sum(all_scores["rouge2"]) / len(all_scores["rouge2"]),
        "rougeL": sum(all_scores["rougeL"]) / len(all_scores["rougeL"]),
    }


# ROUGE 分数参考基准(摘要任务)
# ROUGE-L > 0.4:较好
# ROUGE-L > 0.5:很好
# ROUGE-L > 0.6:优秀
# 注意:不同任务的基准不同,这里是摘要任务的参考值

⚠️ ROUGE 的局限性:

  • 只衡量词重叠,不理解语义(同义词、换一种说法得分会低)
  • 对中文必须先分词,否则结果不可信
  • 不适合开放式问答(参考答案不唯一的场景)
  • 推荐:开放式问答用 LLM 评估,摘要/翻译可用 ROUGE 辅助

5. 第三步:常见问题诊断

训练完发现效果不好,怎么找原因?

现象 可能原因 解决方案
训练集好,测试集差(过拟合) 训练数据太少或重复太多 增加数据多样性、减少 epoch、增大 dropout
效果没有提升(欠拟合) 学习率太低、数据格式错误 检查数据格式、适当提高学习率
微调后失去原有能力(灾难遗忘,Catastrophic Forgetting,模型学了新任务后忘记了原有的通用能力) 训练数据太单一 混入10-20%通用对话数据
输出格式不稳定 格式示例不足 增加更多格式一致的训练样本
回答太短/太长 训练数据长度分布不均 检查并平衡训练数据的长度分布

6. 第四步:A/B 测试(上线后的验证)

内部评估通过后,上线前要做 A/B 测试,用真实用户数据验证效果。

A/B 测试方案:

code
用户请求
    ↓
流量分配(50% vs 50%)
    ├── A组:基础模型(对照组)
    └── B组:微调模型(实验组)
    ↓
收集用户反馈
    ├── 显式反馈:点赞/点踩
    └── 隐式反馈:对话轮数、任务完成率
    ↓
统计分析(至少运行1-2周,收集足够样本)
    ↓
决策:全量切换 / 继续迭代 / 回滚

关键指标:

  • 用户满意度:点赞率、点踩率
  • 任务完成率:用户是否完成了他们想做的事
  • 对话轮数:轮数少通常说明一次就解决了问题

统计显著性判断:
样本量不够时,差异可能只是随机波动。一般需要至少300-500个有效样本,才能判断A/B差异是否显著。

python
# 简单的A/B测试统计显著性检验
from scipy import stats

def ab_test_significance(a_successes, a_total, b_successes, b_total):
    """
    检验A/B测试结果是否统计显著

    a_successes: A组成功数(如点赞数)
    a_total: A组总样本数
    b_successes: B组成功数
    b_total: B组总样本数
    """
    a_rate = a_successes / a_total
    b_rate = b_successes / b_total

    # 使用卡方检验
    contingency_table = [
        [a_successes, a_total - a_successes],
        [b_successes, b_total - b_successes]
    ]
    chi2, p_value, dof, expected = stats.chi2_contingency(contingency_table)

    print(f"A组满意率: {a_rate:.1%}")
    print(f"B组满意率: {b_rate:.1%}")
    print(f"差异: {(b_rate - a_rate):.1%}")
    print(f"p值: {p_value:.4f}")

    if p_value < 0.05:
        if b_rate > a_rate:
            print("✅ 结论:B组(微调模型)显著优于A组,建议全量切换")
        else:
            print("❌ 结论:B组(微调模型)显著差于A组,建议回滚")
    else:
        print("⚠️ 结论:差异不显著(p > 0.05),需要更多样本或继续观察")

# 示例
ab_test_significance(
    a_successes=180, a_total=300,   # A组:300次,180次满意
    b_successes=225, b_total=300    # B组:300次,225次满意
)

7. 第五步:部署方案

评估通过后,选择合适的部署方式。

7.1. 方案1:本地部署(Ollama)

适合:个人使用、内网部署、对隐私要求高的场景

bash
# 第1步:合并 LoRA 权重(如果用了 LoRA 微调)
# 在 Python 中执行:
# from peft import AutoPeftModelForCausalLM
# model = AutoPeftModelForCausalLM.from_pretrained("./lora-output")
# merged = model.merge_and_unload()
# merged.save_pretrained("./merged-model")

# 第2步:转换为 GGUF 格式(llama.cpp 支持的格式)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp && make
python convert_hf_to_gguf.py ../merged-model --outfile ../model.gguf --outtype f16

# 第3步:量化(可选,减小文件体积)
./quantize ../model.gguf ../model-q4_k_m.gguf Q4_K_M

# 第4步:创建 Ollama 模型
# 先写 Modelfile
cat > Modelfile << 'EOF'
FROM ./model-q4_k_m.gguf

# 系统提示词(根据你的微调任务修改)
SYSTEM """你是一个专业的企业内部助手,熟悉公司的政策和流程。
请用简洁、专业的语言回答员工的问题。"""

# 推理参数
PARAMETER temperature 0.1
PARAMETER top_p 0.9
PARAMETER num_ctx 4096
EOF

ollama create my-assistant -f Modelfile
ollama run my-assistant "我们公司的年假政策是什么?"

7.2. 方案2:API 服务(vLLM)

适合:需要高并发、生产环境部署

vLLM:一个专为高并发 LLM 推理设计的开源推理引擎,通过 PagedAttention 内存管理和连续批处理技术,相比普通部署方式可提升 10-30 倍吞吐量,是目前工业界最主流的 LLM 生产服务方案。

bash
# 安装 vLLM
pip install vllm

# 启动 OpenAI 兼容的 API 服务
python -m vllm.entrypoints.openai.api_server \
    --model ./merged-model \
    --port 8000 \
    --max-model-len 4096 \
    --gpu-memory-utilization 0.9
python
# 调用方式和 OpenAI API 完全兼容
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed"  # vLLM 本地部署不需要真实 API key
)

response = client.chat.completions.create(
    model="./merged-model",
    messages=[
        {"role": "system", "content": "你是一个专业的企业内部助手"},
        {"role": "user", "content": "我们公司的年假政策是什么?"}
    ],
    temperature=0.1,
    max_tokens=500
)

print(response.choices[0].message.content)

7.3. 方案3:云端 API(最简单)

适合:快速验证、不想管基础设施

将微调后的模型上传到 Hugging Face,然后使用 Inference API 或部署到 Hugging Face Spaces。


8. 完整评估决策流程

code
训练完成
    ↓
【第1步】人工评估(20-30个case)
    ├── 平均分 < 3.5 → 检查训练数据,重新微调
    └── 平均分 ≥ 3.5 → 继续
    ↓
【第2步】LLM 自动评估(50+ case)
    ├── 综合分 < 6.0 → 补充训练数据,重新微调
    └── 综合分 ≥ 6.0 → 继续
    ↓
【第3步】灾难遗忘检测(通用能力测试)
    ├── 通用能力明显下降 → 混入通用数据,重新微调
    └── 通用能力正常 → 继续
    ↓
【第4步】小流量 A/B 测试(10%流量,1周)
    ├── 用户满意度下降 → 回滚
    └── 用户满意度持平或提升 → 继续
    ↓
【第5步】全量上线 + 持续监控

9. 小结

评估方法 优点 缺点 适用场景
人工评估 最准确,发现细微问题 费时,难以大规模 首次评估,判断方向
LLM 评估 效率高,可大批量 有一定误差 批量评估,快速迭代
ROUGE 客观,可复现 不理解语义,中文需分词 摘要、翻译类任务
A/B 测试 最真实的用户反馈 需要上线才能做 最终上线前验证

微调的完整流程:

code
数据准备 → 微调训练 → 人工评估 → LLM自动评估 → A/B测试 → 全量上线 → 持续迭代

后续应用:评估通过后,可以参考:

本页目录