Appearance
模型微调与定制化
很多开发者学到 RAG 之后,很快就会冒出下一个问题:既然模型已经能接知识库了,那我什么时候还需要微调?
这是一个特别容易走偏的话题。因为“微调模型”听起来很强,很多人会下意识把它当成通用解法,仿佛只要把数据喂进去,模型就会更懂业务。但真实情况恰恰相反:如果问题判断错了,微调往往会比 Prompt 和 RAG 更贵、更慢,也更难验证。
所以这页的目标不是教你立刻去训练一个大模型,而是先建立一个适合应用开发者的微调判断框架。
先说结论:微调解决的是什么问题
微调更适合解决下面几类问题:
- 你希望模型长期稳定地遵守某种输出风格或任务格式
- 你有一批高质量样本,希望模型在某类任务上表现更接近你的分布
- 你希望减少大量重复示例塞进 Prompt 的成本
- 你做的不是“查资料回答”,而是“把某类输入稳定变成某类输出”
它不太适合直接解决下面这些问题:
- 希望模型知道最新资料或私有文档内容
- 希望模型引用可追溯来源
- 希望今天更新知识,明天立刻生效
- 当前问题其实只是 Prompt 写得不清楚、Schema 约束不稳、评测还没建起来
Prompt、RAG、微调分别适合什么
可以先把它们理解成三种不同层级的调优手段:
| 方法 | 它主要改变什么 | 适合场景 | 不足 |
|---|---|---|---|
| Prompt | 当前这次调用的任务描述与约束 | 规则清楚、变化快、先快速验证 | 稳定性有限,长 Prompt 成本高 |
| RAG | 给模型看的外部知识 | 文档问答、企业知识库、需要引用来源 | 检索质量差时整体效果会塌 |
| 微调 | 模型在某类任务上的输出习惯和能力分布 | 长期重复任务、稳定格式、固定风格 | 成本更高,验证更难,更新不如 RAG 灵活 |
最常见的误区是:把“模型不知道资料”当成微调问题。
如果你的真实需求是“让模型回答公司文档、产品手册、知识库里的最新内容”,那通常更应该先看 RAG 原理,而不是直接做微调。
微调最常见的几条路径
1. API 微调
这是对应用开发者最友好的路径。你不需要从零训练一个模型,而是基于平台提供的微调能力,上传样本数据、触发训练、拿到新模型再调用。
它的优点是门槛相对低,能帮助你先理解“样本长什么样、训练后该怎么评估”。缺点是可控范围和底层透明度通常有限。
2. SFT
SFT(Supervised Fine-Tuning,监督微调)可以先理解成:给模型一批“输入应该对应什么输出”的示范样本,让它在这类任务上更贴近你的期望。
对大部分应用开发者来说,真正会接触到的“微调”通常首先就是这一路径,而不是更复杂的强化学习或底层预训练。
SFT 的输入不是“把一堆文档丢给模型”。更常见的形式是一条条对话样本,每条样本都写清楚用户输入、系统约束和理想回复。以 OpenAI 的 chat fine-tuning 格式为例,一行 JSONL 大概长这样:
json
{"messages":[{"role":"system","content":"你是一个只输出 JSON 的客服分类助手。"},{"role":"user","content":"我昨天买的耳机还没发货,能帮我查一下吗?"},{"role":"assistant","content":"{\"intent\":\"order_status\",\"urgency\":\"normal\"}"}]}这个样本教的不是“耳机知识”,而是“遇到类似表达时,稳定输出指定 JSON 结构”。如果你把产品手册原文直接塞进训练集,模型可能会记住一些片段,但它不能像 RAG 那样稳定引用来源,也不适合做频繁更新的知识库。
3. LoRA / PEFT
LoRA、PEFT 这类方法的核心价值是:不必大改全部模型参数,也能做相对轻量的定制化训练。
如果你后面真的进入开源模型微调,这会是更现实的一步。但它已经明显超出纯 API 应用开发,涉及模型、显存、训练资源和推理部署的额外复杂度。
LoRA 的直觉可以这样理解:全量微调会直接更新模型里大量权重矩阵,成本很高。LoRA 不直接改原矩阵 W,而是在旁边加一个很小的增量矩阵:
text
W' = W + ΔW
ΔW = A × BA 和 B 的秩很低,参数量远小于原始 W。训练时冻结原模型,只训练这两个小矩阵;推理时再把增量合并或挂载上去。这样做牺牲了一部分自由度,换来更低的显存占用和更快的训练速度。对“把一个开源模型适配到固定领域表达风格”这类任务,它通常比全量微调现实得多。
SFT 指令数据构建详解
数据质量通常比模型选择更重要。很多微调失败不是因为技术栈选错了,而是训练数据有问题。
指令数据的基本格式
以 OpenAI fine-tuning 格式为例,每条样本是一个 messages 数组:
json
{
"messages": [
{"role": "system", "content": "你是一个合同审查助手,只返回 JSON 格式的风险分析。"},
{"role": "user", "content": "以下合同条款是否存在风险:服务方有权随时终止合同,无需提前通知。"},
{"role": "assistant", "content": "{\"risk_level\": \"high\", \"risk_type\": \"termination_clause\", \"reason\": \"无提前通知要求,甲方权益无法保障\"}"}
]
}每条样本都需要 system / user / assistant 三部分。system 定义角色和格式约束,user 是输入,assistant 是期望输出。
数据量指导原则
微调数据没有"越多越好"的简单规律:
| 场景 | 建议样本量 | 说明 |
|---|---|---|
| 验证流程可行性 | 50-100 条 | 先跑通,再扩量 |
| 单一任务格式微调 | 200-500 条 | 格式稳定任务通常不需要太多 |
| 多类别分类 | 每类 50-100 条 | 保持类别平衡 |
| 复杂生成任务 | 1000+ 条 | 需要更多样本覆盖变化 |
提升数据质量的实用方法
一、从真实输入里采样,而不是手写假数据。真实用户的表达方式通常比你想象的更多样:更口语化、有错字、意图模糊、混杂多种需求。训练数据要覆盖这些变化。
二、先用 GPT-4 生成初始样本,再人工审核和修改。不要直接把 GPT-4 的输出当训练数据——它的输出风格会被微调后的模型学走,可能不是你真正想要的格式。把它作为起草工具,人工确认后再用。
三、为每个边界场景单独设计样本:
text
- 空输入或极短输入
- 包含多个意图的输入
- 语言混杂(中英文混合)
- 格式完全不符合预期的输入
- 需要礼貌拒绝的场景四、把"不应该回答什么"也写进去。纯正面样本训练出来的模型,遇到超出范围的输入时可能会乱答。加入若干拒绝样本:
json
{
"messages": [
{"role": "system", "content": "你只处理订单相关查询。"},
{"role": "user", "content": "帮我写一首诗"},
{"role": "assistant", "content": "抱歉,我只能帮您查询订单状态、物流信息和退款问题。请问您有订单相关的问题吗?"}
]
}数据标注一致性
多人标注时最容易出现标注不一致。比如同样的用户投诉,一个人标为 refund,另一个人标为 complaint。这种不一致会直接降低微调效果。
建议做法:
- 写标注规范文档,明确每个类别的边界
- 抽样做双人标注,计算 Cohen's Kappa 系数(一致性指标),低于 0.7 要重新对齐
- 定期把"标注存疑"的样本拿出来集中讨论
SFT 的完整工作流
一个可靠的 SFT 流程通常分成四步。每一步都很普通,但跳过任何一步都容易把问题拖到线上。
1. 定义任务边界
先把任务写成一句可以验证的话,例如:
- 把客服消息分类为
refund、order_status、shipping、other - 把用户需求转换成固定字段的 JSON
- 把长文本压缩成 200 字以内的结构化摘要
如果任务边界写不清,训练数据就会变成杂烩。SFT 对这种混乱很敏感,因为它会学习样本里的所有模式,包括你不想要的坏习惯。
2. 准备训练集和验证集
训练集用于更新模型,验证集用于判断模型有没有真的泛化。不要把同一批样本既拿来训练又拿来评估,这会让结果看起来很好,线上却不稳定。
我更建议一开始做小而干净的数据集,例如:
- 100-300 条高质量样本先跑通流程
- 每条样本只覆盖一个明确任务
- 保留 20%-30% 作为验证集
- 单独收集边界样本,比如空输入、口语化输入、多意图输入
数据越早被人工抽查,越省钱。等模型训练完才发现标签风格不一致,返工成本会高很多。
3. 触发训练
API 微调的最小流程是:上传 JSONL 文件,创建 fine-tuning job,等待任务完成,再用新模型名调用。下面是一个可以直接改的 Python 示例:
python
from openai import OpenAI
client = OpenAI()
# train.jsonl 每一行都是 {"messages": [...]} 格式
training_file = client.files.create(
file=open("train.jsonl", "rb"),
purpose="fine-tune",
)
job = client.fine_tuning.jobs.create(
training_file=training_file.id,
model="gpt-4o-mini-2024-07-18",
suffix="support-intent-v1",
)
print("job id:", job.id)
# 稍后轮询或在控制台查看;status 为 succeeded 后会有 fine_tuned_model
current = client.fine_tuning.jobs.retrieve(job.id)
print(current.status, current.fine_tuned_model)真实项目里还要加验证文件、训练任务轮询、失败告警和模型版本记录。示例故意没有封装成框架,是为了让你看清楚核心 API 调用。
4. 对比评估
评估不能只看“回答是不是更像训练样本”。至少要比较这些指标:
- 任务准确率,比如分类是否命中、字段是否抽取正确
- 格式稳定性,比如 JSON 是否可解析、字段是否缺失
- 边界输入表现,比如多意图、缺少上下文、无关问题
- 成本和延迟,微调模型是否真的减少了 prompt 长度或重试次数
如果微调后只是常规样本变好了,但边界样本明显变差,这个模型不应该直接上线。
LoRA 原理深入
LoRA 是当前最流行的参数高效微调方法,值得多说一些,因为它在开源模型微调中几乎是标配。
为什么全量微调不现实
一个 7B 参数的模型,全量微调时每个参数都要存储权重、梯度和优化器状态,显存需求通常超过 80GB。这已经是多张高端 GPU 才能覆盖的量级,一般开发团队很难承受。
LoRA 的核心思路
LoRA 基于一个观察:预训练模型的权重矩阵通常具有低秩特性——矩阵的有效信息维度远低于矩阵本身的维度。因此,任务适配的增量也可以用低秩矩阵来近似表达。
具体做法是:冻结原始权重矩阵 W(维度 d × d),在旁边挂载两个小矩阵 A(d × r)和 B(r × d),其中 r 远小于 d(比如 r=8 或 r=16)。
前向传播时:
text
输出 = x × W + x × (A × B) × scale_factorA 用随机初始化,B 初始化为全零(确保训练开始时增量为零)。训练时只更新 A 和 B,冻结原始 W。
参数量的差异有多大
假设原矩阵是 4096×4096,全量微调需要更新约 1680 万个参数。用 r=16 的 LoRA,只需要更新 4096×16 + 16×4096 ≈ 13 万个参数,大约是全量的 0.78%。这就是它能大幅降低显存需求的原因。
关键超参数
r(秩):控制 LoRA 矩阵的宽度,越大容量越强,通常选 4-64lora_alpha:缩放系数,常设为 r 的 1-2 倍,控制 LoRA 输出的幅度target_modules:选择哪些层加 LoRA,通常是 attention 里的 q、k、v、o 矩阵- dropout:防止过拟合,通常设 0.05-0.1
推理时怎么用
训练完后有两种用法:一是保持分离状态(基础模型 + LoRA 适配器单独加载,可以快速切换不同任务);二是合并(把增量加回原始权重,得到一个不含额外开销的新模型)。
PEFT 三种方法对比
PEFT(Parameter-Efficient Fine-Tuning)是参数高效微调方法的统称,LoRA 是其中最主流的,另外还有 Prefix Tuning 和 Adapter:
| 方法 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| LoRA | 在权重矩阵旁加低秩增量矩阵 | 推理无额外延迟(可合并)、参数效率高 | 需要选好 target_modules |
| Prefix Tuning | 在输入序列前加可训练的前缀 token | 原模型完全冻结,便于多任务 | 会占用 context window |
| Adapter | 在 Transformer 层内插入小型 MLP 模块 | 结构清晰,不同任务 Adapter 可分离 | 推理时有额外前向计算开销 |
实际使用中,LoRA 和其变体是目前最常见的选择。QLoRA 是 LoRA 的进一步变体:在 LoRA 基础上,把基础模型用 4-bit 量化加载,进一步降低显存。这让在消费级 GPU(24GB 显存)上微调 7B 甚至 13B 模型成为可能。
DPO 和 RLHF 的边界
SFT 学的是“给定输入时,理想输出长什么样”。RLHF 和 DPO 处理的是另一个问题:当多个答案都说得过去时,模型应该偏向哪一种。
RLHF(Reinforcement Learning from Human Feedback)通常会先训练一个奖励模型,让奖励模型判断哪个回答更符合人类偏好,再用强化学习方法优化语言模型。它的系统链路比较长,工程复杂度高,不是普通应用团队会轻易自建的东西。
DPO(Direct Preference Optimization)省掉了显式奖励模型。它直接使用偏好数据,例如同一个问题下有 chosen 和 rejected 两个回答,让模型更倾向生成被选择的那类答案。你可以把它理解成更直接的偏好对齐训练。
它们和 SFT 的关系可以这样记:
| 方法 | 训练数据长什么样 | 主要解决什么 |
|---|---|---|
| SFT | 输入 + 标准答案 | 学会完成某类任务 |
| RLHF | 回答偏好 + 奖励模型 + 强化学习 | 按人类偏好调整行为 |
| DPO | 同一输入下的 preferred / rejected 回答 | 更轻量地做偏好对齐 |
应用开发者最先要掌握的是 SFT。只有当你已经有稳定任务能力,还要进一步调整“回答风格、拒答边界、风险偏好”时,才需要讨论 DPO 或 RLHF。
训练优化器与混合精度
优化器选择
微调 LLM 最常用的是 AdamW。相比原始 Adam,它修正了权重衰减的实现方式(真正的 L2 正则化),在 Transformer 上效果更稳定。大多数情况下,AdamW 是安全的默认选择。
典型超参数设置(以 LoRA 微调为例):
python
from torch.optim import AdamW
from transformers import get_cosine_schedule_with_warmup
optimizer = AdamW(
trainable_params,
lr=2e-4, # LoRA 微调常用学习率
weight_decay=0.01,
)
# warmup + cosine decay:先从 0 升到目标学习率,再按余弦曲线降低
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=100,
num_training_steps=total_steps,
)学习率选择经验:
- 全量微调通常用 1e-5 ~ 5e-5
- LoRA 微调可以稍高,2e-4 ~ 5e-4 较常见
- 太高会导致训练不稳定,太低会导致收敛慢或不收敛
混合精度训练
现代 GPU 对 fp16 或 bf16 的计算速度远高于 fp32。混合精度训练(AMP)的思路是:前向传播用低精度,梯度累积时用高精度(fp32),再更新权重。
python
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for batch in dataloader:
with autocast(dtype=torch.bfloat16): # 推荐 bf16
outputs = model(**batch)
loss = outputs.loss
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()为什么推荐 bf16 而不是 fp16:bf16 和 fp32 的数值范围相同,只是精度稍低,不容易出现 fp16 训练时常见的数值溢出问题(loss 变成 NaN)。大多数现代 GPU(A100、H100、RTX 4090)和 Apple Silicon 都支持 bf16。
微调效果评估
训练过程中的指标
- 训练 Loss:正常应该单调下降。如果振荡剧烈或不降,说明学习率过高或数据有问题
- 验证 Loss:应该跟着训练 Loss 一起下降;如果训练 Loss 降但验证 Loss 升,是过拟合信号
- 评测指标曲线:最好在训练中途也跑一次任务评测,不要等到最后才发现方向不对
任务特定指标
不同任务需要不同的评估维度:
| 任务类型 | 推荐指标 |
|---|---|
| 分类 | Accuracy、F1、混淆矩阵 |
| 结构化输出(JSON) | 字段抽取准确率、格式合法率(能否 json.loads) |
| 摘要/生成 | ROUGE、人工评分、关键信息覆盖率 |
| 对话/客服 | 任务完成率、回退率、人工抽检满意度 |
对比基准
微调效果需要对比才有意义。至少建立三个基准:
- 微调前的基础模型:用相同 prompt 和评测集
- 最优 Prompt 工程版本:用精心调优的 system prompt + few-shot 示例,代表不微调时的上限
- 微调后的模型:对比以上两个基准
如果微调后的结果只是勉强超过"精心调 Prompt"的版本,性价比可能不高。
线上评估
线下指标好看不代表线上表现好。还需要:
- A/B 测试:同一批流量,一部分走原模型,一部分走微调模型,对比关键业务指标
- 错误分析:收集线上失败案例,分析是哪类输入导致的,是否有系统性问题
- 成本对比:微调后是否减少了 token 用量(更短的 prompt、更少的重试)
- 延迟对比:微调模型的推理延迟是否满足业务要求
做微调之前,先问自己这 5 个问题
1. 我的问题真的是“知识缺失”吗?
如果是知识缺失,优先考虑 RAG;如果是输出习惯、格式稳定、任务分布问题,才更像微调。
2. 我已经把 Prompt 做到合理水平了吗?
如果 Prompt 还混乱、字段定义不稳、示例很少、约束没写清楚,微调只会把混乱放大。
3. 我有足够好的样本吗?
微调最怕“数据量不大,但质量也不高”。样本不一致、标签风格混乱、任务边界模糊,最后往往只会得到一个不稳定的新模型。
4. 我有评测集吗?
如果你训练前后没有一组稳定测试集,你就无法判断模型到底是真的变好了,还是只是换了种方式出错。
5. 我愿意承担更新成本吗?
RAG 的知识可以随文档更新;微调通常意味着重新准备数据、重新训练、重新验证。你的业务是否值得这套成本,是必须先想清楚的问题。
数据准备时最容易踩的坑
样本看起来很多,其实任务不一致
比如你把摘要、分类、改写、问答全混在一个训练集中,却没有明确说明任务边界。这样做通常不会得到“更聪明”的模型,只会得到更混乱的输出。
只看训练样本,不看线上输入
如果训练数据和真实业务输入分布差异太大,模型在线上仍然会失真。所以数据准备不只是“多收集一些例子”,而是要保证样本像真实输入。
没有负例和边界样本
模型最容易出问题的往往不是常规样本,而是边界条件、不完整输入、异常格式、模糊需求。训练和评测里不覆盖这些,线上就会暴露出来。
过拟合和灾难性遗忘怎么防
微调的两个常见风险,一个是过拟合,一个是灾难性遗忘。
过拟合指模型把训练集记得太死。训练样本里的固定措辞、错误格式、特殊案例,都可能被模型当成通用规则。上线后遇到稍微不同的表达,它就开始失真。
灾难性遗忘指模型在学新任务时,把原来已经会的能力削弱了。小模型、数据分布很窄、训练轮数过多时更容易出现这个问题。比如你只用客服分类样本微调,结果模型对普通问答、拒答边界或多语言输入的表现都变差了。
工程上可以用这些办法降低风险:
- 训练集保持任务一致,但表达方式要有变化,不要只复制一种模板
- 保留验证集和回归评测集,训练后同时测新任务和基础能力
- 控制 epoch 数和学习率,先小步试,不要一上来长时间训练
- 在训练集中加入少量通用能力样本或拒答边界样本,防止模型把世界缩成单一任务
- 给微调模型做版本管理,保留可回滚的基座模型调用路径
- 上线先灰度,只让一部分流量使用新模型,观察错误类型再扩大
我会把“评测集”放在这里的第一优先级。没有评测集,过拟合和遗忘都只能靠感觉判断,而感觉在模型评估里不太可靠。
一个最小判断流程
你可以用下面这条顺序来决定是否进入微调:
- 先明确任务目标:是知识增强,还是输出定制?
- 先把 Prompt、Structured Output、工具流程做稳。
- 如果涉及知识接入,先验证 RAG 是否已经足够。
- 建一组小而稳定的评测样本。
- 只有当问题持续稳定存在,且样本明确可收集时,再进入微调。
和其他章节怎么配合
- 如果你还分不清什么时候该接知识库:先看 RAG 原理
- 如果你还没把输出结构做稳:先看 Structured Output
- 如果你还没有评测意识:先看 AI 应用评测
- 如果你还没建立系统视角:再回看 AI 应用系统设计
小结
微调不是 AI 应用开发的起点,也不是默认答案。先把 Prompt、RAG、工具、评测这些基础链路做稳,再决定某个场景是否值得进入微调。
顺序想清楚,微调就是一项普通的工程选择。
常见面试考点
微调方向的题目容易问得很宽,回答时先把“什么时候该微调”讲清楚,再谈具体方法:
- SFT / RLHF / DPO 区别:SFT 用示范样本学习任务输出;RLHF 通过奖励模型和强化学习做偏好对齐;DPO 直接用 chosen / rejected 偏好样本优化模型。
- OpenAI API 微调流程:准备 JSONL 对话样本,上传训练文件,调用
client.fine_tuning.jobs.create创建任务,训练完成后用fine_tuned_model做线上调用。 - LoRA / PEFT 原理:冻结原模型权重,只训练低秩增量矩阵
A × B,用较少参数适配新任务,适合显存有限的开源模型定制。 - 微调 vs RAG:知识更新、私有文档问答和引用来源通常优先 RAG;输出风格、任务格式和固定分布才更像微调问题。
- 数据质量:样本一致性、真实输入分布、边界样本、负例和验证集划分,比单纯数据量更关键。
- 效果评估:训练前要有稳定评测集,对比微调前后在准确性、格式稳定性、幻觉、成本、延迟和边界输入上的变化。
- 过拟合与遗忘防护:控制训练轮数和学习率,保留回归评测,加入边界样本,做模型版本管理和灰度发布。