第4章 微调:把模型调教成你的专家

第 3 章我们给模型"外挂"了一个知识库(RAG),让它能回答你的私有资料;这一章换一种更彻底的做法 - 直接修改模型本身,也就是微调。你不需要从零训练一个模型,只需要把现成的开源模型"调教"成某个领域的专家:语气像你的客服、格式符合你的规范、术语用你的说法。借助 LoRA 这项技术,一块 8GB 显存的消费级显卡就能完成,成本低到超出多数人的想象。

4.1 什么时候需要微调

微调是本书三种定制手段里成本最高的,所以第一步不是写代码,而是想清楚"值不值得"。工程上有句口诀:先提示词,再 RAG,最后才微调 - 从最便宜的方案开始,逐级升级(图 4-1)。

阶梯的第一级是提示词工程(第 1 章)。风格、格式这类需求,往往几条指令加两三个示例(few-shot)就能压住,成本约等于零,改起来最快。第二级是 RAG(第 3 章):当问题依赖你的私有资料、且资料会频繁更新时,把资料放进向量库,让模型"查了再答" - 知识永远是最新的,还能给出引用来源。到了第三级,才轮到微调。

什么时候该微调?特征非常具体:① 风格要固化 - 客服话术、报告文风、公告措辞,要求"每句话都像我们公司的表达",提示词压不住;② 格式要死磕 - 必须严格输出特定 JSON 结构、特定字段名、特定话术模板,few-shot 偶尔还是会飘;③ 要离线私有部署 - 数据不能出域,只能本地跑开源模型,还要它懂你的术语;④ 要省成本降延迟 - 高频调用,把 prompt 里那段又长又贵的"行为规范"直接烧进权重,省 Token 也省延迟。

什么时候不该微调?同样清晰:知识会更新 - 权重里的知识是"死"的,改一次训一次,RAG 随改随查;数据太少 - 几百条以下,收益不如好好写 prompt;必须给出来源 - 微调后的模型照样会编造,RAG 至少能引用。

图 4-1 微调决策阶梯:提示词 → RAG → 微调,从最便宜的方案开始逐级升级

方案 改变什么 成本 什么时候选
提示词工程 不改模型,只改输入 约 0(Token 费) 风格、格式能压住时优先试
RAG 外挂知识库,检索后再生成 中(向量库 + 检索) 知识会更新、要引用来源、数据量大
微调 修改模型权重 高(GPU + 数据) 风格/格式固化、离线私有化、省 Token 延迟
重要

决策口诀
先提示词,再 RAG,最后才微调。一句话分工:知识交给 RAG,说话方式交给微调 - 微调解决"怎么说",不解决"知道什么"。

4.2 微调数据:格式、数量与质量

微调的本质,是用你的数据把模型"重新教一遍",数据就是一切。先看格式。主流开源模型微调都吃两种格式:对话式(messages,像 ChatGPT 的多轮对话,最常用)和指令式(instruction–response,单轮问答对),统一存成 JSONL,一行一条样本(代码块 4-1)。对话式里的 system 角色用来放"行为规范" - 这正是 4.1 里说"烧进权重"的那段话。

# 代码块 4-1 微调数据格式示例(JSONL,一行一条;实际文件中不要写 # 注释)
# 指令式:单轮问答对(instruction + response)
{"instruction": "公司报销单需要哪几个章?", "response": "一般需要经办人、部门负责人、财务三个章,具体以公司财务制度为准。"}

# 对话式:system 放行为规范,user/assistant 交替(最常用,推荐)
{"messages": [
  {"role": "system", "content": "你是公司行政小助手,回答要简洁、用敬语、不超过两句话。"},
  {"role": "user", "content": "打印机卡纸了怎么办?"},
  {"role": "assistant", "content": "您好,请先断电并打开前盖,小心取出卡住的纸张,再合盖开机即可。"}
]}

数量上没有玄学,只有经验:几百条起效 - 纯风格类任务(语气、格式)几百条就肉眼可见地像模像样;几千条更稳 - 行为类任务(按你的规范回答、多分支决策)需要这个量级;上万条收益递减,因为样本里能学到的信息量是有限的。但无论多少,质量永远大于数量:1000 条高质量样本,远胜 1 万条噪音。

质量就三件事:清洗 - 去掉错别字、口水话、空样本,统一全角半角与换行;去重 - 用文本哈希或 Embedding 相似度去重,重复样本会成倍放大过拟合;防泄漏 - 测试集与训练集严格隔离,更不能把"标准答案"直接写进训练样本(比如把检索到的原文当标准答案塞进 instruction),否则评估分数全是假的。最后,分布要贴近真实:用户怎么问,你就怎么训 - 训练样本全是书面提问、上线全是口语,效果必然打折。

提示

类比:数据是食材,模型是厨师
同样的厨师,用烂菜和用新鲜菜做出来的是两道菜。微调同理:模型再强,喂进去的样本脏、乱、重复,学出来的就是脏、乱、重复 - 垃圾进,垃圾出(garbage in, garbage out)

注记

新手第一大坑:格式不统一
同一份数据里,有的样本用 system 开头,有的直接 instruction,有的角色顺序写反 - 模型学到的"规范"本身就是混乱的,上线后输出自然混乱。所有样本必须用同一套模板(建议直接用 tokenizer 的 apply_chat_template,见 4.4 代码块 4-2)。

4.3 从全参微调到 LoRA

最直白的微调叫全参微调(Full Fine-tuning):把模型全部权重当作可训练参数,用你的数据更新一遍。效果上限最高,代价也最大:以 7B 模型为例,fp16 下光权重就要 14GB,再加上梯度与 Adam 优化器的两份动量状态,显存需求轻松破百 GB,需要多卡并行;而且每训一版都要保存一份几十 GB 的全量副本。对大多数业务场景,这是杀鸡用牛刀。

于是有了 PEFT(Parameter-Efficient Fine-Tuning,参数高效微调):原模型权重冻结不动,只在旁边加少量可训练参数。其中最常用的是 LoRA(Low-Rank Adaptation,低秩适配)。直觉是这样:微调对权重的修正 ΔW 其实"很薄" - 它不需要一个完整的 d×d 大矩阵,用两个小矩阵相乘就能近似。于是 LoRA 把 ΔW 分解为 A(d×r)与 B(r×d)的乘积,r 通常取 8~64。训练时前向变成 h = Wx + BAx:主路 W 冻结、梯度不流经它,只有 A、B 两块在动(图 4-2)。B 初始化为 0,所以微调开始时旁路输出为 0,模型行为与基座完全一致,训练极其稳定。推理时把 BA 合并回 W:h = (W + BA)x,矩阵乘法一次都不多,零额外延迟

图 4-2 LoRA 低秩旁路原理:W 冻结,只训练小矩阵 A、B,旁路输出与主路相加

方案(以 7B 模型为例) 可训练参数 训练显存(约) 说明
全参微调 约 70 亿(100%) 100GB+(fp16) 效果上限最高,需多卡,成本高
LoRA 约 1600 万(r=16,约 0.2%) 20~40GB(bf16) 主流选择,效果接近全参
QLoRA(4bit 量化) 同上 8~12GB 消费级显卡可训,本章实战方案
提示

类比:LoRA 像给原画加图层
全参微调是在原画上直接改,改坏了没有后悔药;LoRA 是在原画上叠一层随时可揭掉的透明图层(A、B) - 训练改坏了,揭掉图层,原画(基座模型)完好如初,甚至可以换一层再试。

重要

记住三个数
r 取 8~64;可训练参数不到原模型的 1%(r=16 时约 0.2%);推理零额外开销。

4.4 实战:用 LoRA 微调开源模型

现在完整跑一遍。整体流程六步(图 4-3):准备数据 → 加载基座 → 挂 LoRA → 训练 → 评估保存 → 部署推理。

图 4-3 LoRA 微调全流程:数据 → 加载 → 挂 LoRA → 训练 → 评估 → 部署

选基座:中文场景首选 Qwen(如 Qwen2.5-7B-Instruct),英文与多语场景选 Llama(如 Llama-3.1-8B-Instruct);显存小,先用 0.5B/1.5B 的小模型把流程跑通,再换大模型。依赖三件套:transformers + peft + datasets。关键是 QLoRA:把基座权重量化到 4bit 再训练,7B 模型的显存需求从上百 GB 压到 8~12GB,一块 RTX 3060/4060 就能训(代码块 4-2)。

# 代码块 4-2 train_lora.py:QLoRA 微调 7B 模型,约 8~12GB 显存
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, BitsAndBytesConfig, DataCollatorForSeq2Seq
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# 1) 4bit 量化:显存从 100GB+ 压到 10GB 的关键
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype="bfloat16")

# 2) 加载基座模型(Qwen2.5-7B,中文场景首选)
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct", quantization_config=bnb, device_map="auto")
model = prepare_model_for_kbit_training(model)  # 4bit 训练前准备
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct", trust_remote_code=True)

# 3) 挂 LoRA:冻结 W,只在注意力矩阵旁加 A×B 低秩旁路
lora = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, task_type="CAUSAL_LM")
model = get_peft_model(model, lora)
print(model.print_trainable_parameters())  # 应看到 trainable 约 0.2%

# 4) 加载 JSONL 数据,统一套用对话模板(对应 4.2 的 messages 格式)
data = load_dataset("json", data_files="train.jsonl", split="train")
data = data.map(lambda ex: {"text": tokenizer.apply_chat_template(ex["messages"], tokenize=False)})

# 5) 训练:batch 小 + 梯度累积,模拟大 batch
args = TrainingArguments(output_dir="lora-out", per_device_train_batch_size=1,
                         gradient_accumulation_steps=8, num_train_epochs=3,
                         learning_rate=2e-4, logging_steps=10, save_strategy="epoch",
                         bf16=True, report_to="none")
Trainer(model=model, args=args, train_dataset=data,
        data_collator=DataCollatorForSeq2Seq(tokenizer)).train()

# 6) 只保存 LoRA 适配器(几十 MB),基座模型不动,可随时复用
model.save_pretrained("lora-out/adapter")

训练脚本就四件事:4bit 加载基座(原权重只读)、挂 LoRA 适配器(冻结 W)、按对话模板格式化数据、用 Trainer 训练。训练结束只保存 adapter(几十 MB) - 基座模型全程不变,这是 LoRA 最值钱的地方。推理测试(代码块 4-3):加载基座 + PeftModel.from_pretrained 动态叠加 LoRA,无需合并;要部署成独立模型时,再用 merge_and_unload() 把 BA 合并进 W,得到完整模型。

# 代码块 4-3 推理测试:加载基座 + LoRA 适配器(无需合并,动态生效)
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct", device_map="auto")
model = PeftModel.from_pretrained(base, "lora-out/adapter")  # 叠加 LoRA
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

prompt = "客户说发票金额错了,怎么回复?"
inputs = tokenizer.apply_chat_template([{"role": "user", "content": prompt}],
                                       tokenize=True, return_tensors="pt")
out = model.generate(inputs, max_new_tokens=128, do_sample=False)
print(tokenizer.decode(out[0], skip_special_tokens=True))

# 部署成独立模型时:把 LoRA 合并进权重,得到完整模型(省去每次加载 adapter)
# merged = model.merge_and_unload()
# merged.save_pretrained("qwen-custom")
注记

云 GPU 提醒
用云 GPU 训练,记得设自动关机或训练完手动释放实例 - 按小时计费,忘记关实例是新手最贵的"学费"。本地显存不足时,先把 max_seq_len 截到 1024、batch 调成 1 + 梯度累积。

重要

脚本要点
4bit 量化省显存(QLoRA);r=16 起步、lora_alpha=32;保存的是 adapter 而不是全模型;print_trainable_parameters() 打印出来的可训练参数应该不到 1%。

4.5 评估微调效果与防遗忘

训练完别急着开心,先回答三个问题:训上了吗?变好了吗?别的能力还在吗?

① 看损失曲线(图 4-4:训练损失持续下降,说明模型在"学"。同时留一块验证集(与训练集严格不重叠),观察验证损失 - 先降后升的"微笑曲线"就是过拟合信号,应取验证损失最低的那个 checkpoint(训练脚本里 save_strategy="epoch" 会按轮保存,方便回滚)。

② 与基座对比测试集:固定 50~100 条测试问题,分别问基座模型和微调后模型(代码块 4-4),逐条对比:格式对不对、语气像不像、术语准不准。量大了可以用 LLM-as-a-Judge 批量打分(第 7 章详述),初期人工看 20 条就够。

图 4-4 训练/验证损失曲线:验证损失先降后升,就是过拟合信号,取最低点对应的 checkpoint

# 代码块 4-4 对比评估:同一批问题,基座 vs 微调后(初期人工快速过一遍输出)
questions = ["客户要退款但已过期限,怎么回复?", "报销单最多能报销多少?"]
for q in questions:
    msgs = [{"role": "user", "content": q}]
    inp = tokenizer.apply_chat_template(msgs, tokenize=True, return_tensors="pt")
    out_base = base_model.generate(inp, max_new_tokens=100, do_sample=False)
    out_ft = ft_model.generate(inp, max_new_tokens=100, do_sample=False)
    print("问:", q)
    print("基座:", tokenizer.decode(out_base[0], skip_special_tokens=True))
    print("微调:", tokenizer.decode(out_ft[0], skip_special_tokens=True))

③ 防灾难性遗忘:微调最隐蔽的副作用 - 模型学会了你的客服话术,却忘了写诗、翻译、通用问答,这叫灾难性遗忘。对策:训练集里混入 5%~20% 的通用数据(通用指令数据或历史对话),并在微调后跑一遍通用能力抽查(随便问几句常识题,看有没有明显退化)。LoRA 的另一个好处在这里体现:效果不好就揭图层回滚,重新训一版更稳的数据,成本很低。

注记

微调不解决知识缺失
模型训练时没见过的事实,微调后照样不知道 - 微调改变的是"说错话的方式",不是"不知道"本身。知识缺口请交给 RAG(第 3 章),微调和 RAG 是互补关系,不是替代关系

4.6 微调的坑与成本

最后盘点新手最常见的坑,按"翻车严重程度"排序:

  • ① 数据泄漏:训练集和测试集没隔离,或把检索答案、未来信息写进训练样本 - 评估分数虚高,上线立刻现原形。泄漏的评估等于没评估
  • ② 过拟合:数据少、重复多、轮数多,模型开始"背诵"训练样本,换个问法就答非所问。对策:验证损失卡 checkpoint、样本去重、epoch 2~3 轮打住。
  • ③ 幻觉没解决:很多人指望微调"教会模型新知识",但 LoRA 只改说话方式,事实错误照样会编。想注入知识,训练数据里得有知识,而且要足够多样,否则模型只是学会了"编得更像"。
  • ④ 格式漂移:训练时角色顺序、字段写法不统一,模型学到的"规范"本身就是乱的,务必统一套用同一个模板。
  • ⑤ target_modules 选错:LoRA 只挂注意力矩阵的 q/k/v/o 通常够用;发现"损失不降",先检查是不是漏挂了模块。
  • ⑥ 学习率过大:LoRA 一般取 1e-4~3e-4,调太大直接训崩。

显存不足,按顺序降级:QLoRA 4bit(本章方案)→ 梯度累积(代码里 gradient_accumulation_steps=8)→ 截短序列 → 换更小的基座(7B → 3B → 1.5B,先验证数据价值)→ 云 GPU 按小时租。

方案 硬件 / 资源 费用 适合场景
本地消费卡 RTX 3060 12G / 4060,QLoRA 7B 电费,单次几元 学习、小规模验证
云 GPU RTX 4090 / A100 按小时租 每小时几元~几十元 正式训练(记得关实例)
全参微调 A100 多卡 每小时几十~上百元 效果上限要求极高的场景
API 微调(托管) 平台托管(如 OpenAI fine-tuning) 按 Token 计费 不想管 GPU、数据可出境

成本上(表 4-3):微调 7B QLoRA、约 5000 条数据、3 个 epoch,在 RTX 4090 上大约 1~3 小时,云端花费几十元。相比之下,真正的大头是数据清洗花的时间,不是 GPU 费用 - 数据到位,训练只是一杯咖啡的工夫。

提示

类比:微调像健身,RAG 像吃饭
微调练的是"动作模式"(说话方式),不增加"肌肉量"(知识储备)。知识要靠"吃"(高质量数据 + RAG),姿势要靠"练"(微调) - 只练不吃,还是没力气。

重要

本章要点

  • 决策阶梯:先提示词 → 再 RAG → 最后才微调;微调改"说话方式",RAG 管"知识来源"。
  • 数据是全部:几百条起效、几千条更稳;清洗、去重、防泄漏;格式统一用对话式 messages。
  • LoRA 冻结原权重、只训 A×B 低秩旁路,可训练参数不到 1%,推理零开销。
  • QLoRA 4bit 让 7B 模型在 8~12GB 显存可训;产物 adapter 只有几十 MB。
  • 评估看三样:损失曲线、基座对比、通用能力抽查;验证损失回升就是过拟合。
  • 微调不解决幻觉与知识缺失;成本大头是数据清洗,不是 GPU。
警告

衔接 · 下一站

  • 决策回顾:微调与 RAG 不是二选一 - 知识常更新用 RAG,说话方式要固化用微调,两者可以叠加(微调后的模型 + 检索);第 6 章端到端项目会把这条链路拼起来。
  • 下一站:第 5 章 AI Agent - 微调后的本地模型可以当 Agent 的"大脑",私有化、低延迟;第 6 章实战里用微调模型替换 API,成本从每次调用费变成固定的 GPU 费。
  • 延伸:LLM-as-a-Judge 评估体系见第 7 章;Transformer 原理回顾《人工智能理论与实践》第 5 章