目录

本地微调 LLM:ollama + unsloth 实战经验

先说什么时候值得微调

微调是成本很高的操作。在动手之前先问自己:

问题 1:prompt engineering 能解决吗?
  → 能:不要微调

问题 2:RAG 能解决吗?
  → 能:不要微调

问题 3:模型本身能力不够吗?
  → 是:考虑微调

值得微调的典型场景:

  • 角色/语气:让模型用特定风格回答
  • 格式输出:固定格式的 API 响应
  • 垂直领域术语:医疗、法律、金融等专业词汇
  • 私有知识:公司内部概念、流程

不值得微调的:

  • 引入新知识(RAG 更好)
  • 修复幻觉(RAG + fact-checking)
  • 提升推理能力(更强的 base 模型更值)

工具选择

Ollama + Modelfile

Ollama 支持通过 Modelfile 微调模型:

# 创建微调配置
FROM llama3.2
PARAMETER temperature 0.7
SYSTEM """
你是一个资深 Go 工程师。
回答时用中文,代码示例用 Go。
"""

这个不算真正的微调,但用 System Prompt 调语气够用了。

Unsloth(真正微调)

如果要做真正的微调,用 Unsloth

# 安装
pip install unsloth

# 支持的模型
# - Llama 3.2 (1B, 3B, 8B, 70B)
# - Phi-3.5 (3.8B)
# - Gemma 2 (2B, 9B, 27B)

Unsloth 的优势:QLoRA 微调,24GB GPU 显存可以微调 70B 模型

数据准备

数据格式

{"text": "<|user|>\n帮我写一个 FastAPI endpoint\n<|assistant|>\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\[email protected](\"/users/{user_id}\")\ndef get_user(user_id: int):\n    return {\"id\": user_id}"}

格式要点:

  • user/assistant 角色标记清晰
  • 每个样本独立完整
  • 1000-10000 条足够,不要盲目堆量

数据质量远比数量重要

# 差数据(10000 条)
{"text": "帮我写代码\n写代码"}
{"text": "解释这个\n那个"}

# 好数据(1000 条)
{"text": "<|user|>...<|assistant|>..."}  # 每条完整、有意义

实测:500 条高质量数据 > 10000 条噪音数据。

训练流程

完整流程

from unsloth import FastLanguageModel
import torch

# 1. 加载模型(4-bit 量化)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/llama-3.2-3B",
    max_seq_length=2048,
    load_in_4bit=True,
)

# 2. 添加 LoRA adapter
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
    lora_alpha=16,
    lora_dropout=0,
)

# 3. 训练
from trl import SFTTrainer
from transformers import TrainingArguments

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=train_dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    args=TrainingArguments(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,
        warmup_steps=10,
        max_steps=100,
        fp16=not torch.cuda.is_bf16_supported(),
        logging_steps=10,
    ),
)

trainer.train()

推理

FastLanguageModel.for_inference(model)
inputs = tokenizer([prompt], return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=256)

常见坑

1. 过拟合(最常见)

# 症状:训练后模型在训练数据上表现完美,泛化很差
# 解决:
# - 减少训练步数
# - 增加数据集多样性
# - 降低 LoRA r 值

# 判断标准:看 validation loss 不只是 train loss

2. 灾难遗忘

# 症状:微调后模型丢失了原有能力(比如中文理解)
# 解决:
# - 用 DPO (Direct Preference Optimization) 而不是纯 SFT
# - 在训练数据中混入通用数据(20-30%)

3. 训练不稳定

# 症状:loss 发散、NaN
# 解决:
# - 降低 learning rate(从 2e-4 → 5e-5)
# - 增加 warmup steps
# - 检查数据格式是否有问题

什么配置的 GPU 能跑

GPU 可微调模型 时间(1000 steps)
RTX 4070 8GB Llama 3.2 1B / Phi-3.5 3B ~3 小时
RTX 4080 12GB Llama 3.2 3B / Gemma 2 2B ~2 小时
RTX 4090 24GB Llama 3.2 8B / Gemma 2 9B ~3 小时
A100 40GB Llama 3.2 70B ~1 小时

结论

本地微调已经平民化了。RTX 4080 以上就能玩。

但微调前先问自己:prompt engineering 和 RAG 真的不够吗?微调的成本(时间 + 数据准备 + 迭代)往往比想象的更高。

真正值得微调的场景不多,但一旦用对地方,效果很显著。