模型微调说明(以 Qwen 为例)
这篇文章解决什么问题
很多人第一次接触大模型微调,最常见的困惑不是“代码怎么写”,而是下面这几个更根本的问题:
- 微调到底在改什么
- 它和 prompt、RAG、训练新模型有什么区别
- 什么时候该微调,什么时候不该微调
- 数据应该长什么样
- 最小可运行的 Python 方案应该怎么搭
这篇文章的目标不是把所有训练技术都讲完,而是把最常用的一条路讲清楚:
- 用
Qwen/Qwen2.5-1.5B-Instruct作为 demo 模型 - 用
LoRA做参数高效微调 - 用一份本地
JSONL小样本演示数据做监督微调 - 给出训练脚本和推理脚本
截至 2026-04-22,Qwen2.5 官方 Hugging Face 模型卡明确建议使用最新 Transformers 库;如果版本低于 4.37.0,会遇到 KeyError: 'qwen2'。
先说结论
- 微调不是“教模型从零学会思考”,而是把一个已经会说话的模型,往特定风格、任务和输出方式上再推一段。
- 如果你的问题是“模型不知道最新知识”,优先考虑
RAG,不要先上微调。 - 如果你的问题是“模型会答,但答得不稳定、口吻不统一、格式总跑偏”,微调很有价值。
- 对大多数个人和小团队来说,第一步应该是
SFT + LoRA,而不是全参数微调。 - 这篇 demo 选
Qwen/Qwen2.5-1.5B-Instruct,不是因为它最大,而是因为它足够小、生态成熟、上手成本低,适合先把流程跑通。
微调到底是什么
大模型预训练结束后,已经学会了大量通用语言能力。
微调做的事情,不是重新造一个模型,而是让它在一个更小、更聚焦的数据集上继续训练。
可以把它理解成:
- 预训练:让模型学会“会不会说话”
- 微调:让模型学会“在这个场景里应该怎么说话”
如果写成更工程一点的话,就是:
text
原始模型参数
+ 领域数据 / 指令数据
+ 若干轮额外训练
= 更贴近目标任务的模型它到底改了模型哪一层
这里最容易误解。
微调不是简单保存几条问答样例,也不是把 prompt 固定写进配置文件。
它本质上是在更新参数,让模型对某类输入输出模式更敏感。
如果是全参数微调:
- 模型大部分甚至全部权重都会更新
- 成本高
- 显存占用大
- 产出模型体积也大
如果是 LoRA:
- 原模型权重保持冻结
- 只额外训练一小组可学习的低秩参数
- 训练成本和存储成本都低很多
- 更适合个人和小团队
根据 Hugging Face PEFT 文档,LoRA 的核心思路是:不直接改整块大权重,而是用两块更小的矩阵去表示这次更新,从而显著降低训练参数量。
微调最适合解决什么问题
它最适合下面这些问题:
- 你要固定回复风格,比如客服、销售、法务摘要、内部助手
- 你要固定输出格式,比如 JSON、标签、字段顺序、回复模板
- 你要增强某类任务的稳定性,比如分类、改写、抽取、领域问答
- 你有一批高质量样例,希望模型尽量模仿这种回答方式
它不太适合下面这些问题:
- 你只是想让模型知道“最新事实”
- 你只是缺资料,没有高质量训练样本
- 你只是 prompt 还没写清楚
- 你只是想让模型临时记住一份文档
三种常见方案怎么选
| 方案 | 优点 | 缺点 | 适合什么问题 |
|---|---|---|---|
| 只写 Prompt | 成本最低,改起来最快 | 稳定性有限,复杂格式容易飘 | 规则简单、试错阶段 |
| RAG | 能补新知识,不改模型参数 | 需要检索链路,风格控制一般 | 最新知识、资料问答 |
| 微调 | 风格、格式、任务行为更稳定 | 需要准备数据和训练流程 | 固定场景、固定输出 |
推荐顺序:
- 先把 prompt 写到像样
- 如果问题是“缺知识”,先做 RAG
- 如果问题是“行为和格式不稳定”,再上微调
推荐原因很简单:
- Prompt 最便宜
- RAG 解决知识更新最直接
- 微调最适合解决“行为习惯”问题,而不是“知识缺口”问题
为什么这篇 demo 选 Qwen + LoRA
这篇文章选择的是:
- 模型:
Qwen/Qwen2.5-1.5B-Instruct - 方法:
SFT + LoRA
原因有三点:
1. Qwen2.5-1.5B-Instruct 足够小
它不是玩具模型,但也没有大到一开始就把显存和训练流程卡死。
拿它做第一篇 demo,门槛比较合适。
2. 官方在 Transformers 库 里已经直接支持
Qwen2.5 官方模型卡给了 apply_chat_template 的用法,也明确写了版本要求。
这意味着:
- tokenizer 行为是清楚的
- chat template 是现成的
- 不需要额外写一套奇怪的消息拼接逻辑
3. LoRA 更适合做第一步
这篇文章的重点是先把“微调的基本链路”讲通,不是追求极限压榨显存。
所以这里不用全参数微调,也不先上更复杂的分布式训练。
这次 demo 具体做什么
为了让例子足够直观,这里假设一个场景:
你要做一个 SaaS 产品的客服助手。
你希望模型回复具备这些特点:
- 先给结论
- 再给操作步骤
- 不夸张,不闲聊
- 遇到权限问题会提醒联系管理员
这类需求很适合微调,因为它更像“行为模式”而不是“新增知识库”。
数据应该长什么样
这篇 demo 把原始数据写成最容易维护的三段式:
systemuserassistant
示例文件放在:
examples/qwen-finetune-demo/train.jsonl
样例如下:
json
{"system":"你是企业 SaaS 产品的客服助手。回答要直接、克制、步骤清楚;如果涉及权限限制,要明确提醒联系管理员。","user":"我刚买了专业版,怎么把同事加进团队?","assistant":"可以,路径是“工作台 - 团队管理 - 成员邀请”。先确认你当前账号是否是团队管理员;如果不是,你看不到邀请入口。能看到入口时,输入同事邮箱并发送邀请即可。对方接受后就会进入团队。"}
{"system":"你是企业 SaaS 产品的客服助手。回答要直接、克制、步骤清楚;如果涉及权限限制,要明确提醒联系管理员。","user":"导出的报表为什么一直是空的?","assistant":"先检查筛选条件。最常见原因是时间范围、部门范围或状态条件把数据过滤掉了。你可以先把筛选条件恢复到最宽,再重新导出。如果页面列表本身就没有数据,导出文件也会是空的。"}
{"system":"你是企业 SaaS 产品的客服助手。回答要直接、克制、步骤清楚;如果涉及权限限制,要明确提醒联系管理员。","user":"普通成员能删除项目吗?","assistant":"默认不能。删除项目通常只开放给项目管理员或团队管理员。如果你现在没有看到删除入口,先确认自己的角色;需要处理的话,请联系管理员操作。"}
{"system":"你是企业 SaaS 产品的客服助手。回答要直接、克制、步骤清楚;如果涉及权限限制,要明确提醒联系管理员。","user":"我忘了在哪里开 API Key。","assistant":"入口通常在“设置 - 开发者中心 - API Key”。如果你没有开发者中心入口,通常是权限或套餐限制。先确认当前账号角色和套餐,再决定是否联系管理员开通。"}这只是演示数据。
真正要得到稳定效果,通常需要更多、更干净、风格一致的数据。
先把三个术语讲清楚
1. apply_chat_template 是什么
它是 Transformers 库 里 tokenizer 的一个方法。
作用不是“训练模型”,而是把你手里的对话消息:
systemuserassistant
转换成这个模型真正认识的那种单段文本或 token 序列。
可以把它理解成“把聊天记录翻译成模型的标准进料格式”。
比如你写的是:
python
[
{"role": "system", "content": "你是客服助手"},
{"role": "user", "content": "怎么邀请成员?"},
]模型真正训练和推理时看到的,往往不是这组 Python 字典本身,而是类似下面这种带控制标记的内容:
text
<|system|>
你是客服助手
<|user|>
怎么邀请成员?
<|assistant|>不同模型用的标记不一样。apply_chat_template 就是专门负责这层转换,避免你手写错格式。
2. TRL 是什么
TRL 是 Hugging Face 的一个训练库。
名字原本和 reinforcement learning 有关,但现在它也常被直接拿来做监督微调。
在这篇 demo 里,TRL 主要就是提供:
SFTTrainerSFTConfig
你可以把它理解成:“专门给大模型后训练准备的一套工具箱”。
这篇文章里用它,不是因为一定要做 RL,而是因为它写 SFT 比原生 Trainer 更顺手,和对话数据格式也更贴近。
3. prompt-completion 是什么
它是一种训练数据组织方式,把一条样本拆成两段:
prompt:输入部分completion:模型应该补出来的答案部分
最简单的例子是:
json
{
"prompt": "把下面句子改写得更正式:今天天气不错",
"completion": "今天天气较为晴朗,整体气候条件良好。"
}放到对话场景里,也可以写成:
json
{
"prompt": [
{"role": "system", "content": "你是客服助手"},
{"role": "user", "content": "怎么邀请成员?"}
],
"completion": [
{"role": "assistant", "content": "进入团队管理后发送邀请即可。"}
]
}这类格式的重点是:前半段是题目,后半段是标准答案。
在 TRL SFTTrainer 里,这种格式通常可以只让模型为 completion 部分负责,而不是连 prompt 也一起学着复读。
为什么这里不用原始 messages 直接训练
因为这篇 demo 想把“原始可编辑数据”和“训练输入格式”分开。
原始文件保留成:
- 好读
- 好改
- 好人工审查
训练时再转换成 TRL SFTTrainer 更容易吃进去的 prompt-completion 结构。
根据 Hugging Face TRL 文档,prompt-completion 数据集默认只对 completion 部分计算 loss,这比把整段对话全部拿来训练更稳。
Python 训练脚本
完整脚本放在:
examples/qwen-finetune-demo/train_qwen_lora.py
它做了这几件事:
- 读取本地
JSONL - 转成
prompt-completion对话格式 - 加载 Qwen tokenizer
- 用
LoRA包住模型 - 用
SFTTrainer开始训练 - 把 adapter 和 tokenizer 保存到本地目录
核心代码如下:
python
from pathlib import Path
import torch
from datasets import load_dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import SFTConfig, SFTTrainer
MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
# 按硬件能力选择训练精度;优先用 bf16,其次 fp16,最后退回 fp32
def pick_dtype():
if torch.cuda.is_available():
if torch.cuda.is_bf16_supported():
return torch.bfloat16
return torch.float16
return torch.float32
# 把原始的 system / user / assistant 三段式数据
# 转成 TRL 更容易直接训练的 prompt-completion 结构
def to_prompt_completion(example):
prompt = []
if example["system"].strip():
prompt.append({"role": "system", "content": example["system"].strip()})
prompt.append({"role": "user", "content": example["user"].strip()})
completion = [
{"role": "assistant", "content": example["assistant"].strip()}
]
return {
"prompt": prompt,
"completion": completion,
}
# 训练数据和输出目录都放在脚本同级目录下,方便直接运行 demo
root = Path(__file__).resolve().parent
data_path = root / "train.jsonl"
output_dir = root / "output" / "qwen2_5_1_5b_lora"
# 加载 tokenizer;如果没定义 pad token,就临时复用 eos token
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=False)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 先读 JSONL,再把样本映射成 prompt-completion
dataset = load_dataset("json", data_files=str(data_path), split="train")
dataset = dataset.map(to_prompt_completion, remove_columns=dataset.column_names)
# 加载底座模型;low_cpu_mem_usage 可以减少加载时的内存压力
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
torch_dtype=pick_dtype(),
low_cpu_mem_usage=True,
)
# 训练时关闭 cache,避免和 gradient checkpointing 冲突
model.config.use_cache = False
# 只训练 LoRA adapter,不改原模型大部分参数
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear",
)
# 这是监督微调的训练参数;这里用小 batch 配合梯度累积来降低显存压力
training_args = SFTConfig(
output_dir=str(output_dir),
per_device_train_batch_size=1,
gradient_accumulation_steps=8,
num_train_epochs=3,
learning_rate=1e-4,
logging_steps=1,
save_steps=20,
save_total_limit=2,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
weight_decay=0.01,
max_length=1024,
bf16=torch.cuda.is_available() and torch.cuda.is_bf16_supported(),
fp16=torch.cuda.is_available() and not torch.cuda.is_bf16_supported(),
gradient_checkpointing=True,
report_to="none",
)
# 把模型、数据、tokenizer 和 LoRA 配置交给 TRL 的 SFTTrainer
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
processing_class=tokenizer,
peft_config=peft_config,
)
# 开始训练,并把训练好的 adapter 和 tokenizer 保存下来
trainer.train()
trainer.model.save_pretrained(str(output_dir))
tokenizer.save_pretrained(str(output_dir))这段训练代码为什么这么写
1. 选 Instruct 而不是 Base
这个 demo 的目标不是把一个 base model 训成 chat model,而是做“已有聊天能力上的定向适配”。
所以这里直接拿 Instruct 版更省事。
如果你的目标是:
- 从头建立一套全新指令风格
- 大规模重做聊天行为
- 更深地重塑输出分布
那 Base 会更干净。
但对入门 demo 来说,Instruct 更实用。
2. 数据转成 prompt-completion
这是这篇 demo 的一个刻意选择。
原因是:
- 原始数据文件更容易维护
- 训练时仍然保留 chat 结构
TRL对prompt-completion默认只训练 completion,逻辑更直接
3. target_modules="all-linear"
根据 Hugging Face PEFT 文档,想做 QLoRA 风格的 LoRA 训练时,可以把 target_modules 设成 all-linear,这样不用手写每个模型家族的线性层名字。
这对入门 demo 最省心。
如果后面你发现:
- 显存还是紧
- adapter 参数还是偏多
再往更细的模块选择收。
4. 学习率设得比全参数微调高
TRL 文档对 adapter 训练的建议是:只训练新增参数时,学习率通常会比全参数微调高,常见量级大约在 1e-4 左右。
所以这里用 1e-4 作为起步值。
Python 推理脚本
训练完以后,至少要验证一件事:
这个 adapter 到底有没有把模型往你想要的方向推。
完整脚本放在:
examples/qwen-finetune-demo/chat_with_adapter.py
核心代码如下:
python
from pathlib import Path
import torch
from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer
def pick_dtype():
if torch.cuda.is_available():
if torch.cuda.is_bf16_supported():
return torch.bfloat16
return torch.float16
return torch.float32
root = Path(__file__).resolve().parent
adapter_dir = root / "output" / "qwen2_5_1_5b_lora"
tokenizer = AutoTokenizer.from_pretrained(str(adapter_dir), use_fast=False)
model = AutoPeftModelForCausalLM.from_pretrained(
str(adapter_dir),
torch_dtype=pick_dtype(),
)
model.eval()
messages = [
{
"role": "system",
"content": "你是企业 SaaS 产品的客服助手。回答要直接、克制、步骤清楚;如果涉及权限限制,要明确提醒联系管理员。",
},
{
"role": "user",
"content": "成员邀请邮件一直没收到,应该先查什么?",
},
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer([text], return_tensors="pt")
generated = model.generate(
**inputs,
max_new_tokens=256,
do_sample=False,
)
reply_ids = generated[0][inputs["input_ids"].shape[1]:]
print(tokenizer.decode(reply_ids, skip_special_tokens=True))最小运行方式
先装依赖:
bash
pip install "transformers>=4.37.0" datasets peft trl acceleratetorch 请按你自己的硬件环境单独安装。
如果你是 CUDA 环境,这一步应该按官方 PyTorch 安装说明来,不要盲装通用轮子。
然后运行训练:
bash
python examples/qwen-finetune-demo/train_qwen_lora.py训练日志里的字段怎么读
先说清一点:
你看到的这类输出,不是 safetensors 返回的。
它是 TRL SFTTrainer 在训练过程中打出来的日志。
比如:
python
{
"loss": 2.091,
"grad_norm": 1.751,
"learning_rate": 1.346e-05,
"entropy": 2.781,
"num_tokens": 3.768e+04,
"mean_token_accuracy": 0.5056,
"epoch": 2.39,
}在这篇 demo 里,由于数据被整理成了 prompt-completion,而 TRL 默认只对参与训练的那部分 token 计算指标,所以这里的 loss、entropy、mean_token_accuracy,更应该理解成:
- 主要在看
completion部分 - 不是在看整段
prompt
按当前 Hugging Face TRL SFTTrainer 文档,这几个字段可以这样读:
| 字段 | 它代表什么 | 你这次这个值怎么读 | 一般怎么看 |
|---|---|---|---|
loss | 当前日志区间里,参与训练 token 的平均交叉熵损失 | 2.091 说明模型还在学习,但还没到非常贴合样本的程度 | 通常是往下走更好,但不能单独代表真实效果 |
grad_norm | 梯度的 L2 norm,在 gradient clipping 之前统计 | 1.751 说明这一步梯度规模正常,不像是已经炸掉 | 重点看是不是突然特别大、变成 NaN,而不是追求越小越好 |
learning_rate | 当前这一步实际用到的学习率 | 1.346e-05 表示学习率已经从初始值往下走了 | 结合 scheduler 看是否符合预期,不是单独判断好坏的指标 |
entropy | 模型对下一个 token 预测分布的平均熵 | 2.781 说明模型还有一定不确定性,不是已经死死压成单一答案 | 太高可能说明还不稳,太低也可能说明过早变得过度自信 |
num_tokens | 到当前为止,累计处理过多少 token | 3.768e+04 就是大约 37,680 个 token | 它更像训练进度,不是质量指标 |
mean_token_accuracy | 参与训练 token 里,top-1 预测和标准答案完全一致的比例 | 0.5056 就是大约 50.56% 的 token 预测对了 | 对生成任务只适合看趋势,不适合当最终业务指标 |
epoch | 当前训练走到了第几轮数据 | 2.39 表示已经跑到第 2.39 轮 | 如果总共设了 3 轮,这时大概已经接近训练后段 |
如果把你这条日志直接翻成大白话,大概是:
- 训练已经跑到第
2.39 / 3轮 - 累计看了大约
3.77万个 token - 学习率已经降到
1.346e-05 - 模型对训练答案的 token 级命中率大约
50% - 梯度规模看起来正常,没有明显发散迹象
这里最容易误解的地方有两个:
1. loss 下降,不等于业务效果一定更好
它只能说明模型更会拟合训练样本。
如果数据本身太少、太窄,或者风格过于单一,loss 很漂亮也不代表上线效果漂亮。
2. mean_token_accuracy 不是“回答正确率”
它统计的是 token 级别的命中,不是整条回答有没有答对。
生成任务里,模型就算换了一种同样合理的表达,也可能被算成没命中。
所以这几个字段更适合拿来做:
- 观察训练有没有正常进行
- 判断有没有发散
- 对比不同超参数的训练趋势
不适合直接拿来做:
- 最终业务验收
- 回答质量排名
- “这个模型已经够好了”的唯一依据
训练完后验证:
bash
python examples/qwen-finetune-demo/chat_with_adapter.py训练后应该看什么
不要只看 loss。
至少看这三类结果:
1. 风格有没有更稳定
比如原来回答很散、很长、很爱闲聊。
微调后是不是明显更靠近你的样例风格。
2. 格式有没有更稳定
如果你要求:
- 先结论
- 再步骤
- 再补限制条件
那模型是不是更经常按这个顺序来。
3. 泛化有没有坏掉
不要只拿训练集里的原题去测。
要拿“同类但没见过的问题”测试。
如果它只能复读样本,而不会迁移,那这次微调价值就很有限。
有没有统一的“训练达标线”
没有统一硬标准。
原因很简单:
- 模型大小不同
- tokenizer 不同
- 任务类型不同
- 数据难度不同
- 你到底在追求“格式稳定”还是“开放式回答质量”也不同
所以官方文档通常会告诉你指标是什么意思,但不会告诉你:
loss < 1.8就一定可用mean_token_accuracy > 0.6就一定能上线
这类说法通常都太死。
更实用的做法是把标准拆成三层:
第一层:训练有没有正常进行
这一层先不谈“好不好用”,只看“有没有练坏”。
至少要满足这些:
loss总体在下降,后面逐渐趋稳grad_norm没有频繁爆高,没有出现NaN/inflearning_rate的变化符合你设置的 scheduler- 训练日志没有明显抖成一团
如果连这一层都过不了,后面就不用谈可用。
第二层:验证集上有没有学到东西
这一层才开始接近“可用”。
如果你真的要判断是否达标,最好至少留一份验证集,而不是只看训练集日志。
更稳的判断是:
train loss在降eval loss也在降,或者至少没有越来越坏- 训练集和验证集差距没有持续拉大
如果出现这种情况:
train loss很漂亮eval loss不动,甚至变差
那通常表示:
- 过拟合
- 数据太少
- 样本风格太窄
第三层:业务上能不能先用
这一层才是真正的验收线。
也就是说,你最后要看的不是“训练指标漂不漂亮”,而是:
- 它是不是更符合目标行为
- 它是不是更稳定
- 它是不是更少出错
如果只看训练日志,什么范围可以先算“有戏”
下面这些不是行业硬标准,只是比较实用的经验范围。
而且它们只适合你这种 SFT + prompt-completion 场景下的辅助判断,不适合单独拿来做最终验收。
1. loss
怎么理解更靠谱:
> 3:通常还比较虚,要么还没学到位,要么数据本身噪声比较大2 ~ 3:常见于还在学习中,或者任务本身比较开放1.5 ~ 2.2:很多场景已经开始“有点样子”< 1.5:如果任务本身很窄、答案风格很固定,通常是比较积极的信号
但这里一定要补一句:
- 开放式客服、改写、摘要这类任务,
loss不一定会很低 - 分类、抽取、固定模板输出这类任务,
loss往往能更低
所以 loss = 1.6 在一个开放式任务里可能已经不错,放到固定格式任务里可能还不够稳。
2. mean_token_accuracy
这个值更适合拿来看趋势,不适合直接当“可用率”。
经验上可以这样看:
< 0.3:通常偏弱0.3 ~ 0.5:说明学到了一部分模式,但稳定性通常还一般0.5 ~ 0.7:很多开放式生成任务已经能看出明显改善> 0.7:如果任务比较窄、答案比较固定,通常是很积极的信号
还是那句话:
- 开放式任务里,
0.5左右也可能已经可用 - 固定格式任务里,往往希望更高
3. grad_norm
这个没有公认“合格线”。
你真正要看的是:
- 有没有突然飙很高
- 有没有频繁剧烈跳动
- 有没有直接变成
NaN
像你前面那种 1.x 的量级,通常看起来是正常的。
真正危险的往往不是“它是不是 1.7”,而是“它是不是突然从一段平稳状态跳到很夸张的值”。
4. entropy
这个也没有固定达标线。
更实用的看法是:
- 太高:模型还很不确定
- 太低:可能开始变得过度自信,甚至死板
所以它通常更适合配合 loss 一起看趋势,而不是单独设阈值。
真正能拿来做验收的,应该是这些指标
如果你只是做 demo,可以先人工看结果。
如果你准备把它当成“可用版本”,建议至少给自己定一组业务指标。
按任务类型,常见做法可以这样定:
| 任务类型 | 更有用的验收指标 | 一个比较实用的可用线 |
|---|---|---|
| 固定格式输出 | 格式通过率、字段完整率 | 格式通过率先到 90%~95% 再说 |
| 抽取 / 标注 | Precision、Recall、F1 | F1 >= 0.85 往往已经有业务价值;高风险场景通常要更高 |
| 分类任务 | Accuracy、Macro-F1 | 至少要稳定超过 prompt baseline |
| 客服 / 助手风格 | 人工通过率、风格一致率、严重错误率 | 人工通过率先到 80%+,严重错误率尽量压到 5% 以下 |
| JSON / 结构化输出 | JSON 可解析率、字段命中率 | 可解析率最好接近 100%,否则很难上线 |
上面这些数值也不是行业硬标准,但它们比单看训练 loss 更接近“能不能用”。
一个更稳的判断方法
如果你现在问“练到什么程度先算可用”,我更推荐下面这条线:
- 训练日志正常,没有发散
- 验证集没有明显过拟合
- 人工抽查 20 到 50 条未见样本
- 格式通过率、风格一致率、严重错误率达到你预设阈值
这四条里,前两条是训练线,后两条才是可用线。
常见坑
1. 数据太少,还想要很强泛化
四条样例只能做演示,不可能支撑真正业务效果。
如果你想明显改变模型行为,通常要准备更多高质量样本。
2. 数据风格本身不一致
如果一半样例很简短,一半很啰嗦;一半用“你”,一半用“您”;一半写步骤,一半不写步骤,模型最后只会学得很乱。
3. 想靠微调补“最新知识”
这是最常见的方向错误。
知识会变,文档会变,规则会变。
这类问题通常应该交给 RAG,不是交给微调。
4. 忽略 chat template
Qwen 官方模型卡已经给了 apply_chat_template 的用法。
如果你训练时和推理时的对话格式不一致,效果会被直接拖垮。
5. 只看训练 loss,不做人工验收
loss 下降说明模型更会拟合训练样本,不等于它更适合真实业务。
什么时候该从这个 demo 升级
下面这些情况,说明你该往下一层走了:
- 数据量已经过千,想做更稳的评测和验证集
- 显存太紧,想改成 4-bit QLoRA
- 只做 SFT 不够,想进一步做偏好优化
- 你要的不是聊天风格,而是分类、打标、抽取这类更明确任务
这时你可以继续补:
- 验证集和自动评测
- 更细的 LoRA 模块选择
- 量化训练
- DPO / PPO 这类后训练方法
我的判断
模型微调最容易被神化的地方,是很多人把它当成“让模型变聪明”的总开关。
其实它更像一把校准工具。
你先得知道自己要校准什么:
- 是知识
- 是语气
- 是格式
- 还是任务动作
如果问题没分清,微调很容易变成一轮昂贵的试错。
但如果目标很明确,比如“把一个通用聊天模型,改成稳定、克制、结构固定的客服助手”,那 Qwen + SFT + LoRA 是一条非常实用的起点。