本地模型結構化輸出實戰:用 JSON Schema 逼 Ollama 交出格式準確嘅資料

唔再靠「求」模型聽話——由 GBNF 語法、JSON Schema、到驗證重試迴圈,一步步將本地 LLM 接入真實程式

AI 教學AI 生成

重點整理

  • 痛點:叫本地模型出 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 有效。
  • 一定要加驗證 + 重試,並且失敗要記錄——靜默寫入錯誤資料係最大風險。

延伸閱讀

分享畀朋友

相關文章

更多「AI 教學」

睇全部分類 →