Skip to content

专题 12 | 模型微调入门 — 从通用模型到领域专用模型

本专题面向"已经能用 Prompt 工程和 RAG 构建 Agent,但发现模型在特定场景下总是不够听话"的前端工程师。它要解决的核心问题不是"怎么训练一个大模型",而是怎么用最小的成本和数据量,把一个通用模型调教成你业务场景的专用模型——让它准确输出你要的格式、理解你行业的术语、遵循你设定的行为模式。微调是专题 11 自托管部署的自然延伸:先把模型搬到自己的环境,再把模型调成自己想要的样子。

12.1 何时需要微调

在投入时间和 GPU 资源之前,必须先想清楚:你的问题真的需要微调来解决吗?大多数情况下,Prompt 工程和 RAG 已经够用了。微调是最后一道手段,也是效果上限最高的手段。

微调 vs Prompt 工程 vs RAG 的决策框架

方案适用场景成本效果上限
Prompt 工程通用任务、快速迭代零成本
RAG知识密集型、数据频繁更新低(向量库运维)中高
微调特定格式输出、领域术语、行为风格定制高(GPU + 数据准备)
RAG + 微调领域知识 + 行为定制最高最高

这四种方案不是互斥的,而是层层递进的。实践中最常见的路径是:先用 Prompt 工程快速验证想法,发现知识不够就加 RAG,发现行为不够就加微调,最终形成 RAG + 微调的组合拳。

微调的典型场景

微调真正发挥价值的场景,往往是 Prompt 工程反复调了几十版还是搞不定的时候:

  • 格式遵循:模型总是无法严格遵循特定输出格式(如 JSON Schema、XML 标签),无论你在 Prompt 里怎么强调。这是微调最立竿见影的场景——50 条训练数据就能让模型学会稳定输出指定格式。
  • 领域术语:金融里的"头寸"、医疗里的"主诉"、法律里的"不可抗力"——通用模型经常理解错误或用词不准。微调让模型学会你行业的"方言"。
  • Tool Calling 行为模式:你希望模型在特定条件下调用特定工具,但 Prompt 工程只能覆盖最常见的情况。微调可以让模型内化这些调用模式。
  • 品牌语气定制:客服 Agent 需要保持"专业但亲和"的语气,创作 Agent 需要特定的写作风格。这类"说不清但能举例"的需求,微调比写 Prompt 高效得多。
  • 小模型替代大模型:用 GPT-4 级别的输出作为训练数据,微调一个 8B 的小模型来复现 80% 的效果——这就是知识蒸馏的思路,能把推理成本降低一个数量级。

什么时候不该微调

微调不是万能的,以下场景应该优先使用其他方案:

  • Prompt 工程就能解决的问题:如果你还没认真优化过 Prompt(Few-shot、Chain-of-Thought、结构化指令),不要急着微调。很多时候一个好的 Prompt 模板就够了。
  • 数据不足:低于 50 条高质量样本时,微调效果不可靠。模型容易过拟合到少数样本的特征上,泛化能力反而下降。
  • 需要实时更新的知识:微调是把知识"烧录"进模型权重,更新成本高。如果知识库每周都在变(如产品文档、价格表),用 RAG 动态检索是更好的选择。
  • 通用能力已够用:如果模型在你的场景下表现已经不错,微调的边际收益可能不值得投入。

前端迁移提示:Prompt 工程就像 CSS 调样式——不改源码,纯靠外部配置改变表现;RAG 就像接 API 补数据——运行时动态获取需要的信息;微调就像改组件源码——深入底层重塑行为。层层递进,越深改动越大但效果越彻底。

12.2 微调核心概念

理解微调的原理不需要数学背景,只需要抓住一个核心问题:大模型有几十亿个参数,微调时应该改哪些、改多少?

全量微调 vs LoRA vs QLoRA

方法原理显存需求训练速度适合场景
全量微调更新所有参数极高(8B→60GB+)大规模数据、企业级
LoRA冻结原始参数,训练低秩矩阵中(8B→16GB)通用微调首选
QLoRALoRA + 4bit 量化基础模型低(8B→8GB)较慢消费级硬件首选

全量微调的想法最直接:把所有参数都拿来训练。问题是一个 8B 模型的全部参数以 fp16 精度存储就需要 16GB 显存,加上优化器状态和梯度,总显存需求超过 60GB——普通显卡根本扛不住。

LoRA(Low-Rank Adaptation)的核心思路是:模型在微调时的参数变化量其实是低秩的,不需要更新全部参数。它冻结原始模型权重,在旁边插入两个很小的矩阵来学习"改动量"。训练完成后,这两个小矩阵可以合并回原始权重,推理时不增加任何开销。

QLoRA 在 LoRA 基础上更进一步:把冻结的原始模型权重从 fp16 量化到 4bit,显存占用直接砍掉四分之三。代价是训练速度略慢(需要反量化计算梯度),但效果损失很小。

LoRA 直觉理解

假设原始模型有一个 1000×1000 的权重矩阵(100 万个参数):

  • 全量微调:修改全部 100 万个参数。像重新装修整栋房子。
  • LoRA(r=8):只训练两个小矩阵——1000×8 和 8×1000——合计 1.6 万个参数,仅占原始参数量的 1.6%。像只换家具和软装,不动墙体结构。
  • 效果:大量实验表明 LoRA 的效果接近全量微调。这说明微调需要的"改动量"确实是低秩的——你不需要改变模型的全部知识,只需要在特定方向上做微调。

LoRA 中的 r(rank,秩)控制小矩阵的大小。r 越大,能表达的改动越丰富,但训练的参数也越多、显存消耗越大。实践中 r=8 到 r=32 覆盖了绝大多数场景。

前端迁移提示:LoRA 就像 CSS 变量覆盖——不改组件源码(原始权重冻结),只在外层注入一小组"补丁参数"(低秩矩阵)就能改变整体行为。合并后(CSS 变量展开后),运行时性能完全一样。

12.3 硬件要求与环境准备

微调对硬件有明确要求,但门槛比你想象的低——得益于 QLoRA,一张消费级显卡就能微调 8B 模型。

微调硬件需求速查

模型大小方法最低显存/内存推荐硬件
3BQLoRA6GBRTX 3060 / M2 16GB
7-8BQLoRA8-10GBRTX 3090/4090 / M2 Pro 32GB
7-8BLoRA16GBRTX 4090 / M3 Pro 36GB
14BQLoRA16GBRTX 4090 / M3 Max 48GB
70B+QLoRA48GB+A100 / 云 GPU

Apple Silicon 用户注意:M 系列芯片的统一内存架构意味着"内存就是显存"。一台 32GB 的 MacBook Pro 可以用 Unsloth 微调 8B 模型,虽然速度比 NVIDIA GPU 慢,但完全可用于学习和小规模微调。

没有本地 GPU 也没关系:Google Colab 免费版提供 T4 GPU(16GB 显存),足以完成 8B 模型的 QLoRA 微调。很多人的第一次微调都是在 Colab 上完成的。

环境搭建

微调生态几乎全在 Python 中,这对前端工程师来说是个跨语言的挑战。好消息是你不需要精通 Python,只需要会执行脚本和读懂基本语法。

bash
# 创建 Python 虚拟环境(类似 node_modules 的隔离机制)
python3 -m venv finetune-env
source finetune-env/bin/activate

# 安装 Unsloth(自动处理 PyTorch、transformers 等依赖)
pip install unsloth

# 验证安装
python -c "from unsloth import FastLanguageModel; print('Unsloth ready')"

Python 虚拟环境的概念和 Node.js 项目的 node_modules 类似——每个项目有独立的依赖环境,不会互相污染。source activate 相当于进入这个隔离环境,之后所有 pip install 的包都装在这里面。

微调的工作流中,Python 负责模型训练和导出,但数据准备和效果评估完全可以用你熟悉的 TypeScript 完成。下一节会展示这种混合工作流。

前端迁移提示:微调需要 Python 就像 SSR 需要 Node.js——它是这个领域的运行时,不需要精通但要会用。venv 是 Python 的 node_modulespipnpmrequirements.txtpackage.json

12.4 微调数据准备

微调的效果上限由训练数据决定,而不是训练参数。准备高质量的训练数据,是整个微调流程中最重要也最耗时的环节。

数据格式:对话格式(ChatML)

主流微调框架统一使用 ChatML 对话格式,每条训练样本是一段完整的多轮对话:

json
{
  "messages": [
    {"role": "system", "content": "你是一个金融分析助手,擅长解读财报数据和市场趋势。回答时先给结论,再列支撑数据。"},
    {"role": "user", "content": "分析一下贵州茅台最新一季的营收情况"},
    {"role": "assistant", "content": "贵州茅台本季度营收同比增长 15.3%,主要受益于直销渠道占比提升和系列酒放量。具体数据:营业收入 425.68 亿元,归母净利润 206.3 亿元,毛利率维持在 91.5% 的高位..."}
  ]
}

模型会从这些对话中学习:什么样的 system prompt 下,面对什么样的用户输入,应该生成什么样的回复。训练数据的质量直接决定了微调后模型的行为质量。

数据格式:Tool Calling 格式

如果你的 Agent 需要调用工具,训练数据中应该包含 Tool Calling 的示例:

json
{
  "messages": [
    {"role": "user", "content": "查询北京今天的天气"},
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\": \"北京\"}"
        }
      }]
    },
    {"role": "tool", "content": "{\"weather\": \"\", \"temperature\": 25, \"humidity\": 40}"},
    {"role": "assistant", "content": "北京今天是晴天,气温 25°C,湿度 40%,非常适合户外活动。"}
  ]
}

这种数据教会模型三件事:什么时候应该调用工具(而不是直接回答)、调用哪个工具并传什么参数、拿到工具返回后如何整合成自然语言回复。

用 TypeScript 准备微调数据

数据准备是前端工程师完全能主导的环节——你熟悉的 TypeScript 在这里大有用武之地。以下是一个将业务数据转为训练格式的实用脚本:

typescript
// generate-finetune-data.ts
// 将业务数据库中的 Q&A 记录转为微调训练格式

interface ChatMessage {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string | null;
  tool_calls?: ToolCall[];
}

interface ToolCall {
  type: 'function';
  function: { name: string; arguments: string };
}

interface TrainingSample {
  messages: ChatMessage[];
}

interface BusinessQA {
  question: string;
  answer: string;
  category: string;
}

const SYSTEM_PROMPT = '你是XX领域的专业助手,回答准确、简洁,使用行业标准术语。';

function generateTrainingSamples(rawData: BusinessQA[]): TrainingSample[] {
  return rawData.map(item => ({
    messages: [
      { role: 'system', content: SYSTEM_PROMPT },
      { role: 'user', content: item.question },
      { role: 'assistant', content: item.answer },
    ]
  }));
}

function validateSample(sample: TrainingSample): boolean {
  if (sample.messages.length < 2) return false;
  const hasUser = sample.messages.some(m => m.role === 'user');
  const hasAssistant = sample.messages.some(m => m.role === 'assistant');
  return hasUser && hasAssistant;
}

// 从业务数据源加载(JSON 文件、数据库导出、API 抓取等)
const rawData: BusinessQA[] = JSON.parse(
  Deno.readTextFileSync('business-qa-export.json')
);

const samples = generateTrainingSamples(rawData).filter(validateSample);

// 打乱顺序,避免模型学到数据排列的模式
const shuffled = samples.sort(() => Math.random() - 0.5);

// 按 90/10 切分训练集和验证集
const splitIdx = Math.floor(shuffled.length * 0.9);
const trainSet = shuffled.slice(0, splitIdx);
const evalSet = shuffled.slice(splitIdx);

// 输出为 JSONL 格式(每行一个 JSON 对象,微调框架的标准输入)
Deno.writeTextFileSync(
  'train.jsonl',
  trainSet.map(s => JSON.stringify(s)).join('\n')
);
Deno.writeTextFileSync(
  'eval.jsonl',
  evalSet.map(s => JSON.stringify(s)).join('\n')
);

console.log(`训练集: ${trainSet.length} 条, 验证集: ${evalSet.length} 条`);

JSONL(JSON Lines)格式是微调框架的标准输入格式——每行一个完整的 JSON 对象,不需要外层数组包裹。这种格式方便流式读取,不用一次性把整个文件加载到内存。

数据质量要点

数据量不是越多越好,质量才是关键:

  1. 最低量:50-100 条高质量样本即可开始看到效果。不要为了凑数量而降低标准——10 条精心编写的数据比 100 条粗制滥造的数据更有价值。
  2. 多样性:覆盖目标任务的各种变体。如果你要训练一个客服 Agent,训练数据应该包含咨询、投诉、退款、技术支持等各类场景,而不是都集中在某一类。
  3. 一致性:所有样本的格式统一,标注规范。如果有的样本用 Markdown 格式回答,有的用纯文本,模型会困惑。
  4. 去噪:移除矛盾或低质量样本。如果两条数据对同一个问题给出了相反的回答,模型不知道该学谁。人工检查虽然耗时,但回报最高。

前端迁移提示:微调数据准备就像 Storybook 的 stories 编写——你需要为每种使用场景准备精心设计的示例,覆盖正常路径、边界情况和异常处理。stories 写得越好,组件越可靠;训练数据越好,微调效果越好。

12.5 使用 Unsloth 进行 QLoRA 微调

为什么选 Unsloth

微调框架有很多(Hugging Face TRL、Axolotl、LLaMA-Factory),但对入门者来说 Unsloth 是最友好的选择:

  • 速度快:通过内核优化实现 2 倍训练速度提升,节省 60% 显存占用。
  • 代码简洁:一行代码加载模型 + 配置 LoRA 适配器,不需要手动拼装复杂的训练流水线。
  • 模型覆盖广:支持 Llama、Qwen、Mistral、Gemma 等主流开源模型家族。
  • 导出友好:训练完成后一行代码导出为 GGUF 格式,可直接导入 Ollama 使用。这和专题 11 的部署方案无缝衔接。

完整微调脚本

python
# finetune.py — 使用 Unsloth 对 Qwen3-8B 进行 QLoRA 微调
from unsloth import FastLanguageModel
from datasets import load_dataset
from trl import SFTTrainer
from transformers import TrainingArguments

# 1. 加载基础模型 + 配置 LoRA 适配器
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen3-8B",
    max_seq_length=2048,
    load_in_4bit=True,  # QLoRA:用 4bit 量化加载基础模型
)

model = FastLanguageModel.get_peft_model(
    model,
    r=16,              # LoRA 秩:16 是通用默认值
    lora_alpha=16,     # 缩放因子:通常设为与 r 相同
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0,    # Unsloth 优化下 dropout=0 效果最好
)

# 2. 加载训练数据(上一步生成的 JSONL 文件)
dataset = load_dataset("json", data_files="train.jsonl", split="train")

# 3. 配置训练参数并启动训练
trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=TrainingArguments(
        per_device_train_batch_size=2,   # 每个设备的批大小
        gradient_accumulation_steps=4,   # 梯度累积(等效 batch_size=8)
        warmup_steps=5,                  # 学习率预热步数
        max_steps=60,                    # 总训练步数
        learning_rate=2e-4,              # 学习率
        fp16=True,                       # 混合精度训练
        logging_steps=5,                 # 每 5 步打印一次 loss
        output_dir="outputs",            # 输出目录
    ),
)

trainer.train()

# 4. 保存为 GGUF 格式(可直接导入 Ollama)
model.save_pretrained_gguf(
    "outputs/gguf",
    tokenizer,
    quantization_method="q4_k_m"  # 4bit 量化,平衡质量和大小
)
print("微调完成,GGUF 模型已保存到 outputs/gguf/")

这个脚本做了四件事:加载模型并挂载 LoRA 适配器 → 读取训练数据 → 训练 → 导出。在 RTX 4090 上,60 步训练大约需要 10-15 分钟;在 M2 Pro 上大约 30-45 分钟。

关键参数解释

参数说明建议值调整方向
r (rank)LoRA 秩,越大能学到的变化越多8-328 轻量任务,16 通用,32 复杂任务
lora_alpha缩放因子,控制 LoRA 权重的影响力等于 r通常不需要单独调
max_steps训练步数50-200数据少时减小,loss 没降够时增加
learning_rate学习率1e-4 到 5e-4loss 震荡就降低,loss 不降就提高
per_device_train_batch_size批大小1-4显存不够就降低
load_in_4bitQLoRA 4bit 量化True消费级硬件必须开启

参数调整的优先级:先确保训练能跑起来(batch_size 和显存匹配),再看 loss 曲线调 max_steps 和 learning_rate,最后根据效果调 r。

12.6 微调模型的评估

训练完成不等于大功告成。微调模型可能过拟合(训练集上完美但新问题上一塌糊涂),也可能欠拟合(训练不充分、效果不明显)。评估是确认微调成功的必要步骤。

基础评估方法

  1. Loss 曲线:训练过程中 loss 应稳步下降并趋于平稳。如果 loss 降到很低(< 0.1)但验证集 loss 开始上升,说明过拟合了——减少 max_steps 或增加训练数据。
  2. 人工评估:准备 20-30 条不在训练集中的测试 case,分别用基础模型和微调模型回答,人工对比打分。这是最可靠的评估方式,没有之一。
  3. A/B 对比:同一问题同时发给基础模型和微调模型,看微调模型是否在目标维度(格式、术语、风格)上有明显改善,同时通用能力没有明显退化。

自动评估脚本

对于格式遵循、关键词命中等可量化的维度,可以用脚本批量评估:

typescript
// evaluate-finetune.ts
// 自动对比基础模型和微调模型的表现

interface EvalCase {
  input: string;
  expectedKeywords: string[];  // 期望回复中包含的关键词
  expectedFormat?: RegExp;     // 期望的输出格式(正则)
  tags: string[];              // 分类标签,用于分维度统计
}

interface EvalResult extends EvalCase {
  actual: string;
  keywordHits: number;
  formatMatch: boolean;
}

async function evaluate(
  model: string,
  cases: EvalCase[]
): Promise<EvalResult[]> {
  const results: EvalResult[] = [];

  for (const c of cases) {
    const res = await fetch('http://localhost:11434/v1/chat/completions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model,
        messages: [{ role: 'user', content: c.input }],
        temperature: 0.1,  // 评估时用低温度,减少随机性
      }),
    });
    const data = await res.json();
    const actual = data.choices[0].message.content;

    const keywordHits = c.expectedKeywords.filter(kw =>
      actual.toLowerCase().includes(kw.toLowerCase())
    ).length;
    const formatMatch = c.expectedFormat
      ? c.expectedFormat.test(actual)
      : true;

    results.push({ ...c, actual, keywordHits, formatMatch });
  }

  const keywordAccuracy = results.reduce(
    (sum, r) => sum + r.keywordHits / r.expectedKeywords.length, 0
  ) / results.length;
  const formatAccuracy = results.filter(r => r.formatMatch).length / results.length;

  console.log(`模型: ${model}`);
  console.log(`  关键词命中率: ${(keywordAccuracy * 100).toFixed(1)}%`);
  console.log(`  格式遵循率: ${(formatAccuracy * 100).toFixed(1)}%`);

  return results;
}

// 加载测试用例
const testCases: EvalCase[] = JSON.parse(
  Deno.readTextFileSync('eval-cases.json')
);

// 对比基础模型 vs 微调模型
console.log('=== 基础模型 ===');
const baseResults = await evaluate('qwen3:8b', testCases);

console.log('\n=== 微调模型 ===');
const ftResults = await evaluate('qwen3-finetuned:latest', testCases);

自动评估不能替代人工评估,但可以快速发现大方向上的问题。特别是格式遵循率——如果微调的目的就是让模型稳定输出特定格式,这个指标最有说服力。

12.7 部署微调模型到 Ollama

微调完成后,最后一步是把模型部署起来,让你的 Agent 代码能调用它。如果你已经按照专题 11 搭建了 Ollama 环境,部署微调模型只需要三条命令。

从 GGUF 导入 Ollama

bash
# 1. 创建 Modelfile(Ollama 的模型描述文件,类似 Dockerfile)
cat > Modelfile << 'EOF'
FROM ./outputs/gguf/unsloth.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 stop "<|im_end|>"
PARAMETER num_ctx 4096
EOF

# 2. 导入到 Ollama(构建本地模型镜像)
ollama create my-finetuned-model -f Modelfile

# 3. 验证模型可用
ollama run my-finetuned-model "用你的领域问题测试一下"

Modelfile 中的 TEMPLATE 部分定义了对话模板,需要和训练时使用的模板一致。Qwen 系列模型使用 ChatML 格式(<|im_start|> / <|im_end|> 标签),Llama 系列使用不同的模板。模板不匹配会导致模型输出乱码或行为异常——这是初学者最常踩的坑。

代码中无缝切换

部署完成后,你的 Agent 代码只需要改一个字符串:

typescript
// 微调前:使用基础模型
const response = await client.chat.completions.create({
  model: 'qwen3:8b',
  messages: conversationHistory,
});

// 微调后:切换为微调模型,其他代码完全不变
const response = await client.chat.completions.create({
  model: 'my-finetuned-model',  // 只改这一行
  messages: conversationHistory,
});

这就是专题 11 中强调的"baseURL + model 可配置"架构的价值——无论是云端模型、本地基础模型还是微调模型,业务代码都不需要改动。

前端迁移提示:微调模型部署到 Ollama 就像发布 npm 包——打包(导出 GGUF)、注册(ollama create)、引用(改 model 名称),使用方式与之前完全相同。你的"消费者代码"不需要知道这个包的内部实现变了。

12.8 云端微调对比:OpenAI Fine-tuning API

并不是所有微调都需要本地 GPU。OpenAI 提供了 Fine-tuning API,用 API 调用的方式完成微调。了解两种方案的差异有助于你根据实际情况做选择。

本地微调 vs 云端微调

维度本地微调(Unsloth + Ollama)云端微调(OpenAI Fine-tuning)
成本结构GPU 硬件一次性投入,边际成本低按训练 token 计费,每次微调都花钱
数据隐私数据完全留在本地训练数据上传到 OpenAI 服务器
参数控制完全控制(r、alpha、lr、steps 等)有限参数可调(epochs、lr multiplier)
上手难度需要 Python 环境 + GPU 或 Colab准备 JSONL 文件 + API 调用即可
模型选择任意开源模型(Qwen、Llama、Mistral...)仅 OpenAI 模型(GPT-4o-mini 等)
导出能力可导出为 GGUF,自由部署只能通过 OpenAI API 调用
迭代速度本地即时测试,分钟级反馈提交训练任务,等待排队完成

选择建议:如果你的场景允许数据上云且预算充足,OpenAI Fine-tuning API 的上手门槛最低——准备好 JSONL 文件,调一个 API 就行。但如果你需要数据隐私、灵活的模型选择或长期成本控制,本地微调是更好的投资。

两种方案的训练数据格式是一样的(ChatML JSONL),所以你在数据准备上的投入可以复用。

12.9 常见失败模式与排查

微调不是一次就能成功的。以下是最常见的五种失败模式和对应的排查方法:

1. 过拟合

现象:训练 loss 降到很低,但面对新问题时回答质量反而变差,甚至直接复述训练数据中的回答。

原因:数据太少 + 训练步数太多。模型"背住了"训练数据而非"学会了"规律。

解决:减少 max_steps(先试 30-50 步看效果);增加训练数据量;在训练数据中混入 5-10% 的通用对话数据(防止遗忘)。

2. 灾难性遗忘

现象:微调后模型在目标任务上表现不错,但通用能力明显下降——不会做数学了、常识问答出错、语言组织变差。

原因:微调过度覆盖了原始权重中的通用知识。这在数据集高度同质化时最容易发生。

解决:降低 learning_rate(从 2e-4 降到 5e-5);降低 r 值(从 16 降到 8);在训练数据中混入多样化的通用样本。

3. 格式不学习

现象:微调的目标是让模型输出特定格式(如 JSON),但微调后格式遵循率没有明显提升。

原因:通常是训练数据中格式不一致——有的样本严格遵循了目标格式,有的没有。模型无法从不一致的信号中学到稳定的模式。

解决:逐条检查训练数据的格式一致性;确保每条样本都严格遵循目标格式;考虑增加 system prompt 中的格式说明。

4. OOM(显存不足)

现象:训练启动后报 CUDA out of memoryMPS out of memory

原因:模型 + LoRA 适配器 + 优化器状态 + 批数据超出了可用显存。

解决:降低 per_device_train_batch_size(先试 1);确认 load_in_4bit=True(QLoRA);减小 max_seq_length(从 2048 降到 1024);减小 r 值。

5. 收敛过慢

现象:训练了很多步但 loss 几乎不动。

原因:learning_rate 太低、数据质量有问题、或模型和数据格式不匹配。

解决:提高 learning_rate(从 1e-4 到 5e-4);检查数据是否正确加载(打印前几条看看);确认对话模板和模型匹配。

12.10 面试表达模板

以下是几种场景下的表达参考:

微调经验

"我使用 QLoRA 对 Qwen3-8B 进行了领域微调,用 200 条高质量对话数据训练了约 100 步。微调后在格式遵循和术语准确性上有明显提升——同样的 30 个测试 case,格式遵循率从 65% 提升到 95%。训练在一张 RTX 4090 上完成,全程不到 20 分钟。"

技术选型

"在选择微调 vs Prompt 工程时,我会先评估三个维度:格式遵循度、领域术语准确性和行为一致性。如果 Prompt 工程在任何一个维度上反复调优仍达不到业务要求,才会考虑微调。微调不是默认选项,而是最后一道手段。"

数据准备

"微调的核心不在训练本身,而在数据准备。我的做法是先从生产日志中筛选高质量的 Q&A 对,用 TypeScript 脚本清洗和转换为 ChatML 格式的 JSONL,然后人工审核每一条。100 条精心准备的数据比 1000 条噪声数据效果好得多。"

诚实说明局限

"微调也有其局限性。首先是数据准备成本——高质量训练数据需要领域专家参与标注,不是工程师能独立完成的。其次是灾难性遗忘的风险——微调过度会损害模型的通用能力。我的做法是在训练集中混入通用数据作为'正则化',并在评估时同时检查通用能力的保持度。"

12.11 验收清单

  • [ ] 能解释何时该微调 vs Prompt 工程 vs RAG,给出具体判断依据而非泛泛而谈。
  • [ ] 能说明 LoRA/QLoRA 的原理——冻结原始权重,训练低秩矩阵——以及为什么这样做效果接近全量微调。
  • [ ] 能用 TypeScript 准备符合 ChatML 格式的微调训练数据(JSONL),包含数据验证和训练/验证集切分。
  • [ ] 能用 Unsloth 完成一次 QLoRA 微调,理解 r、lora_alpha、max_steps、learning_rate 等关键参数的作用。
  • [ ] 能通过 Loss 曲线和 A/B 对比评估微调效果,识别过拟合和灾难性遗忘。
  • [ ] 能将微调模型导出为 GGUF 格式并导入 Ollama,在 Agent 代码中无缝切换使用。