上一篇讲了怎么选模型,这篇讲选好了模型之后更关键的决策:要不要微调?怎么微调?选哪个基座模型?

我花3周从零训了一个客服微调模型,踩了4个坑才明白:微调不是"跑个训练脚本就行"——数据准备比训练难10倍,基座选错比训练参数调错代价大10倍。


先说结论(不想看过程的直接抄作业)

场景方案基座模型理由Java类比
知识要灵活更新RAG不需要改知识库就生效改配置文件→重启
话术固定/术语专业微调Qwen3-7B中文最强+7B够用改源代码→重新编译
风格要一致+知识要更新RAG+微调Qwen3-7B微调定风格+RAG补知识改代码+改配置
只是试试效果先RAG,别微调—90%场景RAG够用先改配置,别改代码

3句话决策:

  1. 先RAG,再微调——90%场景RAG够用,别过早微调
  2. 中文业务基座选Qwen3-7B——中文能力最强+LoRA微调8GB显存够
  3. 数据准备>训练参数——50条好数据比500条垃圾数据效果好3倍

坑1:RAG够用非要微调,结果白花3周GPU费

翻车现场

老板要求"客服回答必须用公司话术",我心想"这不就是微调的典型场景吗",直接开始训模型:

# 第1步:准备数据(花了1周)
# 50条客服问答数据,手动标注

# 第2步:微调训练(花了5天)
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-7B")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-7B", load_in_4bit=True)

lora_config = LoraConfig(
    r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05, task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
# ... 训练代码 ...

# 第3步:测试效果(花了2天)

结果微调后准确率从82%提到87%——只提升了5个百分点。

同样的数据用RAG+Prompt方案:

# RAG方案:0 GPU成本,1天搞定
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

rag_prompt = ChatPromptTemplate.from_messages([
    ("system", """你是客服助手。规则:
1. 仅使用以下知识库内容回答,不得自行编造
2. 必须使用知识库中的原文话术,不得改写
3. 知识库没有的信息,回答"暂无相关信息"

知识库内容:
{context}"""),
    ("human", "{question}"),
])

# 测试同一个问题
# RAG准确率:85%(比微调只低2%,但0 GPU成本+1天搞定)

根因

大多数场景RAG就够用。微调的核心价值是"让模型记住行为模式",不是"让模型记住知识"——知识用RAG补充更灵活。

修复:RAG vs 微调决策树

维度RAG微调Java类比
知识更新✅ 改知识库就生效❌ 要重新训练改配置 vs 改代码
话术一致性❌ AI可能改写✅ 按训练数据回答配置可能被忽略 vs 代码强制执行
成本知识库维护GPU+时间配置维护 vs 编译部署
数据量知识库几百篇训练数据至少50条配置文件 vs 源代码
迭代速度分钟级天级热更新 vs 重新发布

决策流程:

需求分析
├── 知识要灵活更新?→ RAG
├── 话术必须严格固定?→ 微调
├── 两者都要?→ 微调定风格 + RAG补知识
└── 只是试试?→ 先RAG,别微调

什么时候才该微调?(3个硬条件,缺一个别微调)

条件说明Java类比
RAG/Prompt搞不定试了3轮Prompt优化+RAG仍然不达标配置改了3版还是不行
行为要固定话术/术语/风格必须严格一致业务规则要硬编码
有足够数据至少50条高质量标注数据有完整测试用例

坑2:基座选错,7B的中文能力比14B差但微调效果更好

翻车现场

以为"基座越强微调越好",选了参数最大的Qwen3-14B做基座:

# 14B基座微调
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-14B")
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-14B",
    load_in_4bit=True,  # 4-bit量化才能跑
    device_map="auto",
)

lora_config = LoraConfig(r=16, lora_alpha=32, ...)
model = get_peft_model(model, lora_config)

# 训练:8GB显存不够,OOM!
# RuntimeError: CUDA out of memory

换到16GB显存的机器上跑通了,但微调后客服准确率只有83%。

对比用Qwen3-7B做基座:

# 7B基座微调
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-7B")
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-7B",
    load_in_4bit=True,  # 8GB显存就够
    device_map="auto",
)

lora_config = LoraConfig(r=16, lora_alpha=32, ...)
model = get_peft_model(model, lora_config)

# 训练:8GB显存跑通,微调后客服准确率87%

根因

微调效果取决于"基座能力×数据匹配度",不是基座越强越好。7B版中文语料占比更高,和客服场景的数据分布更匹配,微调时"继承"的中文能力更强。14B版多语言能力强,但客服只需要中文,多语言能力是"干扰"。

修复:基座模型选择决策表

维度选7B选14B选32B+Java类比
显存需求8GB(4-bit)16GB(4-bit)多卡集群单机 vs 集群
中文业务✅ 首选✅ 但显存翻倍❌ 过重SQLite vs MySQL vs Oracle
代码/推理⚠️ 偏弱✅ 更强✅ 最强MyBatis vs JPA vs 全ORM
数据量<100条✅ 够用⚠️ 容易过拟合❌ 必须过拟合小项目用轻框架
训练时间1-2小时4-6小时1-2天编译秒级 vs 分钟级 vs 小时级

基座选择3原则:

  1. 中文业务→7B(够用+省钱+数据少不容易过拟合)
  2. 代码/推理→14B(推理链更强但需要更多数据)
  3. 千万别选32B+(除非你有100+条数据+多卡集群+业务确实需要)

基座模型推荐速查:

基座模型中文能力微调难度LoRA显存推荐场景Java类比
Qwen3-7B⭐⭐⭐⭐⭐低8GB中文业务首选Spring Boot轻量版
DeepSeek-R1-8B⭐⭐⭐⭐低8GB代码+推理Quarkus专项版
ChatGLM-4-9B⭐⭐⭐⭐⭐低8GB轻量对话MyBatis精简版
Qwen3-14B⭐⭐⭐⭐⭐中16GB数据多+要强推理Spring Boot全功能版
Llama-3-8B⭐⭐⭐低8GB英文场景Express.js轻量版

坑3:50条好数据比500条垃圾数据效果好3倍

翻车现场

第一次微调,心想"数据越多越好",搜集了500条客服对话数据:

# 500条数据——大部分是垃圾
training_data = [
    # 好数据(约50条):
    {"instruction": "退货流程是什么", "output": "退货流程:1.订单页面申请退货 2.等待审核(1-3工作日)..."},
    
    # 噪音数据(约200条):
    {"instruction": "你好", "output": "您好,有什么可以帮您"},  # 太简单
    {"instruction": "你们的东西不错", "output": "谢谢您的认可"},  # 无业务价值
    
    # 错误数据(约150条):
    {"instruction": "退货要多久", "output": "大概几天吧"},  # 模糊回答
    {"instruction": "怎么退款", "output": "你去问问客服"},  # 没有实质内容
    
    # 重复数据(约100条):
    {"instruction": "退货", "output": "退货流程:..."},  # 和第一条重复
]

微调后准确率只有78%——比不微调还低!

清理数据到50条高质量标注后:

# 50条高质量数据
clean_data = [
    # 每条都是:问题明确 + 回答准确 + 格式统一
    {"instruction": "退货流程是什么", "output": "退货流程:\n1. 在订单页面点击「申请退货」\n2. 等待审核(1-3个工作日)\n3. 审核通过后寄回商品(需保留原包装)\n4. 收到商品后3-5个工作日退款至原支付方式"},
    {"instruction": "如何修改订单地址", "output": "修改地址:\n1. 在订单详情页点击「修改地址」\n2. 仅限发货前修改\n3. 修改后需等待系统确认(约30分钟)"},
    # ... 每条格式统一,回答精确
]

微调后准确率87%——50条好数据比500条垃圾数据效果好3倍。

根因

微调是"学习行为模式",不是"灌输知识"。噪音数据教模型学到了"模糊回答""推脱回答"的行为模式;好数据教模型学到"精确回答""格式统一"的行为模式。质量>数量。

修复:数据质量5标准

标准好数据垃圾数据检查方法Java类比
问题明确"退货流程是什么""你好"问题包含具体任务方法签名清晰 vs 参数为null
回答精确4步退货流程"大概几天吧"回答有具体步骤/数字返回完整DTO vs 返回Optional.empty
格式统一都用"1.2.3.4"编号格式混乱所有数据格式一致统一返回JSON vs 有时JSON有时String
无重复50条各不相同100条重复问题去重测试用例不重复
无噪音只留业务核心对话含闲聊/无效回答手动审核每条只测核心业务逻辑

数据清洗代码:

import json
from collections import Counter

def clean_training_data(raw_data: list[dict]) -> list[dict]:
    """清洗训练数据,只保留高质量数据"""
    clean = []
    
    # 1. 去重(按instruction去重)
    seen_questions = set()
    for item in raw_data:
        q = item["instruction"].strip()
        if q not in seen_questions and len(q) > 5:  # 问题太短不要
            seen_questions.add(q)
            clean.append(item)
    
    # 2. 过滤太简单的问题
    simple_words = ["你好", "谢谢", "好的", "再见", "嗯"]
    clean = [item for item in clean 
             if item["instruction"].strip() not in simple_words]
    
    # 3. 过滤太短的回答
    clean = [item for item in clean 
             if len(item["output"].strip()) > 20]  # 回答至少20字
    
    # 4. 过滤模糊回答
    vague_words = ["大概", "可能", "差不多", "你去问", "不知道"]
    clean = [item for item in clean 
             if not any(w in item["output"] for w in vague_words)]
    
    # 5. 统计质量
    print(f"原始数据: {len(raw_data)}条")
    print(f"清洗后: {len(clean)}条")
    print(f"问题分布: {Counter([item['instruction'][:10] for item in clean]).most_common(5)}")
    
    return clean

# 格式化数据为训练格式
def format_to_alpaca(data: list[dict]) -> list[dict]:
    """转换为Alpaca训练格式"""
    formatted = []
    for item in data:
        formatted.append({
            "instruction": item["instruction"],
            "input": "",  # 客服场景通常没有额外input
            "output": item["output"],
        })
    return formatted

# 使用示例
raw_data = json.load(open("raw_customer_service_data.json"))
clean = clean_training_data(raw_data)
formatted = format_to_alpaca(clean)
json.dump(formatted, open("clean_data.json", "w"), ensure_ascii=False, indent=2)

数据量参考表:

任务最低数据量推荐数据量训练时间(7B LoRA)Java类比
客服话术30条50-100条30分钟-1小时单元测试最低10个
文案风格50条100-200条1-2小时集成测试
代码生成100条200-500条2-4小时覆盖率>80%
专业领域200条500-1000条4-8小时全量回归测试

坑4:LoRA参数乱调,r=64比r=16效果还差

翻车现场

以为"LoRA r越大效果越好",从r=8一路调到r=64:

# LoRA r=64(过度参数化)
lora_config = LoraConfig(
    r=64,         # 太大了!
    lora_alpha=128,  # alpha = 2*r
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.1,
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
# 可训练参数:1.2亿(占模型7%)
# 微调后:严重过拟合,训练集90%,测试集65%

对比r=16:

# LoRA r=16(推荐值)
lora_config = LoraConfig(
    r=16,         # 适中
    lora_alpha=32,   # alpha = 2*r
    target_modules=["q_proj", "v_proj"],  # 只微调2个模块
    lora_dropout=0.05,
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
# 可训练参数:800万(占模型0.1%)
# 微调后:训练集87%,测试集87%(不过拟合!)

根因

LoRA r不是越大越好。r=64时可训练参数是r=16的8倍,但数据只有50条——参数远多于数据,必然过拟合。就像用50个测试用例测一个有1.2亿行代码的系统,覆盖率看似100%但全是假阳性。

修复:LoRA参数选择速查

参数推荐值什么时候调大什么时候调小Java类比
r16数据>200条+任务复杂数据<50条(默认16够用)缓存大小:数据多用大缓存
lora_alpha2×r需要更强学习信号过拟合时降低权重因子
target_modulesq_proj+v_proj数据>500条+全模块微调数据少只微调关键2个只改核心Service vs 改全部Controller
lora_dropout0.05过拟合严重时提到0.1数据>200条+不拟合时降到0@Transactional隔离级别
learning_rate2e-4LoRA标准值,别乱改—JVM默认参数,别乱调

LoRA r vs 数据量对照:

数据量推荐r可训练参数(7B)过拟合风险Java类比
30-50条r=8~400万低小项目轻框架
50-100条r=16~800万低中项目标准配置
100-200条r=32~1600万中大项目需要调优
200+条r=64~3200万需要监控大项目+全量回归

完整LoRA微调代码:

"""Qwen3-7B客服微调完整代码"""

import json
from transformers import (
    AutoModelForCausalLM, AutoTokenizer,
    TrainingArguments, Trainer,
    DataCollatorForSeq2Seq,
)
from peft import LoraConfig, get_peft_model, TaskType

# ============ 1. 加载基座模型 ============
model_path = "Qwen/Qwen3-7B"

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    load_in_4bit=True,  # 4-bit量化,8GB显存够
    device_map="auto",
    trust_remote_code=True,
)

# ============ 2. 配置LoRA ============
lora_config = LoraConfig(
    r=16,              # 推荐值,数据<100条别调大
    lora_alpha=32,     # = 2 × r
    target_modules=["q_proj", "v_proj"],  # 只微调2个核心模块
    lora_dropout=0.05,
    task_type=TaskType.CAUSAL_LM,
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出:trainable params: 8,388,608 || all params: 7,615,856,640 || trainable%: 0.11%

# ============ 3. 加载训练数据 ============
def load_data(file_path: str) -> list[dict]:
    """加载清洗后的训练数据"""
    with open(file_path, "r", encoding="utf-8") as f:
        data = json.load(f)
    return data

def format_prompt(example: dict) -> str:
    """格式化为训练Prompt"""
    return f"""你是一个客服助手,请严格按照以下方式回答问题。

问题:{example['instruction']}
回答:{example['output']}"""

def tokenize_function(example: dict) -> dict:
    """tokenize训练数据"""
    prompt = format_prompt(example)
    tokenized = tokenizer(prompt, truncation=True, max_length=512)
    tokenized["labels"] = tokenized["input_ids"].copy()
    return tokenized

# 加载并tokenize
data = load_data("clean_data.json")  # 50条高质量数据
from datasets import Dataset
dataset = Dataset.from_list(data)
tokenized_dataset = dataset.map(tokenize_function)

# ============ 4. 训练配置 ============
training_args = TrainingArguments(
    output_dir="./qwen3-customer-service-lora",
    num_train_epochs=3,          # 3轮够用,别多
    per_device_train_batch_size=2,  # 8GB显存只能batch=2
    gradient_accumulation_steps=8,  # 等效batch=16
    learning_rate=2e-4,          # LoRA标准学习率
    warmup_steps=50,             # 预热50步
    logging_steps=10,
    save_steps=100,
    eval_strategy="no",          # 数据少不评估
    fp16=True,                   # 混合精度加速
    gradient_checkpointing=True, # 节省显存
    report_to="none",            # 不上报
)

# ============ 5. 开始训练 ============
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

print("开始微调训练...")
trainer.train()

# ============ 6. 保存模型 ============
model.save_pretrained("./qwen3-customer-service-lora")
tokenizer.save_pretrained("./qwen3-customer-service-lora")
print("微调完成!LoRA权重已保存")

# ============ 7. 测试效果 ============
from peft import PeftModel

# 加载微调后的模型
base_model = AutoModelForCausalLM.from_pretrained(
    model_path, load_in_4bit=True, device_map="auto", trust_remote_code=True
)
ft_model = PeftModel.from_pretrained(base_model, "./qwen3-customer-service-lora")

# 测试
test_questions = [
    "退货流程是什么",
    "如何修改订单地址",
    "产品保修期是多长时间",
]

for q in test_questions:
    inputs = tokenizer(q, return_tensors="pt").to(ft_model.device)
    outputs = ft_model.generate(**inputs, max_new_tokens=100, temperature=0.1)
    answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(f"问题: {q}")
    print(f"回答: {answer}\n")

微调 vs RAG vs Prompt 3方选择速查

维度Prompt优化RAGLoRA微调Java类比
改什么改提示词加知识库改模型参数改注释→改配置→改代码
效果上限低(AI可能不听)中(AI可能改写)高(AI强制执行)注释→配置→硬编码
成本0知识库维护GPU训练0→低→高
上线速度分钟小时天分钟→小时→天
适用场景通用优化知识补充行为固定通用调→功能加→规则定
风险AI不听指令知识库质量差过拟合/断维护注释被忽略→配置错误→代码bug

选择路径(从左到右升级):

Prompt优化 → 不够 → RAG → 还不够 → 微调
(改注释)    (改配置)    (改代码)

大多数场景在RAG阶段就够了,别跳级到微调。


4坑速查表

坑翻车根因修复Java类比
RAG够用非要微调3周GPU费只提升5%大多数场景RAG够用先试RAG,不行再微调先改配置,别改代码
基座选错14B比7B微调效果差基座能力×数据匹配度中文→7B,代码→14B选SQLite不是选Oracle
数据多但垃圾500条比50条效果差质量>数量5标准清洗+50条起步测试用例质量>数量
LoRA r太大r=64严重过拟合参数远多于数据r=16起步,按数据量调缓存大小看数据量

更多推荐