LLM 工程手册LLM ENGINEERING

CHAPTER 08生产层 · FINE-TUNING

Fine-Tuning 模型微调

prompt 调到极限、RAG 铺满之后仍然顽固的问题,才轮到权重。LoRA 低秩适配、SFT 数据工程、DPO 偏好对齐与蒸馏,一路到合并、量化、vLLM 上线的完整闭环。

开始阅读 难度高级 先修第一章 · 第七章 2.5小时
08.1

什么时候轮到微调

微调是三件套里的重武器。这一节先建立排序:能用便宜手段解决的,永远不要动权重。

一个 LLM 应用调节模型行为有三层手段,成本与收益都在递增:prompt 工程(含 few-shot 示例)改即时指令,改一行立刻生效;RAG(第四章)注入私有知识,知识可随时增删;微调把行为模式写进权重,一劳永逸但每次变更都要重新训练。三者的分工可以用一张分层图说清:

Prompt 工程 + few-shot 即时指令 · 每次请求可变 · 改一行立刻生效 · 成本最低 先试这里 RAG(第四章) 私有知识 · 按检索注入 · 可随时增删更新 · 中等成本 知识问题走这里 Fine-Tuning(本章) 固定行为 · 写进权重 · 变更需重训 · 成本最高,收益最持久 顽固问题才动
三层手段的分工:自上而下成本递增,逐层下沉,而不是三选一

落到具体症状,判断表如下:

症状正确手段原因
模型不知道公司内部数据RAG权重存不住易变知识,还会过时、还会幻觉
输出格式偶尔不合规范prompt / 结构化输出第一章的结构化输出先试,顽固才升级
领域术语、语气常年需要长 few-shotSFT 微调把几页示例压进权重,省 token 也省延迟
小任务想用小模型替代大模型蒸馏 + SFT第九节路线,成本与延迟的主动降级
推理能力不够用换更强模型微调不增加能力,甚至可能削弱(第二节)

什么时候坚决不动权重?三个反信号:数据攒不到几百条高质量样本(微调是数据游戏);问题出在检索或工具(修系统,不是修模型);期望"微调学会这些知识"(知识进知识库,行为进权重)。

微调不是知识注入

把文档灌进训练集指望模型"背下来",得到的是不可靠的记忆:无法精确引用、无法更新、张口就来时无法察觉。知识类需求永远是 RAG 的领地(第四章),微调负责的是"怎么说话",不是"知道什么"。

08.2

微调能改变什么,不能改变什么

一句话原则:微调改变行为,不改变能力边界。分不清这两者,是微调项目失败的第一大原因。

维度微调能做到吗说明
输出格式与结构习惯能,收益最直接几页 few-shot 描述的格式,压成默认行为
风格、语气、称谓、拒答边界DPO(第八节)尤其擅长细粒度偏好
领域词汇与惯用表达医疗、法律、方言类文本的适配主场景
任务套路(先分析再作答)固定流程从 prompt 变成肌肉记忆
学会私有事实不可靠会幻觉、会过时,走 RAG
提升推理与数学能力基本不能能力上限由预训练决定,微调只重新分布
学会调用新工具不该用微调做工具定义进 prompt(第一章),模型泛化调用
通用能力保持不变要小心灾难性遗忘随时可能发生

灾难性遗忘(catastrophic forgetting)是这张表里最隐蔽的一行:在窄领域数据上训练过久,模型把"领域外的自己"覆盖掉了。症状是对话变笨、常识变差、别的任务退化,而你的领域指标还在涨。四个对策从数据到训练都有:训练集混入一至三成通用指令样本;epoch 控制在一到三轮;学习率用 LoRA 推荐量级(第六节);发布前用通用 benchmark 加一道回归(第十节)。

先写失败标准再开训

动手前先回答:微调成功的标准是什么?用第七章的黄金集给当前模型打一个基线分,写下“格式合规率从 85% 到 98%、其他指标不下降超过 2 个点”这样的可证伪目标。没有基线的微调,训练完只能得到“感觉变好了”。

08.3

训练方式全景图

“微调”是个泛称。把全景摆开,你才能说清自己到底要做哪一种,以及它需要什么资源。

方式训练什么数据形态算力量级典型场景
全参微调全部权重万级以上样本多卡 A100/H100基础模型厂商、深度领域改造
LoRA低秩旁路矩阵千级样本单张消费级显卡绝大多数应用团队(本章主线)
QLoRALoRA + 4bit 量化基座千级样本更低的单卡门槛预算受限、大基座
继续预训练 CPT基座权重(全参或 LoRA)亿级 token 纯文本集群领域语言模型(医疗/法律全文语料)
SFT 指令微调LoRA 为主指令-回答对单卡格式、风格、任务套路(第六节)
DPO 偏好对齐LoRA 为主chosen/rejected 偏好对单卡语气、拒答边界等细粒度偏好(第八节)
蒸馏LoRA 为主大模型生成的样本单卡小模型替代大模型降本(第九节)

本章主线是 LoRA + SFT:它是投入产出比最高的组合,也是绝大多数“应用团队微调”的真实含义。DPO 与蒸馏是在 SFT 底座上的两个延伸方向,全参与继续预训练则超出应用工程范畴,这里只建立概念坐标。

名字背后的关系

SFT 与 LoRA 不构成二选一:SFT 描述“训练目标”(拿指令-回答对学行为),LoRA 描述“训练哪些参数”(旁路低秩矩阵)。你完全可以全参 SFT,也可以 LoRA SFT;应用团队几乎总是后者。

08.4

LoRA:低秩适配原理

LoRA 的全部聪明之处在于一个观察:微调时的权重变化量是低秩的。既然如此,就不必存整份变化,只存它的“压缩版”。

Transformer 的每个注意力投影都是一个大矩阵 W(维度 d×d,7B 模型约 4096×4096)。全参微调要更新它的每一个元素。LoRA 把更新量分解成两个小矩阵的乘积:

x W(d×d) 冻结,不参与训练 A r×d r=16 B d×r + 缩放 α/r h 可训练参数:只有 A 与 B,共 2·d·r 个 d=4096、r=16 时约 13 万,占全参的 0.8% 优化器状态与梯度只跟旁路走 显存从“训模型”降到“训两个小矩阵” adapter 文件只有几十 MB,可插拔 同一基座挂多个 adapter 即多套行为
LoRA 前向:h = Wx + (α/r)·BAx;基座冻结,只训练旁路 A 与 B

写成公式就是 h = Wx + (α/r)·BAx,其中 Ar×dBd×r,秩 r 远小于 d。三个超参决定了这条旁路的性格:

超参含义常用取值调整逻辑
r旁路容量,能学多复杂的变化8 / 16 / 64格式风格类 16 够用;复杂任务套路再升 64
lora_alpha输出缩放系数,有效强度为 α/r2×r 起步欠拟合升 α,过拟合降 α 或 r
target_modules给哪些投影挂旁路q/k/v/o_proj,加 MLP 更佳只挂注意力省显存,加 gate/up/down 效果更稳
lora_dropout旁路随机失活0.05小数据集防过拟合的便宜手段
QLoRA:再省一档

QLoRA 把冻结的基座量化到 4bit 再训 LoRA,让单张 24GB 显卡也能调 7B 级模型。代价是训练略慢、精度有一点点损失。起步阶段推荐直接 QLoRA:先低成本验证数据有效,有效再上更高配置重训。

08.5

数据集工程

微调项目的八成工作量在数据,八成败因也在数据。这一节把来源、格式、规模三件事一次定标准。

数据从哪来?三条路线,质量从高到低,成本从高到低:

来源具体做法量级风险
线上回流第七章的用户改写、高赞回答、低分 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 对就能动语气。经验法则始终是质量大于数量,一千条干净一致的样本胜过一万条参差的。

08.6

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_rate1e-4 ~ 3e-4(LoRA)loss 震荡不降则减半;下降平缓则翻倍试一次
epochs1 ~ 3train 降 eval 升就是过拟合,减 epoch 或早停
有效 batch16 ~ 64小 batch 噪声大,大 batch 学得慢;用梯度累积拼
warmup_ratio0.03前期 loss 尖峰说明起步太猛,加大 warmup
max_seq_length覆盖 P95 样本长过长样本被截断会学到残缺行为
从 Instruct 起步,不从 Base 起步

应用团队的微调几乎总是继续调一个已对齐的 Instruct 模型,而不是从 Base 模型训指令能力。Base 需要海量数据重新教会对话;Instruct 已经会说话,你只是教它“在我们场景里怎么说”。这条默认能省掉一个数量级的工作量。

产出的形态

save_model 保存的是 LoRA adapter(两个小矩阵加上配置),不是完整模型。使用时基座与 adapter 分开加载,或第十节合并成单文件。一份基座可以挂多套 adapter,按业务切换,这正是 LoRA 的部署优势。

08.7

训练监控与调参

训练不是按下回车等奇迹。两条 loss 曲线会说话,前提是你看得懂它们的四种典型形态。

要看的东西只有两条:train losseval 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 与一整套曲线。它是后续所有调参的对照组:改一个参数重跑,曲线对比立刻告诉你方向对不对。没有第一版基线的调参,全靠占卜。

08.8

偏好对齐:从 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 收尾

标准顺序是 SFT 到合格,再用 DPO 精修那些“都对,但有好坏之分”的细粒度行为:语气、拒答边界、简洁度。直接对基座跑 DPO 没有意义,它只能在你已有的水平上做选择,不能创造新能力。

DPO 不是拿来兜底的

如果 SFT 后格式仍然不合规,那是数据或 epoch 的问题,回去修第五、六节。用 DPO 去补 SFT 的基本盘,相当于用锉刀拧螺丝:工具不对,还会把模型带偏。

08.9

蒸馏:用大模型造小模型

当任务是固定的,大模型的能力其实用不完。蒸馏把“大模型在这个任务上的表现”转移到小模型,换来数量级的成本与延迟下降。

教科书里的蒸馏是对 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 重写,重写结果既是训练样本,也是下一轮回归集的候选。飞轮转起来后,蒸馏不再是项目,而是周例会上的一道常规工序。

08.10

部署:合并、量化与推理服务

训练完成不等于能用。从 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,显存降到四分之一左右,精度损失通常在小任务上可忽略。三种格式按部署目标选:

格式生态适用环境备注
GGUFllama.cpp / OllamaCPU、Mac、消费级卡本地与边缘首选,量化档位最丰富
AWQvLLM 等GPU 服务化激活感知量化,精度保持好
GPTQvLLM 等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% 逐档放量,每档观察至少一个完整业务周期。微调模型的行为差异是分布性的,不抽样根本看不见;等踩爆了再回滚,代价远大于慢一点。

08.11

常见陷阱与 FAQ

微调圈的高频疑问,多数答案都在劝退:先确认你真的需要它。

微调能让模型学会我们公司的知识吗?

不能可靠地学会。权重适合存行为与模式,不适合存可枚举、会变更的事实。训练时“背下”的内容无法精确引用、无法增量更新、错了无法定位。知识进 RAG(第四章),行为进权重,这条分工线不要模糊。

多少条数据能见效?

格式与简单风格 500 到 2000 条,复杂风格与任务套路 2000 到 10000,DPO 偏好对几百对就能动语气。前提是质量:同一批内风格一致、system prompt 与线上相同、留出集干净。数据不够时优先补数据,而不是调超参。

基座模型选多大?选谁家?

7B 级是应用团队的甜点:单卡可调、多数行为类任务够用、量化后部署轻。选型看三件事:中文能力是否够、许可是否允许商用、社区有没有现成的 LoRA 实践可参考。任务简单时甚至从 1.5B 起步,先用最小成本验证数据有效。

微调后模型变笨了怎么办?

典型灾难性遗忘(第二节)。按顺序排查:epoch 是否超过 3,学习率是否过大,训练集是否混入通用样本。处置是重训而不是修补:降 epoch 到 1 到 2、混 10% 到 30% 通用指令数据,并给通用能力加一道回归门禁。

LoRA 训练到底需要多少显存?

粗略量级:7B QLoRA 约 10GB,7B bf16 LoRA 约 20GB 起,14B QLoRA 约 18GB。梯度累积可以补 batch,不能补模型本身。先按 QLoRA 估算自己手里那张卡能不能跑,跑不动就降基座规模,而不是租集群。

微调、RAG、prompt 能同时用吗?

不但能,而且推荐:三层本来就各管各的。微调管默认行为与风格,RAG 管私有知识,prompt 管每次请求的即时指令。生产系统的常态是一个微调过的模型,带着 RAG 的上下文,按当次 prompt 做事。

08.12

实战练习与资源

七件事走完,微调就从“听说过”变成“能落地”。

延伸阅读