補寫於 2026 年 9 月 6 日,日期掛回文章原本該發的那天。文中的版本號與「目前」指的是撰寫當日的官方文件狀態。

工具清單裡那個工具,你自己知道它不是工具。

它的 description 寫著「用這個回報分析結果」,但你的程式從來沒有真的執行過它。它存在的唯一理由,是它有一個 input_schema 可以填,而模型呼叫工具時塞進 input 的東西,會長成合法 JSON。於是你加上這一行,逼它非呼叫不可:

1
"tool_choice": {"type": "tool", "name": "get_weather"}

外面再包一層 whilejson.loads 炸了就重送,重送三次還不行就寫個 log 認輸。這套組合這幾年是「叫模型吐 JSON」的標準答案。

在 Claude Fable 5.1 與 Claude Mythos 5.1 上,它現在直接回 400。

那幾招原本各自撐住一塊,現在有兩塊塌了

官方〈Increase output consistency〉那頁列的舊工具箱其實一直很誠實:它教的招數沒有一招叫「保證」。第一招是在 prompt 裡把 JSON 形狀描述清楚,官方範例是這種句子:

You're a Customer Insights AI. Analyze this feedback and output in JSON format with keys: "sentiment" (positive/negative/neutral), "key_issues" (list), and "action_items" (list of dicts with "team" and "task").

第二招是 prefill,預填 assistant turn 的開頭,逼模型從你給的那個字元接下去。官方的說法是 “Prefill the Assistant turn with your desired format. This trick bypasses Claude’s friendly preamble and enforces your structure.”。第三招是給範例輸出,官方說這比抽象指示有效。

第二招現在沒了。同一頁的 Note 寫得很白:

“Prefilling is not supported on Claude 4.6 and later models and Claude Mythos Preview. Use structured outputs on models that support it, or system prompt instructions, instead.”

硬送過去會拿到 400 invalid_request_error,訊息是 “This model does not support assistant message prefill. The conversation must end with a user message.”。

至於假工具那招,它不在那頁上,但它是實務上的主力。站上 2026-05-08 那篇〈Claude API Tool Use 完整教學〉教過 tool_choice 的四個值,any 是「必須用其中一個工具」,tool 是「一定要用這個工具」。這兩個值就是假工具那招的引擎。2026-09-01 的 API release note 寫:

“On Claude Fable 5.1 and Claude Mythos 5.1, tool_choice types any and tool aren’t supported and return a 400 error. auto and none are unchanged. To guarantee schema-conformant tool inputs, use strict tool use or structured outputs.”

你會看到的錯誤長這樣:

1
tool_choice: type "tool" and "any" are not supported for this model.

官方在錯誤頁還特別加了一句 “including on the token counting endpoint”。想先用 count_tokens 估一下成本再送正式請求的人,第一步就會撞牆。

那份舊寫法完整長這樣,逐字抄自官方〈Define tools〉頁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
client = anthropic.Anthropic()

tools = [
{
"name": "get_weather",
"description": "Get the current weather in a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
}
},
"required": ["location"],
},
}
]

response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"},
messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}],
)

print(response)

有件事讀起來很順但我沒有官方背書。〈Define tools〉頁自己說了:「when you have tool_choice as any or tool, the API prefills the assistant message to force a tool to be used」。強制呼叫的實作方式本身就是 prefill。prefill 在 4.6 以後被拿掉,靠 prefill 撐著的東西跟著失效,因果鏈接得很漂亮。但官方從頭到尾沒把這兩條寫在一起,這是我自己接的推論,不是文件講的。

同一件事,現在寫成請求參數

新做法不改你的 prompt,改的是請求本體。JSON 輸出那半叫 output_config.format,Python 這樣寫(官方逐字):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
client = anthropic.Anthropic()

response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm.",
}
],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan_interest": {"type": "string"},
"demo_requested": {"type": "boolean"},
},
"required": ["name", "email", "plan_interest", "demo_requested"],
"additionalProperties": False,
},
}
},
)
print(next(block.text for block in response.content if block.type == "text"))

最容易被忽略的一點藏在最後那行:回應沒有新的 content block 型別。官方對回應形狀的描述是 “Valid JSON matching your schema in the response’s text content block”,東西就掉在原本那個 text block 裡:

1
2
3
4
5
6
{
"name": "John Smith",
"email": "john@example.com",
"plan_interest": "Enterprise",
"demo_requested": true
}

所以你撈文字的那段程式碼幾乎不用動。真正要改的是它外面那圈 try/except 還要不要留。Anthropic 自己的 blog 在講這件事帶來什麼差別時,用的是反著描述舊做法的講法:”The end result is a reliable output, reduced retries, and a simplified codebase that no longer needs failover logic or complex error handling.”。翻過來看就知道舊架構長什麼樣:一份主流程,一份 failover,一份專門處理解析失敗的錯誤處理,三份都得有人維護,而後面兩份存在的理由只是「模型可能不照做」。

我的立場是新東西不要再寫假工具了,但這句話有明確的失效條件。structured outputs 在 Amazon Bedrock 與 Microsoft Foundry 上仍然是 public beta(2026-01-29 的 release note 最後一句就是這麼寫的),跨雲部署的人不能照抄。你的模型也得在清單上:

1
2
3
4
5
claude-fable-5-1, claude-mythos-5-1, claude-fable-5, claude-mythos-5,
claude-mythos-preview, claude-opus-5, claude-opus-4-8, claude-opus-4-7,
claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6,
claude-sonnet-4-5-20250929, claude-opus-4-5-20251101,
claude-haiku-4-5-20251001

不在上面,這篇後半對你就沒有用,該留的 retry 迴圈還是得留著。

它保證的是形狀,不是值域

從這裡開始動作換了。前面那半講的是「叫模型吐 JSON」,接下來這半講的是「Claude 呼叫你真的會執行的那些工具時,塞進去的參數怎麼保證合法」——這是另一件事,新做法把它們拆成兩個獨立的入口,後面還會再收一次。這一段也是我認為整件事最容易誤會的地方,所以講久一點。

另外半邊叫 strict tool use,管的是「Claude 怎麼呼叫你的函式」,設定方式是在工具定義裡跟 namedescriptioninput_schema 平行的位置放一個 "strict": true。官方〈Strict tool use〉頁開場那句話講明了機制:

“Setting strict: true on a tool definition guarantees Claude’s tool inputs match your JSON Schema by constraining the model’s token sampling to schema-valid outputs (a technique called grammar-constrained sampling).”

約束下在取樣那一層。它不是事後檢查,是不合 schema 的 token 根本抽不出來。手機上填手機號碼那一格,鍵盤直接跳出數字鍵盤,你打不出注音,跟你打完之後跳一個紅字說「請輸入數字」,是兩種完全不同的做法。舊那套是後者,現在這套是前者。

官方舉的例子很好懂:訂位系統需要 passengers: int,沒開 strict 的時候模型可能給你 passengers: "two"passengers: "2",開了之後回應永遠是 passengers: 2。以前碰到這種東西,能做的跟叫模型吐 JSON 那半一模一樣:收到之後自己驗一次型別,不對就把錯誤餵回去讓它重呼叫一次。官方列的保證只有兩條,很值得記起來:tool 的 input 嚴格照著 input_schema 走,以及 tool 的 name 一定合法(來自你提供的工具或 server tools)。就這兩條,沒有第三條。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
client = anthropic.Anthropic()

response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}],
tools=[
{
"name": "get_weather",
"description": "Get the current weather in a given location",
"strict": True, # Enable strict mode
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The unit of temperature, either 'celsius' or 'fahrenheit'",
},
},
"required": ["location"],
"additionalProperties": False,
},
}
],
)
print(response.content)

strict 有它開不下去的地方。computer use 與 browser use 的 toolset 條目(computer_toolset_20260801browser_toolset_20260801)不吃 strict: true,設了整個請求會被拒。另外,strict tool use 雖然 HIPAA eligible,但 PHI 不可以出現在 tool schema 定義裡,官方點名了 input_schema 的 property 名稱、enum 值、const 值跟 pattern 正則,原因是編譯後的 schema 跟訊息內容分開快取,那份快取沒有跟 prompt 一樣的保護。

然後是踩坑的部分。你寫慣的 JSON Schema 有一大票東西它不支援,官方的 not supported 清單逐字是:

1
2
3
4
5
6
7
- Recursive schemas
- Complex types within enums
- External $ref (for example, '$ref': 'http://...')
- Numerical constraints (minimum, maximum, multipleOf)
- String constraints (minLength, maxLength)
- Array constraints beyond minItems of 0 or 1
- additionalProperties set to anything other than false

看到 minimummaxLength 在裡面就該警覺了。更麻煩的是 SDK 不會噴錯給你看,它會安靜地幫你改:

“Python, TypeScript, Ruby, and PHP SDKs automatically transform schemas with unsupported features: removing unsupported constraints (e.g. minimum, maximum, minLength, maxLength) and updating descriptions with the constraint info. Claude receives a simplified schema, but your code still enforces all constraints through validation.”

C# 跟 Go SDK 在 schema 是從原生型別推導出來時,也做同樣的轉換。

反過來說,支援的東西是這些:

  • enumconstanyOfallOf 都可以用
  • $ref / $def / definitions 限內部引用
  • 字串的 format 支援 date-timetimedatedurationemailhostnameuriipv4ipv6uuid
  • 陣列的 minItems 只認 0 跟 1 兩個值

夠你描述一份資料長什麼樣子,不夠你描述那份資料合不合理。

翻成人話:你寫 minimum: 0,模型看到的 schema 裡沒有這條,它只是變成 description 裡的一句提示,真正把關的還是你自己那端的驗證。所以這套機制保證的是形狀,型別對、必填欄位齊、enum 只會落在你給的值裡;它沒有保證值域。年齡欄位拿到 -3 是完全合法的 schema-valid 輸出。

你的驗證程式碼不能刪,只是它守的東西從「這串字是不是 JSON」變成「這個 JSON 的內容合不合理」。這是兩件難度差很多的事,而後面那件本來就該由你的業務邏輯負責。

三個名字撞在一起的 output_format

這裡值得停一下,因為三個東西同名,出錯的時候查起來會非常難受。

寫法 它是什麼 現在的狀態
output_format(API 請求欄位) 2025 年 11 月 public beta 時的欄位名 已改名 output_config.format,舊名在過渡期內仍被接受
output_format=client.messages.parse() 的參數) Python SDK 的便利參數,內部翻成 output_config.format 還在,正常用,搭 Pydantic model 後讀 response.parsed_output
--output-format(Claude Code CLI 旗標) 控制 CLI 印出來的信封格式 跟本文完全不是同一層東西

第三個要特別切乾淨。站上 2026-07-17 那篇〈Claude Code 結構化輸出完整教學〉寫的是 CLI 的 --output-format json,它做的事是把 session 資訊(.result.session_idstream_event)包成一個 JSON 信封丟給你的 shell 腳本讀,模型講的話本身仍然是自由文字。本文講的 output_config.format 是 Messages API 的請求欄位,約束下在解碼階段。一個是包裝,一個是內容物,名字撞在一起而已。

失敗的樣子換了

舊做法的失敗長在執行期:json.loadsJSONDecodeError,你在 except 裡重送。新做法的失敗往前移了,而且分成兩種症狀,查的方向完全不一樣。

一種是 API 回 400,就是前面那兩條。另一種更陰險,官方寫得很清楚:Python SDK v1.0 以後,output_format={...} 傳給 client.beta.messages.create()count_tokens() 會直接丟 TypeError,要改用 output_config。這是 SDK 端就攔下來,請求根本沒發出去,你在 API log 裡什麼都看不到。同一個症狀查兩個地方,一個要翻 request/response,一個只能翻 stack trace,先分清楚是誰擋的,可以少繞一大圈。

舊的 beta header structured-outputs-2025-11-13 跟舊的 output_format 請求欄位目前還收,官方的用字是 “for a transition period”。順帶一提,那個 header 名字裡的日期是 2025-11-13,但公告這件事的 release note 日期是 2025-11-14,兩個日期本來就不一樣,header 用的是版本日期而不是公告日期,抄的時候別自作聰明去「修正」成同一天。至於過渡期什麼時候結束,官方沒寫,我也不會替它編一個。

2025-11-14 structured outputs 進 public beta,支援 Claude Sonnet 4.5 與 Claude Opus 4.1,要帶 beta header,參數叫 output_format。2025-12-04 加上 Claude Haiku 4.5。2026-01-29 在 Claude API 上脫離 beta,支援 Claude Sonnet 4.5、Claude Opus 4.5、Claude Haiku 4.5,schema 支援範圍變廣、grammar 編譯延遲改善、beta header 不再需要,參數也是在這一天改名成 output_config.format。再來就是 2026-09-01,強制呼叫在兩個最新模型上回 400。前面三個日期你可以慢慢遷移,最後那個不行。

這篇的參數名、錯誤訊息與日期全部抄自官方文件,我沒有實際打過這組請求,所以不會告訴你延遲增加多少、費用怎麼變。另外官方 structured outputs 頁有一段 complexity limits,講的是單一請求裡所有 strict schema 的用量要合計計算(”These limits apply to the combined total across all strict schemas in a single request”),但那一段的內文我這邊抓不到,所以本文不給任何具體上限數字。structured outputs 跟 streaming、token counting 的互動同理,沒讀到就不寫。

從替身變成兩個入口

以前 tool use 被拿來當 JSON 的替身,是因為它是當時唯一「有 schema 欄位可以填」的地方。現在這兩件事被拆開了,官方講得很直接:JSON outputs 是 “Control Claude’s response format”,strict tool use 是 “Guarantee schema validation on tool names and inputs”,而且 “You can use these features independently or together in the same request.”。

底層倒是同一套。〈Strict tool use〉頁的 data retention 段落寫著 “Strict tool use compiles tool input_schema definitions into grammars using the same pipeline as structured outputs.”。同一條 grammar 編譯管線,兩個入口,各管各的事。

在還支援強制呼叫的模型上,官方建議兩個一起下:tool_choice: {"type": "any"} 保證「一定會呼叫某個工具」,strict: true 保證「參數合法」。這兩件事是分開的,別把其中一個當成另一個。到了 Fable 5.1 與 Mythos 5.1,前者做不到了,只剩 autostrict。同一張限制表還有一列講手動開的 extended thinking(thinking: {type: "enabled"}):anytool 一樣不支援,只能用 autonone

遷移那天,先卡 grammar 快取,再清掉 message 快取

前面說 strict tool use 跟 structured outputs 共用同一條 grammar 編譯管線。那條管線編出來的東西會快取 24 小時,第一次請求要多等它編譯。失效規則有點反直覺:schema 結構變了會重編,工具集變了會重編,但只改 namedescription 不會讓快取失效。這跟 prompt caching 是兩套獨立的東西,別混在一起想。

另一種快取的傷害發生在你動手改的那一刻。官方 Note 寫:改 tool_choice 參數會讓已快取的 message block 失效,工具定義跟 system prompt 還留著,但訊息內容要重新處理一次。也就是說你把 tool_choice{"type": "tool", ...} 拔掉的那次部署,既有對話的 message cache 會一起清掉。量大的服務值得挑個離峰時間再動。

格式從說服變成宣告

回頭看那個假工具。它從頭到尾沒有被執行過,它的 description 唯一的讀者是模型,而你的 code review 放它過關,理由只是大家都這樣寫。

以前「輸出格式」是 prompt 的一部分,你用文字去說服模型照做,說服失敗就重來一次。現在它是請求參數的一部分,跟 max_tokens 站在同一個層級,講的是這次請求的規格,不是這次請求的語氣。

差別不在少寫那幾十行 retry。假工具那招最貴的成本從來不是那段程式碼,是它讓「保證」跟「祈禱」在 diff 裡長得一模一樣,你 review 的時候分不出來。

現在分得出來了。想確認你手上這個服務是站在哪一邊,開啟你的請求組裝那支檔案,grep 一下 tool_choice 就知道了。