Learning Objectives
- 說明純文字回答接到程式時會出什麼問題
- 使用 JSON 模式與 JSON Schema(strict),並說明兩者保證什麼
- 用 json 模組解析回答,並自己寫欄位驗證
- 處理解析失敗與被截斷的 JSON:重試一次,仍失敗就回報
程式要的是欄位
給人看的回答是文字就夠了。但如果要把公告的日期寫進行事曆、地點顯示在地圖、是否必到拿來發提醒,程式要的是欄位。
這一課的順序是:先看不做任何設定時會怎樣,再用 JSON 模式,再用 Schema,最後處理它們都擋不住的情況。回應是 2026-11-03 錄下來的真實輸出,公告是虛構的。
逐步看:只在提示詞裡說要 JSON
json.loads 把 JSON 字串變成 Python 物件,失敗時丟出 JSONDecodeError。兩種提示詞各跑三次。第二種多了一句順便說明,看看差別。
提示詞裡說要 JSON,只有大致可靠
| 提示詞 | 三次的結果 |
|---|---|
| 「……給我 JSON:」 | 三次都解析成功 |
| 「幫我整理成 JSON,順便說明你怎麼判斷……」 | 三次都失敗:開頭是 ```json,後面接著說明文字 |
第二種的失敗還有其他問題。三次的欄位結構都不一樣,第一次是平的(課程、日期…),後兩次包在 event 裡,而且 location 變成巢狀的物件。回答也因為說明太長,碰到 150 token 的上限。
用字串處理去掉程式碼框是常見的補救,但這只處理了看到的那一種情況。比較好的做法是請 API 幫忙。
逐步看:JSON 模式
response_format={"type": "json_object"} 要求輸出一定是合法的 JSON。先看一個真實的 400 錯誤:提示詞裡沒有 JSON 這個字。
JSON 模式保證語法,不管內容
從錄音可以看到兩件事。
一定要寫 JSON 這個字。 錯誤訊息是:
'messages' must contain the word 'json' in some form, to use 'response_format' of type 'json_object'.
它只保證是合法的 JSON。 欄位名稱、型別、有沒有缺欄位都由模型決定。這次三次的欄位名稱剛好相同,是因為提示詞寫了「時間、地點、是否必到」,模型照著用了中文。但後面的範例裡,同樣是 JSON 模式,模型把 required 這個欄位填成字串 "全班都要出席",不是我們想要的 true。
要限制欄位與型別,就要提供 Schema。
逐步看:JSON Schema(strict)
Schema 是用 JSON 寫的規格:有哪些欄位、各是什麼型別。兩則公告各跑三次,第二則沒有寫地點。先想想:location 會是 null 嗎?
Schema 管形狀,不管內容對不對
| 層次 | 提示詞要求 | JSON 模式 | Schema(strict) |
|---|---|---|---|
| 是合法的 JSON | 大致 | 是 | 是 |
| 欄位名稱固定 | 否 | 否 | 是 |
| 型別固定 | 否 | 否 | 是 |
| 內容正確 | 否 | 否 | 否 |
官方指南建議,模型支援時優先用 json_schema。但上面的輸出說明它管不到內容:
date永遠是字串,但「下週三 14:00」和「下週三下午兩點」寫法不同。要寫進行事曆,還要程式再處理,或在 Schema 裡加描述、限制格式。- 第二則公告沒有寫地點,我們希望是
null,模型卻填了「線上自學(課程網站)」。這是合法的字串,Schema 檢查不出來。內容對不對要用測試資料評估(第 20 課)。
還有一個真實的意外。 這個範例的第一版把是否必到的欄位命名為 required,而且欄位有中文描述。那一版錄到兩個奇怪的結果:地點變成 工程二館 E2-315 שה上課.,夾了其他語言的文字,而且明明全班要出席,required 卻是 false;另一次地點後面接了一句外文,再輸出一長串空白與換行,直到 150 token 上限,json.loads 解析失敗。
改名為 must_attend、拿掉描述後,這一版的 6 次就沒有再出現。我們沒有查出確切的原因,也不能保證不會再發生(這兩筆回答沒有放進範例的錄音,保留在 datasets/g09_schema_anomaly_raw.json)。重點是:用了 strict,程式仍然要處理解析失敗,也要檢查 finish_reason。 官方指南也提醒,輸出被截斷時可能不是完整的 JSON,模型拒答時會回傳拒答內容(chat 回應的 message.refusal),解析前都要先檢查。
逐步看:被截斷的 JSON,重試一次
上一課看過被截斷的文字。JSON 被截斷時更麻煩,少一個括號整段都解析不了。這裡把驗證與重試的流程寫出來。
接住模型回答的流程
不論用哪一種模式,建議都走同一個流程:
送出請求
↓
finish_reason 是 length? → 是:被截斷,不解析
↓
json.loads 成功? → 否:不是 JSON
↓
欄位和型別都對? → 否:驗證失敗
↓
通過,交給後面的程式
失敗時的處理:
- 重試一次。 再送一次,或把錯誤告訴模型(「
must_attend應該是布林值」),請它改正。 - 有次數上限。 通常一兩次就好,每次都要花錢。
- 仍然失敗就回報。 回傳
None或丟出例外,把原始回應記下來方便除錯。不要猜一個預設值填進去,例如把解析失敗的must_attend當成false,同學就會收不到必到的提醒。
驗證可以用 Pydantic 或 jsonschema 這類套件,本課先用標準函式庫自己寫一次,下面的實作題就是這個部分。
實作:把模型的回應分類
完成 check_reply(text, finish_reason),依序檢查,回傳第一個符合的結果:
finish_reason是length:回傳被截斷json.loads失敗:回傳不是合法的 JSON- 解析結果不是字典:回傳
不是物件 - 缺少
date、location、must_attend其中一個(依這個順序檢查):回傳缺少欄位 欄位名稱 must_attend不是布林值:回傳must_attend 不是布林值- 都沒問題:回傳
通過
這一題不呼叫模型,回應是寫死的字串,其中幾段改寫自這一課看過的真實回答。
小測驗:結構化輸出
用 response_format={"type": "json_object"},但提示詞沒有 JSON 這個字。範例裡的 API 怎麼回應?
strict 的 Schema 規定 location 是字串或 null。公告沒寫地點,模型卻填了「線上自學(課程網站)」。這代表什麼?
拿到回答後,最合理的檢查順序是什麼?
重試一次後,回答還是解析失敗。must_attend 應該怎麼處理?
下面的程式會印出什麼?
import json
text = '{"must_attend": "true"}'
data = json.loads(text)
print(isinstance(data["must_attend"], bool))
重點整理
- 只在提示詞要求 JSON,回答可能帶程式碼框或說明文字,加一句話就可能全部解析失敗。
- JSON 模式保證語法(這個模型要求 messages 含 json 這個字);Schema(strict)再保證欄位與型別。
- 兩者都不保證內容正確,也擋不了截斷與其他意外。
- 接回答的流程:看
finish_reason→json.loads→ 驗證欄位 → 通過。失敗就重試有限次,再失敗就回報,不要編造預設值。
下一課: 回到文字的意思:把文字變成向量,用數字比較兩段話有多接近。