Structured Output 實作課:讓 Agent 的輸出從「希望是 JSON」變成「保證是 JSON」
今日主題:Structured Output:約束解碼讓 Agent 輸出保證合規
2026-10-22 | 資料來源:OpenAI 官方文件、Taskade 2026 指南、XGrammar 論文(arXiv:2411.15100)、vLLM/SGLang 評測、llmbook 第 4.3 節
前言:每個上線的 Agent 都撞過同一面牆
幾乎每個把 Agent 推上生產環境的團隊,都經歷過同一個深夜:模型回傳了正確的資訊,但格式全毀——欄位缺了一個、數字變成字串、JSON 前面多了一句「好的,這是結果:」。你的 json.loads 炸掉,後續管線跟著陪葬,於是加了 try/except、寫了正則兜底、套了重試迴圈,然後在某個邊界案例上全部失效。根據 2025 年一份開發者調查,高達 78% 使用 LLM API 的開發者把「解析失敗或格式不如預期」列為反覆出現的痛點。
過去幾年的解法都是「拜託模型配合」:提示詞寫得更兇、重試寫得更勤。但 2024 年 8 月 OpenAI 推出 Structured Outputs 之後,遊戲規則變了——保證從你的程式碼搬進了 API 本身。到了 2026 年,三大雲端供應商都支援在解碼時強制執行 JSON Schema,自建開源模型也有成熟的函式庫可用。這篇文章帶你看懂三層輸出控制、背後的約束解碼原理,以及上線時真正會踩的坑。
一、三層輸出控制:從請求到保證
輸出格式的控制強度,可以分成三個等級(見圖一):
第一級:提示詞工程(Prompt Engineering)——「請只回傳 JSON,欄位有 name、email、score」。便宜、零門檻,但保證強度約 80–95%,邊界案例會靜默失敗,沒有任何型別保證。
第二級:JSON Mode——保證回傳「語法合法的 JSON」,但不保證符合你的 schema。模型可能給你 {"foo": "bar"},而你明明要的是 {"name": string, "age": number}。它解決了「括號沒關好」的問題,沒解決「欄位不對」的問題。
第三級:Strict Structured Outputs(嚴格結構化輸出)——你給出 JSON Schema,推理引擎在每次產生 token 時遮掉所有會違反 schema 的候選。模型在物理上無法輸出不合規的內容。OpenAI 的內部評測顯示:同一個困難的 JSON 測試集,舊模型無約束時低於 40%,新版模型無約束約 93%,而開啟強制執行後達到 100% schema 合規。93% 聽起來不錯,但換算下來大約每 14 次回應就炸一次解析器——而每一次炸掉,都是你要付兩次錢的重試。結論:2026 年的生產環境,凡是輸出要被程式吃掉的地方,都該用第三級。
二、原理:Constrained Decoding——在解碼時動手腳
LLM 產生文字時,每一步都從十幾萬個 token 的詞彙表中預測下一個 token。約束解碼(Constrained Decoding)在這個過程中加了一層約束層:
你的 JSON Schema 會先被編譯成文法(通常是有限狀態機 FSM 或上下文無關文法 CFG)。解碼的每一步,狀態機會根據「目前已經輸出到 schema 的哪個位置」,回傳一個合法 token 集合;解碼器把其他所有 token 的 logit 設為負無限大,再做取樣。模型的機率分佈在合法集合內被保留並重新正規化——品質不受損,但結構被鎖死(見圖二)。
舉個例子,schema 是 {"name": string, "age": integer},狀態機大概長這樣:
START → 期待 "{" → 期待 "\"name\"" → 期待 ":" → 期待字串值
→ 期待 "," 或 "}" → 若是 ",":期待 "\"age\"" → 期待 ":"
→ 期待整數值 → 期待 "}" → 完成
在「期待 {」的狀態下,"{" 和空白以外的所有 token 都被遮掉,模型別無選擇,只能輸出 {。這不是後處理,而是發生在生成當下的硬約束。
這不是新架構,而是有文獻支撐的解碼期技術:2026 年 9 月一篇 arXiv 論文實測五個開源小模型,套用 Outlines 或 XGrammar 後 schema 有效性從約八成直接拉到 100%——不需重訓、不需換供應商。
三、三大雲端供應商的實作現況
三家大廠的參數名稱各不相同,但底層都是同一套約束解碼機制:
| 供應商 | 參數 | 嚴格保證 |
|---|---|---|
| OpenAI | response_format: {"type": "json_schema", "json_schema": {...}, "strict": true}(Responses API 則用 text.format) |
有,文法約束 |
| Anthropic | output_config.format: {"type": "json_schema", "schema": ...};或對 tool 定義設 strict: true |
有(2025/11 公開測試、2026/2 全面上線;舊的 output_format 參數已棄用) |
| Google Gemini | responseMimeType: "application/json" + responseSchema |
有,schema 導向解碼 |
OpenAI 的 Python / JS SDK 更進一步:你可以直接傳 Pydantic 的 BaseModel 或 Zod 的 z.object 當 schema,回傳的物件已經是型別安全的解析結果,不必自己再 json.loads 一次。
四、自建路線:開源模型一樣有解
如果你自己 serving 開源模型,生態系同樣成熟:
- vLLM:SamplingParams 搭配 GuidedDecodingParams(json=schema),支援 JSON Schema、正則、枚舉選項、完整文法四種模式;後端可選 xgrammar(複雜文法最快)、Outlines、lm-format-enforcer。約束帶來約 5–15% 的 token 生成開銷,對擷取管線來說是合理的代價。
- Outlines:把正則、JSON Schema 或 Pydantic 模型編譯成 FSM,一次編譯、重複使用,適合 schema 固定的熱路徑。
- llama.cpp:GBNF 文法檔,能表達任何上下文無關文法——不只 JSON,SQL、程式碼、自訂 DSL 都能約束。
- Instructor(Jason Liu 開發,已成 Python 生態的實質標準):用 Pydantic 模型定義 schema,自動選用各供應商的原生嚴格模式,驗證失敗還會自動重問,最後直接還你一個型別化的 Python 物件。
實務上最順手的寫法長這樣:
import instructor
from pydantic import BaseModel, Field
from openai import OpenAI
class IncidentReport(BaseModel):
severity: str = Field(description="P1、P2、P3 或 P4")
summary: str = Field(description="一句話摘要")
confidence: float = Field(ge=0.0, le=1.0)
reasoning: str = Field(description="判斷依據,先寫推理再下結論")
client = instructor.from_openai(OpenAI())
report = client.chat.completions.create(
model="gpt-4o",
response_model=IncidentReport,
messages=[{"role": "user",
"content": f"分析這則告警:{alert_text}"}],
)
print(report.severity, report.confidence) # 保證是合法物件
注意上面有個細節:reasoning 欄位放在 severity 之後。順序是有講究的——模型是按欄位順序生成的,如果讓它先下結論、後寫理由,等於要求它「先決定、再想」;把推理欄位放在前面,結論品質會更好。
五、實務陷阱:保證的是「形狀」,不是「正確」
嚴格模式消滅了一整類 bug,但也帶來新的坑,上線前最好都知道:
1. 嚴格模式有規矩:OpenAI 的 strict 模式要求所有欄位都列為 required、additionalProperties 必須為 false。Anthropic 則直接忽略 schema 裡的 minimum、maximum、minLength 等數值約束——SDK 會把它們搬進欄位描述,退化成「禮貌請求」。
2. 拒絕與截斷會穿透保證:安全拒絕(refusal)和 max_tokens 截斷時,API 照樣回 HTTP 200,但內容不符合 schema。你的程式碼還是要做驗證,只是從「每次都驗」變成「處理例外」。
3. 形狀保證 ≠ 值正確:約束解碼保證的是結構,不是事實。模型照樣可以給你一個格式完美但胡說八道的答案——語意驗證還是你的責任。
4. Schema 也要付錢:schema 的 token 按正常輸入費率計費,沒有額外加價,但複雜 schema 的首次編譯會有 50–200ms 延遲,之後會被快取。
5. 推理模型要分段約束:對 o1 這類推理模型或 Anthropic 的 extended thinking,通常只約束「答案段」,讓 chain-of-thought 保持自由發揮,最後再切進約束狀態機輸出結構化答案。
結論:起手清單
- 生產環境凡是「輸出要被程式吃掉」的地方,一律用 strict structured outputs,不要再寫正則解析 JSON。
- 雲端用原生參數(OpenAI / Anthropic / Gemini 三家都有);自建用 vLLM guided decoding 或 Outlines;Python 專案直接上 Instructor+Pydantic。
- Schema 設計三原則:欄位順序讓模型「先想後答」、描述寫清楚每個欄位何時用、數值範圍約束不要只依賴 schema(Anthropic 會忽略)。
- 保留驗證碼,但只處理拒絕與截斷這兩種例外;語意正確性另做驗證。
從「拜託模型回 JSON」到「模型只能回合法的 JSON」,差別不只是少寫幾行解析碼,而是把一整類非確定性故障從架構裡移除。當 Agent 的每一步輸出都是型別安全的,整個多智能體管線才真正站得上生產環境。如圖一、圖二所示,這套機制在 2026 年已經是各家的標配——還在用正則解析 LLM 回應的,是時候升級了。
來源
- OpenAI 官方文件:Structured Outputs(response_format / text.format)
- Taskade 2026 指南:Structured Outputs: Reliable JSON from LLMs(Anthropic GA 時間 2026/2/4)
- Tokens.bd 2026 指南:三家供應商參數對照(2026/10/19 核對)
- Techsy 2026 指南:JSON Mode vs Strict Mode 對照表
- XGrammar 論文(arXiv:2411.15100,CMU):上下文無關文法的高效結構化生成引擎,最高 100 倍加速
- ibl.ai(2026/9 arXiv 實證):五個開源小模型套用 Outlines/XGrammar 後 schema 有效性達 100%
- SqueezeBits 部落格:vLLM 與 SGLang 上的 Guided Decoding 效能評測
- llmbook.icsgen-ai.org 第 4.3 節:Instructor 實務介面與案例(錯誤輸出從 12% 降至 0%)
- ZeroEntropy:Constrained Decoding 概念文件(推理模型分段約束)
- TopToolStack:Structured Outputs 成本分析(93% vs 100% 的重試成本)
沒有留言:
張貼留言