Qwen3.5-9B 微调避坑:用 LLaMA-Factory 做企业 SFT 的 10 个踩坑全记录

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 个真实踩坑,每个坑都给出可直接用的修复命令或配置。

文章结构:

  1. 环境搭建(新旧版本差异)
  2. Qwen3.5-9B 的 template 选择陷阱
  3. 数据格式踩坑(4 个坑)
  4. 训练配置踩坑(3 个坑)
  5. 模型合并与导出踩坑(2 个坑)
  6. 微调后接入 vLLM 踩坑(1 个坑)
  7. 训练曲线诊断指南

一、环境搭建

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 控制字符(保留换行和制表符).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 规范

八、总结:踩坑速查表

#现象解法
1template 用了 qwen3 而非 qwen3_nothink输出充满 <think> 标签企业 SFT 统一用 qwen3_nothink
2Base 模型 + LoRA 生成不停止输出到 max_tokens 才停改用 Instruct 版本
3训练数据混入 <think> 标签模型学会乱用思维链格式上线前清洗数据
4数据量少但 epoch 设太多eval loss 反弹,过拟合控制 epoch ≤ 3,加验证集
5数据含特殊字符JSONDecodeError数据预处理脚本清洗
6lora_rank 设太大小数据集过拟合从 rank=16 开始调
7学习率对 Instruct 模型设太高loss 震荡 / NaNInstruct 版用 5e-5 以下
8合并用 GPU 或旧格式合并后精度下降export_device: cpu + export_legacy_format: false
9忘记复制 tokenizer 文件vLLM 输出乱码手动复制 tokenizer 相关文件
10vLLM 继承默认 think 模式微调后又多了 <think>覆盖 chat_template 或设 enable_thinking: false

参考资料


微调是一个迭代过程,第一次跑通只是开始。建议把每次实验的配置文件和 loss 曲线都保存下来,方便对比。如果你遇到了其他坑,欢迎评论区补充,后续会持续更新。

Read more

【保姆级】无需公网 IP!Windows 本地一键部署 OpenClaw,10 分钟打造你的飞书 AI 数字员工

【保姆级】无需公网 IP!Windows 本地一键部署 OpenClaw,10 分钟打造你的飞书 AI 数字员工

目录 写在前面 OpenClaw 是什么? 蓝耘平台是什么?与 OpenClaw 的关系 步骤一:极速安装,一行命令搞定环境 步骤二:启动向导,初始化配置参数 步骤 三:注入灵魂,获取蓝耘MaaS API Key 步骤四:打通渠道,搭建飞书长连接桥梁 步骤五:引擎点火,启动核心网关服务 步骤六:仪表盘检阅,后台状态可视化 步骤七:实战演练,验证智能交互效果 快速排错提示 写在末尾 写在前面 本文面向:想在 Windows 本地(PowerShell)一键部署 OpenClaw,使用蓝耘MaaS作为大模型,并通过飞书长连接模式实现 AI 机器人的用户。 内容涵盖:从零开始安装配置、对接飞书机器人、验证与排错的完整流程,

OpenClaw 源码解读:从「只会聊天」到「真正干活」的 AI 框架是怎么炼成的

写在前面:这篇文章是给小白看的,所以我会说得比较啰嗦,尽量把每一个概念都掰开揉碎了讲。如果你已经是老司机了,可以直接跳到架构部分。另外,我是个程序员,不是 AI,所以这篇文章里没有那种 AI 写出来的车轱辘话,都是我的大白话。 一、先聊聊:OpenClaw 到底是个啥? 1.1 不是爬虫,是 AI 助手运行时 先说个可能让大家误会的事儿。我第一次听到 OpenClaw 这个名字的时候,还以为它是个爬虫框架(毕竟 Claw 是爪子的意思,感觉像是抓取数据用的)。结果一查,完全不是这么回事儿。 OpenClaw 是一个本地优先的开源 AI Agent 运行时框架。 这句话里有几个关键词,我来逐个解释: * 本地优先(Local-first):你的数据都在你自己的电脑上,不上传到任何云服务。这意味着隐私安全,但也意味着你的电脑得一直开着。 * 开源(Open

国产AI智能体协作平台CoPaw

国产AI智能体协作平台CoPaw

简介 什么是 CoPaw ? CoPaw 是一个为大型语言模型(LLM)驱动的智能体(Agent)打造的协作平台。它构建于 AgentScope 库之上,能让多个独立的 AI 智能体能够像一个团队一样共同合作,以完成单个智能体难以解决的复杂任务。它提供了一个可视化的工作空间,用户可以在其中定义任务、分配角色、并实时观察智能体团队的协作过程。 主要特点 * 多平台聊天支持:支持 DingTalk、飞书、QQ、Discord、iMessage 等多种频道 * 多智能体协作:支持智能体之间进行复杂的对话、角色扮演和工具使用,以实现群体智能。 * 本地模型支持:可使用 llama.cpp 或 MLX 在本地运行模型,无需 API Key * 定时任务:支持心跳检测和定时任务自动化 * 记忆与学习:支持长期记忆功能,记住用户的偏好和习惯 * 工作流编排:允许用户通过简单的配置来设计和管理智能体团队的协作流程(

ESP32 小智 AI 机器人入门教程从原理到实现(自己云端部署)

此博客为一篇针对初学者的详细教程,涵盖小智 AI 机器人的原理、硬件准备、软件环境搭建、代码实现、云端部署以及优化扩展。文章结合了现有的网络资源,取长补短,确保内容易于理解和操作。 简介: 本教程将指导初学者使用 ESP32 微控制器开发一个简单的语音对话机器人“小智”。我们将介绍所需的基础原理、硬件准备、软件环境搭建,以及如何编写代码实现语音唤醒和与云端大模型的对接。通过本教程,即使没有深厚的 AI 或嵌入式经验,也可以一步步制作出一个能听懂唤醒词并与人对话的简易 AI 机器人。本教程提供详细的操作步骤、代码示例和图示,帮助您轻松上手。 1. 基础原理 ESP32 架构及其在 AI 领域的应用: ESP32 是一款集成 Wi-Fi 和蓝牙的双核微控制器,具有较高的主频和丰富的外设接口,适合物联网和嵌入式 AI 应用。特别是新版的 ESP32-S3 芯片,不仅运行频率高达 240MHz,还内置了向量加速指令(