1.2.5 【动手一】用 QLoRA 微调 Qwen2.5-7B 指令模型
实验目标
完成本节后,你将能够:
- 从零搭建 QLoRA 微调流水线:通过 4-bit 量化加载 7B 基座模型,注入 LoRA adapter,在指令数据集上完成微调
- 理解核心超参选型:
r、lora_alpha、target_modules等参数的含义与取值逻辑 - 验证微调后模型效果:加载 adapter 权重进行推理,观察客服场景下的对话质量变化
核心学习点:BitsAndBytesConfig 的 NF4 量化机制、LoRA 权重注入、SFTTrainer 训练流程、ChatML 指令格式。
架构总览
系统由 4 个核心模块组成,通过 main.py 统一调度:
┌─────────────────────────────────────────────────────┐
│ main.py (入口) │
│ argparse → --finetune / --test / --list-models │
└──────────────┬──────────────────┬────────────────────┘
│ │
┌────────▼────────┐ ┌──────▼─────────┐
│ finetune.py │ │ smoke_test.py │
│ 微调训练流程 │ │ adapter 推理验证 │
│ │ │ │
│ • 量化加载模型 │ │ • 加载 adapter │
│ • 注入 LoRA │ │ • ChatML 推理 │
│ • SFTTrainer │ │ • 3 条测试用例 │
│ • 保存权重 │ │ │
└────────┬─────────┘ └──────┬──────────┘
│ │
└────────┬──────────┘
│
┌────────▼─────────┐
│ core_config.py │
│ 模型注册 + 配置 │
│ │
│ • MODEL_REGISTRY│
│ • FINETUNE_BASE │
│ _MODEL │
└──────────────────┘
组件清单:
| 组件 | 文件 | 职责 |
|---|---|---|
| 入口调度 | main.py |
CLI 参数解析,分发到训练或测试流程 |
| 微调训练 | finetune.py |
量化加载 → LoRA 注入 → SFTTrainer 训练 → 保存 |
| 冒烟测试 | smoke_test.py |
加载微调后的 adapter,用 3 条客服对话验证效果 |
| 全局配置 | core_config.py |
模型注册表(LiteLLM 调用层)+ 微调基座模型标识 |
数据流向:
- 外部数据集(HuggingFace
customer_service_qa)→format_sample()→ ChatML 文本 - 基座模型(
Qwen/Qwen2.5-7B-Instruct)→ BitsAndBytes 4-bit 量化 → LoRA 注入 - SFTTrainer 训练循环 → 输出 adapter 权重到
./outputs/qwen2.5-7b-customer-service/ - 推理时加载 adapter → 生成客服回复
环境准备
硬件要求:单张 GPU,显存 ≥ 16GB(RTX 3090/4090、A10G 可跑通全流程)。CPU 仅可加载 adapter 做推理验证,无法完成训练。
# 创建虚拟环境(Python 3.11+)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 安装依赖
pip install -r requirements.txt
依赖清单(来自 requirements.txt):
transformers>=4.30.0
peft>=0.6.0
accelerate>=0.20.0
bitsandbytes>=0.41.0
datasets>=2.13.0
torch>=2.0.0
evaluate>=0.4.0
wandb>=0.15.0
python-dotenv>=1.0.0
trl>=0.7.0
pytest>=7.0.0
环境变量配置:
📁 对应文件:
libs/.../1.2.5 _动手一_用 QLoRA 微调 Qwen2.5-7B 指令模型/.env.example
复制 .env.example 为 .env 并填入你的 API Key:
# DeepSeek API Key
DEEPSEEK_API_KEY=your_deepseek_api_key_here
# Qwen API Key(阿里云 DashScope)
DASHSCOPE_API_KEY=your_dashscope_api_key_here
说明:这些 API Key 用于
core_config.py中 LiteLLM 调用层的模型注册。微调流程本身不依赖这些 Key(微调通过 HuggingFace 本地加载模型)。
Step-by-Step 实现
Step 1:配置全局模型注册表与微调基座
📁 对应文件:
libs/.../1.2.5/core_config.py
项目使用 core_config.py 统一管理大模型配置。微调流程通过 FINETUNE_BASE_MODEL 常量引用基座模型,与 LiteLLM 调用层的 MODEL_REGISTRY 共存于同一文件,互不干扰。
# core_config.py 关键片段
# LiteLLM 模型注册表(供其他模块调用时使用)
MODEL_REGISTRY: dict[str, ModelConfig] = {
"DeepSeek-V3": {
"litellm_id": "deepseek/deepseek-chat",
"price_in": 0.00027, "price_out": 0.0011,
"max_tokens_limit": 4096,
"api_key_env": "DEEPSEEK_API_KEY", "base_url": None,
},
"Qwen-Max": {
"litellm_id": "qwen/qwen-plus",
"price_in": 0.001, "price_out": 0.004,
"max_tokens_limit": 4096,
"api_key_env": "DASHSCOPE_API_KEY",
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
}
# 微调目标模型(HuggingFace Hub 标识符,不参与 LiteLLM 注册表)
FINETUNE_BASE_MODEL: str = "Qwen/Qwen2.5-7B-Instruct"
# 当前激活的 LiteLLM 模型 key
ACTIVE_MODEL_KEY: str = "Qwen-Max"
代码解析:
FINETUNE_BASE_MODEL是 HuggingFace Hub 的模型 ID,训练时AutoModelForCausalLM.from_pretrained()会从此路径下载并加载MODEL_REGISTRY和ACTIVE_MODEL_KEY是 LiteLLM 调用层的配置,用于项目中其他 Agent 的 LLM 调用,与微调流程解耦- 修改
FINETUNE_BASE_MODEL即可更换微调基座(如改为Qwen/Qwen2.5-3B-Instruct以降低显存需求)
Step 2:定义量化与 LoRA 超参数
📁 对应文件:
libs/.../1.2.5/finetune.py第 19-45 行
在 finetune.py 中,两个函数分别定义 4-bit 量化和 LoRA adapter 的配置:
def get_quantization_config() -> BitsAndBytesConfig:
"""4-bit 量化配置(QLoRA)。"""
return BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # Normal Float 4,比 INT4 精度损失更小
bnb_4bit_compute_dtype=torch.bfloat16, # 前向计算反量化到 BF16
bnb_4bit_use_double_quant=True, # 双重量化,额外节省 ~200MB 显存
)
def get_lora_config() -> LoraConfig:
"""LoRA adapter 配置。"""
return LoraConfig(
r=16, # LoRA 秩
lora_alpha=32, # 缩放系数(通常设为 r 的 2 倍)
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj", # Attention 投影
"gate_proj", "up_proj", "down_proj", # MLP 前馈层
],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
代码解析:
- NF4 量化:将 7B 模型从 \~14GB(FP16)压缩到约 5GB 显存,使得单卡 16GB 即可加载
target_modules覆盖范围:同时覆盖 Attention 的 q/k/v/o 和 MLP 的 gate/up/down,共 7 个投影矩阵,适合小数据集(500 条级)场景lora_alpha / r = 2:经验黄金比例,控制 LoRA 更新幅度的缩放- 可训练参数占比:运行
model.print_trainable_parameters()输出约 0.5%,即 \~40M / 7.6B
Step 3:数据集格式化(ChatML 指令格式)
📁 对应文件:
libs/.../1.2.5/finetune.py第 48-57 行
微调使用外部 HuggingFace 数据集,通过 format_sample() 将原始问答转换为 ChatML 文本:
def format_sample(sample: dict) -> dict:
"""将数据集样本格式化为 ChatML 指令格式。"""
messages = [
{"role": "system", "content": "你是一位专业的电商客服助手,负责解答用户关于订单、物流、退换货等问题。"},
{"role": "user", "content": sample["question"]},
{"role": "assistant", "content": sample["answer"]},
]
tokenizer = AutoTokenizer.from_pretrained(FINETUNE_BASE_MODEL, trust_remote_code=True)
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
return {"text": text}
代码解析:
- 数据集格式要求包含
question和answer两个字段(如"customer_service_qa"数据集) apply_chat_template()使用 tokenizer 内置的 ChatML 模板自动拼接特殊 token(如<|im_start|>、<|im_end|>),无需手动拼接- 输出为单条
text字段,由SFTTrainer的dataset_text_field="text"消费 - ⚠️ system prompt 在训练和推理时必须保持一致,否则模型角色行为会漂移
Step 4:执行微调训练
📁 对应文件:
libs/.../1.2.5/finetune.py第 60-119 行
run_finetuning() 是完整的训练流程函数:
def run_finetuning() -> None:
"""执行完整的 QLoRA 微调流程。"""
print(f"加载基座模型: {FINETUNE_BASE_MODEL}")
# 1. 加载 tokenizer
tokenizer = AutoTokenizer.from_pretrained(
FINETUNE_BASE_MODEL, trust_remote_code=True
)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 2. 加载量化基座模型
bnb_config = get_quantization_config()
model = AutoModelForCausalLM.from_pretrained(
FINETUNE_BASE_MODEL,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
model = prepare_model_for_kbit_training(model)
# 3. 注入 LoRA adapter
lora_config = get_lora_config()
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 4. 加载并格式化数据集
dataset = load_dataset(DATASET_NAME, split="train")
dataset = dataset.map(format_sample, remove_columns=dataset.column_names)
# 5. 训练
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_strategy="epoch",
save_total_limit=2,
)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
args=training_args,
max_seq_length=MAX_SEQ_LENGTH,
dataset_text_field="text",
)
print("开始训练...")
trainer.train()
trainer.save_model(OUTPUT_DIR)
print(f"训练完成,模型保存至: {OUTPUT_DIR}")
训练超参数汇总:
| 参数 | 值 | 说明 |
|---|---|---|
num_train_epochs |
3 | 训练轮次 |
per_device_train_batch_size |
4 | 单卡 batch size |
gradient_accumulation_steps |
4 | 梯度累积,有效 batch = 4×4 = 16 |
learning_rate |
2e-4 | LoRA 推荐学习率 |
fp16 |
True | 混合精度训练 |
max_seq_length |
512 | 最大序列长度(客服对话通常 < 512 token) |
save_strategy |
epoch | 每轮结束后保存 checkpoint |
save_total_limit |
2 | 最多保留 2 个 checkpoint |
代码解析:
prepare_model_for_kbit_training()冻结基座参数、将 LayerNorm 层升精度到 FP32(稳定训练)device_map="auto"自动分配可用 GPU,多卡时自动切分- 数据集名称通过
DATASET_NAME常量配置(默认"customer_service_qa"),需替换为实际可用的 HuggingFace 数据集路径 - 训练结束后,LoRA adapter 权重保存至
./outputs/qwen2.5-7b-customer-service/
Step 5:加载 Adapter 推理验证
📁 对应文件:
libs/.../1.2.5/smoke_test.py
微调完成后,通过 smoke_test.py 加载 adapter 进行端到端推理验证:
def test_with_transformers(adapter_path: str = "./outputs/qwen2.5-7b-customer-service/final_adapter") -> None:
"""直接加载 adapter 推理(无需合并,适合快速验证)。"""
from peft import PeftModel
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
base_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(adapter_path, trust_remote_code=True)
model = PeftModel.from_pretrained(base_model, adapter_path)
model.eval()
test_cases = [
"我的订单还没发货,已经等了5天了",
"收到的商品和图片不一样,要怎么处理?",
"如何修改收货地址?",
]
for query in test_cases:
messages = [
{"role": "system", "content": "你是一位专业的电商客服助手,负责解答用户关于订单、物流、退换货等问题。"},
{"role": "user", "content": query},
]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=200,
temperature=0.3,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
response = tokenizer.decode(
outputs[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True,
)
print(f"用户:{query}")
print(f"客服:{response}")
print("-" * 60)
代码解析:
- 使用
PeftModel.from_pretrained()将 adapter 加载到量化后的基座上,无需先合并权重 - 推理超参:
temperature=0.3(低温度,保证客服回复稳定一致)、max_new_tokens=200 - system prompt 与训练时完全一致(这是微调后模型表现正常的关键前提)
- 3 条测试用例覆盖:物流查询、退换货、地址修改 — 典型电商客服场景
Step 6:统一入口调度
📁 对应文件:
libs/.../1.2.5/main.py
main.py 提供统一的 CLI 入口,支持三种操作模式:
def main():
parser = argparse.ArgumentParser(description="QLoRA 微调 Qwen2.5-7B 指令模型")
parser.add_argument("--test", action="store_true", help="运行冒烟测试")
parser.add_argument("--finetune", action="store_true", help="执行微调训练")
parser.add_argument("--adapter-path", default=None, help="微调 adapter 路径(用于测试)")
parser.add_argument("--list-models", action="store_true", help="列出已注册模型")
args = parser.parse_args()
if args.list_models:
print("已注册模型:", get_model_list())
print(f"微调基座模型: {FINETUNE_BASE_MODEL}")
return
if args.finetune:
print(f"开始微调,基座模型: {FINETUNE_BASE_MODEL}")
run_finetune()
return
# 默认运行冒烟测试
print("运行冒烟测试...")
run_smoke_test(adapter_path=args.adapter_path)
使用方式:
# 1. 运行冒烟测试(默认行为)
python main.py
# 2. 执行微调训练(需要 GPU 和数据集)
python main.py --finetune
# 3. 指定 adapter 路径进行测试
python main.py --test --adapter-path ./outputs/my-adapter/
# 4. 查看已注册模型列表
python main.py --list-models
完整运行验证
训练模式
# 前置条件:
# 1. GPU 环境(显存 ≥ 16GB)
# 2. 已安装依赖:pip install -r requirements.txt
# 3. 可访问 HuggingFace Hub(或设置 HF_ENDPOINT 镜像)
# 4. customer_service_qa 数据集可用(或修改 DATASET_NAME)
python main.py --finetune
预期输出:
加载基座模型: Qwen/Qwen2.5-7B-Instruct
trainable params: 39,976,960 || all params: 7,655,890,944 || trainable%: 0.5222
开始训练...
{'loss': 1.8234, 'learning_rate': 1.9e-04, 'epoch': 0.33}
{'loss': 1.2456, 'learning_rate': 1.5e-04, 'epoch': 0.67}
...
训练完成,模型保存至: ./outputs/qwen2.5-7b-customer-service
推理验证模式
# 加载微调后的 adapter 进行推理
python main.py
# 或显式指定路径
python main.py --test --adapter-path ./outputs/qwen2.5-7b-customer-service
预期输出示例:
运行冒烟测试...
用户:我的订单还没发货,已经等了5天了
客服:非常抱歉让您久等了!请您告知订单号,我来为您查询最新的物流状态。
如果确认存在异常,我会第一时间为您协调加急处理。
------------------------------------------------------------
用户:收到的商品和图片不一样,要怎么处理?
客服:您好,非常抱歉给您带来困扰!商品与描述不符属于卖家原因,
您可以申请退货退款。请在订单页面点击「申请售后」,
选择「商品与描述不符」,上传对比照片后提交。
------------------------------------------------------------
用户:如何修改收货地址?
客服:您好!修改收货地址需要在订单发货前操作。请进入「我的订单」→
选择对应订单 → 点击「修改地址」。如果订单已发货,则无法直接修改。
------------------------------------------------------------
冒烟测试(pytest)
cd "libs/AI Agent 与大模型应用开发实战手册/第一章 大模型基础与 API 实战/1.2.5 _动手一_用 QLoRA 微调 Qwen2.5-7B 指令模型"
python -m pytest tests/ -v
常见报错与解决方案
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
CUDA out of memory |
显存不足 | 将 per_device_train_batch_size 改为 2,gradient_accumulation_steps 改为 8 |
ValueError: You need to install bitsandbytes... |
bitsandbytes 未安装或版本不兼容 | 确认安装了 bitsandbytes>=0.41.0;Windows 用户需安装 bitsandbytes-windows |
AttributeError: 'NoneType' object has no attribute 'pad_token_id' |
tokenizer 的 pad_token 未设置 | 代码已处理:tokenizer.pad_token = tokenizer.eos_token |
The model is too large to be loaded... |
磁盘模型文件下载不完整 | 删除 ~/.cache/huggingface/hub 中对应模型目录,重新下载;或设置 HF_ENDPOINT=https://hf-mirror.com 使用镜像 |
Dataset customer_service_qa not found |
HuggingFace 数据集不存在 | 修改 finetune.py 中的 DATASET_NAME 为实际可用的数据集路径,或自行构建 JSONL 数据集 |