任務的起點
使用者拿來一份用 Gemini 兩輪對話產出的 Gemma 4 E2B Agent 微調指南, 要做邏輯分析。流程看起來很直接:讀指南、查資料、找問題、輸出修訂版。
但流程很快就繞了一圈。第一輪分析看到 <|channel>thought 這個 token,
直覺認定這是「格式錯亂的幻覺」——因為它看起來不對稱、長相奇怪。
搜尋官方資料後發現這是 Gemma 4 的真實內部 token,當場更正。
第一個錯:把合法 token 誤判為幻覺
Google 在 2026-04-02 發布 Gemma 4,有五個尺寸(E2B / E4B / 12B / 26B-A4B / 31B)。 E2B 是「有效 2.3B 參數」的 MoE 小模型,壓縮後 <1.5GB,128K context, 可以跑在 Raspberry Pi 5 上。
它的 special tokens 用的是 Gemini 系統的命名:sot/eot=turn(而非 user/assistant),
soc/eoc=channel(思考通道),std/etd=tool,stc/etc=tool_call,str/etr=tool_response。
<|channel>thought 是思考通道的開始標記,完全合法。
對「看起來不對稱」的新模型 token,正確做法是先查官方 tokenizer_config.json,不是憑舊知識否定。
真正的錯:ChatML 混進了 Gemma 4 原生格式
確認 Gemma 4 本身的 token 沒問題之後,再回頭看指南的訓練範例格式。 這裡才找到真正的問題。
原 Gemini 指南的範例用 <|im_start|>/assistant 對話標記,
工具呼叫用純 JSON code block。
但 Gemma 4 原生的格式是 <|turn>/model(非 assistant),
工具呼叫是 <|tool_call>call:fn{...}<tool_call|>。
兩個格式完全不相容。
照原範例製作訓練資料、微調後,serving 時 apply_chat_template
用的是 Gemma 4 原生模板,輸出的格式就和模型學到的不一致,推論結果直接錯亂。
這是「訓練資料格式」和「serving 解析格式」不吻合的經典問題,而且在訓練完才會爆。
三件套的核心設計
修訂版輸出三個檔案:修訂指南(含逐字核對的 Part 1 範例)、
train.py(QLoRA 微調)、runtime.py(LangGraph 推論)。
設計上唯一不變量是:三個檔案共用同一份 chat template,
才能確保訓練產出和 serving 解析不斷鏈。
train.py 用 QLoRA 4-bit 加 train_on_responses_only,
marker 設成 <|turn>user\n/<|turn>model\n(非 ChatML),
並內建 assert 防呆:訓練資料中若出現 <|im_start|> 直接報錯,
確保 ChatML 格式不會混入。
runtime.py 用 LangGraph 的 plan/act 雙節點加條件邊,
每個節點只暴露 1–2 個工具(JIT 暴露),kv_state 只餵最新的 observation,
避免 context 因為多輪對話越積越長。硬編碼 MAX_STEPS 和 MAX_RETRY
防止推論無限迴圈。
兩個踩坑值得記
一是 PEP 668 問題:macOS 系統 Python 禁止直接 pip install(externally-managed 錯誤)。
沒有用 --break-system-packages 汙染系統,改在 scratchpad 建拋棄式 venv
裝 langgraph 驗證邏輯。對「不能動的環境」,隔離 venv 是標準解法。
二是 langgraph 無 __version__:import 成功但讀版本號會拋 AttributeError,
不要誤判為安裝失敗。用 pip show langgraph 確認版本。
另外:本機沒有 GPU,.train() 和真實 vLLM serving 都沒有實跑。
已驗證的是格式對官方 zero-diff,以及 LangGraph runtime 邏輯跑通。
相關性能數據需要 GPU 環境才能量。
關鍵教訓
新模型 token 先查官方再下結論:<|channel>thought 看起來奇怪,但是合法標記。「不認識」不等於「幻覺」,官方 tokenizer_config.json 是 ground truth。
訓練格式和 serving 格式必須用同一 template:ChatML 和 Gemma 4 原生格式不相容,混用會讓訓練好的模型在推論時輸出錯亂。
防呆勝過文件:assert 無 <|im_start|> 放進 train.py,讓格式錯誤在訓練前就報錯,而不是等到 serving 才發現。
隔離 venv 是「不能動的環境」的標準解:PEP 668 環境不用 --break-system-packages,建拋棄式 venv 驗完就刪。
驗證邊界要說清楚:已驗證格式正確性、LangGraph 邏輯;未驗證訓練效果、推論速度——這兩件事需要 GPU,不能靠邏輯推斷替代。