Chapter 10

实战:微调一个领域专家模型

完整端到端项目:从数据收集到 Ollama 本地部署,构建一个真正好用的领域专家模型。以"中国劳动法律助手"为例。

项目目标与规格

目标:微调 Qwen2.5-7B 为中国劳动法律咨询助手

第一步:数据收集与构建

import anthropic
import json

client = anthropic.Anthropic()

LABOR_LAW_TOPICS = [
    "劳动合同签订与解除", "试用期规定", "工资待遇",
    "加班费计算", "工伤认定", "社保缴纳",
    "竞业限制", "劳动仲裁程序", "女职工保护", "裁员补偿"
]

def generate_qa_pair(topic: str) -> list:
    """为指定主题生成 5 个问答对"""
    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=3000,
        messages=[{"role": "user", "content": f"""
针对主题"{topic}"生成5个真实劳动者可能问的问题及专业法律回答。
格式:JSON 数组,每条包含 instruction 和 output。
要求:
- 问题要真实具体(有场景细节)
- 回答要准确引用法条,给出实操建议
- 回答不少于 200 字"""}]
    )
    return json.loads(response.content[0].text)

# 生成训练数据
all_data = []
for topic in LABOR_LAW_TOPICS:
    pairs = generate_qa_pair(topic)
    all_data.extend(pairs)
    print(f"{topic}: {len(pairs)} 条")

# 加入系统提示
SYSTEM = "你是一位专业的中国劳动法律顾问,熟悉《劳动法》《劳动合同法》及相关司法解释。"
formatted = []
for item in all_data:
    formatted.append({
        "conversations": [
            {"from": "system", "value": SYSTEM},
            {"from": "human",  "value": item["instruction"]},
            {"from": "gpt",   "value": item["output"]}
        ]
    })

with open("labor_law_train.jsonl", "w") as f:
    for item in formatted:
        f.write(json.dumps(item, ensure_ascii=False) + "\n")

第二步:QLoRA 训练

from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth.chat_templates import get_chat_template
from unsloth import train_on_responses_only

# 加载 Qwen2.5-7B(Unsloth 预量化版)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen2.5-7B-Instruct-bnb-4bit",
    max_seq_length=2048,
    load_in_4bit=True
)

tokenizer = get_chat_template(tokenizer, chat_template="qwen-2.5")

model = FastLanguageModel.get_peft_model(
    model, r=16, lora_alpha=32,
    target_modules=["q_proj","k_proj","v_proj","o_proj",
                    "gate_proj","up_proj","down_proj"],
    use_gradient_checkpointing="unsloth", random_state=42
)

from datasets import load_dataset
dataset = load_dataset("json", data_files="labor_law_train.jsonl", split="train")

def apply_template(examples):
    texts = [tokenizer.apply_chat_template(
        c, tokenize=False, add_generation_prompt=False
    ) for c in examples["conversations"]]
    return {"text": texts}

dataset = dataset.map(apply_template, batched=True)

trainer = SFTTrainer(
    model=model, tokenizer=tokenizer,
    train_dataset=dataset, dataset_text_field="text",
    max_seq_length=2048,
    args=TrainingArguments(
        output_dir="./labor-law-model",
        num_train_epochs=3,
        per_device_train_batch_size=2,
        gradient_accumulation_steps=8,
        learning_rate=2e-4,
        lr_scheduler_type="cosine",
        bf16=True, logging_steps=20,
    )
)
trainer = train_on_responses_only(trainer,
    instruction_part="<|im_start|>user\n",
    response_part="<|im_start|>assistant\n"
)
trainer.train()

第三步:合并并转 GGUF

# 合并 LoRA 并保存
model.save_pretrained_merged("labor-law-merged", tokenizer, save_method="merged_16bit")

# 转换为 GGUF(Unsloth 内置支持)
model.save_pretrained_gguf("labor-law-q4", tokenizer, quantization_method="q4_k_m")
# 输出文件:labor-law-q4.gguf(约 4.5 GB)

第四步:Ollama 部署

# 创建 Modelfile
cat > Modelfile <<'EOF'
FROM ./labor-law-q4.gguf

SYSTEM """你是一位专业的中国劳动法律顾问,熟悉《劳动法》《劳动合同法》
及相关司法解释。回答问题时请准确引用法条,并给出实操建议。"""

PARAMETER temperature 0.3
PARAMETER top_p 0.9
EOF

# 创建 Ollama 模型
ollama create labor-law-expert -f Modelfile

# 测试
ollama run labor-law-expert "公司以试用期不合格为由解雇我,我有什么权利?"

效果验证

import requests

def ask_expert(question: str) -> str:
    response = requests.post(
        "http://localhost:11434/api/generate",
        json={"model": "labor-law-expert", "prompt": question, "stream": False}
    )
    return response.json()["response"]

# 测试用例
test_cases = [
    "被公司强制要求加班不给加班费怎么办?",
    "劳动合同到期公司不续签,我能拿到经济补偿金吗?",
    "工作满 10 年是否可以要求签无固定期限劳动合同?",
]

for q in test_cases:
    print(f"\nQ: {q}")
    print(f"A: {ask_expert(q)[:300]}...")
项目总结

完整流程:Claude 生成合成数据(3000条)→ Unsloth QLoRA 训练(Colab T4,约 2 小时)→ 合并 + GGUF 量化 → Ollama 本地部署。整个项目成本约 $5-15(API 调用费),即可获得一个专业领域助手。

训练监控与调参指南

Loss 曲线的正常形态
健康的训练 loss 曲线:前 10-20 步快速下降(模型适应数据格式),之后缓慢平稳下降,最终趋于平稳。警示信号:训练 loss 下降但 eval loss 上升(过拟合,减少 epoch 或增加数据多样性);loss 震荡剧烈(学习率太高,降低 learning_rate);loss 一开始就不下降(chat template 错误或数据格式不对,先检查 tokenizer 输出)。
train_on_responses_only 的必要性
默认 SFT 训练会计算所有 token(包括 user 问题)的 loss,但我们只希望模型学习"如何回答"而非"如何提问"。train_on_responses_only 通过 instruction/response token 掩码只计算 response 部分的 loss,避免模型学习提问风格。在 Qwen2.5 格式中:instruction_part = "<|im_start|>user\n",response_part = "<|im_start|>assistant\n"。
超参数调整建议
learning_rate:首选 2e-4(LoRA 适配器标准值),若 loss 不稳定降到 1e-4;num_train_epochs:3000 条数据建议 3-5 个 epoch,观察 eval_loss 确定最佳停止点;r(LoRA rank):从 16 开始,如果效果不够可以尝试 32 或 64(显存占用随之增加);batch_size + gradient_accumulation:等效 batch_size = per_device_batch × accumulation_steps,建议等效 batch 为 16-32。

端到端项目关键经验

合成数据的质量控制
Claude/GPT-4 生成的合成数据在格式和逻辑上通常较好,但存在两类主要问题:① 事实幻觉——法条引用错误或捏造不存在的条款;② 废话模板——大量的"当然!很高兴为您解答..."前置语。质量控制方法:人工抽检 20% 样本,验证关键法条引用的准确性;自动过滤前缀模板(正则匹配 "^(当然|好的|当然可以)");关键业务场景至少保留 30% 真实数据与合成数据混合。
Colab T4 微调的限制与应对
T4 仅有 16GB 显存,训练 7B 模型必须采用 QLoRA(4-bit 量化基座 + LoRA 适配器)。主要限制:batch_size 只能设 1-2(配合 gradient_accumulation_steps=8-16 等效更大 batch);序列长度超过 2048 容易 OOM;训练 3000 条数据约 2-3 小时,接近 Colab 免费会话限制。应对方案:开始训练前先验证 3-5 步不报错再完整运行;使用 resume_from_checkpoint 支持中断续训;关闭不必要的 wandb、evaluate 调用。
SYSTEM 提示词的训练时嵌入
通过 ShareGPT 格式的 "from": "system" 字段将系统提示词嵌入每条训练数据,模型会"记住"这个角色设定。这使得推理时可以使用更短的 system prompt(甚至省略),节省推理成本。但需注意:如果训练数据的 system prompt 与推理时不一致(如测试时完全省略),会导致模型行为偏移。建议保持训练和推理的 system prompt 一致。
评估模型质量的实用方法
领域专家模型的评估比通用模型更难——没有现成的评测集。实用方法:① 构建 50-100 条人工标注的"黄金测试集"(覆盖所有关键场景),每次调整参数后评测;② 使用 LLM-as-Judge(让 GPT-4 评分微调模型输出 vs 基座模型输出,比较偏好率);③ 实际用户灰度测试(10% 流量走微调模型,收集用户满意度评分)。目标:在核心任务上比基座模型提升 15% 以上才值得部署。
将项目迁移到其他领域
本章的劳动法律助手方案可直接迁移到其他领域(医疗咨询、金融分析、技术文档)。核心变更点:① 替换 LABOR_LAW_TOPICS 列表为目标领域主题;② 修改 system prompt 角色定义;③ 根据领域特点调整输出长度要求(技术文档可能更长);④ 领域数据验证标准不同(医疗要严格,代码可以自动运行验证)。平台本身(Unsloth + QLoRA + GGUF + Ollama)保持不变。
常见项目失败原因
  • 数据太少 + 质量差:合成数据未经人工审核,直接用于训练。解决:至少抽检 20%,保证准确率 > 90%。
  • 过拟合:训练 epoch 太多(5+ epoch 且数据量少)。解决:观察 eval_loss,一旦停止下降就停止训练。
  • Chat Template 不匹配:基座模型是 Qwen 却用了 Llama 的模板。解决:始终使用 get_chat_template(tokenizer, chat_template="qwen-2.5")。
  • GGUF 转换格式不匹配:Modelfile 中的 SYSTEM 格式与训练时不一致。解决:训练 system prompt 和部署 system prompt 保持完全一致。
课程完结:LLM 微调实战总结
  • 技术选型路径:从需求出发 → 确认微调必要性(优先考虑 Prompt / RAG)→ 选择基座模型(7B 起步)→ 选择方法(QLoRA for 消费级 GPU,LoRA for 生产)→ 准备高质量数据(质量 > 数量)→ 训练监控(防过拟合)→ 合并量化 → 部署验证。
  • 核心工具栈:数据处理 Datasets/Pandas → 训练框架 Unsloth + TRL(SFTTrainer)→ 量化部署 llama.cpp GGUF / AWQ + vLLM → 本地测试 Ollama。这套工具链覆盖从实验到生产的完整需求。
  • 最重要的教训:500 条高质量、多样化的训练数据,通常优于 5000 条低质量数据;训练时的监控(eval_loss、reward margin)比事后调参更重要;在部署前必须重新测量合并量化后的模型精度。
  • 下一步进阶:掌握 DPO 偏好对齐,解决模型"怎么说"的问题;学习持续预训练,让模型真正掌握新领域知识;探索多模态微调(视觉 + 语言);参与开源社区(如 Open-Instruct、Axolotl),了解生产级微调的最新实践。