任務的起點

使用者拿來一份用 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_STEPSMAX_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,不能靠邏輯推斷替代。

來源:個人開發日誌 2026-07-01 · Gemma 4 E2B(有效 2.3B,Apache 2.0)· gemma4-e2b-agent-kit · QLoRA + LangGraph