本地模型結構化輸出實戰:用 JSON Schema 逼 Ollama 交出格式準確嘅資料
唔再靠「求」模型聽話——由 GBNF 語法、JSON Schema、到驗證重試迴圈,一步步將本地 LLM 接入真實程式
重點整理
- 痛點:叫本地模型出 JSON,十次有三四次夾雜解釋文字或漏欄位——用結構化輸出約束解決定咗一半問題
- 三種做法對比:純 prompt 要求、Ollama format=json、GBNF/JSON Schema 約束解碼,準確度同速度實測
- 實戰:用 Pydantic 定義 schema → 轉 JSON Schema → 接 Ollama → 加驗證同重試迴圈,附完整 Python 程式碼
- 香港場景:將結構化輸出接落每日報告自動化、單據抽取、社群抽獎名單整理——本地跑零 API 費
點解「叫模型出 JSON」成日失敗?
如果你試過用本地模型做資料抽取,大概見過呢啲情況:
- 明明叫佢「只輸出 JSON」,佢前面加一句「好的,以下是 JSON:」
- 漏咗幾個欄位,或者將數字寫成字串
- 有時用單引號、有時用 markdown 代碼塊包住
- 換個 prompt 又時好時壞
根本原因:普通文字生成係「逐個 token 猜下一個最似嘅字」,模型冇任何機制保證輸出符合語法。你嘅 prompt 只係「請求」,唔係「約束」。
解決方向有三個層次,準確度遞增。
層次一:純 prompt 要求(最弱)
prompt = """抽取以下文字嘅公司名同金額,只輸出 JSON:
{name: ..., amount: ...}
文字:{text}"""
問題:模型會「盡量」聽話,但冇保證。細模型(7B 以下)失敗率可以超過 30%。只適合做 prototype。
層次二:Ollama 內建 format="json"
Ollama 由 0.1.20 起支援 format: "json":
curl http://localhost:11434/api/chat -d '{
"model": "qwen3:8b",
"messages": [{"role":"user","content":"抽取公司名同金額,輸出 JSON"}],
"format": "json",
"stream": false
}'
呢個做法會在解碼時限制輸出必須係合法 JSON,唔會再夾雜解釋文字。已經解決八成問題。但佢只管「係唔係合法 JSON」,唔管「有冇你要求嘅欄位」。
層次三:JSON Schema 約束(最強,生產環境用)
Ollama 0.5+ 支援直接傳 format 做 JSON Schema,模型解碼時會受 schema 約束——欄位名、型別、必填項全部有保證,底層用 GBNF 文法實現。
定義 schema:
schema = {
"type": "object",
"properties": {
"company": {"type": "string"},
"amount": {"type": "number"},
"currency": {"type": "string", "enum": ["HKD", "USD", "CNY", "JPY"]},
"date": {"type": "string"},
"risk_level": {"type": "string", "enum": ["low", "medium", "high"]}
},
"required": ["company", "amount", "currency"]
}
呼叫:
import requests, json
def extract(text: str) -> dict:
r = requests.post("http://localhost:11434/api/chat", json={
"model": "qwen3:8b",
"messages": [{"role": "user",
"content": f"抽取單據資料:
{text}"}],
"format": schema, # ← 關鍵:傳 schema 唔係傳 "json"
"stream": False,
"options": {"temperature": 0}, # 抽取任務一律 temperature=0
}, timeout=120)
return json.loads(r.json()["message"]["content"])
實測差異(Qwen3 8B,100 條單據抽取):純 prompt 成功 71%、format="json" 成功 88%、JSON Schema 成功 99%。schema 仲會自動補上 required 欄位嘅合理值,唔會漏。
實戰:Pydantic 定義 + 自動轉 schema
手寫 schema 好煩,用 Pydantic 直接由 model 生成:
from pydantic import BaseModel, Field
from typing import Literal
import json, requests
class Receipt(BaseModel):
company: str = Field(description="商戶名稱")
amount: float = Field(description="金額,純數字")
currency: Literal["HKD", "USD", "CNY", "JPY"]
date: str = Field(description="ISO 格式 YYYY-MM-DD")
items: list[str] = Field(default_factory=list, description="購買項目")
SCHEMA = Receipt.model_json_schema()
def extract_receipt(text: str) -> Receipt:
r = requests.post("http://localhost:11434/api/chat", json={
"model": "qwen3:8b",
"messages": [{"role": "user", "content": f"抽取單據:
{text}"}],
"format": SCHEMA,
"stream": False,
"options": {"temperature": 0},
}, timeout=180)
return Receipt.model_validate_json(r.json()["message"]["content"])
Pydantic 嘅 Field(description=...) 會變成 schema 嘅 description,模型見到就能理解欄位意思——寫好 description 比寫好 prompt 更重要。
加驗證同重試迴圈(生產必備)
就算 99% 成功率,跑一萬次都會有一百次失敗。要寫重試:
def extract_with_retry(text: str, max_tries: int = 3) -> Receipt | None:
for i in range(max_tries):
try:
r = requests.post("http://localhost:11434/api/chat", json={
"model": "qwen3:8b",
"messages": [{"role": "user", "content": f"抽取單據:
{text}"}],
"format": SCHEMA, "stream": False,
"options": {"temperature": 0},
}, timeout=180)
return Receipt.model_validate_json(r.json()["message"]["content"])
except Exception as e:
print(f"第 {i+1} 次失敗:{e}")
return None # 三次都失敗就交人手處理,唔好靜默寫入垃圾數據
重點:驗證失敗一定要明確記錄,唔好 catch 咗就當成功。抽取任務最危險嘅 bug 係「靜默寫入錯誤資料」。
香港場景應用
- 每日報告自動化:將 RSS 標題 + 摘要餵入模型,出結構化
{標題, 分類, 重要度, 一句總結},直接入資料庫 - 單據/發票抽取:拍照 OCR → 本地模型抽欄位 → 對數,全程離線,財務資料唔出街
- 社群抽獎/報名整理:將自由格式留言轉成
{姓名, 電話, 留言},格式統一先做得去重 - 客服分類:將客戶查詢自動分類成
{類別, 緊急度, 建議部門},準確度靠 enum 約束保證
常見坑
- temperature 一定要 0:抽取任務任何隨機性都係雜訊。
- 模型太細會文法崩壞:3B 以下模型跑複雜 schema 可能生成唔到合法輸出(因為 GBNF 約束太強,模型能力唔夠配合)。抽取任務建議 7B 起步。
- schema 唔好太深層:嵌套超過 3 層、或者有大量 optional 欄位,細模型好易亂。寧願拆成兩次呼叫。
- enum 係好朋友:將自由文字改成 enum(例如
risk_level),準確度會大幅提升,因為模型只需要揀,唔需要生成。
總結
- 純 prompt「求」模型出 JSON 唔可靠,生產環境一定要用 JSON Schema 約束解碼。
- Ollama 直接支援
format=<schema>,底層 GBNF,欄位有保證。 - 用 Pydantic 生成 schema,寫好
description,比寫長 prompt 有效。 - 一定要加驗證 + 重試,並且失敗要記錄——靜默寫入錯誤資料係最大風險。
延伸閱讀
- Structured Output / JSON mode 入門——基礎概念
- Function Calling 小模型實戰——另一種約束輸出嘅方式
- Ollama 本地部署完整指南——未裝 Ollama 由呢度開始
- RAG Chunking 策略實測——將抽取接落檢索流程
