這一關要打倒什麼?
Boss 是「格式不服從症候群」:你明明說「請輸出 JSON」,模型卻回你一大段散文,或在 JSON 前面先加一句「好的,以下是結果」。程式一解析,直接報錯。
類比:交班時你要求同事填「結構化交班單」,他卻口頭講了五分鐘。內容也許都對,但下游系統(下一班)沒辦法直接用。
這一關有兩個任務:
- 學會用 prompt 讓本地模型做三件事:摘要、結構化抽取、PHI 標記。
- 建立主線專案「病歷守門人」的 prompt-only baseline(只靠提示、不訓練的對照成績)。
提醒:本章所有「病歷」都是合成資料(見 LV10)。真實病歷即使在本機處理,也需要院方核准與倫理審查(IRB);本地不等於合法。
先講為什麼:沒有 baseline,就不知道 fine-tune 值不值得
臨床試驗要有對照組,才知道新療法的效果。fine-tune 也一樣:你得先知道「不訓練、只下指令」能做到什麼程度,才有資格說「訓練之後進步了多少」。
而且 baseline 可能直接給你答案:如果 prompt 加上格式約束已經夠用,就不必花時間訓練;反過來,如果 baseline 在某些項目(例如人名)表現很差,你就知道 fine-tune 要補什麼。
Prompt engineering 的三個基本招式
醫學領域的 prompt engineering 已有整理過的文獻(例如 JMIR 2024 的 scoping review,PMID 39255030)。對這一關夠用的招式有三個:
- 角色與任務說明:講清楚你要它做什麼,例如「你是病歷去識別化助手,請找出病歷中的個人識別資訊」。
- few-shot(少量範例):給 1–3 個範例,示範輸入與期望輸出的長相。就像教新進同仁:先給他看兩份寫好的範本。
- 輸出約束:明確要求只輸出 JSON,並給出欄位定義;若工具支援,直接傳入 JSON schema,讓引擎限制輸出格式。
輸出約束的真實案例(研究階段)
德國 Würzburg 大學醫院團隊(Eur Radiol,PMID 39390261)用 Llama-2-70B-chat,在院內伺服器上以 grammar 控制輸出,把胸部 X 光報告轉成結構化範本,全部產出皆為有效的結構化報告;與人類評讀者(reader) 的一致性差異落在預先設定的等效範圍內(MCC 英文 0.75、德文 0.66)。
要誠實說明它的定位:這是研究,使用 70B 模型與專用伺服器,沒有 fine-tune,也沒有顯示已進入常規報告流程。它說明的是「格式約束有效」,不是「本地 LLM 已進入臨床常規」。
另一份研究(Mukherjee 等,Radiology 2023,PMID 37815442)用 Vicuna-13B,未再微調,以多步 prompt 標註胸部 X 光報告的 13 項發現;在 100 份放射科專家標註的子集中,AUC 中位數為 0.84。同樣是研究階段、可在本機執行的概念驗證。
規則式也要有一席之地
CT 報告匿名化的研究(Eur Radiol,PMID 39480533)指出,規則式程式在日期、病歷號等項目全部移除,而離線 LLaMa-2 的日期 recall 僅 0.52,且有 10 份報告誤刪醫療資訊。這是單一任務的結果,但它提醒我們:格式固定的欄位,規則式可能更穩。所以本站的 baseline 同時包含 regex 對照。
實作:建立 prompt-only baseline
準備資料
LV10 會教你怎麼自己生成合成病歷。本關先用 20 份合成病歷(text 為病歷全文,phi 為標準答案的 PHI 位置)。若你尚未完成 LV10,可以先下載本站 Hugging Face 公開資料集(連結見 LV10 結尾)的 test.jsonl。
🍎 Mac:Ollama + Python
import json, requests
SYSTEM = (
"你是病歷去識別化助手。找出病歷中所有個人識別資訊(PHI),"
"只輸出 JSON,格式:{\"phi\": [{\"type\": \"NAME|DOCTOR|MRN|IDNO|DATE|PHONE|ADDRESS|HOSPITAL\", \"text\": \"原文片段\"}]}。"
"不要輸出任何其他文字。"
)
def ask(note: str, model: str = "gemma4:e2b") -> str:
r = requests.post(
"http://localhost:11434/api/chat",
json={
"model": model,
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": note},
],
"stream": False,
"think": False,
"format": "json",
"options": {"temperature": 0, "num_predict": 800},
},
timeout=300,
)
return r.json()["message"]["content"]
def is_valid(s: str) -> bool:
try:
obj = json.loads(s)
return isinstance(obj.get("phi"), list)
except Exception:
return False
notes = [json.loads(l) for l in open("test.jsonl", encoding="utf-8")][:20]
outs = [ask(n["text"]) for n in notes]
valid = sum(is_valid(o) for o in outs)
print(f"valid JSON: {valid}/{len(notes)}")
逐行解釋:
SYSTEM:系統提示,規定角色、輸出格式與欄位。PHI類型與 LV10 的標註一致。ask(...):把一份病歷送進本機 Ollama 的/api/chat。"think": False:關閉思考模式。作者實測 Gemma 4 E2B 若不關,思考過程會用完輸出預算,導致沒有 JSON。此參數是否被你的 Ollama 版本與模型支援,請以官方文件為準。"format": "json":要求 Ollama 限制輸出為合法 JSON(也可改傳 JSON schema,約束更嚴)。temperature: 0:降低隨機性,讓結果可重現。num_predict:最多產生的 token 數。is_valid:嘗試解析並檢查有沒有phi清單;這就是「valid JSON 比例」的定義。- 最後三行:讀 20 份病歷、逐份呼叫、統計有效比例。
計算 span recall
valid JSON 只是「長相合法」。更重要的是它有沒有找對。用 span recall 衡量:標準答案中的 PHI 片段,有多少比例被模型找到。
def span_recall(note, pred_json):
try:
ptexts = {p["text"] for p in json.loads(pred_json)["phi"]}
except Exception:
return 0.0
caught = [g["text"] in ptexts for g in note["phi"]]
return sum(caught) / len(caught) if caught else 1.0
ptexts:把模型預測的原文字串整理成集合;解析失敗就當 0 分。caught:標準答案裡的每一個 PHI 片段(重複出現的也各算一次),只要原文字串出現在預測集合中就算抓到。- 回傳抓到的比例,就是這份病歷的 span recall。
這裡只比對文字、不比對類型,因為遮蔽程式只看字串;這和 LV14 evaluate.py 的定義一致。正式評估會把所有病歷的片段合併計算,並附上信賴區間(見 LV14)。
🍎 Mac 與 ☁️ Colab 的對照
- Mac:上面的流程,記錄 valid JSON 比例與 span recall,作為 baseline M1。
- Colab:用 Unsloth 載入同一個基底模型(見 LV07),餵同一份 20 筆資料,得到雲端環境的對照數字。兩邊數字不一定相同,因為量化、生成設定、引擎都不同,記錄差異即可。
regex 對照組
對日期、電話、身分證字號這類有固定格式的欄位,寫幾條正規表示式(regular expression)當對照:
import re
RULES = {
"PHONE": r"09\d{2}[-\s]?\d{3}[-\s]?\d{3}",
"IDNO": r"\b[A-Z][12]\d{8}\b",
"DATE": r"\b(?:19|20)\d{2}[/-]\d{1,2}[/-]\d{1,2}\b",
}
- 每個項目一條規則;
\d代表數字,{3}代表連續三個。 - 人名、醫院名沒有固定格式,規則很難涵蓋,這是 regex 的天然弱點。
作者的初步觀察(冒煙測試,非正式結果)
以下是作者在 M5 32GB 做的早期冒煙測試,樣本小、未重複、也未做正式的資料切分,只能當方向性觀察:
- Gemma 4 E2B 不關閉 thinking:valid JSON 為 0%,因為思考過程用完了 500 token 的輸出預算。
- 關閉 thinking:span recall 約 0.37,valid JSON 約 45%。
- regex:span recall 約 0.37,人名完全抓不到。
這幾個數字不要引用成「Gemma 4 E2B 的能力」。它們的用途是示範:baseline 的價值在於讓你看見具體的落差(格式不穩、人名抓不到),並知道 fine-tune 該補哪裡。你自己的 20 份資料的正式 baseline 數字:【實測數據待補】。
RAG 迷你版(選修)
若你想讓模型讀自己的去識別化筆記或 PDF,可以用 AnythingLLM(MIT 授權,桌面版)或 LM Studio 的文件問答。原理是「先檢索相關段落,再讓模型根據段落回答」,適合需要引用資料的情境。何時用 RAG、何時用 fine-tune,下一關會整理。
本關重點
- prompt-only baseline 是 fine-tune 的對照組,沒有它就無法解讀訓練的效果。
- 三個基本招式:角色與任務、few-shot、輸出約束(JSON schema / grammar)。
- 格式約束能保證「長相合法」,不保證內容正確;相關案例屬研究階段,不代表已進入常規臨床。
- Gemma 4 E2B 預設會輸出 thinking,需要關閉;作者的冒煙結果(valid JSON 約 45%、span recall 約 0.37)是初步、小樣本,不能當正式數字。
- 格式固定的欄位可用 regex 當對照;任務要挑對才用 LLM。