localaiforum
登入
指南 › 模型運用實務

結構化輸出與函式呼叫

跟模型要 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 模式不見得)。第二,語意:必要鍵都在、枚舉在範圍內、數字合理、字串非空。失敗時,把驗證錯誤餵回提示詞重試一次——受約束的重試很便宜,而且通常第一次反彈就成功。

這個精確的迴圈——發出呼叫、驗證、執行、觀察——就是智慧代理整天在跑的東西:智慧代理。

相關章節: 智慧代理 · 提示詞 · 取樣 · RAG