Qwen3.5-9B 微调避坑:用 LLaMA-Factory 做企业 SFT 的 10 个踩坑全记录
适用版本:LLaMA-Factory v0.9.4(2025-12-31)、Transformers v5、PyTorch CUDA 13
训练模型:Qwen3.5-9B(2026-03-02 发布)
硬件环境:单卡 A100 40G(LoRA)/ 单卡 RTX 4090 24G(QLoRA)
前言
用 vLLM 把 Qwen3.5-9B 跑起来之后,下一步想让模型学会公司特定的回复风格和专业术语,这就得微调了。LLaMA-Factory 是目前最主流的开源微调框架(ACL 2024 论文,GitHub 40k+ Star),支持 100+ 模型、多种训练方法,不写一行代码就能开始训练。
听起来很美好,实际上坑不少。Qwen3.5-9B 本身有推理模式/非推理模式之分,加上 v0.9.4 版本的不少 API 变更,网上的教程很多已经过时。
本文记录从数据准备到 vLLM 部署的全链路 10 个真实踩坑,每个坑都给出可直接用的修复命令或配置。
文章结构:
- 环境搭建(新旧版本差异)
- Qwen3.5-9B 的 template 选择陷阱
- 数据格式踩坑(4 个坑)
- 训练配置踩坑(3 个坑)
- 模型合并与导出踩坑(2 个坑)
- 微调后接入 vLLM 踩坑(1 个坑)
- 训练曲线诊断指南
一、环境搭建
v0.9.4 推荐安装方式:改用 uv
v0.9.4 把包管理器从 pip 迁移到了 uv,旧的 pip 安装方式会遇到依赖冲突(尤其是 Transformers v5 和旧版 bitsandbytes 不兼容):
# 1. 安装 uv(如果还没装)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. clone 仓库(--depth 1 只拉最新提交,省带宽)
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
# 3. 创建虚拟环境并安装(uv 自动处理 CUDA 版本匹配)
uv venv --python 3.12
source .venv/bin/activate
uv pip install -e ".[torch,bitsandbytes,metrics]"
# 4. 验证安装
llamafactory-cli version
# v0.9.4 还支持快捷命令 lmf(等价于 llamafactory-cli)
lmf version
Transformers v5 兼容性:v0.9.4 已全面迁移到 Transformers v5,如果你机器上有旧项目依赖 Transformers v4,务必用
uv venv创建独立虚拟环境,不要混用。
从 ModelScope 下载模型(国内推荐)
Qwen3.5-9B 在 HuggingFace 下载经常断线,推荐走 ModelScope:
# 设置环境变量,llamafactory-cli 自动走 ModelScope
export USE_MODELSCOPE_HUB=1
# 模型 ID 格式:Qwen/Qwen3.5-9B-Instruct(ModelScope 和 HuggingFace 同名)
# 也可以提前手动下载
pip install modelscope
modelscope download --model Qwen/Qwen3.5-9B-Instruct --local_dir /data/models/Qwen3.5-9B-Instruct
二、Qwen3.5-9B 的 Template 选择陷阱(最高频踩坑)
坑1:template 选错,模型输出满是 <think> 标签
Qwen3.5-9B 继承了 Qwen3 系列的双模式设计:思维链模式和普通对话模式,在 LLaMA-Factory 里对应两个不同的 template:
| template | 模式 | 行为 |
|---|---|---|
qwen3 |
思维链模式(默认) | 输出包含 <think>...</think> 推理过程,再给出最终答案 |
qwen3_nothink |
普通对话模式 | 直接输出答案,无思维链 |
企业 SFT 的数据通常是"问题-答案"对,不含思维链。如果用 qwen3 模板训练,模型会在输出里疯狂生成 <think> 占位符,消耗大量 token:
# ❌ 错误:用了思维链模板训练普通对话数据
template: qwen3
# ✅ 正确:企业 SFT(问答、风格迁移、领域知识)用 nothink 模板
template: qwen3_nothink
⚠️ 关键原则:训练时用了哪个 template,推理时必须用同一个 template。训练用
qwen3_nothink,推理就也要用qwen3_nothink,不能混用,否则输出格式完全乱掉。
坑2:用 Base 模型做 SFT,LoRA 训练后生成不停止
Qwen3.5-9B 有两个版本:Base(预训练基座)和 Instruct(经过指令对齐的对话版)。很多人误以为用 Base 做 SFT 效果会更纯粹,实际上有一个已知 Bug:
Base 模型 + LoRA 微调后,推理时生成不会停止,模型不知道何时输出 EOS(结束符),会一直循环输出直到达到 max_tokens 上限。
原因:Base 模型没有经过 RLHF 对齐,EOS token 的使用权重极低,LoRA 的参数量不足以让模型重新学会停止生成。
# ❌ 踩坑:Base 模型 + LoRA = 生成不停止
model_name_or_path: Qwen/Qwen3.5-9B # Base 模型
# ✅ 正确:用 Instruct 版本做 SFT 起点
model_name_or_path: Qwen/Qwen3.5-9B-Instruct
如果业务上确实需要从 Base 模型开始,必须改用全量微调(finetuning_type: full),代价是显存需求从 ~20G 升到 ~80G+。
三、数据格式踩坑
LLaMA-Factory 支持 Alpaca 和 ShareGPT 两种格式,企业场景推荐 ShareGPT(支持多轮对话,格式更灵活)。
ShareGPT 格式示例
[
{
"conversations": [
{
"from": "human",
"value": "我们公司的差旅报销政策是什么?"
},
{
"from": "gpt",
"value": "根据公司2026年差旅管理规定:\n1. 经济舱机票实报实销,上限3000元/次\n2. 酒店标准:一线城市500元/晚,二线城市350元/晚\n3. 需在出行后5个工作日内提交报销申请..."
}
]
}
]
注册到 data/dataset_info.json:
{
"company_qa": {
"file_name": "company_qa.json",
"formatting": "sharegpt",
"columns": {
"messages": "conversations"
}
}
}
坑3:数据里混入了 <think> 标签,污染训练
如果你的训练数据来自已有的 Qwen3 / DeepSeek-R1 模型输出,数据里可能带有 <think>...</think> 思维链标签。用这样的数据训练 qwen3_nothink 模板,模型会学到错误的输出格式。
# 数据清洗脚本:去除 <think> 块
import re, json
def clean_think_tags(text: str) -> str:
# 去除 <think>...</think> 及其内容
cleaned = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
return cleaned.strip()
with open('raw_data.json') as f:
data = json.load(f)
cleaned = []
for item in data:
for conv in item['conversations']:
if conv['from'] == 'gpt':
conv['value'] = clean_think_tags(conv['value'])
# 过滤掉清洗后答案为空的条目
if all(c['value'].strip() for c in item['conversations']):
cleaned.append(item)
with open('company_qa_clean.json', 'w', encoding='utf-8') as f:
json.dump(cleaned, f, ensure_ascii=False, indent=2)
print(f"原始: {len(data)} 条,清洗后: {len(cleaned)} 条")
坑4:数据量不足导致严重过拟合
企业 SFT 的数据量往往有限,常见误区是凑不够数据就把同一批数据跑很多 epoch,导致:
- 训练 loss 极低(< 0.1),但实际输出只会复读训练集
- eval loss 反弹上升,模型泛化能力崩溃
各规模数据量建议:
| 目标 | 最少数据量 | 推荐数据量 | epoch |
|---|---|---|---|
| 回复风格迁移 | 200 条 | 500~1000 条 | 2~3 |
| 领域术语学习 | 500 条 | 2000~5000 条 | 2~3 |
| 复杂业务逻辑 | 2000 条 | 5000~20000 条 | 1~2 |
数据质量比数量更重要,100 条高质量标注的价值远超 1000 条机器生成的低质量数据。
坑5:中文标点和特殊字符导致 JSON 解析失败
企业数据往往从 Word/Excel 导出,含有大量"弯引号"、全角空格、零宽字符,这些会让 LLaMA-Factory 的数据加载直接崩溃,且报错信息非常不友好(只显示 JSONDecodeError,不指出具体位置)。
# 数据预处理:清洗不可见字符和特殊标点
import json, unicodedata
REPLACEMENTS = {
'\u201c': '"', '\u201d': '"', # 弯引号
'\u2018': "'", '\u2019': "'", # 弯单引号
'\u3000': ' ', # 全角空格
'\u00a0': ' ', # 非断行空格
'\ufeff': '', # BOM
'\u200b': '', '\u200c': '', # 零宽字符
}
def clean_text(text: str) -> str:
for old, new in REPLACEMENTS.items():
text = text.replace(old, new)
# 去除 Unicode 控制字符(保留换行和制表符)
text = ''.join(
c for c in text
if unicodedata.category(c) != 'Cc' or c in '\n\t'
)
return text.strip()
# 验证 JSON 可解析性
try:
with open('company_qa_clean.json', encoding='utf-8') as f:
data = json.load(f)
print(f"✅ JSON 格式正确,共 {len(data)} 条")
except json.JSONDecodeError as e:
print(f"❌ JSON 解析失败:{e}")
四、训练配置踩坑
核心训练配置(YAML,A100 40G 单卡 LoRA)
# qwen35_9b_lora_sft.yaml
### 模型
model_name_or_path: /data/models/Qwen3.5-9B-Instruct
trust_remote_code: true
flash_attn: fa2 # FlashAttention-2 加速,A100 必开
### 方法
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 16 # 见坑6
lora_alpha: 32 # 通常 = lora_rank × 2
lora_dropout: 0.05
lora_target: all # 作用于所有线性层(比 q_proj,v_proj 效果更好)
### 数据
dataset: company_qa
template: qwen3_nothink # 见坑1:企业 SFT 用 nothink
cutoff_len: 2048 # 根据你的最长样本设置,不要无脑设 8192
max_samples: 10000 # 调试时先限制数量,跑通再去掉
overwrite_cache: true
preprocessing_num_workers: 8
### 输出
output_dir: saves/qwen35-9b/lora/sft-v1
logging_steps: 10
save_steps: 200
save_total_limit: 3 # 只保留最新 3 个 checkpoint,省磁盘
plot_loss: true
overwrite_output_dir: true
### 训练参数
per_device_train_batch_size: 2
gradient_accumulation_steps: 8 # 等效 batch_size = 2 × 8 = 16
learning_rate: 5.0e-5 # 见坑7
num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true # Qwen3.5 用 bf16,不要用 fp16
val_size: 0.05 # 5% 数据作验证集,必须设!
per_device_eval_batch_size: 2
eval_strategy: steps
eval_steps: 200
启动训练:
llamafactory-cli train qwen35_9b_lora_sft.yaml
# v0.9.4 支持快捷命令
lmf train qwen35_9b_lora_sft.yaml
坑6:LoRA rank 设太大,训练 loss 低但泛化差
LoRA rank(lora_rank)控制可训练参数量,很多人误以为越大越好:
| lora_rank | 可训练参数量(9B 模型,all target) | 适用场景 |
|---|---|---|
| 8 | ~40M | 风格微调、少量数据(< 1000 条) |
| 16 | ~80M | 领域知识注入(推荐起点) |
| 32 | ~160M | 复杂业务逻辑(数据量 > 5000 条) |
| 64+ | ~320M+ | 几乎等于全量微调,显存占用暴增 |
rank 太大的问题:对小数据集来说,过多的参数反而更容易过拟合,loss 曲线下降飞快但 eval loss 同步飙升。
调参策略:从 rank=16 开始,观察 eval loss 是否稳定。如果 training loss 远低于 eval loss(差距 > 1.0),说明过拟合,降 rank 或增加 dropout。
坑7:学习率设太高,训练前期 loss 爆炸
Qwen3.5-9B 的 Instruct 版本已经经过 RLHF 对齐,对学习率非常敏感。网上很多教程的 learning_rate: 1e-4 是针对 Base 模型的,用在 Instruct 版本上会导致前几百步 loss 剧烈震荡甚至 NaN:
Step 10: loss=2.85
Step 20: loss=4.12 ← 上升了!说明学习率过高
Step 30: loss=nan ← 崩了
推荐学习率范围:
| 模型版本 | 推荐学习率 |
|---|---|
| Base 模型 | 1e-4 ~ 5e-5 |
| Instruct 版本(SFT) | 5e-5 ~ 1e-5 |
| Instruct 版本(少量数据,< 500 条) | 1e-5 ~ 5e-6 |
配合 warmup(warmup_ratio: 0.1)让学习率从 0 线性升到目标值,再走 cosine 下降,可以有效避免前期震荡。
QLoRA 配置(RTX 4090 24G 单卡)
显存不够用 A100 时,QLoRA 4bit 量化可以在 RTX 4090 上跑 9B 模型:
# qwen35_9b_qlora_sft.yaml(RTX 4090 适配)
model_name_or_path: /data/models/Qwen3.5-9B-Instruct
flash_attn: fa2
quantization_bit: 4 # 4bit 量化
quantization_method: bitsandbytes # 也可以用 hqq,速度更快
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 8 # QLoRA 显存受限,rank 调小
lora_target: all
lora_alpha: 16
dataset: company_qa
template: qwen3_nothink
cutoff_len: 1024 # 4090 显存紧张,上下文要再压
per_device_train_batch_size: 1
gradient_accumulation_steps: 16 # 等效 batch = 16
learning_rate: 1.0e-4 # QLoRA 学习率可以比 LoRA 高一点
num_train_epochs: 3.0
bf16: true
显存占用参考(Qwen3.5-9B):
- LoRA BF16:约 22G(A100 40G 足够)
- QLoRA 4bit:约 12G(RTX 4090 24G 绰绰有余)
- 全量微调 BF16:约 75G+(需要至少 2×A100 40G 或 1×H100 80G)
五、模型合并与导出踩坑
训练完得到的是 LoRA adapter(增量权重),需要合并回基础模型才能用 vLLM 部署。
坑8:合并时 dtype 不一致导致精度下降
合并命令很简单,但有一个隐蔽的坑:export_device 和 dtype 设置不当,会让合并后的模型精度悄悄劣化。
# merge_lora.yaml
model_name_or_path: /data/models/Qwen3.5-9B-Instruct
adapter_name_or_path: saves/qwen35-9b/lora/sft-v1
template: qwen3_nothink
finetuning_type: lora
export_dir: /data/models/Qwen3.5-9B-SFT-merged
export_size: 5 # 每个分片最大 5GB
export_device: cpu # ✅ 用 cpu 合并,避免 GPU 显存不够
export_legacy_format: false # ✅ 必须 false,用新的 safetensors 格式
llamafactory-cli export merge_lora.yaml
常见精度下降场景:
# ❌ 错误:用 GPU 合并,部分精度会被强制转成 float32 再截断
export_device: cuda
# ❌ 错误:旧格式保存,加载时可能有精度损失
export_legacy_format: true
# ✅ 正确:CPU 合并 + safetensors 格式
export_device: cpu
export_legacy_format: false
坑9:合并后模型输出乱码,忘记复制 tokenizer 文件
llamafactory-cli export 只导出模型权重,不会自动复制 tokenizer 相关文件。用 vLLM 加载合并后的模型,如果目录里没有 tokenizer 文件,输出会是乱码或报错。
# 检查导出目录是否有 tokenizer 文件
ls /data/models/Qwen3.5-9B-SFT-merged/
# 应该包含:tokenizer.json、tokenizer_config.json、
# special_tokens_map.json、vocab.json(或 sentencepiece 文件)
# 如果缺少,从原始模型目录复制
cp /data/models/Qwen3.5-9B-Instruct/tokenizer*.json \
/data/models/Qwen3.5-9B-SFT-merged/
cp /data/models/Qwen3.5-9B-Instruct/special_tokens_map.json \
/data/models/Qwen3.5-9B-SFT-merged/
cp /data/models/Qwen3.5-9B-Instruct/generation_config.json \
/data/models/Qwen3.5-9B-SFT-merged/
六、微调后接入 vLLM 踩坑
坑10:vLLM 加载微调模型需要显式指定 chat template
vLLM 默认从 tokenizer_config.json 里的 chat_template 字段读取对话格式。Qwen3.5-9B 的 chat_template 默认启用思维链模式,如果你的 SFT 训练用了 qwen3_nothink,推理时必须覆盖 chat template,否则输出会重新带上 <think> 标签。
# ❌ 直接启动,继承 tokenizer 默认的 think 模式
vllm serve /data/models/Qwen3.5-9B-SFT-merged \
--served-model-name qwen35-9b-sft
# ✅ 通过 chat template override 关闭思维链
vllm serve /data/models/Qwen3.5-9B-SFT-merged \
--served-model-name qwen35-9b-sft \
--chat-template /path/to/qwen3_nothink_template.jinja \
--reasoning-parser qwen3 \
--max-model-len 8192
qwen3_nothink_template.jinja 可以从 LLaMA-Factory 的 src/llamafactory/data/template.py 里提取 qwen3_nothink 对应的 Jinja2 模板,或者直接在 generation_config.json 里设置:
{
"chat_template": "qwen3_nothink",
"enable_thinking": false
}
七、训练曲线诊断指南
每次训练结束后,打开 output_dir/trainer_log.jsonl 或 LlamaBoard 看 loss 曲线,对照以下模式诊断问题:
正常曲线(理想):
train_loss: 2.5 → 1.8 → 1.2 → 0.8(平滑下降)
eval_loss: 2.6 → 1.9 → 1.3 → 0.9(略高于 train,同步下降)
过拟合:
train_loss: 2.5 → 0.3(下降过快)
eval_loss: 2.6 → 1.5 → 2.1 → 2.8(先降后升 ← 危险信号)
→ 解法:降低 lora_rank、增加 lora_dropout、减少 epoch
学习率过高:
train_loss: 2.5 → 4.8 → nan(前期爆炸)
→ 解法:降低 learning_rate 10 倍,加大 warmup_ratio 到 0.15
欠拟合:
train_loss: 2.5 → 2.1(下降极慢,几乎平)
→ 解法:提高 learning_rate、增大 lora_rank、检查数据格式是否正确
数据格式错误:
train_loss 从第一步就极高(> 5.0)且不下降
→ 解法:检查 template 是否匹配、数据格式是否符合 ShareGPT/Alpaca 规范
八、总结:踩坑速查表
| # | 坑 | 现象 | 解法 |
|---|---|---|---|
| 1 | template 用了 qwen3 而非 qwen3_nothink |
输出充满 <think> 标签 |
企业 SFT 统一用 qwen3_nothink |
| 2 | Base 模型 + LoRA 生成不停止 | 输出到 max_tokens 才停 | 改用 Instruct 版本 |
| 3 | 训练数据混入 <think> 标签 |
模型学会乱用思维链格式 | 上线前清洗数据 |
| 4 | 数据量少但 epoch 设太多 | eval loss 反弹,过拟合 | 控制 epoch ≤ 3,加验证集 |
| 5 | 数据含特殊字符 | JSONDecodeError | 数据预处理脚本清洗 |
| 6 | lora_rank 设太大 | 小数据集过拟合 | 从 rank=16 开始调 |
| 7 | 学习率对 Instruct 模型设太高 | loss 震荡 / NaN | Instruct 版用 5e-5 以下 |
| 8 | 合并用 GPU 或旧格式 | 合并后精度下降 | export_device: cpu + export_legacy_format: false |
| 9 | 忘记复制 tokenizer 文件 | vLLM 输出乱码 | 手动复制 tokenizer 相关文件 |
| 10 | vLLM 继承默认 think 模式 | 微调后又多了 <think> |
覆盖 chat_template 或设 enable_thinking: false |
参考资料
- LLaMA-Factory GitHub
- LLaMA-Factory 官方文档
- Qwen 官方 LLaMA-Factory 接入指南
- LLaMA-Factory v0.9.4 Release Notes
- Qwen3.5-9B HuggingFace
微调是一个迭代过程,第一次跑通只是开始。建议把每次实验的配置文件和 loss 曲线都保存下来,方便对比。如果你遇到了其他坑,欢迎评论区补充,后续会持续更新。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)