API 開發
用 tool use 拿到穩定的結構化輸出:別再拜託模型「只回 JSON」
用 tool use 拿到穩定的結構化輸出:別再拜託模型「只回 JSON」
很多人第一次想從 Claude 拿 JSON,走的是「在 prompt 裡拜託它只回 JSON」這條路。運氣好的時候可行,運氣不好它會在前後多聊兩句,或欄位跟你要的差一點,解析就炸了。
其實 tool use 有個常被忽略的用法:不是為了讓 Claude 拿資料,而是為了讓它交資料。把你想要的輸出結構定義成一個工具的 input_schema,Claude 填參數的過程,就是在產生一份符合結構的 JSON。
你將學到什麼
自由文字為什麼容易碎
開場白、圍欄、欄位漂移,每一種都要多寫防禦程式。
把結構定義成工具
input_schema 就是你的輸出規格書,Claude 照著填。
API 端的 JSON 驗證
串流時逐組驗證頂層鍵值對,通過才送到你手上。
fine-grained 的取捨
用驗證換速度之前,先想清楚要不要。
為什麼「請用 JSON 回答」常常不夠穩
用純文字指令要求 JSON,輸出畢竟還是自由文字:模型可能加一句開場白、在後面補說明、用 markdown 圍欄把內容包起來,甚至欄位名稱跟你要的有出入。每一種變化,你都得多寫一段解析與防禦程式。隨手玩玩無妨,要進正式環境就嫌脆弱。
更麻煩的是,這種失敗是機率性的:十次有九次好好的,第十次歪掉,而你的下游程式已經假設它永遠是合法 JSON。與其把穩定性押在模型今天的心情上,不如換一條天生就有規格的路。
換個思路:把想要的結構做成一個工具
tool use 的機制是:你用 input_schema 描述工具的參數,Claude 依照 schema 產生參數去呼叫。把這件事反過來用:定義一個工具,讓它的參數結構就是你想要的輸出結構,然後請 Claude 呼叫它。Claude 為了發出合法的工具請求,會照著 schema 的欄位與型別填值;你再從 tool use 區塊把參數字典整包取走,這就是你的結構化輸出。
換句話說,schema 從「描述輸入」變成了「規定輸出」,同一套機制,換個方向用。
實際範例:save_article
來看一個實際例子:save_article 工具,用來把一篇文章的整理結果存下來。它的參數包含文章摘要 abstract,以及一個裝著字數 word_count 與評語 review 的 meta 物件。Claude 呼叫這個工具時,產生的參數天生就是這個形狀:
{
"abstract": "This paper presents a novel...",
"meta": {
"word_count": 847,
"review": "This paper introduces QuanNet..."
}
}
你不需要再從散文裡把摘要和字數撈出來,也不用擔心模型今天心情好多送你三段說明。要摘要就有 abstract 欄位,要字數就有 word_count 欄位,拿了就能用。
同樣的做法可以套到各種場景:從履歷抽出姓名與工作經歷、把客服對話整理成分類與摘要、從文章抓出評分與標籤,任何「我要的是一份固定形狀的資料」的需求,都適合走這條路。
API 還會幫你把關:內建的 JSON 驗證
更棒的是,這條路不是全靠模型自律。先補一個背景:開啟串流時,工具參數的生成會以 InputJsonEvent 事件送達,每個事件帶兩樣東西:partial_json 是這一小段新生成的 JSON 片段,snapshot 是到目前為止累積出來的完整內容,方便你隨時拿到當下的全貌。
for chunk in stream:
if chunk.type == "input_json":
# Process the partial JSON chunk
print(chunk.partial_json)
# Or use the complete snapshot so far
current_args = chunk.snapshot
重點來了:Anthropic API 不會把 Claude 生成的每個片段立刻丟給你,而是先緩衝起來做驗證。以上面的結構為例,流程是這樣:
- 等
abstract的值完整生成。 - 對照你的 schema 驗證這組鍵值對。
- 把 abstract 相關的緩衝片段一次送出。
- 再對
meta物件重複同樣流程。
這解釋了為什麼開了串流還是會看到「停頓一下、噴出一段」的節奏:片段被扣住,直到湊成一組完整且合法的頂層鍵值對。對想拿穩定結構化輸出的人來說,這個看似惱人的行為其實是保障:送到你手上的參數,已經過了一層驗證。
不過要留意,驗證層遇到有問題的值時,可能把它包成字串處理,型別不一定符合你原本的預期,關鍵欄位仍值得在自己這端多驗一次。
需要更即時?fine-grained tool calling 的交換條件
如果你的介面需要即時顯示生成進度,可以在呼叫時開啟 fine_grained=True。它做的事只有一件:關掉 API 端的 JSON 驗證。片段一生成就送到你手上,不再有緩衝延遲,但代價是 Claude 偶爾會生出不合法的 JSON,例如把數字欄位寫成 undefined,你的程式必須自己接得住:
try:
parsed_args = json.loads(chunk.snapshot)
except json.JSONDecodeError:
# Handle invalid JSON appropriately
print("Received invalid JSON, continuing...")
開了 fine-grained 之後,你可能在串流很早的階段就先拿到 word_count 這種小欄位,不用等整個 meta 物件生成完畢,介面的即時感確實好很多。一句話總結取捨:預設模式用延遲換驗證,fine-grained 用驗證換速度。多數應用用預設就好;真的需要逐字進度時,記得把錯誤處理寫紮實。
實務建議
- 結構化輸出需求明確時,優先考慮 tool use,而不是在 prompt 裡懇求格式。
- schema 的欄位描述寫清楚,Claude 填值的品質會跟著提升。
- 關鍵欄位在自己這端再驗一次型別與範圍,防線不嫌多。
- 除非使用者體驗真的需要逐字串流,否則留著預設的驗證機制。