通用大模型什么都能聊两句,但一碰到你的业务就露馅:让它按公司话术写客服回复,它写成"亲亲在吗";让它把工单归类成你那套三十几个内部业务线,它硬往通用类目上靠;让它输出固定 JSON schema,它十次里漏两次字段。这些问题,prompt 调到头也压不住,RAG 又只能补知识、改不了"行为风格"。这时候才轮到微调。但全参微调一个 7B 模型,光 Adam 优化器状态就要吃掉几十 G 显存,单卡根本扛不住。这篇 SOP 走一遍用 LoRA / QLoRA 做参数高效微调的全流程:先讲清楚什么时候才该微调、LoRA 怎么省显存,再按数据→基座→配置→训练→评估→部署六步给可复制代码,最后附踩坑和 FAQ。和本站《RAG 系统评估 SOP》《AI agent 评测与基准测试 SOP》不同,那两篇讲怎么"评",这篇讲怎么"训"——但评估那一节会复用同样的评测思路。
一、先决策:该微调,还是 RAG,还是 prompt
微调不是万能锤子,动手前先过一遍这张决策表。选错路径,后面所有功夫都白费。
| 路径 | 改的是什么 | 适合 | 不适合 |
|---|---|---|---|
| Prompt / Few-shot | 不动模型,靠上下文约束 | 通用任务、临时需求、样本<20 条 | 需要稳定输出格式、要蒸馏到小模型 |
| RAG | 检索外部知识,拼进上下文 | 知识频繁更新、要引用来源、文档量大 | 需要改变输出"风格/行为"、要离线跑 |
| 微调 | 改模型权重 | 固定输出风格/格式、领域术语、蒸馏降本 | 知识天天变(该用 RAG)、没评测集(无法验证) |
一句话判断:知识用 RAG 补,行为用微调改,临时需求用 prompt 顶。 最常见的误判是"知识变了就微调"——模型权重是静态快照,微调完知识又过时了,白训。另一个误判是"只有几十条样例就直接微调"——这个量级 prompt few-shot 更划算,微调要么过拟合、要么学不到东西。微调的门槛是:有几百到几千条高质量标注、行为需求稳定、有 held-out 集能验证"训完确实变好了"。
二、LoRA / QLoRA 原理:为什么不全参微调
全参微调更新每一层权重矩阵 W,对 7B 模型就是 70 亿参数都要算梯度、存优化器状态(Adam 还要额外 2 倍参数存动量和方差),24G 显存的单卡根本装不下。
LoRA(Low-Rank Adaptation) 的核心假设:微调时权重的变化量 ΔW 是低秩的,可以分解成两个小矩阵相乘 W = W₀ + B·A,其中 B 是 d×r、A 是 r×d,秩 r 远小于维度 d(比如 r=16,d=4096)。训练时冻住 W₀,只训 B 和 A。参数量从 d×d 降到 2×d×r,对 4096 维单层就是从约 1678 万降到约 13 万,不到 1%。显存压力骤降,训完只导出几十 MB 的适配器文件。
几个关键超参先记住,后面配置要用:
- r(rank):低秩矩阵的秩。r 越大表达能力越强、也越容易过拟合。经验起点 8-16,任务复杂、数据多可到 32-64。
- alpha:缩放系数,实际作用是 ΔW × (alpha/r)。经验值 alpha = 2×r(r=16 用 alpha=32),用来平衡学习率。
- target_modules:挂 LoRA 的层。挂全 attn 线性层(q/k/v/o_proj)是常见做法,MLP 层(gate/up/down_proj)可加可不加。
- dropout:适配器上的 dropout,防过拟合,0.05-0.1 常用。
QLoRA 在 LoRA 基础上把冻住的基座 W₀ 量化到 4-bit(nf4 量化 + 双重量化),前向反传时用 bfloat16 计算,梯度只回传到 LoRA 适配器。效果:7B 模型单张 16-24G 消费级显卡就能训,13B 勉强可上。这也是为什么"LoRA + QLoRA"几乎是个人开发者和小团队微调的事实标准。
三、准备数据:指令格式与清洗
数据决定上限,模型只决定逼近上限的速度。微调翻车八成是数据问题,不是配置问题。
指令微调的标准样本是三段式:instruction(指令)、input(可选输入)、output(期望输出):
import json
from datasets import Dataset
# 原始数据:instruction / input / output 三段式
raw = [
{"instruction": "把下面的客服工单归类为标准业务类型,只输出类目名。",
"input": "客户反馈:下单后 3 天还没发货,要求催办。",
"output": "物流催办"},
{"instruction": "把下面的客服工单归类为标准业务类型,只输出类目名。",
"input": "客户反馈:收到的商品有破损,要求换货。",
"output": "售后换货"},
# ... 经验值:风格/格式类任务 500-2000 条起步,
# 领域知识注入类需要更多;少于 200 条建议先回 prompt few-shot
]
def format_prompt(ex):
"""拼成模型认识的 chatml / alpaca 式 prompt(以基座官方模板为准)"""
text = f"### 指令:\n{ex['instruction']}\n"
if ex.get("input"):
text += f"### 输入:\n{ex['input']}\n"
text += f"### 输出:\n{ex['output']}"
return {"text": text}
ds = Dataset.from_list(raw).map(format_prompt)
ds = ds.train_test_split(test_size=0.1, seed=42) # 留 10% 做 held-out 评估
print(ds)清洗三件事别省:去重(完全重复的样本会让模型死记)、筛长度(超 max_length 的截断或拆)、查格式(output 要不要带标点、JSON 要不要合法)。最低样本量经验值见代码注释——风格/格式类任务 500 条能见效,注入领域行为通常要 2000 条以上,且必须留出模型没见过的 held-out 集,否则评估无意义。
四、选基座模型
基座选型三原则:开源可商用、中文能力强(中文场景)、显存扛得住。
| 基座(示例) | 显存(QLoRA 4-bit) | 说明 |
|---|---|---|
| Qwen2.5-7B-Instruct | ~8-10G | 中文强,社区微调样例多 |
| Llama-3.1-8B-Instruct | ~10G | 英文生态最成熟,中文需评估 |
| DeepSeek 系 | 视版本而定 | 以官方仓库为准 |
注意:上表显存为经验估算,实际取决于序列长度、batch、是否开梯度检查点。模型 ID 和许可协议以官方仓库为准,选型前务必确认许可允许你的商用场景。
五、配置 LoRA + QLoRA 量化加载
先装依赖,再加载 4-bit 量化基座、挂上 LoRA 适配器:
# pip install peft transformers bitsandbytes accelerate
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct" # 以官方仓库为准
# QLoRA:4-bit 量化加载基座
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
model = prepare_model_for_kbit_training(model) # 冻住量化层、开梯度检查点等
# LoRA 适配器配置
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 确认可训参数占比 <1%prepare_model_for_kbit_training 做的事:把量化层设为不可训、启用梯度检查点、把 LayerNorm 转 fp32 保数值稳定。target_modules 挂 q/k/v/o_proj 是 attn 全线性层的常见做法;想更强可加上 MLP 的 gate/up/down_proj,代价是可训参数翻倍。
六、训练
用 transformers.Trainer 跑最稳,配置一把梭:
def tokenize(ex):
tok = tokenizer(
ex["text"], truncation=True, max_length=2048, padding=False,
)
tok["labels"] = tok["input_ids"].copy()
return tok
train_ds = ds["train"].map(tokenize, remove_columns=ds["train"].column_names)
from transformers import TrainingArguments, Trainer, DataCollatorForSeq2Seq
training_args = TrainingArguments(
output_dir="./output/lora-run1",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 等效 batch=16
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=10,
save_strategy="epoch",
bf16=True,
optim="paged_adamw_8bit", # QLoRA 专用,省显存
gradient_checkpointing=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_ds,
data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True),
)
trainer.train()
model.save_pretrained("./output/lora-adapter") # 只存适配器(几十 MB)超参怎么调:epochs 经验 2-5,数据少用 4-5 轮(盯过拟合),数据多用 2-3 轮;学习率 1e-4 到 3e-4 是 LoRA 安全区,太高发散太低学不动;batch 受显存限制,靠 gradient_accumulation_steps 凑到等效 16-32。TRL 的 SFTTrainer 封装了 prompt 模板和 loss masking,比手搓 Trainer 省事,但版本间参数变动较大,以官方文档为准。上面代码里 labels = input_ids.copy() 会对整段 prompt(含指令)算 loss,更严谨的做法是只对 output 部分算 loss、把 prompt 部分置 -100,SFTTrainer 默认帮你处理这件事。
七、评估:训完到底变好没
没有评估的微调等于盲调。评估思路复用本站《RAG 系统评估 SOP》和《AI agent 评测与基准测试 SOP》的框架:必须用训练时没见过的 held-out 集(上一步 train_test_split 留的那 10%),分三层看。
# 拿 held-out 集跑推理,收集模型输出
import torch
model.eval()
results = []
for ex in ds["test"]:
prompt = ex["text"].rsplit("### 输出:\n", 1)[0] + "### 输出:\n"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
out = model.generate(**inputs, max_new_tokens=64, do_sample=False)
pred = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
results.append({"pred": pred, "gold": ex["text"].rsplit("### 输出:\n", 1)[1]})三类评估任选:
- 任务指标:格式校验(JSON 合法率)、分类准确率、字段命中率,能自动算的优先自动算。
- LLM-as-judge:拿一个更强的模型当裁判,逐条评"忠实度、格式正确性、是否跑题",prompt 模板同 RAG 评估那篇。
- 人工 spot-check:至少抽 30 条人工看,专挑 bad case。只看自动分会漏"流畅但方向错"的 case。
关键一句:必须和基座模型(没微调的)在同一个 held-out 集上对比。 微调后分数没涨甚至掉了,说明数据或配置有问题,别硬上。
八、合并与部署
适配器是外挂,部署通常要合并回完整权重再量化导出:
from peft import AutoPeftModelForCausalLM
adapter_path = "./output/lora-adapter"
merged = AutoPeftModelForCausalLM.from_pretrained(
adapter_path, device_map="auto", torch_dtype=torch.bfloat16,
)
merged = merged.merge_and_unload() # 把 LoRA 权重并回基座
merged.save_pretrained("./output/merged-model", safe_serialization=True)
tokenizer.save_pretrained("./output/merged-model")合并后的模型可走 vLLM、Ollama、llama.cpp 部署。Ollama 走 GGUF 量化格式,vLLM 直接加载 safetensors。部署成本主要看显存:7B 4-bit 量化后约 5-6G,单张消费级显卡或 12G 云实例即可推理。注意 QLoRA 训练用的是量化基座,但合并时要加载 fp16/bf16 原始权重再合并适配器,不要直接拿 4-bit 权重合并——否则精度损失叠加。
九、踩坑记录
坑一:数据质量 > 数量。 1000 条噪声样本不如 200 条精标。常见噪声:output 格式不统一(有的带句号有的不带)、instruction 和 input 信息重复、样本之间互相矛盾。先洗 100 条人工抽检全过,再扩量。
坑二:灾难性遗忘。 微调太狠,模型把通用能力丢了——能归类工单但不会正常聊天。防法:epochs 别超 5、学习率别超 3e-4、混入一部分通用指令数据保底。
坑三:过拟合。 loss 一直降但 held-out 分数不涨甚至掉。表现为模型把训练样本死记,换种问法就崩。防法:盯 held-out 指标早停、降 rank、加 dropout、扩数据。
坑四:显存溢出(OOM)。 三招依次试:降 batch 到 1 再靠 gradient_accumulation 凑、开 gradient_checkpointing、换 QLoRA(若还没用)。序列长度超 2048 也是显存大户,能截短就截短。
坑五:只看训练 loss 不看评估集。 训练 loss 走得漂亮不代表模型变好,过拟合时 loss 照降。held-out 指标才是唯一判据,没评估集别训。
坑六:target_modules 挂漏了。 只挂 q_proj 不挂 v_proj,适配器学不到该学的东西,效果差一截。经验做法是 attn 四件套(q/k/v/o_proj)全挂,不确定时挂比不挂强。
FAQ
Q1:LoRA 和全参微调到底差在哪?什么时候必须上全参? LoRA 只训低秩适配器(不到 1% 参数),省显存、训完只存几十 MB、可热插拔;全参微调改所有权重,效果上限略高但显存是 LoRA 的几倍到几十倍。绝大多数风格/格式/领域适配任务 LoRA 够用。只有当任务需要模型学"全新能力"且数据量充足(万级以上)、算力不限时,才值得考虑全参。个人和小团队默认走 LoRA/QLoRA。
Q2:多少数据够?几百条能训吗? 风格/格式类任务 500-2000 条能见效;注入领域行为通常要 2000 条以上。几百条能训出"格式对了"的效果,但泛化差。少于 200 条建议先回 prompt few-shot。数据质量比数量重要——200 条精标 > 2000 条噪声。
Q3:基座选哪个?7B 够不够? 中文场景 Qwen2.5-7B-Instruct 是稳妥起点,社区微调资料多、中文强。英文生态 Llama 系最成熟。7B 在 QLoRA 下单卡可训、单卡可部署,是性价比甜点。基座太小(3B 以下)微调后能力天花板低,太大(70B)个人训不动,除非有云算力。许可协议务必查官方仓库确认可商用。
Q4:怎么评估微调效果?必须搭评测框架吗? 最小可用:held-out 集跑推理,人工抽 30 条看 + 自动算任务指标(格式合法率、准确率)。进阶用 LLM-as-judge 批量打分,思路同本站《RAG 系统评估 SOP》。关键是必须和未微调基座同集对比,否则无法证明微调有用。不一定要 Ragas/DeepEval 这种框架,手搓脚本也能评。
Q5:部署成本多少?微调完怎么上线? 7B 模型 4-bit 量化后约 5-6G 显存,一张 12G 消费级显卡或月费几十美元的云实例即可推理。流程:合并 LoRA 适配器→(可选)量化导出 GGUF/AWQ→vLLM 或 Ollama 加载。vLLM 吞吐高适合线上并发,Ollama 适合本地单机。训练成本看时长:7B QLoRA 在单张 24G 卡上跑 3 epochs、2000 条样本,几小时内能完成。
参考来源