Skip to content

模型微调与定制化

很多开发者学到 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 × B

AB 的秩很低,参数量远小于原始 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. 定义任务边界

先把任务写成一句可以验证的话,例如:

  • 把客服消息分类为 refundorder_statusshippingother
  • 把用户需求转换成固定字段的 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),在旁边挂载两个小矩阵 Ad × r)和 Br × d),其中 r 远小于 d(比如 r=8 或 r=16)。

前向传播时:

text
输出 = x × W + x × (A × B) × scale_factor

A 用随机初始化,B 初始化为全零(确保训练开始时增量为零)。训练时只更新 AB,冻结原始 W

参数量的差异有多大

假设原矩阵是 4096×4096,全量微调需要更新约 1680 万个参数。用 r=16 的 LoRA,只需要更新 4096×16 + 16×4096 ≈ 13 万个参数,大约是全量的 0.78%。这就是它能大幅降低显存需求的原因。

关键超参数

  • r(秩):控制 LoRA 矩阵的宽度,越大容量越强,通常选 4-64
  • lora_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)省掉了显式奖励模型。它直接使用偏好数据,例如同一个问题下有 chosenrejected 两个回答,让模型更倾向生成被选择的那类答案。你可以把它理解成更直接的偏好对齐训练。

它们和 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、人工评分、关键信息覆盖率
对话/客服任务完成率、回退率、人工抽检满意度

对比基准

微调效果需要对比才有意义。至少建立三个基准:

  1. 微调前的基础模型:用相同 prompt 和评测集
  2. 最优 Prompt 工程版本:用精心调优的 system prompt + few-shot 示例,代表不微调时的上限
  3. 微调后的模型:对比以上两个基准

如果微调后的结果只是勉强超过"精心调 Prompt"的版本,性价比可能不高。

线上评估

线下指标好看不代表线上表现好。还需要:

  • A/B 测试:同一批流量,一部分走原模型,一部分走微调模型,对比关键业务指标
  • 错误分析:收集线上失败案例,分析是哪类输入导致的,是否有系统性问题
  • 成本对比:微调后是否减少了 token 用量(更短的 prompt、更少的重试)
  • 延迟对比:微调模型的推理延迟是否满足业务要求

做微调之前,先问自己这 5 个问题

1. 我的问题真的是“知识缺失”吗?

如果是知识缺失,优先考虑 RAG;如果是输出习惯、格式稳定、任务分布问题,才更像微调。

2. 我已经把 Prompt 做到合理水平了吗?

如果 Prompt 还混乱、字段定义不稳、示例很少、约束没写清楚,微调只会把混乱放大。

3. 我有足够好的样本吗?

微调最怕“数据量不大,但质量也不高”。样本不一致、标签风格混乱、任务边界模糊,最后往往只会得到一个不稳定的新模型。

4. 我有评测集吗?

如果你训练前后没有一组稳定测试集,你就无法判断模型到底是真的变好了,还是只是换了种方式出错。

5. 我愿意承担更新成本吗?

RAG 的知识可以随文档更新;微调通常意味着重新准备数据、重新训练、重新验证。你的业务是否值得这套成本,是必须先想清楚的问题。

数据准备时最容易踩的坑

样本看起来很多,其实任务不一致

比如你把摘要、分类、改写、问答全混在一个训练集中,却没有明确说明任务边界。这样做通常不会得到“更聪明”的模型,只会得到更混乱的输出。

只看训练样本,不看线上输入

如果训练数据和真实业务输入分布差异太大,模型在线上仍然会失真。所以数据准备不只是“多收集一些例子”,而是要保证样本像真实输入。

没有负例和边界样本

模型最容易出问题的往往不是常规样本,而是边界条件、不完整输入、异常格式、模糊需求。训练和评测里不覆盖这些,线上就会暴露出来。

过拟合和灾难性遗忘怎么防

微调的两个常见风险,一个是过拟合,一个是灾难性遗忘。

过拟合指模型把训练集记得太死。训练样本里的固定措辞、错误格式、特殊案例,都可能被模型当成通用规则。上线后遇到稍微不同的表达,它就开始失真。

灾难性遗忘指模型在学新任务时,把原来已经会的能力削弱了。小模型、数据分布很窄、训练轮数过多时更容易出现这个问题。比如你只用客服分类样本微调,结果模型对普通问答、拒答边界或多语言输入的表现都变差了。

工程上可以用这些办法降低风险:

  • 训练集保持任务一致,但表达方式要有变化,不要只复制一种模板
  • 保留验证集和回归评测集,训练后同时测新任务和基础能力
  • 控制 epoch 数和学习率,先小步试,不要一上来长时间训练
  • 在训练集中加入少量通用能力样本或拒答边界样本,防止模型把世界缩成单一任务
  • 给微调模型做版本管理,保留可回滚的基座模型调用路径
  • 上线先灰度,只让一部分流量使用新模型,观察错误类型再扩大

我会把“评测集”放在这里的第一优先级。没有评测集,过拟合和遗忘都只能靠感觉判断,而感觉在模型评估里不太可靠。

一个最小判断流程

你可以用下面这条顺序来决定是否进入微调:

  1. 先明确任务目标:是知识增强,还是输出定制?
  2. 先把 Prompt、Structured Output、工具流程做稳。
  3. 如果涉及知识接入,先验证 RAG 是否已经足够。
  4. 建一组小而稳定的评测样本。
  5. 只有当问题持续稳定存在,且样本明确可收集时,再进入微调。

和其他章节怎么配合

小结

微调不是 AI 应用开发的起点,也不是默认答案。先把 Prompt、RAG、工具、评测这些基础链路做稳,再决定某个场景是否值得进入微调。

顺序想清楚,微调就是一项普通的工程选择。

常见面试考点

微调方向的题目容易问得很宽,回答时先把“什么时候该微调”讲清楚,再谈具体方法:

  1. SFT / RLHF / DPO 区别:SFT 用示范样本学习任务输出;RLHF 通过奖励模型和强化学习做偏好对齐;DPO 直接用 chosen / rejected 偏好样本优化模型。
  2. OpenAI API 微调流程:准备 JSONL 对话样本,上传训练文件,调用 client.fine_tuning.jobs.create 创建任务,训练完成后用 fine_tuned_model 做线上调用。
  3. LoRA / PEFT 原理:冻结原模型权重,只训练低秩增量矩阵 A × B,用较少参数适配新任务,适合显存有限的开源模型定制。
  4. 微调 vs RAG:知识更新、私有文档问答和引用来源通常优先 RAG;输出风格、任务格式和固定分布才更像微调问题。
  5. 数据质量:样本一致性、真实输入分布、边界样本、负例和验证集划分,比单纯数据量更关键。
  6. 效果评估:训练前要有稳定评测集,对比微调前后在准确性、格式稳定性、幻觉、成本、延迟和边界输入上的变化。
  7. 过拟合与遗忘防护:控制训练轮数和学习率,保留回归评测,加入边界样本,做模型版本管理和灰度发布。

面向开发者系统学习 AI 应用开发、RAG、Agent 与 Vibe Coding。