Skip to content

模型微调说明(以 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能补新知识,不改模型参数需要检索链路,风格控制一般最新知识、资料问答
微调风格、格式、任务行为更稳定需要准备数据和训练流程固定场景、固定输出

推荐顺序:

  1. 先把 prompt 写到像样
  2. 如果问题是“缺知识”,先做 RAG
  3. 如果问题是“行为和格式不稳定”,再上微调

推荐原因很简单:

  • 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 把原始数据写成最容易维护的三段式:

  • system
  • user
  • assistant

示例文件放在:

  • 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 的一个方法。
作用不是“训练模型”,而是把你手里的对话消息:

  • system
  • user
  • assistant

转换成这个模型真正认识的那种单段文本或 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 主要就是提供:

  • SFTTrainer
  • SFTConfig

你可以把它理解成:“专门给大模型后训练准备的一套工具箱”。
这篇文章里用它,不是因为一定要做 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

它做了这几件事:

  1. 读取本地 JSONL
  2. 转成 prompt-completion 对话格式
  3. 加载 Qwen tokenizer
  4. LoRA 包住模型
  5. SFTTrainer 开始训练
  6. 把 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 结构
  • TRLprompt-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 accelerate

torch 请按你自己的硬件环境单独安装。
如果你是 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 计算指标,所以这里的 lossentropymean_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到当前为止,累计处理过多少 token3.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 / inf
  • learning_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、F1F1 >= 0.85 往往已经有业务价值;高风险场景通常要更高
分类任务Accuracy、Macro-F1至少要稳定超过 prompt baseline
客服 / 助手风格人工通过率、风格一致率、严重错误率人工通过率先到 80%+,严重错误率尽量压到 5% 以下
JSON / 结构化输出JSON 可解析率、字段命中率可解析率最好接近 100%,否则很难上线

上面这些数值也不是行业硬标准,但它们比单看训练 loss 更接近“能不能用”。

一个更稳的判断方法

如果你现在问“练到什么程度先算可用”,我更推荐下面这条线:

  1. 训练日志正常,没有发散
  2. 验证集没有明显过拟合
  3. 人工抽查 20 到 50 条未见样本
  4. 格式通过率、风格一致率、严重错误率达到你预设阈值

这四条里,前两条是训练线,后两条才是可用线。

常见坑

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 是一条非常实用的起点。

参考链接

基于 VitePress 的个人知识库骨架