# Bedrock 的結構化輸出現在支援 Claude Sonnet 5 了 — 但不再強制 tool use 之後，thinking 會自動開啟

> Amazon Bedrock 上的 Claude Sonnet 5 現在可以使用 outputConfig.textFormat。我用它來取代強制 tool use 之後，JSON 格式的問題解決了。但同時也必須關閉 thinking，否則會拖累處理速度。

- Source: https://oharu121.com/zh-tw/blog/bedrock-structured-outputs-sonnet-5-textformat-thinking/
- Published: 2026-10-23T08:26:47+09:00
- Tags: Amazon Bedrock, Claude, 結構化輸出

---
## 引言

這個月稍早，Amazon Bedrock 上的 Claude Sonnet 5 還不支援結構化輸出。所以在我參與的一條翻譯工作流裡，一直是靠[一條提示詞規則，以及把錯誤回傳給模型重試](/blog/bedrock-structured-outputs-claude-sonnet-5-error-feedback/)來維持 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 確實會讀取這個欄位，而不是選擇直接忽略：

```text
ValidationException: The json schema definition at outputConfig.textFormat.structure.jsonSchema is invalid.
```

Converse 要求先用 `json.dumps()` 把 schema 轉成一段 JSON 字串再傳入，InvokeModel 則直接接受 Python 的 dict 物件，所以**這兩種請求的寫法不能直接互換**：

```python title="Converse 請求"
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",
        }},
    }},
)
```

```python title="InvokeModel 請求"
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。

*Figure — ThreeKnobs: 只有 toolChoice 能強制模型呼叫工具。strict 和 textFormat 則是要求 JSON 符合 schema：strict 針對工具的參數，textFormat 則針對模型的回應。*

`strict` 和 `textFormat` 是結構化輸出的兩種用法。兩者都採用受限解碼：Bedrock 會把 schema 編譯成文法，模型只能取樣文法允許的 token。`toolChoice` 是更早之前就有的設定，不屬於結構化輸出。

*Figure — ConstrainedDecoding: Bedrock 把 schema 編譯成文法，文法不允許的 token 全部會被遮罩。圖中的 token 只是示意。*

### 強制 tool use 和結構化輸出只能二選一嗎？

不是。我原本以為結構化輸出是以工具的形式實作的，必須搭配強制 tool use 才能發揮作用。但其實結構化輸出本身不是工具，而是限制模型能選哪些 token 的一種方法，有兩種用法：`strict` 是工具定義上的一個屬性，只在模型呼叫這個工具時限制它的參數；`textFormat` 則直接寫在請求上，限制整個回應。

[前一篇文章](/blog/bedrock-structured-outputs-claude-sonnet-5-error-feedback/)的權宜做法，正是典型的強制 tool use 做法。首先定義一個工具，工具裡定義了模型回應時應該採用的 schema，再用 `toolChoice: {"tool": {"name": ...}}` 強制呼叫，相當於讓模型答題前再看一次小抄。**但強制 tool use 只保證一定會呼叫該工具，而從不檢查輸出的內容**，前一篇裡每個 key 都被寫成 `text`，就是因為這個原因。

`strict` 正好彌補了傳統強制 tool use 的不足。強制 tool use 保證模型一定會呼叫這個假工具，而 `strict` 保證模型填進這個假工具的 input 一定會符合工具裡定義的 schema。也就是說，這個做法並不是透過執行假工具來取得結果，而是直接從模型放入假工具裡的 input 取得符合 schema 的 JSON。

*Figure — FakeTool: 兩種做法都會提供工具，讓模型填入參數。一般工具接著會被執行；假工具則不會被執行，參數本身就是答案。*

Sonnet 5 接受了同時含有 `strict` 和強制 tool use 的請求，並且每次輸出的結果都符合 schema。至於 `textFormat`，則是獨立於工具之外的功能，並不屬於工具的一部分，所以**完全不需要工具**，也不必強制呼叫。

### toolChoice 的 auto、any 與 tool 是什麼意思？

`toolChoice` 的值會決定模型怎麼使用 `tools` 清單：

1. `{"auto": {}}`：模型自己決定要呼叫哪個工具，甚至可以不選工具直接用文字回答。省略 `toolChoice` 時，API 預設就是這個值。

   ```python
   tool_config = {
       "tools": [get_weather, get_time],
       "toolChoice": {"auto": {}},  # 可以呼叫任一個工具，也可以只用文字回答
   }
   ```

2. `{"any": {}}`：模型至少要呼叫清單裡的一個工具，不會產生文字。

   ```python
   tool_config = {
       "tools": [get_weather, get_time],
       "toolChoice": {"any": {}},  # 一定要呼叫工具，但呼叫哪一個由模型決定
   }
   ```

3. `{"tool": {"name": ...}}`：模型必須呼叫指定的那個工具。

   ```python
   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` 可以單獨使用，也可以在同一個請求裡一起使用。

*Figure — StrictVsTextFormat: strict 把 schema 放在一個不會被執行的假工具上，答案從 tool_use 的 input 回來；textFormat 則把 schema 放在請求上，答案就是回應本身。*

| | `strict: true` | `outputConfig.textFormat` |
| --- | --- | --- |
| 寫在哪裡 | 工具定義裡 | 請求的最上層 |
| 約束什麼 | 模型填進假工具 input 的 JSON | 模型用文字回應時的輸出 |
| 何時生效 | 模型選擇呼叫這個工具時 | 模型選擇不呼叫工具、直接以文字回應時<br />如果沒有提供工具則是每一次都會被套用 |

兩者放在同一個請求裡也能運作，而且各自只約束自己那一部分的輸出。我對 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 次。

*Figure — RequestShapes: S1 強制呼叫工具，A1 讓模型決定要不要呼叫，T1 則完全不使用工具。只有 T1 從未失敗過。*

| 請求 | 模型 | 格式正確 | 耗時中位數 |
| --- | --- | --- | --- |
| 強制 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`，和[前一篇文章](/blog/bedrock-structured-outputs-claude-sonnet-5-error-feedback/)描述的是同一個錯誤。S1 能夠用最小的改動解決這個問題，但 **Sonnet 5.5 和 Opus 5.5 會直接拒絕強制 tool use**：

```text
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 的那段程式（轉接器）逐一檢查：

1. **因為 `max_tokens` 被截斷的回應，或是模型拒絕回答的回應，可能不符合 schema**。Anthropic 的結構化輸出頁面對這兩種情況都有說明。Converse 沒有 `refusal` 這個停止原因，沒辦法直接分辨模型是不是拒絕回答，所以轉接器只在 `stopReason` 為 `end_turn` 時才讀取回應。
2. **項目數量和 id 不受約束**。schema 的 `minItems`（陣列至少要有幾個項目）只接受 0 或 1，沒辦法要求一定要有 63 個，A1 那個空陣列也因此通過了。所以轉接器仍然保留和輸入 id 的比對。
3. **每個物件都必須加上 `"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 每次都在不拋出例外的情況下自動重試，所以**每一次逾時的嘗試都被計費了，卻沒有一次被記錄成失敗**。

*Figure — ThinkingLatency: 143 個和 80 個段落的兩個 batch，在強制 tool use、開啟 thinking 的 textFormat、關閉 thinking 的 textFormat 下的耗時。開啟 thinking 時，143 個段落的 batch 超過了 120 秒的逾時 5 次，第 6 次嘗試才終於完成。*

為了找出原因，我把變慢的 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 專屬的欄位：

```python title="Converse 請求" ins={3}
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：

```python title="Bedrock 轉接器"
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](https://aws.amazon.com/jp/about-aws/whats-new/2026/10/claude-structured-outputs-bedrock/)
- [Amazon Bedrock: Get validated JSON results from models (structured outputs)](https://docs.aws.amazon.com/bedrock/latest/userguide/structured-output.html)
- [Amazon Bedrock model card: Claude Sonnet 5](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-5.html)
- [Amazon Bedrock API: ToolChoice（auto、any、tool 的定義）](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolChoice.html)
- [Claude API: Structured outputs（含不保證的範圍）](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)
- [Claude API: Troubleshooting thinking（含各模型關閉 thinking 的表格）](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting)
- [Claude API: Define tools（含強制 tool use 的限制）](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools)
