跳转至

1.2.5 【动手一】用 QLoRA 微调 Qwen2.5-7B 指令模型


实验目标

完成本节后,你将能够:

  1. 从零搭建 QLoRA 微调流水线:通过 4-bit 量化加载 7B 基座模型,注入 LoRA adapter,在指令数据集上完成微调
  2. 理解核心超参选型rlora_alphatarget_modules 等参数的含义与取值逻辑
  3. 验证微调后模型效果:加载 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 调用层)+ 微调基座模型标识

数据流向

  1. 外部数据集(HuggingFace customer_service_qa)→ format_sample() → ChatML 文本
  2. 基座模型(Qwen/Qwen2.5-7B-Instruct)→ BitsAndBytes 4-bit 量化 → LoRA 注入
  3. SFTTrainer 训练循环 → 输出 adapter 权重到 ./outputs/qwen2.5-7b-customer-service/
  4. 推理时加载 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_REGISTRYACTIVE_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}

代码解析

  • 数据集格式要求包含 questionanswer 两个字段(如 "customer_service_qa" 数据集)
  • apply_chat_template() 使用 tokenizer 内置的 ChatML 模板自动拼接特殊 token(如 <|im_start|><|im_end|>),无需手动拼接
  • 输出为单条 text 字段,由 SFTTrainerdataset_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 数据集