什么时候轮到微调
微调是三件套里的重武器。这一节先建立排序:能用便宜手段解决的,永远不要动权重。
一个 LLM 应用调节模型行为有三层手段,成本与收益都在递增:prompt 工程(含 few-shot 示例)改即时指令,改一行立刻生效;RAG(第四章)注入私有知识,知识可随时增删;微调把行为模式写进权重,一劳永逸但每次变更都要重新训练。三者的分工可以用一张分层图说清:
落到具体症状,判断表如下:
| 症状 | 正确手段 | 原因 |
|---|---|---|
| 模型不知道公司内部数据 | RAG | 权重存不住易变知识,还会过时、还会幻觉 |
| 输出格式偶尔不合规范 | prompt / 结构化输出 | 第一章的结构化输出先试,顽固才升级 |
| 领域术语、语气常年需要长 few-shot | SFT 微调 | 把几页示例压进权重,省 token 也省延迟 |
| 小任务想用小模型替代大模型 | 蒸馏 + SFT | 第九节路线,成本与延迟的主动降级 |
| 推理能力不够用 | 换更强模型 | 微调不增加能力,甚至可能削弱(第二节) |
什么时候坚决不动权重?三个反信号:数据攒不到几百条高质量样本(微调是数据游戏);问题出在检索或工具(修系统,不是修模型);期望"微调学会这些知识"(知识进知识库,行为进权重)。
把文档灌进训练集指望模型"背下来",得到的是不可靠的记忆:无法精确引用、无法更新、张口就来时无法察觉。知识类需求永远是 RAG 的领地(第四章),微调负责的是"怎么说话",不是"知道什么"。
微调能改变什么,不能改变什么
一句话原则:微调改变行为,不改变能力边界。分不清这两者,是微调项目失败的第一大原因。
| 维度 | 微调能做到吗 | 说明 |
|---|---|---|
| 输出格式与结构习惯 | 能,收益最直接 | 几页 few-shot 描述的格式,压成默认行为 |
| 风格、语气、称谓、拒答边界 | 能 | DPO(第八节)尤其擅长细粒度偏好 |
| 领域词汇与惯用表达 | 能 | 医疗、法律、方言类文本的适配主场景 |
| 任务套路(先分析再作答) | 能 | 固定流程从 prompt 变成肌肉记忆 |
| 学会私有事实 | 不可靠 | 会幻觉、会过时,走 RAG |
| 提升推理与数学能力 | 基本不能 | 能力上限由预训练决定,微调只重新分布 |
| 学会调用新工具 | 不该用微调做 | 工具定义进 prompt(第一章),模型泛化调用 |
| 通用能力保持不变 | 要小心 | 灾难性遗忘随时可能发生 |
灾难性遗忘(catastrophic forgetting)是这张表里最隐蔽的一行:在窄领域数据上训练过久,模型把"领域外的自己"覆盖掉了。症状是对话变笨、常识变差、别的任务退化,而你的领域指标还在涨。四个对策从数据到训练都有:训练集混入一至三成通用指令样本;epoch 控制在一到三轮;学习率用 LoRA 推荐量级(第六节);发布前用通用 benchmark 加一道回归(第十节)。
动手前先回答:微调成功的标准是什么?用第七章的黄金集给当前模型打一个基线分,写下“格式合规率从 85% 到 98%、其他指标不下降超过 2 个点”这样的可证伪目标。没有基线的微调,训练完只能得到“感觉变好了”。
训练方式全景图
“微调”是个泛称。把全景摆开,你才能说清自己到底要做哪一种,以及它需要什么资源。
| 方式 | 训练什么 | 数据形态 | 算力量级 | 典型场景 |
|---|---|---|---|---|
| 全参微调 | 全部权重 | 万级以上样本 | 多卡 A100/H100 | 基础模型厂商、深度领域改造 |
| LoRA | 低秩旁路矩阵 | 千级样本 | 单张消费级显卡 | 绝大多数应用团队(本章主线) |
| QLoRA | LoRA + 4bit 量化基座 | 千级样本 | 更低的单卡门槛 | 预算受限、大基座 |
| 继续预训练 CPT | 基座权重(全参或 LoRA) | 亿级 token 纯文本 | 集群 | 领域语言模型(医疗/法律全文语料) |
| SFT 指令微调 | LoRA 为主 | 指令-回答对 | 单卡 | 格式、风格、任务套路(第六节) |
| DPO 偏好对齐 | LoRA 为主 | chosen/rejected 偏好对 | 单卡 | 语气、拒答边界等细粒度偏好(第八节) |
| 蒸馏 | LoRA 为主 | 大模型生成的样本 | 单卡 | 小模型替代大模型降本(第九节) |
本章主线是 LoRA + SFT:它是投入产出比最高的组合,也是绝大多数“应用团队微调”的真实含义。DPO 与蒸馏是在 SFT 底座上的两个延伸方向,全参与继续预训练则超出应用工程范畴,这里只建立概念坐标。
SFT 与 LoRA 不构成二选一:SFT 描述“训练目标”(拿指令-回答对学行为),LoRA 描述“训练哪些参数”(旁路低秩矩阵)。你完全可以全参 SFT,也可以 LoRA SFT;应用团队几乎总是后者。
LoRA:低秩适配原理
LoRA 的全部聪明之处在于一个观察:微调时的权重变化量是低秩的。既然如此,就不必存整份变化,只存它的“压缩版”。
Transformer 的每个注意力投影都是一个大矩阵 W(维度 d×d,7B 模型约 4096×4096)。全参微调要更新它的每一个元素。LoRA 把更新量分解成两个小矩阵的乘积:
写成公式就是 h = Wx + (α/r)·BAx,其中 A 是 r×d、B 是 d×r,秩 r 远小于 d。三个超参决定了这条旁路的性格:
| 超参 | 含义 | 常用取值 | 调整逻辑 |
|---|---|---|---|
| r | 旁路容量,能学多复杂的变化 | 8 / 16 / 64 | 格式风格类 16 够用;复杂任务套路再升 64 |
| lora_alpha | 输出缩放系数,有效强度为 α/r | 2×r 起步 | 欠拟合升 α,过拟合降 α 或 r |
| target_modules | 给哪些投影挂旁路 | q/k/v/o_proj,加 MLP 更佳 | 只挂注意力省显存,加 gate/up/down 效果更稳 |
| lora_dropout | 旁路随机失活 | 0.05 | 小数据集防过拟合的便宜手段 |
QLoRA 把冻结的基座量化到 4bit 再训 LoRA,让单张 24GB 显卡也能调 7B 级模型。代价是训练略慢、精度有一点点损失。起步阶段推荐直接 QLoRA:先低成本验证数据有效,有效再上更高配置重训。
数据集工程
微调项目的八成工作量在数据,八成败因也在数据。这一节把来源、格式、规模三件事一次定标准。
数据从哪来?三条路线,质量从高到低,成本从高到低:
| 来源 | 具体做法 | 量级 | 风险 |
|---|---|---|---|
| 线上回流 | 第七章的用户改写、高赞回答、低分 trace 重写 | 持续积累 | 需要先有可观测性与反馈入口 |
| 人工标注 | 资深同事按规范逐条写作 | 数百到数千 | 贵、慢、风格不一致 |
| 大模型合成 | 用最强模型按模板批量生成再人工抽检 | 任意 | 同质化,继承 teacher 的错 |
顺序上先在线回流,不足再人工补齐,合成只作曲线增稠。第七章的价值在这里兑现:没有 trace 和反馈闭环,你连“用户真实会问什么”都不知道,合成数据只能凭空想象分布。
JSONL:训练集的标准形态
SFT 数据的主流格式是每行一个 messages 数组的 JSONL,角色结构与第一章的消息列表完全一致,这让“从线上 trace 导出训练集”几乎零转换成本:
{"messages": [
{"role": "system", "content": "你是云数据库技术支持,回答先给结论再给步骤,不确定就明说。"},
{"role": "user", "content": "只读实例延迟突然变高,怎么排查?"},
{"role": "assistant", "content": "先看主从复制延迟:控制台实例页的复制延迟曲线若超过 2 秒,进入第一步;若正常,转向负载分析……"}
]}
{"messages": [
{"role": "system", "content": "你是云数据库技术支持,回答先给结论再给步骤,不确定就明说。"},
{"role": "user", "content": "备份期间业务卡顿正常吗?"},
{"role": "assistant", "content": "不正常。备份走快照机制,不应阻塞写入。若观察到卡顿,按以下顺序排查……"}
]}
写样本时三条纪律:system prompt 与线上一致(微调学的是“在这个 system 下的行为”,不一致则迁移打折);答案风格统一(同一批数据里先结论后步骤与先铺垫后结论混着写,模型学到的是平均噪声);每条只教一件事,多轮长对话拆成多个单意图样本。
chat template:交给 tokenizer,不要手拼
不同模型对对话的序列化方式不同(ChatML 的 <|im_start|>、Llama 3 的 <|start_header_id|>)。手拼这些标记是最经典的错误源:训练时与推理时的模板差一个 token,线上效果立刻打折。SFTTrainer 会自动调用基座 tokenizer 自带的 chat template,所以只要数据是 messages 结构,这层就无需关心。自己写训练循环时,用 tokenizer.apply_chat_template(messages, tokenize=False) 预览确认。
开工第一件事是把数据按 95/5 切成 train 与 test 并固定随机种子。留出集只用于选 checkpoint 与验收(第六、七节),一旦泄漏进训练集,所有 eval 指标都是自欺欺人。
格式与简单风格:500 到 2000 条通常可见明显效果;复杂风格与任务套路:2000 到 10000;DPO 偏好对:500 对就能动语气。经验法则始终是质量大于数量,一千条干净一致的样本胜过一万条参差的。
SFT 实战:跑通第一版
Hugging Face 的 TRL 把 LoRA + SFT 压成十几行配置。代码很短,但每个参数都值得知道自己在调什么。
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
# 第五节沉淀好的 JSONL,已提前切成 train / test 两个文件
dataset = load_dataset("json", data_files={
"train": "support_train.jsonl",
"test": "support_test.jsonl",
})
peft_config = LoraConfig(
r=16,
lora_alpha=32, # 有效缩放 = alpha / r = 2
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
task_type="CAUSAL_LM",
)
args = SFTConfig(
output_dir="sft-support-v1",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 有效 batch = 2 x 8 = 16
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
bf16=True,
eval_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=50,
save_total_limit=3,
logging_steps=10,
max_seq_length=2048,
load_best_model_at_end=True, # 训练结束回滚到 eval 最优的 checkpoint
metric_for_best_model="eval_loss",
)
trainer = SFTTrainer(
model="Qwen/Qwen2.5-7B-Instruct", # 基座:从 instruct 模型继续训,不从 base 起步
args=args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
peft_config=peft_config,
)
trainer.train()
trainer.save_model("sft-support-v1/final") # 保存的是 adapter,几十 MB
超参不是玄学,每一项都有明确的调整方向:
| 超参 | 常用值 | 症状与调整 |
|---|---|---|
| learning_rate | 1e-4 ~ 3e-4(LoRA) | loss 震荡不降则减半;下降平缓则翻倍试一次 |
| epochs | 1 ~ 3 | train 降 eval 升就是过拟合,减 epoch 或早停 |
| 有效 batch | 16 ~ 64 | 小 batch 噪声大,大 batch 学得慢;用梯度累积拼 |
| warmup_ratio | 0.03 | 前期 loss 尖峰说明起步太猛,加大 warmup |
| max_seq_length | 覆盖 P95 样本长 | 过长样本被截断会学到残缺行为 |
应用团队的微调几乎总是继续调一个已对齐的 Instruct 模型,而不是从 Base 模型训指令能力。Base 需要海量数据重新教会对话;Instruct 已经会说话,你只是教它“在我们场景里怎么说”。这条默认能省掉一个数量级的工作量。
save_model 保存的是 LoRA adapter(两个小矩阵加上配置),不是完整模型。使用时基座与 adapter 分开加载,或第十节合并成单文件。一份基座可以挂多套 adapter,按业务切换,这正是 LoRA 的部署优势。
训练监控与调参
训练不是按下回车等奇迹。两条 loss 曲线会说话,前提是你看得懂它们的四种典型形态。
要看的东西只有两条:train loss 与 eval loss(在第五节的留出集上算)。把训练日志接进 TensorBoard 或 WandB(report_to="wandb")画到同一张图上,四种形态对应四种处置:
| 曲线形态 | 诊断 | 处置 |
|---|---|---|
| 两条都高且平 | 欠拟合:容量不够或学习率太小 | 升 r、升学习率、多训一轮 |
| train 降,eval 先降后升 | 过拟合:开始背训练集 | 回滚到 eval 最低的 checkpoint,减 epoch |
| 两条都突然飙升 | 坏样本或学习率过大 | 检查最新数据批次;学习率减半重跑 |
| eval 异常地低 | 留出集泄漏或与训练集同源 | 立即停止,重新切分数据 |
选 checkpoint 的铁律是只认 eval loss,不认最后一步:第六节里 load_best_model_at_end=True 就是干这件事的。手动选择时看 eval_loss 的最低点,而不是训练结束点,两者经常差好几个点。
第一次训练用默认超参跑完全程,得到一个能用的 checkpoint 与一整套曲线。它是后续所有调参的对照组:改一个参数重跑,曲线对比立刻告诉你方向对不对。没有第一版基线的调参,全靠占卜。
偏好对齐:从 RLHF 到 DPO
SFT 教会“该说什么”,对齐技术负责“哪种说法更好”。DPO 用一个聪明的重构,把强化学习问题变成了普通监督学习。
经典 RLHF 分三阶段:SFT 之后训练一个奖励模型(RM),再用 PPO 按奖励信号微调策略,同时用 KL 惩罚拉住它不许离太远。效果硬,但工程上出了名地难训:策略、参考、奖励、价值四个模型同时在显存里,超参敏感,奖励容易被 hack。
DPO(Direct Preference Optimization)推导出一个等价目标:直接拿“同一个 prompt 的两个回答,一个好一个坏”这样的偏好对做监督训练,不需要显式的 RM 与 PPO。训练目标直观上就是:拉大 chosen 与 rejected 的概率差,同时不许离参考模型太远。TRL 里的用法:
// 偏好数据:每行一个 prompt、一个好回答、一个坏回答
{"prompt": [{"role": "user", "content": "你们的价格为什么这么贵?"}],
"chosen": [{"role": "assistant", "content": "您说的对,单价确实高于入门套餐。性价比要看两点:一是自动扩缩容节省的运维人力,二是 SLA 赔付。给您算一笔账……"}],
"rejected": [{"role": "assistant", "content": "不贵啊,我们的价格很便宜的,你可以去看看别家。"}]}
from trl import DPOConfig, DPOTrainer
args = DPOConfig(
output_dir="dpo-support-v1",
beta=0.1, # 偏离参考模型的惩罚强度,越小越放得开
learning_rate=5e-6, # 远小于 SFT,DPO 是精修不是改造
num_train_epochs=1,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
bf16=True,
logging_steps=10,
)
trainer = DPOTrainer(
model="sft-support-v1/final", # SFT 产物作底座
ref_model=None, # 不传则自动从底座克隆一份作参考
args=args,
train_dataset=pref_dataset,
peft_config=peft_config,
)
trainer.train()
偏好对从哪来?三个来源按可信度排序:人工 A/B 标注(贵而准);第七章的用户赞踩与改写(真实分布);强模型给两个候选打分(量大但要抽检)。DPO 对数据质量的敏感高于 SFT,几百对脏数据就能把语气带偏。
标准顺序是 SFT 到合格,再用 DPO 精修那些“都对,但有好坏之分”的细粒度行为:语气、拒答边界、简洁度。直接对基座跑 DPO 没有意义,它只能在你已有的水平上做选择,不能创造新能力。
如果 SFT 后格式仍然不合规,那是数据或 epoch 的问题,回去修第五、六节。用 DPO 去补 SFT 的基本盘,相当于用锉刀拧螺丝:工具不对,还会把模型带偏。
蒸馏:用大模型造小模型
当任务是固定的,大模型的能力其实用不完。蒸馏把“大模型在这个任务上的表现”转移到小模型,换来数量级的成本与延迟下降。
教科书里的蒸馏是对 logits 做温度缩放,需要拿到 teacher 的权重与内部分布;应用层的主流做法简单得多:生成数据蒸馏,让最强的模型(teacher)针对你收集的真实 query 生成理想回答,把这些(query, 理想回答)对拿去 SFT 一个小模型(student)。流程与第五、六节完全复用,只换了数据来源:
| 步骤 | 做什么 | 关键点 |
|---|---|---|
| 1. 采 query | 从线上 trace 抽真实用户问题 | 真实分布,不是想象中的问题 |
| 2. 生成 | teacher 加上你的 system prompt 逐条作答 | 把要学的行为写进 teacher 的指令 |
| 3. 过滤 | 规则初筛加人工抽检 10% | teacher 也会错,错样本会被 student 完整继承 |
| 4. SFT | 第六节流程照跑,基座换成小模型 | system prompt 保持与线上一致 |
| 5. 对照 | 同一黄金集上对比 teacher / student | 差距小于 2 个点即可切换 |
这条路线上限就是 teacher 的水平,所以适用条件很明确:任务固定、query 分布稳定、teacher 在这个任务上已经够好。满足时收益巨大,第七章的账本会直接告诉你省了多少:
| 维度 | 大模型 API | 蒸馏后的小模型 |
|---|---|---|
| 单次调用成本 | 高,按 token 计费 | 自托管,边际成本接近电费 |
| 延迟 | 网络往返加推理,秒级 | 本地推理,百毫秒级可期 |
| 能力上限 | teacher 全量能力 | 仅限任务内,出圈即退化 |
| 数据边界 | 数据出内网 | 可完全私有化 |
| 维护 | 无,供应商托管 | 自己负责升级、监控、容量 |
蒸馏数据最好的采集器就是可观测性系统:低分 trace 拿给 teacher 重写,重写结果既是训练样本,也是下一轮回归集的候选。飞轮转起来后,蒸馏不再是项目,而是周例会上的一道常规工序。
部署:合并、量化与推理服务
训练完成不等于能用。从 adapter 到一个可被第一章应用调用的服务,中间还有三步:合并、量化、起服务。
第一步:合并或保留 adapter
两种形态先选一个。保留 adapter 的好处是一份基座挂多套行为,切换业务零成本;合并成单模型的好处是部署简单、推理少一次旁路计算。线上单业务长期跑,选合并:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
model = PeftModel.from_pretrained(base, "sft-support-v1/final")
merged = model.merge_and_unload() # W + BA 算进原权重,旁路消失
merged.save_pretrained("sft-support-v1/merged")
AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct").save_pretrained(
"sft-support-v1/merged") # tokenizer 一并带过去
第二步:量化,把显存打下来
合并后是 bf16 全精度(7B 约 15GB)。量化把权重压到 4bit,显存降到四分之一左右,精度损失通常在小任务上可忽略。三种格式按部署目标选:
| 格式 | 生态 | 适用环境 | 备注 |
|---|---|---|---|
| GGUF | llama.cpp / Ollama | CPU、Mac、消费级卡 | 本地与边缘首选,量化档位最丰富 |
| AWQ | vLLM 等 | GPU 服务化 | 激活感知量化,精度保持好 |
| GPTQ | vLLM 等 | GPU 服务化 | 老牌方案,生态成熟 |
第三步:vLLM 起服务,接回 LangChain
vllm serve sft-support-v1/merged --served-model-name support-7b
vLLM 暴露 OpenAI 兼容接口,于是第一章的一切照旧:换一个 base_url 与模型名,应用栈零改动。
from langchain.chat_models import init_chat_model
llm = init_chat_model(
"openai:support-7b",
base_url="http://llm-gpu-host:8000/v1",
api_key="EMPTY", # vLLM 默认不校验,填任意串
)
第七章的黄金集在这里就是发布门禁:微调模型与旧模型跑同一套实验,领域指标达标且通用能力不回退才能切流量。上线后前两周,Langfuse 的 trace 与踩赞比盯紧一点,新模型的坏习惯往往在长尾里最先出现。
流量按 5%、20%、50%、100% 逐档放量,每档观察至少一个完整业务周期。微调模型的行为差异是分布性的,不抽样根本看不见;等踩爆了再回滚,代价远大于慢一点。
常见陷阱与 FAQ
微调圈的高频疑问,多数答案都在劝退:先确认你真的需要它。
不能可靠地学会。权重适合存行为与模式,不适合存可枚举、会变更的事实。训练时“背下”的内容无法精确引用、无法增量更新、错了无法定位。知识进 RAG(第四章),行为进权重,这条分工线不要模糊。
格式与简单风格 500 到 2000 条,复杂风格与任务套路 2000 到 10000,DPO 偏好对几百对就能动语气。前提是质量:同一批内风格一致、system prompt 与线上相同、留出集干净。数据不够时优先补数据,而不是调超参。
7B 级是应用团队的甜点:单卡可调、多数行为类任务够用、量化后部署轻。选型看三件事:中文能力是否够、许可是否允许商用、社区有没有现成的 LoRA 实践可参考。任务简单时甚至从 1.5B 起步,先用最小成本验证数据有效。
典型灾难性遗忘(第二节)。按顺序排查:epoch 是否超过 3,学习率是否过大,训练集是否混入通用样本。处置是重训而不是修补:降 epoch 到 1 到 2、混 10% 到 30% 通用指令数据,并给通用能力加一道回归门禁。
粗略量级:7B QLoRA 约 10GB,7B bf16 LoRA 约 20GB 起,14B QLoRA 约 18GB。梯度累积可以补 batch,不能补模型本身。先按 QLoRA 估算自己手里那张卡能不能跑,跑不动就降基座规模,而不是租集群。
不但能,而且推荐:三层本来就各管各的。微调管默认行为与风格,RAG 管私有知识,prompt 管每次请求的即时指令。生产系统的常态是一个微调过的模型,带着 RAG 的上下文,按当次 prompt 做事。
实战练习与资源
七件事走完,微调就从“听说过”变成“能落地”。