AI 专题 FEB 28, 2026

大模型微调实战:从 LoRA 到部署上线的完整方案

#微调#LoRA#QLoRA#Unsloth#vLLM

大模型微调实战:从 LoRA 到部署上线的完整方案

本文是【AI 专题精讲】系列第 11 篇。 上一篇:Token 计算与上下文窗口管理实战 下一篇:Cursor Skill 开发:把工程经验变成 AI 可执行的技能


这篇文章你会得到什么

前十篇我们一直在”用”大模型——调 API、做 RAG、搞缓存、管 Token。但有些场景你会发现:

  • Prompt 怎么调都不满意,输出风格始终不对
  • RAG 检索到了正确内容,但模型回答还是不专业
  • 需要模型说行业术语,它只会说”通用话术”
  • 想要模型以特定格式、特定角色回答,靠 Prompt 不够稳定

这时候就该考虑**微调(Fine-tuning)**了。

很多人觉得微调是”炼丹”——需要 8 张 A100、需要几万条数据、需要 ML 博士背景。实际上,2024 年之后的微调已经非常平民化了:一张 24GB 显卡、几百条数据、半天时间就能训出一个明显优于基座模型的垂直领域模型。

今天给你:什么时候该微调、怎么准备数据、怎么低成本训练、怎么部署上线,全流程走一遍。


微调 vs RAG vs Prompt 工程:三者怎么选

这三个不是互斥的,但各有适用场景:

维度Prompt 工程RAG微调
解决什么控制输出格式和风格注入外部知识改变模型内在能力
数据需求0文档库几百~几千条标注数据
成本低(向量库 + Embedding)中(GPU 训练 + 数据标注)
生效速度即时即时小时~天
效果上限高(知识方面)最高(能力方面)

决策树

问题出在哪?
├── 模型不知道某些知识(公司制度、产品文档等)
│   └── → RAG(给它检索资料)
├── 模型知道但表达方式不对(格式、风格、角色)
│   ├── Prompt 能解决吗?
│   │   ├── 是 → Prompt 工程
│   │   └── 否(不稳定/太长)→ 微调
├── 模型在特定任务上能力不够(分类准确率低、提取不全)
│   └── → 微调
└── 以上都有
    └── → 微调 + RAG(最强组合)

我的经验:先用 Prompt 工程,再加 RAG,最后才考虑微调。微调是最后一张牌,但也是效果最好的那张。


微调的核心概念

全量微调 vs LoRA

全量微调:更新模型所有参数。7B 模型需要 ~56GB 显存(FP16),一般人玩不起。

LoRA(Low-Rank Adaptation):只训练一个小的”补丁”矩阵,冻结原始模型参数。

原始模型参数: W (7B 参数,冻结不动)
LoRA 补丁:    ΔW = A × B (只有几 MB,可训练)
推理时:       W' = W + ΔW

LoRA 的参数量只有原模型的 0.1%~1%,显存需求大幅降低。

QLoRA:在 LoRA 基础上把原模型量化到 4-bit,进一步降低显存。7B 模型用 QLoRA 只需要 ~6GB 显存。

方案7B 模型显存需求效果
全量微调~56 GB最好
LoRA (FP16)~16 GB接近全量
QLoRA (4-bit)~6 GB95%+ 全量效果

关键参数

参数含义推荐值
rank (r)LoRA 矩阵的秩,越大表达能力越强8~64
alpha缩放因子,通常 = rank 或 2×rank16~128
target_modules对哪些层加 LoRAq_proj, k_proj, v_proj, o_proj
learning_rate学习率1e-4 ~ 2e-4
epochs训练轮数2~5
batch_size批次大小4~16(看显存)

数据准备:最关键的一步

微调的效果 80% 取决于数据质量,20% 取决于训练参数。 垃圾数据训出来的模型比基座模型更差。

数据格式

三种主流格式:

1. Alpaca 格式(最常用)

[
  {
    "instruction": "根据以下产品描述,提取关键信息",
    "input": "这款蓝牙耳机续航30小时,支持主动降噪,重量仅25g",
    "output": "产品类型:蓝牙耳机\n续航:30小时\n特点:主动降噪\n重量:25g"
  }
]

2. ShareGPT 格式(多轮对话)

[
  {
    "conversations": [
      {"from": "human", "value": "退货流程是什么?"},
      {"from": "gpt", "value": "退货流程如下:\n1. 登录账号...\n2. 选择订单...\n3. 填写原因..."},
      {"from": "human", "value": "需要多久能退款?"},
      {"from": "gpt", "value": "一般3-5个工作日..."}
    ]
  }
]

3. OpenAI JSONL 格式

{"messages": [{"role": "system", "content": "你是专业客服"}, {"role": "user", "content": "退货流程"}, {"role": "assistant", "content": "退货流程如下..."}]}
{"messages": [{"role": "system", "content": "你是专业客服"}, {"role": "user", "content": "怎么改地址"}, {"role": "assistant", "content": "修改地址步骤..."}]}

数据准备脚本

import json
from pathlib import Path

def convert_to_alpaca(
    raw_data: list[dict],
    instruction_key: str = "question",
    output_key: str = "answer",
    system_prompt: str = "",
) -> list[dict]:
    dataset = []
    for item in raw_data:
        entry = {
            "instruction": system_prompt or item.get("instruction", ""),
            "input": item[instruction_key],
            "output": item[output_key],
        }
        dataset.append(entry)
    return dataset


def validate_dataset(dataset: list[dict]) -> dict:
    """验证数据集质量"""
    issues = []
    lengths = []

    for i, item in enumerate(dataset):
        output = item.get("output", "")

        if not output.strip():
            issues.append(f"第 {i} 条: output 为空")
        if len(output) < 10:
            issues.append(f"第 {i} 条: output 太短 ({len(output)} 字)")
        if len(output) > 5000:
            issues.append(f"第 {i} 条: output 太长 ({len(output)} 字)")

        lengths.append(len(output))

    import numpy as np
    return {
        "total": len(dataset),
        "issues": issues[:20],
        "avg_output_length": int(np.mean(lengths)),
        "min_output_length": min(lengths),
        "max_output_length": max(lengths),
    }


def split_dataset(dataset: list[dict], train_ratio: float = 0.9) -> tuple:
    import random
    random.shuffle(dataset)
    split = int(len(dataset) * train_ratio)
    return dataset[:split], dataset[split:]

数据量多少够

场景推荐数据量说明
风格调整50~200 条改变回答语气和格式
单一任务200~500 条如:信息提取、分类
垂直领域500~2000 条如:行业客服、专业问答
通用提升5000+ 条全面提升某领域能力

关键原则:200 条高质量数据 > 5000 条低质量数据。

数据质量检查清单

  1. 输出一致性:同类问题的回答格式和风格是否统一
  2. 无错误信息:答案是否准确,有无事实错误
  3. 长度合理:输出不要太短(信息不足)或太长(冗余)
  4. 覆盖全面:各种典型场景是否都覆盖到了
  5. 无敏感内容:不含隐私、不当内容

实战:用 Unsloth 微调 Qwen2.5

Unsloth 是目前最受欢迎的微调框架——速度快 2~5 倍,显存省 60%

环境准备

pip install unsloth
pip install trl datasets

微调脚本

from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset

# 1. 加载模型(4-bit 量化)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen2.5-7B-Instruct-bnb-4bit",
    max_seq_length=2048,
    load_in_4bit=True,
)

# 2. 添加 LoRA
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                     "gate_proj", "up_proj", "down_proj"],
    use_gradient_checkpointing="unsloth",
)

# 3. 准备数据
dataset = load_dataset("json", data_files="train.json", split="train")

chat_template = """{% for message in messages %}
{% if message['role'] == 'system' %}{{ message['content'] }}
{% elif message['role'] == 'user' %}### 用户:{{ message['content'] }}
{% elif message['role'] == 'assistant' %}### 助手:{{ message['content'] }}{% endif %}
{% endfor %}"""

def format_data(example):
    messages = example.get("messages", [])
    if not messages:
        messages = [
            {"role": "user", "content": example.get("input", "")},
            {"role": "assistant", "content": example.get("output", "")},
        ]
        if example.get("instruction"):
            messages.insert(0, {"role": "system", "content": example["instruction"]})
    text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
    return {"text": text}

dataset = dataset.map(format_data)

# 4. 训练
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    args=TrainingArguments(
        output_dir="./output",
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,
        warmup_steps=10,
        num_train_epochs=3,
        learning_rate=2e-4,
        fp16=True,
        logging_steps=10,
        save_steps=100,
        save_total_limit=3,
    ),
)

trainer.train()

训练参数调优经验

参数数据 < 500 条数据 500~2000 条数据 > 2000 条
epochs3~52~31~2
learning_rate2e-41e-45e-5
rank (r)8~1616~3232~64
batch_size4816

过拟合信号:训练 loss 持续下降但验证集效果变差。数据少的时候特别容易过拟合,减少 epochs 或降低 learning_rate。


用 LLaMA-Factory 微调(Web UI)

如果你更喜欢图形界面,LLaMA-Factory 提供了 Web UI:

git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]"

# 启动 Web UI
llamafactory-cli webui

在浏览器里选模型、上传数据、调参数、点开始训练,全程可视化。

LLaMA-Factory 配置文件

也可以用 YAML 配置:

# train_config.yaml
model_name_or_path: Qwen/Qwen2.5-7B-Instruct
stage: sft
finetuning_type: lora
lora_rank: 16
lora_alpha: 32
lora_target: q_proj,k_proj,v_proj,o_proj
dataset: my_dataset
template: qwen
output_dir: ./saves/qwen2.5-7b-lora
per_device_train_batch_size: 4
gradient_accumulation_steps: 4
learning_rate: 2.0e-4
num_train_epochs: 3
fp16: true
llamafactory-cli train train_config.yaml

模型合并与导出

合并 LoRA 权重

训练完成后,LoRA 权重和基座模型是分开的。合并成一个完整模型:

# Unsloth 方式
model.save_pretrained_merged(
    "merged_model",
    tokenizer,
    save_method="merged_16bit",
)
# 通用方式(PEFT)
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
lora_model = PeftModel.from_pretrained(base_model, "./output/checkpoint-xxx")
merged = lora_model.merge_and_unload()
merged.save_pretrained("./merged_model")

GGUF 量化(给 Ollama 用)

# Unsloth 直接导出 GGUF
model.save_pretrained_gguf(
    "gguf_model",
    tokenizer,
    quantization_method="q4_k_m",
)

或者用 llama.cpp

python llama.cpp/convert_hf_to_gguf.py ./merged_model --outfile model.gguf --outtype q4_k_m

常用量化级别:

量化大小(7B)质量速度
Q8_0~7.5 GB接近无损较快
Q4_K_M~4.5 GB推荐
Q4_0~4.0 GB稍有损失最快
Q2_K~3.0 GB明显损失极快

部署上线

Ollama 部署(最简单)

创建 Modelfile:

FROM ./model-q4_k_m.gguf

TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}<|im_start|>user
{{ .Prompt }}<|im_end|>
<|im_start|>assistant
"""

PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER stop "<|im_end|>"
ollama create my-model -f Modelfile
ollama run my-model "退货流程是什么"

接入现有代码(和第 04 篇的 Ollama Embedding 一样的方式):

import httpx

def call_finetuned_model(prompt: str, system: str = "") -> str:
    response = httpx.post(
        "http://localhost:11434/api/chat",
        json={
            "model": "my-model",
            "messages": [
                {"role": "system", "content": system},
                {"role": "user", "content": prompt},
            ],
            "stream": False,
        },
        timeout=60,
    )
    return response.json()["message"]["content"]

vLLM 部署(高并发生产环境)

pip install vllm
python -m vllm.entrypoints.openai.api_server \
    --model ./merged_model \
    --host 0.0.0.0 \
    --port 8000 \
    --max-model-len 4096 \
    --gpu-memory-utilization 0.9

vLLM 暴露的是 OpenAI 兼容接口,现有的 openai SDK 代码零改动:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

response = client.chat.completions.create(
    model="./merged_model",
    messages=[{"role": "user", "content": "退货流程是什么"}],
)
print(response.choices[0].message.content)

效果评估

人工评测

准备 50~100 条测试问题,基座模型和微调模型分别生成回答,人工打分:

def generate_eval_pairs(
    test_questions: list[str],
    base_model_fn,
    finetuned_model_fn,
) -> list[dict]:
    pairs = []
    for q in test_questions:
        pairs.append({
            "question": q,
            "base_answer": base_model_fn(q),
            "finetuned_answer": finetuned_model_fn(q),
        })
    return pairs

# 导出为 CSV 让人工打分
import csv
with open("eval_pairs.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["question", "base_answer", "finetuned_answer", "winner"])
    writer.writeheader()
    writer.writerows(pairs)

AI-as-Judge

用 GPT-4o 当裁判,自动化评测:

def ai_judge(question: str, answer_a: str, answer_b: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """你是一个回答质量评估专家。比较两个回答,从以下维度打分(1-5):
- accuracy: 准确性
- completeness: 完整性
- professionalism: 专业度
- format: 格式规范性

返回 JSON:{"answer_a": {"accuracy": x, ...}, "answer_b": {"accuracy": x, ...}, "winner": "a"或"b"或"tie"}""",
            },
            {
                "role": "user",
                "content": f"问题:{question}\n\n回答A:{answer_a}\n\n回答B:{answer_b}",
            },
        ],
        response_format={"type": "json_object"},
        temperature=0,
    )
    return json.loads(response.choices[0].message.content)


def batch_evaluate(pairs: list[dict]) -> dict:
    results = {"a_wins": 0, "b_wins": 0, "ties": 0}
    for pair in pairs:
        result = ai_judge(pair["question"], pair["base_answer"], pair["finetuned_answer"])
        winner = result.get("winner", "tie")
        if winner == "a":
            results["a_wins"] += 1
        elif winner == "b":
            results["b_wins"] += 1
        else:
            results["ties"] += 1

    total = len(pairs)
    return {
        "base_model_win_rate": results["a_wins"] / total,
        "finetuned_win_rate": results["b_wins"] / total,
        "tie_rate": results["ties"] / total,
    }

持续迭代:数据飞轮

微调不是一次性的事。模型上线后要持续改进:

用户使用

收集 bad case(回答不好的样本)

人工修正为正确答案

加入训练数据集

重新微调

A/B 测试新模型

效果更好 → 上线替换
class DataFlywheel:
    def __init__(self, dataset_path: str):
        self.dataset_path = dataset_path
        self.feedback_buffer = []

    def collect_feedback(self, question: str, ai_answer: str, rating: int, corrected_answer: str = ""):
        self.feedback_buffer.append({
            "question": question,
            "ai_answer": ai_answer,
            "rating": rating,
            "corrected_answer": corrected_answer,
            "timestamp": time.time(),
        })

    def export_training_data(self, min_rating: int = 4) -> list[dict]:
        """导出高分和人工修正的数据为训练格式"""
        training_data = []

        for item in self.feedback_buffer:
            if item["corrected_answer"]:
                training_data.append({
                    "instruction": "",
                    "input": item["question"],
                    "output": item["corrected_answer"],
                })
            elif item["rating"] >= min_rating:
                training_data.append({
                    "instruction": "",
                    "input": item["question"],
                    "output": item["ai_answer"],
                })

        return training_data

总结

  1. 先试 Prompt 和 RAG,最后再微调——微调是重武器,但成本和复杂度也最高。
  2. 数据质量 >> 数据数量——200 条高质量数据胜过 5000 条垃圾数据。
  3. QLoRA 是平民首选——7B 模型只需 6GB 显存,一张消费级显卡就能训。
  4. Unsloth / LLaMA-Factory 降低门槛——一个脚本或 Web UI 就能完成微调。
  5. GGUF + Ollama 最简单部署——量化导出,一条命令启动,OpenAI 兼容接口。
  6. 评估不能省——AI-as-Judge + 人工抽检,量化微调效果。
  7. 数据飞轮持续迭代——上线不是终点,收集反馈 → 修正数据 → 重新训练。

系列回顾

主题核心能力
01-02文件解析 + 切片RAG 数据准备
03大文件上传前后端工程化
04-05Embedding + 向量库RAG 存储层
06检索优化RAG 质量提升
07意图识别AI 路由层
08结构化输出输出稳定性
09AI 缓存成本和速度优化
10Token 管理上下文和成本控制
11大模型微调模型定制化
12Cursor SkillAI 可执行的工程技能
13Harness Engineering驾驭 AI 的工程效能革命

讨论话题:你微调过大模型吗?用的什么基座模型、多少数据、效果怎么样?评论区聊聊你的微调经验。

评论