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

我花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类比
r 16 数据>200条+任务复杂 数据<50条(默认16够用) 缓存大小:数据多用大缓存
lora_alpha 2×r 需要更强学习信号 过拟合时降低 权重因子
target_modules q_proj+v_proj 数据>500条+全模块微调 数据少只微调关键2个 只改核心Service vs 改全部Controller
lora_dropout 0.05 过拟合严重时提到0.1 数据>200条+不拟合时降到0 @Transactional隔离级别
learning_rate 2e-4 LoRA标准值,别乱改 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优化 RAG LoRA微调 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起步,按数据量调 缓存大小看数据量

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐