Bedrock 的結構化輸出現在支援 Claude Sonnet 5 了 — 但不再強制 tool use 之後,thinking 會自動開啟
Amazon Bedrock 上的 Claude Sonnet 5 現在可以使用 outputConfig.textFormat。我用它來取代強制 tool use 之後,JSON 格式的問題解決了。但同時也必須關閉 thinking,否則會拖累處理速度。

本頁目錄
引言
這個月稍早,Amazon Bedrock 上的 Claude Sonnet 5 還不支援結構化輸出。所以在我參與的一條翻譯工作流裡,一直是靠一條提示詞規則,以及把錯誤回傳給模型重試來維持 JSON 格式。到了 2026-10-20,AWS 宣布 Bedrock 上的所有 Claude 模型都支援結構化輸出,於是我想確認能不能換掉這套權宜做法。
經過實測後結論是可行的。拿掉提示詞規則之後,在一個實際的 batch 上測試時,強制 tool use 的 20 次呼叫中有 5 次出錯,outputConfig.textFormat 則一次都沒有。不過,切換之後必須關閉 thinking,才能維持原本的速度。原本強制 tool use 會讓 Sonnet 5 完全不做 thinking,不再強制之後因為做了 thinking,143 個段落的那個 batch,執行時間從 47 秒被拉長到了 731 秒。
現在 schema 改由 Bedrock 在模型生成時強制套用,請求也不必再依賴強制 tool use,而且 Claude 的 Sonnet 5.5 和 Opus 5.5 本來就不接受強制 tool use。
Bedrock 上的 Claude Sonnet 5 現在接受哪些設定?
我在 Bedrock 上能呼叫的每一個 Claude 模型,現在都能指定一份 JSON Schema 來約束模型給出的回答。我對 ap-northeast-1 的 global.anthropic.* 推論設定檔裡的每個模型各送出了一個小請求,並測試了兩種 API:Converse(Bedrock 不限模型的對話 API),以及 InvokeModel(直接使用 Anthropic 原生的請求主體格式)。
| 模型 | Converse outputConfig.textFormat | InvokeModel output_config.format |
|---|---|---|
| Sonnet 4.5、4.6、5、5.5 | 接受 | 接受 |
| Opus 4.5、4.6、4.7、4.8、5、5.5 | 接受 | 接受 |
| Haiku 4.5、5.5 | 接受 | 接受 |
| Fable 5、5.1 | 無法測試 | 無法測試 |
我還在提示詞裡故意要求模型用一般文字回答、使用 schema 以外的 enum 值,並多加一個 schema 裡沒有的 key。即使如此,只要請求被接受,模型給出的回應就都符合 schema。Fable 因為帳戶設定的問題無法使用,因此我沒有確認 Fable 是否支援結構化輸出。Sonnet 5 的模型卡現在也把 Structured outputs 列在 Supported 底下了。
我刻意送出一份格式錯誤的 schema {"type": "bogus"},結果請求被拒絕了,可見 Bedrock 確實會讀取這個欄位,而不是選擇直接忽略:
ValidationException: The json schema definition at outputConfig.textFormat.structure.jsonSchema is invalid.Converse 要求先用 json.dumps() 把 schema 轉成一段 JSON 字串再傳入,InvokeModel 則直接接受 Python 的 dict 物件,所以這兩種請求的寫法不能直接互換:
client.converse( modelId="global.anthropic.claude-sonnet-5", messages=messages, outputConfig={"textFormat": { "type": "json_schema", "structure": {"jsonSchema": { "schema": json.dumps(schema), # 字串;InvokeModel 則接受 dict "name": "translations", }}, }},)client.invoke_model( modelId="global.anthropic.claude-sonnet-5", body=json.dumps({ "anthropic_version": "bedrock-2023-05-31", "max_tokens": max_tokens, "messages": anthropic_messages, "output_config": {"format": { "type": "json_schema", "schema": schema, # dict;Converse 則接受 JSON 字串 }}, }),)toolChoice、strict 與 textFormat 各自保證了什麼?
toolChoice 控制的是模型從 tools 清單選用工具時的行為:交給模型自己決定(auto)、強制呼叫任一個工具(any),或強制呼叫指定的工具(tool)。加在工具定義上的 strict: true,則是讓模型呼叫這個工具時,傳入的參數一定符合工具的 schema。outputConfig.textFormat 不經過任何工具,直接讓模型回覆的 JSON 符合指定的 schema。
strict 和 textFormat 是結構化輸出的兩種用法。兩者都採用受限解碼:Bedrock 會把 schema 編譯成文法,模型只能取樣文法允許的 token。toolChoice 是更早之前就有的設定,不屬於結構化輸出。
強制 tool use 和結構化輸出只能二選一嗎?
不是。我原本以為結構化輸出是以工具的形式實作的,必須搭配強制 tool use 才能發揮作用。但其實結構化輸出本身不是工具,而是限制模型能選哪些 token 的一種方法,有兩種用法:strict 是工具定義上的一個屬性,只在模型呼叫這個工具時限制它的參數;textFormat 則直接寫在請求上,限制整個回應。
前一篇文章的權宜做法,正是典型的強制 tool use 做法。首先定義一個工具,工具裡定義了模型回應時應該採用的 schema,再用 toolChoice: {"tool": {"name": ...}} 強制呼叫,相當於讓模型答題前再看一次小抄。但強制 tool use 只保證一定會呼叫該工具,而從不檢查輸出的內容,前一篇裡每個 key 都被寫成 text,就是因為這個原因。
strict 正好彌補了傳統強制 tool use 的不足。強制 tool use 保證模型一定會呼叫這個假工具,而 strict 保證模型填進這個假工具的 input 一定會符合工具裡定義的 schema。也就是說,這個做法並不是透過執行假工具來取得結果,而是直接從模型放入假工具裡的 input 取得符合 schema 的 JSON。
Sonnet 5 接受了同時含有 strict 和強制 tool use 的請求,並且每次輸出的結果都符合 schema。至於 textFormat,則是獨立於工具之外的功能,並不屬於工具的一部分,所以完全不需要工具,也不必強制呼叫。
toolChoice 的 auto、any 與 tool 是什麼意思?
toolChoice 的值會決定模型怎麼使用 tools 清單:
-
{"auto": {}}:模型自己決定要呼叫哪個工具,甚至可以不選工具直接用文字回答。省略toolChoice時,API 預設就是這個值。tool_config = {"tools": [get_weather, get_time],"toolChoice": {"auto": {}}, # 可以呼叫任一個工具,也可以只用文字回答} -
{"any": {}}:模型至少要呼叫清單裡的一個工具,不會產生文字。tool_config = {"tools": [get_weather, get_time],"toolChoice": {"any": {}}, # 一定要呼叫工具,但呼叫哪一個由模型決定} -
{"tool": {"name": ...}}:模型必須呼叫指定的那個工具。tool_config = {"tools": [get_weather, get_time],"toolChoice": {"tool": {"name": "get_weather"}}, # 一定要呼叫 get_weather}
後兩種屬於強制 tool use,會要求模型必須使用工具,不能直接用文字回答。
在前面我對每個模型送出的小請求裡,Opus 4.7、Opus 4.8 和 Haiku 5.5 都以 stopReason: end_turn 直接用文字回答,沒有呼叫工具。所以用 strict 搭配 auto 時,只有在模型真的呼叫工具時,schema 才會生效。
strict 和 textFormat 是同一件事嗎?
底層機制相同,但約束的對象不同。AWS 的文件也寫到,strict 和 textFormat 可以單獨使用,也可以在同一個請求裡一起使用。
strict: true | outputConfig.textFormat | |
|---|---|---|
| 寫在哪裡 | 工具定義裡 | 請求的最上層 |
| 約束什麼 | 模型填進假工具 input 的 JSON | 模型用文字回應時的輸出 |
| 何時生效 | 模型選擇呼叫這個工具時 | 模型選擇不呼叫工具、直接以文字回應時 如果沒有提供工具則是每一次都會被套用 |
兩者放在同一個請求裡也能運作,而且各自只約束自己那一部分的輸出。我對 Sonnet 5 送出了 30 次請求,同時設定了 strict 工具、toolChoice: auto 和 textFormat。每次回來的不是工具呼叫就是文字,從來沒有兩者同時出現過。不過在 auto 之下,模型多半會選擇呼叫工具,連明確要求不要使用任何工具的 10 次裡也有 4 次呼叫了工具,所以只有模型決定不呼叫工具時,textFormat 才會生效。
strict 和 textFormat 走的是兩條不同的路徑。模型選擇呼叫工具時,套用的是 strict;模型決定不呼叫工具、直接用文字回答時,套用的則是 textFormat。不論走哪一條,結果都符合 schema。24 次工具呼叫和 6 次文字回應,全都符合各自的 schema。
在實際的 batch 上,用三種請求取得符合同一份 schema 的 JSON
我嘗試了強制 tool use + strict、strict + auto、textFormat 三種組合。只有 textFormat 在 Sonnet 5 和 Sonnet 5.5 上都穩定成功,而且不會把要不要呼叫工具的決定權留給模型。
測試用的 batch 有 63 個段落。為了讓格式錯誤有機會出現,我拿掉了提示詞裡禁止使用 text 這個 key 的規則。每種請求在 Sonnet 5 上執行 20 次,在 Sonnet 5.5 上執行 10 次。
| 請求 | 模型 | 格式正確 | 耗時中位數 |
|---|---|---|---|
| 強制 tool use,保留提示詞規則 | Sonnet 5 | 20 次中 20 次 | 25 秒 |
| 強制 tool use,拿掉提示詞規則 | Sonnet 5 | 20 次中 15 次 | 25 秒 |
S1:強制 tool use + strict | Sonnet 5 | 20 次中 20 次 | 24 秒 |
A1:strict + auto | Sonnet 5 | 20 次中 19 次 | 24 秒 |
T1:textFormat | Sonnet 5 | 20 次中 20 次 | 24 秒 |
S1:強制 tool use + strict | Sonnet 5.5 | 400 拒絕 | — |
A1:strict + auto | Sonnet 5.5 | 10 次中 10 次 | 36 秒 |
T1:textFormat | Sonnet 5.5 | 10 次中 10 次 | 36 秒 |
拿掉規則後出錯的 5 次,每個 key 都被寫成了 text,和前一篇文章描述的是同一個錯誤。S1 能夠用最小的改動解決這個問題,但 Sonnet 5.5 和 Opus 5.5 會直接拒絕強制 tool use:
tool_choice: type "tool" and "any" are not supported for this model.在 A1 唯一失敗的那一次,模型花了兩秒,只輸出了 75 個 token,用一個空的 translations 陣列呼叫了工具。空陣列也符合 schema,所以 strict 批准了這個結果。我最後選擇用 T1,理由是 A1 在 auto 模式下可能選擇不去呼叫工具。而 T1 本來就沒有工具,也就沒有這個風險。
結構化輸出保證不了什麼?
schema 只保證 JSON 的結構。結構以外的事情,仍由工作流裡負責呼叫 Bedrock 的那段程式(轉接器)逐一檢查:
- 因為
max_tokens被截斷的回應,或是模型拒絕回答的回應,可能不符合 schema。Anthropic 的結構化輸出頁面對這兩種情況都有說明。Converse 沒有refusal這個停止原因,沒辦法直接分辨模型是不是拒絕回答,所以轉接器只在stopReason為end_turn時才讀取回應。 - 項目數量和 id 不受約束。schema 的
minItems(陣列至少要有幾個項目)只接受 0 或 1,沒辦法要求一定要有 63 個,A1 那個空陣列也因此通過了。所以轉接器仍然保留和輸入 id 的比對。 - 每個物件都必須加上
"additionalProperties": false(不允許出現 schema 沒定義的 key)。設成其他值的話,Bedrock 會回傳 400。
不再強制 tool use 之後,adaptive thinking 就自動開啟了
強制 tool use 會一直讓 Sonnet 5 沒辦法做 thinking,改用 textFormat 之後,Sonnet 5 又會開始做 thinking 了。adaptive thinking 是 Claude 在 Sonnet 5 上的預設模式。模型會針對每個請求,決定回答前要不要推理、推理多久,而且推理用掉的 token 也會算進 maxTokens。
我用 textFormat 翻譯一整份文件時,大部分 batch 的耗時都和之前一樣,但其中 3 個明顯變慢。最慢的一個 batch 總共嘗試了 6 次,這是因為其中 5 次都超過了 120 秒的 read_timeout。botocore 是 boto3 底下的 AWS SDK,botocore 每次都在不拋出例外的情況下自動重試,所以每一次逾時的嘗試都被計費了,卻沒有一次被記錄成失敗。
為了找出原因,我把變慢的 3 個 batch 各自再執行了 3 次,並把逾時改成 300 秒、同時關閉了重試:
| 請求 | 耗時 | 停止原因 | 輸出 token | 出現推理的執行 |
|---|---|---|---|---|
| 強制 tool use | 42–47 秒 | 6 次中 6 次 tool_use | 6,767–7,304 | 9 次中 0 次 |
textFormat,開啟 thinking | 45–138 秒 | 6 次中 2 次 max_tokens | 6,854–16,384 | 9 次中 5 次 |
textFormat,關閉 thinking | 41–46 秒 | 6 次中 6 次 end_turn | 6,598–6,871 | 9 次中 0 次 |
耗時、停止原因和輸出 token 只統計最慢的 2 個 batch(共 6 次執行),最後一欄則統計全部 9 次。
不論哪一種模式,JSON 本身的長度都一樣,一個 batch 大約 13,000 個字元,所以多出來的 token 都是推理。整份文件第二次完整執行時,143 個段落的 batch 一樣很慢,而那時文法早就編譯好也快取了,所以延遲並不是編譯造成的。
Anthropic 的 thinking 疑難排解頁面記載了原因:「A forced tool_choice ({"type": "any"} or a named tool) also returns no thinking block: the response starts with the tool call.」(強制的 tool_choice 也不會回傳 thinking 區塊,回應會直接從工具呼叫開始。)之前採用強制 tool use 的時候,這條工作流其實一直都在沒有 thinking 的情況下翻譯。因此改用 textFormat 之後,需要關閉 thinking 才能避免逾時以及 token 消耗變多等問題。
關閉 thinking,以及不接受關閉 thinking 的模型
我送出 thinking: {"type": "disabled"} 之後,textFormat 的耗時和輸出 token 都回到了強制 tool use 時的水準。Converse 透過 additionalModelRequestFields 傳遞 Anthropic 專屬的欄位:
client.converse( # … additionalModelRequestFields={"thinking": {"type": "disabled"}}, outputConfig={"textFormat": {"type": "json_schema", "structure": {"jsonSchema": { "schema": json.dumps(schema), "name": "translations", }}}},)我在 23 個 batch 上測試,每一個回應都符合 schema,輸入的 id 也全都對得上,最長的一次呼叫也只花了 45 秒,沒有任何一次重試。在工作流的評估套件上,翻譯品質也落在強制 tool use 版本每次執行之間的波動範圍內。
但並不是每個模型都接受 thinking: {"type": "disabled"}。Anthropic 的文件依模型列出了支援情況:
| 模型 | thinking: {"type": "disabled"} |
|---|---|
| Sonnet 5 | 接受 |
| Opus 5、Haiku 5.5 | effort(控制模型推理多深的參數)設為 high 以下時接受 |
| Sonnet 5.5 | 400;要改送 {"type": "between_tools"} |
| Opus 5.5、Fable 5、Fable 5.1 | 400;thinking 永遠開啟 |
disabled 會完全關閉 thinking;between_tools 則只有 Sonnet 5.5 接受,關閉的是模型回答前的 thinking,但模型在工具呼叫之間寫的簡短進度說明,仍然會以 thinking 區塊回傳。依照文件的描述,像這條工作流這樣不用工具、只呼叫一次的請求,兩者應該沒有差別。
這條工作流會在執行時從 SSM Parameter Store(AWS 存放設定值的服務)讀取模型 ID,所以不必重新部署就能切換到 Sonnet 5.5。但 Sonnet 5.5 不接受 disabled,一旦切換過去,之後的每個 batch 都會因為 400 而失敗。因此我讓轉接器在呼叫 Bedrock 之前,就先拒絕還沒驗證過能關閉 thinking 的模型 ID:
THINKING_DISABLED_MODELS = frozenset({"claude-sonnet-5"})
def __init__(self, *, model_id: str) -> None: # 省略其他參數 if model_id.rsplit("anthropic.", 1)[-1] not in THINKING_DISABLED_MODELS: raise ConfigurationError( f"還沒驗證過能關閉 thinking 的模型: {model_id}" )這個錯誤會附上模型 ID 一次報出來,而不是變成一連串 Bedrock 的 400 錯誤;想要嘗試新模型,就得先測試過。
總結
| 問題 | 處理方式 |
|---|---|
| 強制 tool use 只保證會呼叫,不保證內容 | 不用工具的 outputConfig.textFormat:拿掉提示詞規則後 20 次中 20 次 |
| Sonnet 5.5 和 Opus 5.5 拒絕強制 tool use | 同一個 textFormat 請求:Sonnet 5.5 上 10 次中 10 次 |
| 不再強制 tool use 後,adaptive thinking 自動開啟 | thinking: {"type": "disabled"},耗時與 token 數回到強制 tool use 時的水準 |
max_tokens 截斷、拒絕回答、項目數量和 id | 只讀取 end_turn 的回應,並保留 id 比對 |
切換到不接受 disabled 的模型 | 用允許清單在呼叫 Bedrock 之前就擋下,並在錯誤訊息中附上模型 ID |
在 Bedrock 上,Claude Sonnet 5 現在可以指定 JSON Schema 來約束回應。不過,不再強制 tool use 也會讓模型開始做 thinking,所以切換之前,除了確認 JSON 的結構,也需要用量大點的 batch 量測耗時和輸出 token。
參考連結
- AWS What’s New: Amazon Bedrock supports structured outputs for all Anthropic Claude models
- Amazon Bedrock: Get validated JSON results from models (structured outputs)
- Amazon Bedrock model card: Claude Sonnet 5
- Amazon Bedrock API: ToolChoice(auto、any、tool 的定義)
- Claude API: Structured outputs(含不保證的範圍)
- Claude API: Troubleshooting thinking(含各模型關閉 thinking 的表格)
- Claude API: Define tools(含強制 tool use 的限制)

