iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0

前幾天都在講輸入端:注意力、成本、快取。今天換到輸出端。模型吐出來的是自然語言,是給人讀的;但系統裡下一站往往是程式:要進資料庫、要走 if/else、要呼叫下一個 API。「讓 LLM 的輸出變成可靠的結構」,是把模型接進真實系統的第一道門檻,也是最多人踩坑的地方。

兩條路:用求的,還是用逼的

要從 LLM 拿到 JSON,有兩條技術路線。

第一條是用求的:在 prompt 裡描述格式(「請輸出 JSON,欄位有 name 和 age」),拿回文字再解析。這條路的問題是模型會客氣:它可能在 JSON 前面加一句「好的,以下是您要的資料:」,可能用 markdown 的程式碼區塊包起來,可能在欄位名稱上發揮創意。你的 parser 要處理所有這些變體,而且處理得再好,也擋不住模型哪天心情不同。

第二條是用逼的:用模型原生的 tool calling。把 schema 宣告成一個工具,強制模型「呼叫」它,模型生成的就是符合 schema 的參數。約束直接發生在生成層面,可靠性完全是另一個等級。

現在主流模型(OpenAI、Anthropic、Google)都支援 tool calling,所以預設答案很簡單:能用逼的就不要用求的。求的那條路留給兩種情況:模型太老不支援,或你需要串流輸出部分結果。

從 LangChain 的實作看設計

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

如果專案還沒綁定框架,另一個選項是直接用天生以型別為核心的 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 本身就是 prompt

還有一個容易被忽略的點:你的 schema 會被塞進 context,它就是 prompt 的一部分,Day 2(Prompt 為什麼有效)的所有機制對它都成立。

欄位名稱取得好(sentiment 配上 description="正面或負面"),模型填得又快又準;欄位名稱含糊、巢狀七層、二十個 optional 欄位,模型的錯誤率跟著上升。設計 schema 時把它當成你在跟模型說話的一部分:欄位少、命名直白、每個欄位一句 description。這跟你寫 API 給人類用的直覺完全一致。

順帶一提,昨天講的 cache 在這裡也有戲份:schema 每次呼叫都一樣,放在 prompt 的固定前綴區,它的 token 就走快取價。

結構化是評估的前提

今天這層做好之後,你得到的不只是「能接管線的輸出」,還有一個更重要的東西:可以被程式打分數的輸出。自然語言的輸出只能靠人看,結構化的輸出可以自動比對正確答案。而一旦輸出可以量測,prompt 就不必用手調了。明天講 prompt 的自動優化:讓系統從自己的錯誤裡,學會怎麼寫自己的 prompt。


上一篇
Day 4|你的系統在重複想同一件事:Prompt Cache 與 Semantic Cache
系列文
模型動不了,那你能動什麼?AI Engineering 四層工程觀:Prompt、Context、Harness、Loop5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言