Day 23:微調 — 什麼時候該做、LoRA 的帳怎麼算
主軸二的最後一個技術主題。前七天我們讓模型「知道」(RAG)、「能做」(Agent),今天讓它「像我們要的樣子」。
我把微調放在最後,是刻意的——因為它是最後才該考慮的手段。
今天要解決的問題
一個我經常被問的問題,以及一個經常錯誤的答案:
「我們公司有很多內部文件,是不是該微調一個自己的模型?」
答案通常是「不是」。 因為提問者真正想解決的是「讓模型知道公司的事」,而這是 RAG 的工作,不是微調的工作。
微調解決的是另一類問題:讓模型用你要的格式、風格、術語回答。分清楚這兩件事,能省下你三個月的時間與可觀的 GPU 費用。
📊 職缺訊號
「模型微調與客製化」只出現在 13.9% 的生成式 AI 職缺中,是八項任務中最低的。這個數字很誠實地反映了實務:多數應用不需要微調。但它出現時通常在資深職缺,因為它要求你能判斷「該不該做」——這比「會不會做」更難。
一、現象:三種手段的分工

圖 23-1:模型表現不佳時的手段選擇(示意決策流程)。微調在最右下角——它是最後一個選項,不是第一個。
| 手段 | 解決 | 不解決 | 成本 |
|---|---|---|---|
| 提示工程 | 格式、簡單風格、單點任務 | 複雜格式的穩定性 | 最低 |
| RAG | 事實知識、時效性、可追溯 | 風格、語氣、輸出格式 | 中 |
| 微調 | 風格、格式、領域術語、任務特化 | 注入新知識(效果差且不可追溯) | 高 |
「微調不適合注入知識」這句話 值得展開。 你可以硬用微調把公司文件塞進模型,但會遇到三個問題:模型會用「機率上合理」的方式重組事實(也就是幻覺)、無法追溯來源、文件更新就要重訓。同樣的需求,RAG 全部都做得更好。
二、原理:LoRA 為什麼可行
如果決定要微調,LoRA 幾乎是唯一合理的起點。它的核心觀念是:模型適應新任務時所需的權重變化,其實是低秩的——換句話說,那個巨大的變化矩陣可以用兩個瘦長的小矩陣相乘來近似。

圖 23-2:LoRA 的參數量與顯存效益(8B 模型、32 層、4 個投影,依公式精算)。左圖:r=8 時可訓練參數僅約 840 萬,是全參數的 0.1%。右圖:QLoRA 讓 8B 模型的訓練顯存需求落在單張消費級顯卡可負擔的範圍。
具體算一次(這也是面試會考的):
每個 LoRA 模組的參數量 = r × (d_in + d_out)
8B 模型:d = 4096、32 層、對 q/k/v/o 四個投影加 LoRA
→ 模組數 = 32 × 4 = 128
→ r = 8 時:128 × 8 × (4096 + 4096) ≈ 8.4M 個參數
→ 佔全參數比例 = 8.4M / 8.03B ≈ 0.10%
顯存的差別更關鍵:全參數微調要存權重、梯度、以及 Adam 的兩個動量狀態,8B 模型約需 96 GB;LoRA 只需要為那 0.1% 的參數存梯度與動量,加上 4-bit 量化的基底權重(QLoRA),總需求可壓到 10 GB 上下——這就是「單張消費級顯卡能微調 8B 模型」的來源。
三、動手:一次可重現的 LoRA 微調
3.1 資料準備(這一步決定八成成敗)
"""prepare_data.py —— 微調資料的品質遠比數量重要。"""
import json
# 格式必須符合模型的 chat template,不同模型不一樣
examples = [
{"messages": [
{"role": "system", "content": "你是法律事務所的書狀助理,"
"用本所慣用格式與用語草擬文件。"},
{"role": "user", "content": "幫我擬一份租賃契約終止通知"},
{"role": "assistant", "content": "受文者:○○○ 君\n主旨:..."}, # ← 真實的範例輸出
]},
# ... 50–500 筆
]
with open("train.jsonl", "w", encoding="utf-8") as f:
for ex in examples:
f.write(json.dumps(ex, ensure_ascii=False) + "\n")
四個資料準備的原則:
- 品質 > 數量。 50 筆高品質、格式一致的資料,勝過 5,000 筆雜訊資料。
- 輸出必須是你真正想要的樣子。 模型會忠實地學會你給的樣本——包括它們的缺點。
- 保留一份 held-out 集(10–20%),用來與基礎模型對照。
- 確認授權與個資。 用客戶文件微調前,先確認合約允許,並完成去識別化(Day 27)。
3.2 訓練
"""train_lora.py —— 用 TRL + PEFT 做 LoRA 監督微調。"""
from datasets import load_dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from trl import SFTTrainer, SFTConfig
import torch
MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct" # 從小模型開始驗證流程
# QLoRA:以 4-bit 載入基底模型,大幅降低顯存
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16)
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, quantization_config=bnb,
device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
lora = LoraConfig(
r=8, # 秩:8–16 適合多數風格/格式任務;越大越貴且易過擬合
lora_alpha=16, # 慣例是 r 的 2 倍
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
task_type="CAUSAL_LM",
)
trainer = SFTTrainer(
model=model,
train_dataset=load_dataset("json", data_files="train.jsonl")["train"],
peft_config=lora,
processing_class=tokenizer,
args=SFTConfig(
output_dir="./lora-out",
num_train_epochs=3, # 小資料集 2–4 輪即可,太多會過擬合
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=2e-4, # LoRA 的學習率比全參數微調高一個量級
logging_steps=10,
bf16=True,
seed=42, # 可重現(Day 05 的第五根柱子)
),
)
trainer.train()
trainer.save_model("./lora-out") # 只存 adapter,通常只有幾十 MB
💡 沒有 GPU 怎麼辦
用 Google Colab 或 Kaggle 的免費 T4(16 GB)即可跑 1.5B 模型的 QLoRA。先用小模型把整條流程跑通——資料格式、訓練、評估、部署——再考慮換大模型。多數人卡住的是流程,不是算力。
3.3 評估:一定要與基礎模型對照
"""evaluate_lora.py —— 沒有對照組的微調評估沒有意義。"""
cases = load_holdout() # 訓練時沒看過的資料
for name, mdl in [("基礎模型", base_model), ("微調後", tuned_model)]:
fmt_ok = sum(check_format(mdl.generate(c["input"])) for c in cases) / len(cases)
judge = sum(llm_judge_score(mdl.generate(c["input"]), c["expected"])
for c in cases) / len(cases)
print(f"{name}:格式合規率 {fmt_ok:.1%}、風格評分 {judge:.2f}")
# 也要檢查「災難性遺忘」——微調後是否失去了原本的通用能力
for q in GENERAL_ABILITY_PROBES: # 一般常識、推理、多語能力的題目
print(q, "→", tuned_model.generate(q))
災難性遺忘是最常被忽略的檢查。 模型可能把你要的格式學得很好,卻失去了原本的推理或多語能力。微調後一定要跑一組「與微調任務無關」的題目確認沒有退化。
四、取捨:微調 vs 其他手段的決策
| 情境 | 建議 | 理由 |
|---|---|---|
| 要模型知道公司政策 | RAG | 可更新、可追溯、成本低 |
| 要模型用固定的公文格式 | 先提示 + few-shot,不穩定再微調 | 提示通常就夠了 |
| 要模型使用大量業界術語 | 微調 |
