前幾天都在講輸入端:注意力、成本、快取。今天換到輸出端。模型吐出來的是自然語言,是給人讀的;但系統裡下一站往往是程式:要進資料庫、要走 if/else、要呼叫下一個 API。「讓 LLM 的輸出變成可靠的結構」,是把模型接進真實系統的第一道門檻,也是最多人踩坑的地方。
要從 LLM 拿到 JSON,有兩條技術路線。
第一條是用求的:在 prompt 裡描述格式(「請輸出 JSON,欄位有 name 和 age」),拿回文字再解析。這條路的問題是模型會客氣:它可能在 JSON 前面加一句「好的,以下是您要的資料:」,可能用 markdown 的程式碼區塊包起來,可能在欄位名稱上發揮創意。你的 parser 要處理所有這些變體,而且處理得再好,也擋不住模型哪天心情不同。
第二條是用逼的:用模型原生的 tool calling。把 schema 宣告成一個工具,強制模型「呼叫」它,模型生成的就是符合 schema 的參數。約束直接發生在生成層面,可靠性完全是另一個等級。
現在主流模型(OpenAI、Anthropic、Google)都支援 tool calling,所以預設答案很簡單:能用逼的就不要用求的。求的那條路留給兩種情況:模型太老不支援,或你需要串流輸出部分結果。
LangChain 的 with_structured_output() 是個好教材,因為它把整條路線的取捨都寫在原始碼裡。
用起來長這樣:
class Movie(BaseModel):
title: str = Field(description="電影標題")
year: int = Field(description="上映年份")
rating: float = Field(ge=0, le=10, description="評分 0-10")
structured_llm = llm.with_structured_output(Movie)
result = structured_llm.invoke("推薦一部電影")
# → Movie(title="...", year=2024, rating=8.5),直接是型別物件
它的內部實作就是上面說的第二條路:把你的 Pydantic model(Python 最常用的資料驗證函式庫,用 class 宣告欄位和型別)用 bind_tools 綁成工具、強制呼叫,再接一個 parser 把參數轉回 Pydantic 物件。你拿到的是有 runtime 驗證的型別物件:rating 超出 0 到 10、year 不是整數,當場爆錯,不會默默流進下游。
有兩個設計取捨值得看:
驗證和串流不可兼得。 JsonOutputParser 支援串流時逐步吐出「解析到一半的 JSON」,使用者可以看到欄位逐漸填充;但 Pydantic 驗證需要完整物件,解析到一半的東西驗證語意不明確,所以帶驗證的 parser 乾脆不支援 partial 輸出。要體感就放棄驗證,要驗證就等完整結果,這是機制決定的,換框架也一樣。
修不好就再問一次,但要設上限。 OutputFixingParser 的做法是:解析失敗時,把錯誤訊息和原輸出丟回給 LLM 請它修正。這招有效,但預設最多重試一次,因為每次重試都是一次完整的 LLM 呼叫,沒有上限的自我修正是在燒錢等奇蹟。
如果專案還沒綁定框架,另一個選項是直接用天生以型別為核心的 SDK。PydanticAI(Pydantic 團隊自己做的 agent 框架)把輸出型別做成 Agent 宣告的一部分,體驗接近 FastAPI:
from pydantic_ai import Agent
agent = Agent('anthropic:claude-sonnet-4-5', output_type=Movie)
result = agent.run_sync('推薦一部電影')
result.output # → Movie(title="...", year=2024, rating=8.5)
同一個 Movie schema,宣告在 agent 建立的那一刻,之後每次執行拿到的都是驗證過的物件,不用在呼叫端各自接 parser。對重視型別的 Python 團隊,這條路的預設值比較符合直覺。
還有一個容易被忽略的點:你的 schema 會被塞進 context,它就是 prompt 的一部分,Day 2(Prompt 為什麼有效)的所有機制對它都成立。
欄位名稱取得好(sentiment 配上 description="正面或負面"),模型填得又快又準;欄位名稱含糊、巢狀七層、二十個 optional 欄位,模型的錯誤率跟著上升。設計 schema 時把它當成你在跟模型說話的一部分:欄位少、命名直白、每個欄位一句 description。這跟你寫 API 給人類用的直覺完全一致。
順帶一提,昨天講的 cache 在這裡也有戲份:schema 每次呼叫都一樣,放在 prompt 的固定前綴區,它的 token 就走快取價。
今天這層做好之後,你得到的不只是「能接管線的輸出」,還有一個更重要的東西:可以被程式打分數的輸出。自然語言的輸出只能靠人看,結構化的輸出可以自動比對正確答案。而一旦輸出可以量測,prompt 就不必用手調了。明天講 prompt 的自動優化:讓系統從自己的錯誤裡,學會怎麼寫自己的 prompt。