結構化輸出與函式呼叫
跟模型要 JSON,它會很樂意產出「差一點的 JSON」:尾逗號、沒引號的鍵、第一個大括號前一句親切的話。自由形式取樣永遠可能吐出無效 token,因為取樣器對你的 schema 一無所知。結構化輸出補上那個缺口,而函式呼叫是同一機制戴的另一頂帽子。
為什麼自由形式 JSON 會壞
每個生成的 token 都是從分布抽樣。每一步都有某個機率質量——很小,但溫度高於 0 時絕不為零——落在會弄壞格式的 token 上:比開括號更早的閉括號、陣列最後一個元素後的逗號、小寫 true 拼成 True(烤進訓練資料的 Python 習慣)。把「幾乎永遠有效」乘上 200 個輸出 token,你得到的是間歇性、無法重現的損壞——最糟的 bug。連溫度 0 都只讓輸出確定,不保證符合 schema。
解法階梯
| 階 | 機制 | 保證 | 哪裡有 |
|---|---|---|---|
| 純提示詞(「請回覆有效 JSON」) | 好好拜託 | 無——取樣仍可能出錯 | 任何引擎 |
| JSON 模式 | 提示詞+限制成 JSON 語法 | 語法有效的 JSON;不保證形狀 | OpenAI 相容 API、Ollama format: "json" |
| Schema 約束 | Schema → 文法 → logits 遮罩 | 輸出永遠可解析且符合 schema | llama.cpp(json_schema/GBNF)、vLLM guided_json、Ollama structured outputs、outlines/xgrammar |
每往上一階,付出彈性、買到確定性。只要消費端是程式,就用最上層。
文法約束解碼
把 schema 編譯成文法。每個解碼步驟中,無法延長出有效文件的 token,其 logit 在取樣之前就被設成 −無限大。無效輸出不是不太可能——是不可能。llama.cpp 內建 GBNF 文法;十進位數字的文法一行搞定:
root ::= "-"? [0-9]+ ("." [0-9]+)?
同樣的想法包住 JSON Schema。對本機 llama.cpp 伺服器(OpenAI 相容端點):
import json, requests
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
"component": {"type": "string",
"enum": ["auth", "upload", "ui", "api"]},
"severity": {"type": "string",
"enum": ["low", "medium", "high"]}
},
"required": ["title", "component", "severity"],
"additionalProperties": False
}
r = requests.post("http://localhost:8080/v1/chat/completions", json={
"messages": [{"role": "user",
"content": "Extract the bug: " + report}],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "bug", "strict": True,
"schema": schema}
},
"temperature": 0
})
bug = json.loads(r.json()["choices"][0]["message"]["content"])
Ollama 在 format 欄位放 JSON Schema 做同樣的事;vLLM 用 guided_json。同一機制、三種拼法——你引擎的確切欄位名稱在 引擎參數。
受約束不等於正確
文法保證形狀,不保證真相。模型可以對當機報告吐 {"severity": "low"},或用填充物塞滿字串欄位。遮罩也可能傷害開放式欄位:當模型想要分段而文法禁止換行時,品質下降。實用規則:
- 值域已知時,用窄枚舉代替自由字串。
- 你消費的每個欄位都加
required;additionalProperties: false阻止發明。 - 字串欄位保持簡短;把長散文移出受約束的回應。
- 無論如何在提示詞放一個期望輸出的範例——它引導內容,而文法做不到。
函式與工具呼叫
工具呼叫是結構化輸出加一條慣例:模型回傳要呼叫哪個函式、帶什麼參數;你的程式碼執行它並附加結果。OpenAI 的 tools 格式是本機引擎模仿的事實方言:
{
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["C", "F"]}
},
"required": ["city"]
}
}
}]
}
模型在訊息中用 tool_calls 回覆;你把輸出以 role: "tool" 訊息回傳,迴圈繼續。模型是針對特定工具方言訓練的——本機引擎上支援最好的是 Hermes 與 Qwen 血統模板(llama.cpp 加 --jinja、Ollama、vLLM 的自動工具選擇)。沒受過工具訓練的模型,無論你怎麼約束都會產出貌似合理但參數名是幻覺的呼叫,所以先為工作挑對模型:挑選適合的模型。
驗證解析與內容
每次呼叫後兩項檢查。第一,解析——json.loads 成功(文法通常保證這點;JSON 模式不見得)。第二,語意:必要鍵都在、枚舉在範圍內、數字合理、字串非空。失敗時,把驗證錯誤餵回提示詞重試一次——受約束的重試很便宜,而且通常第一次反彈就成功。
這個精確的迴圈——發出呼叫、驗證、執行、觀察——就是智慧代理整天在跑的東西:智慧代理。