iT邦幫忙

2026 iThome 鐵人賽

DAY 16
1
Claude AI

Claude Code 實戰筆記:AI coding 沒有新問題系列 第 16 篇

# Day 16:「請回傳 JSON」只是請求,不是保證

  • 分享至 

  • xImage
  •  

凌晨三點的 parser

一支每晚跑的腳本,把一批資料丟給模型分類,prompt 最後一行寫著:「請只回傳 JSON,不要其他文字。」前兩個月都沒事。

某天凌晨三點,模型照樣回了 JSON,只是在外面多包了一層 markdown 的程式碼標記。解析那一行直接丟出例外,整批資料卡住,早上才有人發現。

有人把這種寫法戲稱為 parse-and-pray:先解析,再祈禱。大部分時候祈禱都會應驗,所以很難察覺它其實一直沒有保證。

一份寫清楚的合約

1986 年,Bertrand Meyer 提出了 Design by Contract,並把它做進自己設計的 Eiffel 語言。他的想法是把每一次函式呼叫都當成一份合約。

呼叫的一方要滿足前置條件,例如傳進來的數字不能是負的;被呼叫的一方要滿足後置條件,例如回傳的結果一定落在某個範圍內。還有一種叫類別不變式,寫的是物件在每次對外呼叫前後都該成立的規則,像是「帳戶餘額永遠不小於零」。這些條件直接寫在程式裡,開啟檢查時會在執行中驗證,一旦違反就立刻報錯。

這套做法最實用的地方,是出錯時知道該怪誰。前置條件被違反,是呼叫者的錯;後置條件被違反,是被呼叫者的錯。錯誤在違約的那一刻就被攔下來,不用再從一路壞掉的資料往回追。

口頭約定與兩種保證

「請回傳 JSON」是寫在 prompt 裡的一句話,模型大多會遵守,但沒有人檢查,違反了也沒有任何機制會發現。用 Meyer 的角度看,這只是一份口頭約定。

Claude 提供了兩種把它變成真正保證的方式,機制剛好不一樣。

第一種在 Claude API。開發者交出一份 JSON Schema,寫明要哪些欄位、每個欄位是什麼型別,API 會把它編譯成一套文法。模型每生成一個 token,不符合文法的選項就被擋掉,所以格式錯誤的輸出根本產生不出來,這種做法叫 constrained decoding。同樣的機制也能用在工具呼叫上:工具定義加上 strict: true,模型傳給工具的參數就一定符合定義。這比較接近型別系統,錯誤在源頭就不存在。

第二種在 Claude Code。非互動模式下執行 claude -p,加上 --output-format json 和 --json-schema,回傳的結果就會照 schema 放在 structured_output 欄位裡。但它的做法是事後驗證。claude -p 底下跑的是 Agent SDK,而 SDK 的文件寫明:模型先完成工作,輸出拿去比對 schema,不符合就要求模型重來。重試到上限還是不行,結果會標成 error_max_structured_output_retries。這就是 Meyer 那一套:執行時檢查,違約就報錯。

官方文件還提醒了一種情況:結果標示成功,卻沒有 structured_output。這也要當成失敗處理。

Claude Code 的文件裡還有一段版本紀錄:在 v2.1.205 之前,如果給的 schema 本身寫錯了,Claude Code 會默默忽略它,改回傳一般文字。合約壞了,卻沒有人被通知。新版改成直接報錯退出,這正是 Meyer 堅持的事:違約不能安靜地發生。

保證的邊界

就算是 API 的 constrained decoding,官方文件也列出了幾種輸出可能不符合 schema 的情況。模型基於安全理由拒絕回答時,回應的 stop_reason(說明模型為什麼停止輸出的欄位)會是 refusal;輸出長度碰到上限被截斷時,則是 max_tokens。這兩種情況下,JSON 都可能不完整。

還有一種連 stop_reason 都不會標:enum 的大小寫不保證。schema 寫的是 "Conversation topic 3",模型可能回 "Conversation Topic 3"。官方的建議是比對時忽略大小寫,而且不要設計只差在大小寫的選項。

schema 能寫的條件也有限。JSON Schema 本身可以規定數值範圍和字串長度,但 API 的 structured outputs 不支援這些限制,需要的話得在收到結果之後自己檢查。Claude Code 則會接受 email 這類格式標記,但只當作註解,不會真的驗證 — 寫在 schema 裡看起來像是有保證,其實沒有。

形狀對,不代表內容對

更根本的限制是,schema 管的是形狀。

一個欄位要求是數字,模型就一定會給數字,但這個數字是不是正確的金額,schema 管不到。分類欄位只能從五個值裡選一個,模型一定會選一個,但選的是不是對的那一個,是另一回事。Day 2 講過模型會自信地講錯;structured outputs 做的,是讓它在格式完全正確的前提下自信地講錯。

Meyer 的合約可以寫下跨欄位的業務規則,例如「出貨日不得早於下單日」,schema 很難表達這種欄位之間的關係。「這筆分類對不對」「這段摘要有沒有漏掉重點」,更沒辦法寫成任何一種合約。這些要靠 Day 6 講的測試,或是人的判斷。

驗證不過時怎麼辦

Meyer 反對在程式裡到處重複檢查,但他也特別說過,assertion 不是用來檢查外部輸入的;來自系統外的資料,要在邊界上交給專門的模組明確檢查。模型的輸出正是這種資料,跟使用者的輸入、第三方 API 的回應同一類,最好的檢查點就是它跨進系統的那一刻。

格式交給 structured outputs,schema 管不到的數值範圍、大小寫、業務規則,在這個入口再驗一次。更重要的是決定驗證不過時要做什麼:這一筆可以重試一次,還是先放到待處理的清單、發一則告警,讓其他資料繼續跑。開頭那支腳本真正的問題,不只是沒有保證格式,還有一筆壞掉就卡住整批。

四十年前的合約

Meyer 在 1986 年想解決的,是模組之間互相猜測:猜對方會傳什麼進來,猜對方會回什麼出去。猜對的時候一切正常,猜錯的時候錯誤到處流竄,沒人知道該從哪裡查起。

模型成為系統的一部分之後,同樣的猜測又回來了,只是被猜的對象換成了模型。解法也還是那一套:把期待寫下來,讓違反的那一刻被看見。


延伸閱讀


上一篇
# Day 15:AI 讓寫程式變便宜,所以程式變多了
下一篇
# Day 17:改了 CLAUDE.md,怎麼知道沒有改壞
系列文
Claude Code 實戰筆記:AI coding 沒有新問題 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
helenanova
iT邦新手 5 級 ‧ 2026-09-30 23:19:18

我會把重試的邊界再拆成「重新產生輸出」和「重跑整個 agent」。如果 agent 前面已經寄信或寫入資料,最後的 JSON 驗證失敗,重跑整趟就可能把副作用做兩次。能否保留原本的執行結果,只重試結構化輸出那一步?若必須重跑,至少讓寫入帶同一個 operation ID。隔離壞資料之外,也要隔離重試會重做哪些事。

AI醬 iT邦新手 4 級 ‧ 2026-10-01 13:59:20 檢舉

感謝補充!很實用

我要留言

立即登入留言