iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
AI Engineering

從 MCP 到專屬 Agentic 模型:30 天走完一條可評測、可微調、可自架的 AI Agent 模型與服務製作流程系列 第 19

[ Model Fine-Tuning ] Day 19 — Prompt Template 取捨與訓練前 Sanity Check:先跑通再放大

  • 分享至 

  • xImage
  •  

Day 19 今日地圖:今天在整條閉環的位置、承接與產出

I. 前言:訓練與推論之間,有一個容易被忽略的契約

微調的效益之一是節省 Token。當工具的使用規則被內化進權重之後,System Prompt 就不必每次注入完整的規則說明與 few-shot 範例。

這聽起來很直覺,但實際操作時會遇到一個問題:訓練時要餵多少 Prompt?

這個問題之所以棘手,是因為它牽涉到一個隱含的契約 —— 模型會依賴訓練時看到的上下文結構。如果訓練時 System Prompt 裡有完整的規則說明,模型會把那段文字當成環境的一部分;推論時若突然拿掉,它面對的分佈就與訓練時不同,表現自然會下滑。

換句話說,「訓練時給多少」與「推論時給多少」必須一致,而這個一致點該定在哪裡,是需要事先決定、而非事後調整的。

除此之外,今天還有一件更重要的事:在投入數小時的完整訓練之前,先用極小資料集確認整條管線真的能動。

這個步驟只需要幾分鐘,卻能提前攔下五類會讓整輪訓練白費的錯誤 —— 而這五類錯誤有一個共同特徵:它們都不會拋出異常

以下的內容,將會說明四種 Prompt 配置的取捨、工具呼叫格式的選擇、資料切分的洩漏風險,以及過擬合測試的完整判讀方式。

II. 四種 Prompt 配置

四種 Prompt 配置與訓練前 Sanity Check 五大把關

先把選項列清楚。以 Leave Copilot 為例,System Prompt 可以有四種粒度:

表格:配置、內容、相對 Token

需要特別說明的是,tools 欄位在四種配置下都必須保留。它是工具呼叫的必要資訊,而且 TRL 的 chat template 會自動處理它 —— 我們能夠壓縮的只有規則說明與範例的部分。

而這個壓縮值得認真看待,因為 System Prompt 是一項永久成本:它不是只付一次,而是每一次請求都要重付。一段兩千 token 的規則與 few-shot,在每天一萬次請求的服務上,就是每天兩千萬個 input token。

換句話說,微調要換回來的不只是準確率,還有這筆持續發生的開銷。系列後期驗收時會把它一起量出來。

各配置的實際樣貌

配置 A(完整) 大概是這樣:

你是差勤助手,協助同仁查詢與處理請假申請。

處理假單修改請求時,必須依下列步驟:
1. 先用 search_leaves 找出目標假單,取得真實的 leave_id。
   絕對不要自行推測或編造 leave_id。
2. 確認假單目前狀態。
3. 呼叫 update_leave_status 時,狀態只能依
   draft → submitted → approved → taken 逐級推進,不可跳級。

執行破壞性操作(withdraw_leave、cancel_approved_leave)時:
- 系統會要求使用者確認,這是必要流程。
- 若使用者拒絕(decline),立即停止,不得改用其他工具達成相同目的。
- 若使用者取消(cancel),詢問使用者的意向,不要直接重試。

以下是正確操作的範例:
(此處接三到五段完整的工具呼叫軌跡)

配置 C(精簡) 則壓縮成三句話:

你是差勤助手。操作假單前須先查詢取得真實 ID;狀態須逐級推進;
破壞性操作被拒絕後即終止。

III. 為什麼選配置 C

本系列採用配置 C 訓練,並在系列後期驗收時同時測試 C 與 D。 理由分別如下。

為什麼不用 A

用配置 A 訓練,模型會依賴 few-shot 範例的存在。它在訓練時看到的每一筆樣本前面都有那幾段正確軌跡,於是把它們當成上下文的固定組成。推論時若拿掉,表現會下滑 —— 等於我們什麼 Token 都沒省到。

而如果推論時仍然保留完整的 few-shot,那就回到了微調前的成本結構,微調的節省效益完全消失。

為什麼不用 D

配置 D 完全不給規則提示,模型純粹靠權重運作。理論上這是最省的,但它帶來一個實際的問題:失去了透過 Prompt 微調行為的彈性

假設上線後發現假單狀態機多了一個階段,或是某條規則需要臨時調整。用配置 D 的模型只能重新訓練;而配置 C 保留了一個「錨點」,可以先用 Prompt 做臨時修正,爭取到重訓的時間。

配置 C 的定位

配置 C 的三句話不是為了「教會模型」—— 那是訓練資料的工作。它的作用是在推論時提供一個對齊訊號,讓模型知道現在處於哪一種行為模式。

這有點像是給一個已經受過訓練的員工一張便利貼,上面寫著今天的重點事項。他本來就會做,但便利貼讓他不容易分心。

最關鍵的一條原則:訓練與推論的 System Prompt 必須完全一致。 這是最容易犯的錯誤,因為訓練腳本與推論腳本通常是分開寫的。建議把 System Prompt 抽成共用常數,用 import 的方式引用,而不是複製貼上。

IV. 工具呼叫的輸出格式

這裡有一個好消息:不需要自己設計格式

TRL 的 chat template 會把資料中的 tool_calls 轉換成該模型的原生工具呼叫格式 —— Gemma 有 Gemma 的寫法,Qwen 有 Qwen 的寫法,模板會自動處理。

有些人會想自己發明一套格式,例如:

<tool>update_leave_status</tool>
<args>{"leave_id": "LV-7f3a91", "status": "submitted"}</args>

這是應該避免的做法。 代價會在系列後期部署時出現:推論框架都內建了針對常見模型家族的工具呼叫解析器,但它們認不得自訂標籤。屆時您得自己寫 parser,而且每換一個推論框架就要重寫一次。

用原生格式,下游全部相容。

一定要看渲染結果

在開始訓練之前,把一筆樣本渲染出來實際檢視:

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained(MODEL_ID)
text = tok.apply_chat_template(
    sample["messages"],
    tools=sample["tools"],
    tokenize=False,
)
print(text)

這一步不能省。 眼睛看過渲染結果,才知道模型實際接收到的是什麼 —— 包括 tools schema 被放在哪個位置、佔了多少篇幅、以及特殊 token 的處理方式。

我看過不只一次的狀況是:資料格式看起來完全正確,但渲染之後發現 tool_calls 被模板吃掉了,或是 tools schema 根本沒有被插入。這類問題只有在實際渲染時才看得出來。

V. 資料切分與洩漏檢查

Day 16 的擴增是從同一批原始軌跡衍生出來的,這意味著單純的隨機切分會造成資料洩漏

具體來說:同一個原始案例的 8 個問法變體,隨機切分後可能訓練集分到 6 個、驗證集分到 2 個。而那 2 個驗證樣本,實際上模型在訓練時已經看過非常相似的內容 —— 驗證分數自然漂亮,卻完全反映不了真實的泛化能力。

正確做法是依原始案例分組切分

import random
from collections import defaultdict

groups = defaultdict(list)
for s in samples:
    groups[s["_origin_case_id"]].append(s)   # 擴增時記錄的來源案例 ID

case_ids = list(groups)
random.seed(42)
random.shuffle(case_ids)

split = int(len(case_ids) * 0.9)
train = [s for cid in case_ids[:split] for s in groups[cid]]
valid = [s for cid in case_ids[split:] for s in groups[cid]]

這也意味著 Day 16 擴增時就必須記錄每筆樣本的來源案例 ID。若當時沒記,現在補還來得及;訓練之後才發現就晚了。

建議把驗證留在腳本裡:

train_origins = {s["_origin_case_id"] for s in train}
valid_origins = {s["_origin_case_id"] for s in valid}
assert not (train_origins & valid_origins), "資料洩漏:有案例同時出現在訓練與驗證集"

VI. 過擬合測試:五分鐘攔下五類錯誤

這是 Day 15 至 Day 20 這六天裡最划算的一個步驟。

做法:取 20–50 筆樣本,訓練 20 個 epoch,觀察 loss 能不能降到接近 0。

from trl import SFTConfig, SFTTrainer
from peft import LoraConfig

tiny = train[:32]

trainer = SFTTrainer(
    MODEL_ID,
    train_dataset=tiny,
    peft_config=LoraConfig(),
    args=SFTConfig(
        output_dir="./sanity",
        learning_rate=1e-4,          # LoRA 用 1e-4,不是預設的 2e-5
        num_train_epochs=20,
        per_device_train_batch_size=2,
        assistant_only_loss=True,
        max_length=2048,
        logging_steps=1,             # 每步都印,方便觀察
        report_to="none",
    ),
)
trainer.train()

這個測試的邏輯是:如果模型連 32 筆資料都背不起來,那它一定不可能學會 3,000 筆。

反過來說,如果它能把這 32 筆背到 loss 接近 0,代表整條管線(資料格式、chat template、loss mask、優化器設定)是通的。

判讀方式

表格:現象、判斷、可能原因

最陰險的那一種

上表最後兩列之中,「Loss 一開始就很低」是最需要警覺的。

如果 assistant_only_loss 因為 chat template 不相容而靜默失效,模型會把 user 與 system 訊息也算進 loss。而那些內容它在推理時「看得到」,預測起來相當容易 —— 於是 loss 掉得特別漂亮,表面上訓練得非常成功。

判斷方法:觀察 mean_token_accuracy起始值

正常情況下,模型在訓練第一步應該預測不準,這個數值會偏低。若它從第一步就超過 0.9,八成是 mask 沒有生效。

TRL 訓練時會記錄以下指標,值得一併留意:

表格:指標、意義

VII. 訓練前最終檢查清單

在啟動完整訓練之前,逐項確認:

  • [ ] 過擬合測試通過,loss 降到接近 0
  • [ ] mean_token_accuracy 起始值合理(不是異常高)
  • [ ] 渲染後的樣本用眼睛實際看過
  • [ ] 訓練/驗證依原始案例分組,assert 通過
  • [ ] learning_rate=1e-4(不是預設的 2e-5
  • [ ] assistant_only_loss=True
  • [ ] max_length 依實際長度分佈設定,超長樣本比例可接受
  • [ ] System Prompt 配置確定(配置 C)並抽成共用常數
  • [ ] 訓練用的 JSONL 已存版本副本,可重現
  • [ ] output_dir 指向掛載的 Drive(若使用 Colab)

最後一項在 Day 18 說明過 —— Colab 的 VM 是暫時性的,checkpoint 沒寫到 Drive 就等於沒有保存。

而倒數第二項的理由是:之後若需要調整重訓,您會想知道「這次與上次的資料差在哪裡」。沒有版本副本,這個比較就無從進行。

VIII. 結語

Day 15 至 Day 20 到此告一段落。六天下來,我們從 Google ADK 的 Event 日誌出發,經過軌跡切分、資料擴增、負面樣本設計、基座選型與環境建置,最後完成了訓練前的所有把關。

總結來說,今天有三個重點值得帶走:

  • 訓練與推論的 Prompt 必須一致,這是一個隱含契約: 模型會依賴訓練時看到的上下文結構。用完整 few-shot 訓練、卻在推論時拿掉,等於什麼都沒省到;而完全不給規則錨點,則失去了臨時調整行為的彈性。配置 C 是在兩者之間取得平衡的位置。
  • 用模型原生的工具呼叫格式,不要自創標籤: 自訂格式的代價會在部署時出現 —— 推論框架內建的解析器認不得,每換一個就要重寫一次 parser。
  • 過擬合測試只花五分鐘,卻能攔下五類錯誤: 資料格式錯誤、chat template 不相容、學習率設錯、loss mask 未生效、顯存不足。這五項的共同特徵是都不會拋出異常,任何一項在完整訓練跑三小時之後才發現,都是三小時的浪費。

明天正式進入訓練。而在那之前,我想先提醒一個明天會詳談的觀念:loss 下降並不等於工具呼叫變準

Day 19 Cheat Sheet:指令、參數與容易踩的地方


參考來源

查證日期:2026-08-24


I am Simon

大家好,我是 Simon 劉育維,是一位 AI 領域解決方案專家,目前也擔任 Google Cloud AI 領域開發者專家 (GDE),期待能夠幫助企業導入人工智慧相關技術解決問題。如果這篇文章對您有幫助,歡迎在我的 Linkedin 上留言提供意見,並與我一起討論有關人工智慧的主題,期待能夠對大家有所幫助!

我的個人部落格資訊:https://medium.com/@simon3458


上一篇
[ Model Fine-Tuning ] Day 18 — Google Colab Pro 與 Colab CLI 工具鏈實戰:低成本跑通雲端訓練
下一篇
[ Model Fine-Tuning ] Day 20 — 執行 SFT 微調與 Loss 曲線判讀:從訓練損失看見工具能力
系列文
從 MCP 到專屬 Agentic 模型:30 天走完一條可評測、可微調、可自架的 AI Agent 模型與服務製作流程30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言