把模型练成你的 Agent:微调方案
作者:toy ---
作者:toy
一、为什么需要微调
Prompt Engineering 的三类失效
用 Prompt 调教通用模型,是大多数 Agent 项目的第一站。这条路走得顺时,能省掉大量工程复杂度。但在三类场景下,它会系统性失效。
第一类是领域术语密集的专业场景。法律条文、医疗规范、工业设备手册,这些文本在通用训练数据中占比极低。模型对"设备校准误差补偿系数"这类术语的处理,本质上是在用统计近似覆盖一个稀疏分布区域,Prompt 再长也补不上训练数据的缺口。第二类是输出格式高度受限的场景。假如下游系统要求每次输出都是一段严格合规的 JSON、一个固定字段的表格,或者遵循某个内部文档模板,通用模型对"格式"的理解是模糊的,即使 Prompt 里写了五遍示例,在生产流量下仍然以一定概率漂移。第三类是推理链路风格固定的场景。有些 Agent 任务要求模型按特定顺序思考(先检查 X,再判断 Y,最后输出 Z),通用模型没有这个"反射弧",每次都要靠 Prompt 重新唤醒。
这三类失效的本质是一样的:Prompt 是在推理时给模型注入约束,微调是在训练时把约束烙进权重。烙进去的比注入的稳定。
当你的 Prompt 越写越长、规则越加越多、还在频繁手动修复输出,这就是一个强烈的信号:问题应该用微调解决,而不是继续延长 Prompt。每次在 Prompt 里新增一条规则,实际上是在暗示模型"刚才你做错了",但模型没有记忆,下一个用户还会踩同样的坑。把这些规则压进权重,模型才会默认正确。
微调 vs RAG vs 上下文学习的选择框架
这三种技术经常被放在一起比较,但它们解决的问题不同,不是简单替代关系。
RAG(检索增强生成)解决的是知识问题,模型不知道某件事,给它查。这个查的动作发生在推理时,知识库可以实时更新,适合信息密集、内容频繁变化的场景。上下文学习(In-Context Learning, ICL)用少量示例在 Prompt 里演示,让模型类比推理,适合快速验证想法、任务量小、没有训练资源的情况。微调(Fine-tuning)解决的是风格、格式、专业术语的内化问题,让模型"默认就会"某件事,不需要在每次推理时提示。三者的核心区别:知识用 RAG,行为用微调,快速试验用 ICL。
在实际工程中,这三者往往组合使用。一个好的 Agent 可能是:先微调让模型掌握领域格式和推理风格,再挂 RAG 检索实时知识,再用 ICL 做个别特殊案例的适配。把微调当成"解决所有问题的银弹",或者觉得"有了 RAG 就不需要微调",都是误解。
微调的成本构成
微调不便宜,在决定投入前,必须把成本拆清楚。
数据成本通常是最贵的。高质量的指令-输出对,靠人工标注的话,每条 $1-5 是正常范围,几千条就是几千美元。用模型生成再人工审核能降低一半成本,但审核质量直接决定微调效果。算力成本取决于模型大小和方法:全量微调 7B 模型大概需要 8×A100(80GB)跑几天,用 LoRA+QLoRA 在单张消费级 GPU 上也能训练。时间成本常被低估。数据清洗、格式对齐、超参实验、模型评估,加在一起通常比"跑训练"本身花的时间长三倍。
哪类场景值得投入?如果 Prompt 方案已经稳定,核心问题是格式不稳定或风格漂移,值得微调。如果核心问题是知识不够,微调不会有用,应该转向 RAG 或者扩充训练数据。如果任务量每天不足百次,微调的工程成本大概率无法被业务价值覆盖。
还有一类场景特别适合微调却经常被忽略:推理延迟敏感的生产服务。通用大模型配合长 Prompt 的方案,每次请求都要处理几百 token 的 Prompt,推理时间长、成本高。把行为压进权重之后,系统 Prompt 可以从几百 token 压缩到几十 token,在同等效果下把推理成本降低 30%-50%。对于日均百万次以上请求的 Agent 服务,这个成本差距是实实在在的收益。
二、SFT:监督微调的基础
数学本质
监督微调(Supervised Fine-Tuning,SFT)在数学上是最朴素的语言模型训练。给定输入 x,最大化模型生成正确输出 y 的条件概率:
L_SFT(θ) = -∑ log P_θ(yᵢ | x, y₁, ..., yᵢ₋₁)
这和预训练的目标函数形式相同,区别在于:预训练用的是海量无标注文本,SFT 用的是人工构造的"正确示范"。模型通过反向传播把"在这种输入下应该生成这种输出"的模式压进权重。SFT 不是在教模型"理解"任务,而是在压缩一批示例到权重里。示例的质量决定了模型最终能学到的能力上限。
训练数据格式
SFT 数据通常以"指令-输入-输出"三元组的形式组织,这也被叫做 Alpaca 格式(来自 Stanford Alpaca 项目):
{
"instruction": "将下面这段文本翻译成英文",
"input": "人工智能正在改变软件工程的边界",
"output": "Artificial intelligence is reshaping the boundaries of software engineering"
}
对于不需要额外输入的纯指令任务,input 字段可以为空。实际工程中更常见的是 ChatML 格式,直接按对话轮次组织:
{
"messages": [
{"role": "system", "content": "你是一位专业的代码审核工程师"},
{"role": "user", "content": "帮我检查这段 Python 代码的性能问题"},
{"role": "assistant", "content": "我发现了以下三个性能问题..."}
]
}
ChatML 格式的优势是天然支持多轮对话,和大多数推理框架的接口格式保持一致,方便后续部署。
学习率和 epoch 的选择经验
SFT 的超参数调整没有银弹,但有几个经验规律值得记录。学习率通常设在 1e-5 到 5e-5 之间,比预训练低一到两个数量级,原因是微调阶段不希望破坏已有权重中的通用能力。学习率过高会导致灾难性遗忘,模型在新任务上表现良好,但通用对话能力大幅退化。
训练轮数方面,一个反直觉的发现是:小数据集多 epoch 训练,效果往往好于大数据集跑一个 epoch。这在多篇 2024 年的研究中得到验证,几千条高质量数据跑 3-5 个 epoch,比几万条低质量数据跑一遍效果更好。epoch 过多(超过 5 轮)则会出现明显的过拟合,模型开始死记硬背训练集,而不是泛化规律。
Warmup 步数建议设为总训练步数的 5%-10%,让学习率从零逐渐爬升,避免在训练初期因梯度过大而破坏基础权重。
常见失败模式
SFT 有几个典型的失败模式,在项目早期需要重点关注。
灾难性遗忘是最常见的问题。模型在新任务上学得很好,但原有能力明显下降。缓解方法是在训练集中混入一定比例的通用对话数据,通常 10%-20% 的比例就能有效防止遗忘。格式崩溃在数据质量参差不齐时频繁出现:模型某些时候输出正确格式,某些时候完全忽略格式要求。检查方法很简单,把训练集里所有 output 字段过一遍,确认格式一致性。指令不遵循通常源于训练集中指令-输出对的语义对齐不好,也可能是 system prompt 的设计问题。如果训练时用了 system prompt,推理时也必须用同样的 system prompt,否则模型会表现出奇怪的行为。
还有一类失败模式不太被讨论:过度遵循训练分布。模型学得"太好",对训练集里每种输入都有非常确定的固定回答,遇到训练集里没出现过的输入变体时输出质量急剧下降。这是泛化能力不足的表现,根本原因是训练数据多样性不够。修复方法是增加训练数据的输入变体(同一个意思用十种不同问法),或者在训练时加轻微的 dropout(0.05-0.1)。另一个实用做法是在训练数据里混入约 5% 的"不确定"示例:对于超出领域的问题,正确的回答是"我不确定,建议咨询专业人员",而不是编造一个听起来有道理的答案。
三、DPO:用偏好数据替代人工奖励
为什么 RLHF 难以工程化
RLHF(基于人类反馈的强化学习)是早期对齐大模型的主流方案,InstructGPT 用它证明了 1.3B 对齐模型可以在用户评价上打败 175B 基础模型。但 RLHF 的工程复杂度极高。
完整的 RLHF 流程需要三个阶段:先用 SFT 训练一个监督微调模型,再用人类标注的偏好数据训练一个奖励模型(Reward Model),最后用 PPO(近端策略优化)算法,让语言模型在奖励模型的反馈下进行强化学习。这三个阶段需要维护三组权重,PPO 阶段要同时跑语言模型和奖励模型,内存需求翻倍。更严重的问题是奖励模型的不稳定性。RM 一旦训偏,PPO 阶段的策略模型就会被带歪,出现"奖励黑客"现象,即模型找到方法欺骗 RM 拿高分,而不是真正提升质量。重现 RLHF 的结果,需要极其精细的工程控制。
奖励黑客的典型表现之一:语言模型学会用更长、更流畅、更有自信语气的方式包装错误答案,因为这类回答在 RM 的评分里往往更高。这不是奖励模型设计者的疏忽,而是人类偏好数据本身的固有偏差,人类标注者也倾向于给语言更流畅的回答打高分,即使内容不准确。DPO 没有从根本上解决这个问题,但通过消除独立奖励模型,减少了引入额外偏差的环节。
DPO 的核心公式
DPO(Direct Preference Optimization,arXiv:2305.18290,Rafael Rafailov 等,NeurIPS 2023)的核心想法是:奖励模型可以被隐式地参数化为策略比率,因此可以直接用偏好数据训练语言模型,跳过显式奖励模型的训练。
DPO 基于 Bradley-Terry 偏好概率模型,将人类偏好建模为:
p*(y₁ ≻ y₂ | x) = exp(r*(x,y₁)) / [exp(r*(x,y₁)) + exp(r*(x,y₂))]
其中 r*(x, y) 是真实的奖励函数。DPO 的关键重参数化是:将显式奖励 r(x, y) 替换为策略比率 β log π_θ(y|x)/π_ref(y|x)。这样,奖励模型就被语言模型自身编码了,不需要单独训练。最终的 DPO 损失函数为:
L_DPO(θ) = -E[ log σ(β log π_θ(y+|x)/π_ref(y+|x) - β log π_θ(y-|x)/π_ref(y-|x)) ]
其中 y+ 是偏好(chosen)回复,y- 是拒绝(rejected)回复,β 是 KL 惩罚系数,σ 是 sigmoid 函数。这个损失函数的直观理解是:让模型倾向于生成 y+,同时避免生成 y-,并通过 β 控制偏离参考策略的程度。
chosen / rejected 数据对的构建
DPO 的数据格式是"提示 + 好回复 + 差回复"三元组:
{
"prompt": "解释什么是量子纠缠",
"chosen": "量子纠缠是两个粒子之间的一种量子关联。当两个粒子处于纠缠态时,对其中一个粒子的测量会立即影响另一个粒子的状态,无论它们相距多远。这一现象已被多个实验证实,包括阿斯佩(Aspect)等人在1982年的贝尔不等式实验。",
"rejected": "量子纠缠就是两个粒子互相感应的现象,爱因斯坦叫它'幽灵般的超距作用',意思是信息传播比光速还快。"
}
构建这类数据对有几种常见方法。第一种是人工标注:让标注员对同一个问题的多个回答进行排序,选出 chosen 和 rejected。第二种是模型采样:用当前 SFT 模型对每个 prompt 生成多个回答,再用奖励模型或人工挑出好坏对。第三种是基于规则:对于可验证任务(代码执行结果正确 vs 错误,数学答案对 vs 错),可以自动生成偏好对,这也是拒绝采样微调(RFT)的思路。
β 超参数的作用
β 是 DPO 中最重要的超参数,默认值为 0.1。它控制训练后的模型与参考策略(π_ref,通常是 SFT 模型)之间的 KL 散度约束强度。
β 越小,更新越激进,模型可以大幅偏离参考策略,向偏好方向猛烈调整。β 越大,更新越保守,模型倾向于停留在参考策略附近,每次调整的幅度很小。实际工程中,β 的典型取值范围是 0.05 到 0.5。对话类任务通常用 0.1 到 0.2,对安全性要求极高的场景可以调到 0.3 以上防止模型漂移过远。β 过小容易导致模型在目标分布上过度优化,出现类似"奖励黑客"的问题,只是这次 hacking 的对象是 DPO 损失本身,而不是外部奖励模型。
DPO vs PPO 的实测对比
arXiv:2404.10719("Is DPO Superior to PPO for LLM Alignment?")对二者做了系统性评测。在 HH-RLHF 数据集上用 GPT-4 评测,PPO 赢 42 局、DPO 赢 30 局,28 局平。在代码生成任务(APPS 数据集,CodeLlama-34B),PPO 的 pass@5 达到 44.4%,DPO-Iter 是 34.2%,差距明显。DPO 的优势集中在情感控制等对话类任务上,且训练稳定性远优于 PPO。
这个对比说明了一个分工原则:DPO 是训练对话风格、减少幻觉、提升遵循度的高效工具;PPO 在需要精确推理、代码执行、严格安全约束的场景有不可替代的优势。两者不是互斥的。DeepSeek-R1 的后训练方案就是先跑 RL(GRPO,一种 PPO 变体),再用拒绝采样 SFT,最后加偏好对齐(接近 DPO 的思路),分阶段组合使用。
TRL 里的 loss_type 变体
HuggingFace TRL 库的 DPOTrainer 支持多种 loss 变体,每种针对不同问题:
| loss_type | 原理 | 适用场景 |
|---|---|---|
| sigmoid(默认) | Bradley-Terry 二分类 | 大多数对话任务 |
| ipo | 恒等变换,防止 logit 过拟合 | 数据噪声较大时 |
| hinge | SLiC 边距损失 | 需要 margin 约束 |
| robust | 标签噪声鲁棒版本 | 众包标注数据 |
| sigmoid_norm | 按 token 数归一化 | 消除长度偏差 |
长度偏差是 DPO 训练中一个容易忽视的问题:如果 chosen 样本普遍比 rejected 样本更长,模型可能学会"更长的回答更好"这个伪规律,用 sigmoid_norm 归一化可以消除这个偏差。
以下是完整的 DPO 训练代码示例:
# DPO 训练完整示例(HuggingFace TRL 官方文档)
from datasets import load_dataset
from trl import DPOTrainer, DPOConfig
from peft import LoraConfig
# 偏好数据集格式:(prompt, chosen, rejected) 三元组
# {"prompt": "解释量子纠缠",
# "chosen": "量子纠缠是两个粒子之间的量子关联...(准确回答)",
# "rejected": "量子纠缠就是两粒子互相感应...(含误导信息)"}
dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train")
# DPO loss 公式:
# L_DPO = -E[log σ(β·(log π_θ(y+|x)/π_ref(y+|x) - log π_θ(y-|x)/π_ref(y-|x)))]
# β 控制 KL 约束强度,默认 0.1;越小更新越激进
training_args = DPOConfig(
beta=0.1, # KL 惩罚系数,典型范围 0.05-0.5
learning_rate=1e-6, # DPO 典型 lr 比 SFT 低一个数量级
num_train_epochs=1,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
loss_type="sigmoid", # Bradley-Terry 默认;可选 ipo/hinge/robust/sigmoid_norm
output_dir="./dpo_output",
)
# 结合 LoRA 节省显存,不传 peft_config 则做全量 DPO
peft_config = LoraConfig(r=16, lora_alpha=32, task_type="CAUSAL_LM")
trainer = DPOTrainer(
model="Qwen/Qwen3-0.6B",
args=training_args,
train_dataset=dataset,
peft_config=peft_config,
)
trainer.train()
# 监控指标:rewards/margins 越大越好;rewards/accuracies > 0.5 说明模型在学
四、LoRA:低秩分解让微调变得可行
数学原理
LoRA(Low-Rank Adaptation,arXiv:2106.09685,Edward Hu 等,Microsoft Research)的出发点是一个观察:大模型在适配下游任务时,权重的更新矩阵 ΔW 实际上是低秩的,不需要更新完整的 d_in × d_out 大矩阵。
LoRA 把权重更新分解为两个低秩矩阵的乘积:
W = W₀ + ΔW = W₀ + BA·(α/r)
其中 A ∈ R^{r×d_in} 用高斯随机数初始化,B ∈ R^{d_out×r} 初始化为零(确保训练开始时 ΔW = 0,不破坏预训练权重),r 是秩(rank),α 是缩放因子。训练时 W₀ 冻结,只更新 A 和 B。
可训练参数量的计算非常直观:单层 LoRA 的可训练参数数 = 2 × r × (d_in + d_out),远小于完整更新的 d_in × d_out。以 GPT-3 175B 为例,设 r=1,全部注意力层的可训练参数从 1750 亿降到约 470 万;r=8 时约 3780 万,仍然是完整参数量的 0.02%。GPU 显存需求减少约三倍,因为优化器状态(Adam 的一阶矩和二阶矩)只需要为可训练参数分配,不需要为冻结参数分配。
rank 和 alpha 的选择
rank(r)控制 LoRA 的表达能力,越大捕获的信息越丰富,但参数量也越多。经验范围是 4 到 64,大多数任务用 r=8 或 r=16 效果已经足够。如果任务非常简单(如格式调整),r=4 就够;如果任务需要注入大量新知识,可以试到 r=64,但超过这个值收益边际递减。
alpha(α)通常设为 r 的 2 倍,即 scaling_factor = α/r = 2.0。这是一个来自原始论文的经验设置。当 r 变化时,保持 α/r 为常数,能让学习率的有效值保持稳定,避免每次换 r 都要重新调 lr。
HuggingFace PEFT 库还提供了 RSLoRA(Rank-Stabilized LoRA),将 scaling 改为 α/√r,理论上在高 rank 场景下训练更稳定:
peft_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
bias="none",
task_type=TaskType.CAUSAL_LM,
use_rslora=True, # scaling = alpha/sqrt(r) = 32/4 = 8
)
应该微调哪些层
原始 LoRA 论文建议只适配注意力层的 q_proj 和 v_proj,在 GPT-3 上的实验表明这两层足够覆盖大多数下游任务。但实践中,加上 k_proj 和 o_proj 通常会带来进一步提升,代价是参数量翻倍。
全参数微调 vs LoRA 的效果对比:在大多数 NLP 下游任务上,r=8 的 LoRA 和全量微调的差距在 1-2% 以内,考虑到可训练参数减少了 99.98%,这个代价完全可以接受。只有在需要大量注入新领域知识的场景(比如从英文基础模型微调出医学中文模型),全量微调才有明显优势。
merge_and_unload:推理时消除延迟
LoRA 在训练时额外引入了 A 和 B 矩阵,推理时会增加一点计算开销。对于对延迟敏感的生产服务,可以在部署前执行 merge:
from peft import PeftModel
from transformers import AutoModelForCausalLM
# 加载基础模型和 LoRA adapter
base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-3B-Instruct")
model = PeftModel.from_pretrained(base_model, "./lora_adapter")
# 合并权重:W_merged = W₀ + BA·(α/r)
# 合并后的模型与原基础模型结构完全相同,推理时无额外延迟
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged_model")
merge 之后,模型文件和普通模型没有任何区别,可以直接用 vLLM、llama.cpp 等推理框架部署,没有额外的计算图分叉。
完整的 LoRA 训练配置如下:
from transformers import AutoModelForCausalLM
from peft import LoraConfig, TaskType, get_peft_model
model_id = "Qwen/Qwen2.5-3B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cuda")
# r=16, alpha=32 => scaling = 32/16 = 2.0
# 可训练参数 per layer = 2 * 16 * (d_in + d_out)
peft_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# 输出示例:trainable params: 3,686,400 || all params: 3,089,625,088 || trainable%: 0.1193
五、QLoRA:在消费级 GPU 上微调大模型
从 LoRA 到 QLoRA 的动机
LoRA 解决了"训练哪些参数"的问题,但没有解决"基础模型本身就放不进 GPU"的问题。一个 65B 的模型,用 fp16 精度加载需要约 130GB 显存,8 张 A100(80GB)才能装下,这已经超出大多数团队的资源预算。QLoRA(Quantized LoRA,arXiv:2305.14314,Tim Dettmers 等,NeurIPS 2023)在 LoRA 的基础上增加了三项关键创新,把 65B 模型的微调需求压缩到单张 48GB 显卡可承受的范围内。
4-bit NF4 量化
NF4(4-bit NormalFloat)是 QLoRA 的核心量化格式。其设计基于一个观察:预训练模型的权重分布近似正态分布。针对这个分布,可以设计一个信息论最优的 4-bit 量化格式,用 16 个量化级别(2^4 = 16),在正态分布上等分概率密度。
NF4 的量化公式为:
qᵢ = ½(Q_X(i/2^k+1) + Q_X((i+1)/2^k+1))
其中 Q_X 是正态分布的分位函数。与 FP4(直接截断浮点数)相比,NF4 对正态分布权重的表示是信息论最优的,在相同 bit 数下量化误差更小。实际效果:在 65B 模型上,NF4 量化相比 FP16 的精度损失极小,通常在最终任务指标上影响不到 1%。
4-bit 量化的存储节省很直接:fp16 每个参数 16 bits,NF4 只需 4 bits,存储压缩比约为 4 倍。65B 模型从 130GB 压缩到约 33GB。推理(前向计算)时,NF4 参数会被反量化到 bfloat16 执行计算,因此不影响计算精度,只影响存储占用。
双重量化
量化本身也引入了"量化常数"(用于反量化的缩放因子)。对于 block_size=64 的一级量化,每 64 个参数共享一个 fp32 量化常数,这个常数额外占用 32/64 = 0.5 bits/param。
双重量化(Double Quantization)把这些量化常数自身也再量化一遍:一级量化 block_size=64,二级量化 block_size=256,量化常数从 fp32 降到 fp8 或 fp4。这额外节省了约 0.37 bits/param,对 65B 模型来说大约节省了额外 3GB 显存。量化常数通常被忽视,但在几十亿参数的规模上,几百 MB 的节省就是能不能跑起来的差距,这个设计很精巧。
分页优化器
深度学习训练的显存消耗不仅来自模型权重,Adam 优化器需要为每个可训练参数维护一阶矩(m)和二阶矩(v),额外占用约 2×参数量×精度的显存。对于梯度检查点(gradient checkpointing)场景,如果在某些批次输入下激活内存激增,可能导致 OOM。
分页优化器(Paged Optimizer)利用 NVIDIA 统一内存(Unified Memory)机制,在 GPU 内存不足时自动将优化器状态迁移到 CPU 内存,需要时再换回 GPU。这不会改变计算结果,只是把 OOM 转化为一次 GPU-CPU 内存换页(带来一点速度损失)。实际训练中,这个机制大幅提升了训练稳定性:原本不定期出现的 OOM crash,变成了可预期的性能抖动。
实际显存需求
以下是各规模模型在不同训练方式下的显存需求(来自 arXiv:2305.14314 和 HuggingFace 博客):
| 模型规模 | 全精度 FP16 微调 | QLoRA | 可用硬件 |
|---|---|---|---|
| 7B | ~60GB | ~6GB | RTX 3090 可跑 |
| 13B | ~110GB | ~10GB | RTX 3090 可跑 |
| 33B | ~280GB | ~21GB | RTX 3090 + 超频可跑 |
| 65B | >780GB | ~41GB | 单张 A100 80GB 可跑 |
Guanaco-65B 是 QLoRA 论文中的验证案例:用单张 48GB GPU,24 小时训练完成,在 Vicuna 基准上达到 ChatGPT 性能的 99.3%。这个数字的意义在于:它把"需要超算集群"的工作,拉回到了单 GPU 可完成的范围。
完整的 QLoRA 配置代码如下:
import torch
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType
# 1. 配置 4-bit NF4 量化
# bnb_4bit_quant_type="nf4":信息论最优,适用于正态分布权重(优于 fp4)
# bnb_4bit_use_double_quant=True:双重量化,再省 0.37 bits/param(65B 约省 3GB)
# 一级量化 block_size=64,二级量化 block_size=256
nf4_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True, # 双重量化
bnb_4bit_compute_dtype=torch.bfloat16 # 前向计算时反量化到 bf16
)
# 65B 正常需 >780GB;QLoRA 仅需约 41GB(单张 48GB GPU 可运行)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=nf4_config,
device_map="auto"
)
# 2. 在量化基础模型上叠加 LoRA adapter
# 4-bit 基础权重本身不参与梯度计算,只训练 bf16 精度的 LoRA 部分
lora_config = LoraConfig(
r=64,
lora_alpha=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 分页优化器由 bitsandbytes 自动处理 GPU/CPU 内存交换,防止 OOM
六、数据集清洗:垃圾进垃圾出
数据质量比数量更重要
"垃圾进垃圾出"在 SFT 场景里不是比喻,而是字面意思。一个对行业从业者来说显而易见的事实:用 1000 条高质量数据微调,通常好于用 10000 条低质量数据微调。2024 年的多篇研究(包括 Superfiltering,arXiv:2402.00530)对此做了量化验证:在 Alpaca 风格的指令微调数据集上,通过质量过滤将数据量从 52K 降到 6K,模型在多项指令遵循基准上的得分反而上升。
一个更具体的实验结论是:去重效果往往超过增加 5% 的数据量。换句话说,你花在去重和质量过滤上的时间,比收集更多数据的时间,产出的期望价值更高。
去重方法
去重分精确去重和近似去重两种。精确去重用 MD5 哈希,把每条训练样本的文本做哈希,删除哈希冲突的重复项。这个方法快速可靠,但只能处理完全相同的样本,对语义相近但文字稍有不同的重复无效。
近似去重用 MinHash 签名 + LSH(局部敏感哈希)分桶。MinHash 把文本转化为一个固定长度的签名向量,两个文本的 Jaccard 相似度越高,它们的 MinHash 签名越接近。LSH 把相似的签名分到同一个桶里,只在桶内做精细比较,把全量两两比较的 O(n²) 复杂度降到近似线性。大规模数据集(百万条以上)的去重通常用这个方案。
实操步骤:先做精确去重(MD5),再做近似去重(MinHash + LSH,相似度阈值 0.8-0.9),最后人工抽检确认去重质量。
质量过滤指标
去重之后,还需要过滤低质量样本。几个有效的过滤维度:
困惑度(Perplexity,PPL)过滤:用一个小型参考模型(比如 GPT-2)计算每条样本的困惑度。PPL 过高的样本往往是乱码或格式异常,PPL 过低的样本往往是极简单或重复的文本。过滤两端,保留中间分布的样本,能有效提升数据质量。
长度分布过滤:计算全量数据的输出长度分布,删除过短(少于 20 token,往往是无意义的"好的、明白")和过长(超过 2048 token 且内容无实质信息密度)的样本。
语言检测:如果目标是中文 Agent,混入大量英文训练数据会稀释中文能力;用 langdetect 或 fasttext 做语言识别,过滤掉语言不匹配的样本。
格式合规检测:如果输出格式有要求(JSON、Markdown 表格等),写一个简单的解析函数,过滤掉格式不合规的样本。这一步很多人跳过,但它能避免大量"格式崩溃"问题。
指令遵循数据的配比建议
训练 Agent 专用模型时,数据集的组成比例很重要。一个经验配比:
- 格式/结构化输出指令(JSON 生成、Markdown 格式化、特定模板填充):约 30%
- 知识问答(专业领域 QA,确保知识准确性):约 35%
- 推理/规划任务(多步骤问题分解、工具调用链路):约 25%
- 对话/拒绝(边界案例处理,防止越界):约 10%
这个比例不是绝对的,要根据 Agent 的实际任务分布调整。通用原则是:训练集的任务分布,要接近模型上线后的实际使用分布;如果模型 80% 的时间在做结构化输出,训练集里这类数据就应该占主导。
人工标注 vs 模型生成标注
Self-Instruct(arXiv:2212.10560)和 WizardLM 系列工作证明了模型生成标注(又叫"合成数据")的可行性:用 GPT-4 或更强的模型生成大量指令-回答对,再进行人工过滤或质量评分,最终的训练数据质量可以接近全人工标注。
实际工程经验:纯模型生成的数据质量上限受限于生成模型的能力,在创造性任务和高度专业化任务上,人工标注仍然不可替代。更实用的方式是"人机协同标注":人工设计多样化的种子指令(100-500 条),用模型批量扩写生成更多变体,再由专业人员审核过滤,效率和质量都比纯人工或纯模型高。
七、幻觉微调:专门针对幻觉问题
幻觉在数据层面的根源
幻觉(Hallucination)不是一个可以单靠推理时干预彻底解决的问题,它的根源在训练数据层面。预训练数据中存在大量"看起来合理但实际上错误"的陈述,包括错误的维基百科编辑、误导性的博客文章、年份错误的技术描述。模型通过最大化 token 预测概率把这些模式都吸收进来,没有区分"真实陈述"和"合理的错误陈述"的机制。
SFT 阶段会进一步放大这个问题:如果训练数据中的"输出"本身包含幻觉(比如用低质量模型生成的回答),SFT 会让模型更"熟练"地产出幻觉,因为它在学习模仿这些示例。这是一个被很多人低估的陷阱,标注者觉得回答"听起来不错",但没有核实其中每个事实断言的准确性。
拒绝采样微调(RFT)
拒绝采样微调(Rejection Sampling Fine-tuning,RFT)是 DeepSeek-R1 后训练方案的第三阶段,也是缓解幻觉的有效工具。核心思路是:用当前模型对每个问题采样多个回答,只保留"高质量轨迹"用于下一轮 SFT。
"高质量"的判断标准取决于任务:对于可验证任务(数学、代码),直接执行验证,代码能跑通才保留;对于知识问答,用 FactScore 等工具评估事实准确率;对于开放对话,可以用奖励模型或人工评分筛选。
具体流程:
- 用当前 SFT 模型对每个 prompt 生成 N=4-8 个回答
- 对每个回答打分(执行验证 / 奖励模型 / FactScore)
- 只保留得分超过阈值的回答,丢弃其余
- 用保留的(prompt, 高质量回答)对做下一轮 SFT
这个循环可以迭代多次,每轮 SFT 之后模型能力提升,生成的高质量回答比例也提升,采样效率不断改善。DeepSeek-R1 的后训练方案就是用这个思路,把 RL 阶段的成功轨迹转化为 SFT 数据。
用 DPO 对抗幻觉
DPO 的数据构建框架天然适合针对幻觉问题。F-DPO(arXiv:2601.03027)的做法是将含有幻觉的输出作为 rejected 样本,将事实准确的输出作为 chosen 样本,构建偏好对进行 DPO 训练。论文还引入了"事实性 margin"的概念:不只是区分好坏,而是量化两个回答的事实准确率差距,只有差距超过阈值的偏好对才纳入训练。
RS-DPO(Amazon,arXiv:2402.10038)将拒绝采样和 DPO 结合:先从 SFT 模型采样多个回答,用奖励模型评分,选出得分最高和最低的配对组成偏好对,再做 DPO。实验显示这个方案优于单独使用 PPO、DPO 或拒绝采样,因为它结合了三者的优势:采样多样性(拒绝采样)+ 稳定训练(DPO)+ 奖励信号(RM 评分)。
迭代对比学习方案(Iter-AHMCL,arXiv:2410.12130)在 TruthfulQA 上实现了平均 10.1 分的提升。TruthfulQA 的满分是 100 分,10 分的提升有实际意义,对应的是模型在"容易产生幻觉的问题上"(进化心理学、阴谋论、医疗建议等)的回答准确率明显提高。
幻觉评估指标
微调效果的量化很重要,"感觉幻觉少了"不够,需要有数字。常用指标有三个。
FactScore 把模型输出分解为独立的原子事实陈述,用检索增强的方式逐条核实,返回一个 0-100 的事实准确率分数,适合评估知识密集型任务。
TruthfulQA 是一个专门测试模型是否会产生人类常见错误信念的基准集,涵盖医疗、历史、法律等领域的 817 个"陷阱问题",用 MC1(单选正确率)和 MC2(多选正确率)量化。
Hallucination Leaderboard 是 HuggingFace 维护的专项幻觉评测榜单,定期更新主流模型的表现,可以用来和行业基准对齐。
八、模型蒸馏:把大模型的能力搬进小模型
知识蒸馏的三种模式
知识蒸馏(Knowledge Distillation)由 Hinton 在 2015 年提出,核心想法是:小模型(Student)学习大模型(Teacher)的"思考输出"而不仅仅是标注答案,能获取更多信息。在 LLM 时代,蒸馏演化出三种差异明显的范式。
黑盒蒸馏是最简单的方式:Teacher 模型(GPT-4、Claude 等)作为数据生成器,批量产出高质量的指令-回答对,Student 模型用这批数据做 SFT。Stanford Alpaca 用 GPT-3 生成了 52K 条指令数据;WizardLM 系列用 Evol-Instruct 方法让 ChatGPT 生成越来越复杂的指令变体。黑盒蒸馏不需要访问 Teacher 的内部权重,接口调用就够,成本低、易工程化。
白盒蒸馏(中间层对齐)需要访问 Teacher 的所有中间层输出。经典的 DistilBERT 用这个方法:不只让 Student 模仿输出分布(软标签),还对齐每一层的 hidden state 和 attention pattern。这种方式能传递更多"思维过程",但要求 Teacher 和 Student 的架构可以对齐,工程复杂度高。DistilBERT 压缩了 BERT 40% 的参数,保留了 97% 的性能,速度提升 60%。
序列蒸馏针对生成模型:Teacher 生成完整的推理链(Chain-of-Thought),Student 学习模仿这个推理链。DeepSeek-R1 的蒸馏方案就属于这种,把 DeepSeek-R1(671B)生成的完整推理轨迹,作为训练数据让 Qwen/Llama 等小模型学习。推理能力(先想后答的习惯)比知识内容更容易被蒸馏,一个 7B 的推理蒸馏模型,在数学问题上经常超过没有蒸馏的同量级原生模型。
Teacher-Student 配对策略
Teacher 和 Student 的选择直接影响蒸馏效果。几个实践原则:
Teacher 和 Student 的参数差距不能过大。实验表明,171B Teacher 对 7B Student 的蒸馏,效果通常好于 671B Teacher 对 7B Student。差距太大时 Student 无法有效吸收 Teacher 的输出分布,等效于白盒蒸馏中"层级差距过大导致对齐失效"。经验上,Teacher 是 Student 参数量的 10-30 倍是较优范围。
对于黑盒蒸馏,Teacher 生成数据的多样性很重要。用 temperature=0.7-1.0 生成多个候选回答,再过滤,比 temperature=0.0 生成单一"最佳"回答效果更好。原因是低 temperature 的输出往往缺乏覆盖边界案例的能力。
DeepSeek-R1 蒸馏方案
DeepSeek-R1(671B)的开源方案提供了一个完整的推理能力蒸馏案例。核心步骤:
- 用 DeepSeek-R1 对一批数学、代码、逻辑推理问题生成完整的"思考链 + 答案"(格式为
<think>...</think><answer>...</answer>) - 这批数据(约 800K 条)作为 SFT 训练集
- 在 Qwen2.5-7B/14B/32B 和 Llama-3-8B/70B 上做 SFT 微调
结果是几个 7B-32B 的小模型,在 AIME 2024(美国数学邀请赛)上得分超过了 OpenAI-o1-mini,在 MATH-500 数据集上超过了 GPT-4o。这个结果的重要性在于它证明了一件事:推理能力的蒸馏,可以跨越模型架构和大小,传递给结构完全不同的小模型。
蒸馏的法律风险需要在这里明确说明:OpenAI 在使用条款中禁止用其模型输出训练竞争性模型,Anthropic 有类似限制。使用商业模型做蒸馏教师,需要仔细核查 ToS,否则可能在合规层面产生问题。这不是技术问题,是商业和法律问题。用开源模型(如 DeepSeek-R1、Llama-3 系列)作为 Teacher 则没有这个限制。
蒸馏的局限性
蒸馏并非没有代价,在使用前需要清楚两类限制。
第一类是容量天花板。小模型的参数量决定了它的信息存储上限,无论 Teacher 多强,Student 能吸收的能力都受制于自身的参数量。一个 7B 的 Student 模型,即使用 671B 的 Teacher 蒸馏,在需要长程推理或海量知识检索的任务上仍然无法接近 Teacher。蒸馏擅长传递"推理风格"和"输出格式",不擅长传递"海量参数中沉淀的隐式知识"。
第二类是泛化下降。用 Teacher 在特定任务上生成的数据蒸馏出的模型,往往是一个专注于该任务的"专才",在目标任务上表现很好,但在其他任务上的泛化能力低于同体量的通用模型。如果 Agent 需要处理多种不同类型的任务,过度蒸馏单一任务会导致整体效果变差。
蒸馏 vs 从头训练的取舍
什么时候蒸馏更划算?三个判断维度。
数据稀缺时:如果你的任务领域缺乏公开的大规模训练数据,但有一个强 Teacher 可以按需生成数据,蒸馏是最直接的解法。反之,如果已有大量高质量数据,从头训练的天花板更高。
计算预算受限时:蒸馏产生的小模型,训练成本远低于从头预训练同体量模型。如果目标是在 10 亿参数以内做一个特定领域的强模型,用 GPT-4 或开源大模型做 Teacher 生成数据 + 小模型 SFT,通常比从头预训练性价比高得多。
需要迁移推理能力时:如果目标是让小模型具备推理(CoT)能力,蒸馏比全量 SFT 更有效。全量 SFT 让模型"学会了走流程",而蒸馏在数据层面直接展示了"推理的样子",学得更快、更稳。
蒸馏 vs 微调不是对立选择。常见的最优实践是:先用 Teacher 做黑盒蒸馏产出数据,对该数据做 SFT(= 从蒸馏产出微调),如果还有偏好对齐需求,再接一轮 DPO。三阶段串联,每个阶段解决一个独立的问题。
九、PEFT 家族全景:LoRA 不是唯一的路
Prefix Tuning 和 P-tuning v2
在 LoRA 之前,有另一类参数高效微调方案值得了解:前缀调整(Prefix Tuning,arXiv:2101.00190,Xiang Lisa Li & Percy Liang,ACL 2021)。
Prefix Tuning 的思路是在输入序列前加一段可训练的"虚拟 token"前缀,这些 token 不对应真实词汇,纯粹作为可训练参数存在,影响后续所有 attention 的计算。可训练参数量约为 0.1%,在 table-to-text 和摘要任务上,比同等参数量的 Adapter 高 4.1 BLEU 分。
但 Prefix Tuning 在小模型和分类任务上效果不稳定。P-tuning v2(arXiv:2110.07602,Liu et al., ACL 2022)解决了这个问题:不只在输入层加前缀,而是在每一个 Transformer 层都独立加可训练的 prefix(deep prompt tuning)。用 0.1% 的可训练参数,在 SuperGLUE 等 NLU 任务上匹配了全量微调的效果。P-tuning v2 的实践意义在于:对于需要共享骨干模型、只切换任务头的多任务场景,它是比 LoRA 更合适的选择。不同任务只需要切换各自的 prefix 参数,骨干模型完全共享。
Adapter 方法
Adapter 在每个 Transformer 层的 Attention 和 FFN 子层后串联一个小型瓶颈网络(两个线性层 + 非线性激活),可训练参数量约 0.5-4%。优点是模块化程度高,不同任务可以插拔不同的 Adapter 模块。缺点是推理时会引入额外的计算层,有固定延迟开销,merge 不像 LoRA 那样干净。
综合对比
| 方法 | 可训练参数比例 | 推理延迟 | 适用场景 |
|---|---|---|---|
| Prompt Tuning / IA³ | <0.01% | 极低 | 多任务共享骨干 |
| Prefix Tuning | ~0.1% | 低 | 生成任务 |
| P-tuning v2 | ~0.1% | 低 | NLU 多任务 |
| Adapter | 0.5-4% | 有固定开销 | 模块化扩展 |
| LoRA | 0.2-0.9% | merge 后无延迟 | 工业界综合最优 |
| QLoRA | 同 LoRA | merge 后无延迟 | 单 GPU 大模型微调 |
| 全量微调 | 100% | 无额外延迟 | 知识密集注入 |
LoRA 在工业界是目前综合最优的选择,原因不只是参数效率,更在于"merge 后无推理延迟"这个特性,它让训练时的工程优化和部署时的零成本实现了统一。
十、端到端实施路径
从零到部署的工程检查清单
微调不是一次性的实验,而是一个持续迭代的工程系统。以下是从零开始的参考路径:
阶段一:数据准备(占总工作量 50% 以上)
定义任务的成功指标,必须是可量化的,不是"效果变好",而是"在验证集上的准确率 > 85%" 或 "格式合规率 > 99%"。收集或生成种子数据(100-500 条),人工仔细审核,确认质量基线。用种子数据扩充(模型生成 + 人工过滤)到目标数量。执行精确去重(MD5)和近似去重(MinHash),再跑质量过滤(长度、语言、格式)。最终划分训练集/验证集(9:1 或 8:2),确保验证集覆盖主要使用场景。
阶段二:快速验证(SFT + LoRA)
用小规模数据(1K-5K 条)先跑一个 baseline,验证数据格式对不对、训练流程通不通。学习率 2e-5,3 个 epoch,LoRA r=8,训练完马上在验证集上跑一遍,看指标有没有朝正确方向移动。如果指标没有动,先查数据,不要急着调超参。
阶段三:规模化和对齐
基础 SFT 跑通之后,根据需求决定是否加 DPO。如果有明确的"好回答 vs 差回答"偏好,DPO 能带来明显提升;如果没有这类数据,就不要强加,烂的偏好对比没有偏好对更差。DPO 学习率要比 SFT 低至少一个数量级(1e-6 vs 1e-5),β 从 0.1 开始试。
阶段四:评估和部署
部署前执行 merge_and_unload,消除推理延迟。在一组覆盖主要使用场景的测试案例上跑评估,包含边界案例(格式崩溃、越界请求、领域外问题)。设置灰度流量,对比微调前后的指标,有回滚方案。
资源受限时的优先级
如果算力有限,优先级是: 1. 数据质量 > 模型大小。1K 条高质量数据 + 7B 模型,通常好于 10K 条低质量数据 + 7B 模型 2. QLoRA 让单卡跑更大的模型。7B 用 RTX 3090(24GB)可以跑,13B 也可以跑 3. 不要跳过评估。没有测量,就不知道微调有没有效果。一个最简评估集(50-100 条人工确认的测试案例)是最低投入
微调不是终点。模型上线之后,生产流量里的错误案例是最有价值的新训练数据来源。收集用户反馈中的坏案例,整理成 DPO 的 rejected 样本,下一轮微调会更有针对性。把这个循环建起来,模型才会随时间持续变好,而不是在一次微调后停滞。
微调系统的可观测性
上线之后需要持续监控几个关键指标,否则模型质量的退化很难被及时发现。
格式合规率:如果模型输出有格式要求,每次请求后自动解析输出,统计格式合规率。这是最直接的行为指标,格式合规率下降通常意味着某些输入 pattern 触发了格式崩溃,或者生产分布和训练分布出现了漂移。
拒绝率和兜底率:记录模型说"我不知道"或触发兜底逻辑的比例。拒绝率突然上升,可能说明新来的用户输入超出了模型的训练分布;拒绝率下降到接近零,要警惕模型是否在"硬撑",对不熟悉的问题强行给出答案。
人工采样抽检:每周从生产流量中随机采样 50-100 条,人工评分。这是最贵但最可靠的质量门禁。自动化指标不能覆盖所有维度,人工评分能发现指标看不出来的系统性问题(比如回答语气越来越生硬、越来越常出现特定的错误模式)。
版本对比实验:每次迭代微调前,用 A/B 实验框架对比新旧模型在线上的用户满意度指标。没有对照组的迭代,不知道到底有没有进步。
把这四个指标纳入常规运营,微调就从"一次性实验"变成了"持续优化系统"。这个循环(生产数据 → 采样标注 → 迭代微调 → 上线验证 → 生产数据)是 Agent 模型能力持续提升的核心飞轮。
建议从格式合规率入手,选一个最高频的输出格式,在生产环境里接入自动解析,把这个数字跑起来。有了第一个可测量的指标,后续的迭代就有了锚点。
作者:toy|本文为「Agent 知识丛书」系列第 6/9 篇