# Bedrock 的結構化輸出只支援到 Claude 4.6 — 用一行提示詞與錯誤回傳守住 Sonnet 5 的 JSON 輸出

> Amazon Bedrock 上 Claude Sonnet 5 以後的模型不支援 strict tool use。少了它之後，我靠著一行提示詞規則，並且以 tool_result 回傳錯誤後重試，讓模型的 JSON 輸出維持了正確的格式。

- Source: https://oharu121.com/zh-tw/blog/bedrock-structured-outputs-claude-sonnet-5-error-feedback/
- Published: 2026-10-06T22:46:55+09:00
- Tags: Amazon Bedrock, LLM, AWS

---
## 引言

我負責的一條文件翻譯工作流，有一次 batch 在執行時因為 `ValueError: 応答要素の形式が不正: ['id', 'text']` 而失敗，這代表回應的 JSON 結構不正確。這條工作流會強制 Claude Sonnet 5 使用一個工具來回答，工具要求模型回傳含有 `id` 和 `translated_text` 這兩個 key 的 JSON。但在某幾次執行裡，它把 `translated_text` 寫成了 `text`，回傳了 `{id, text}` 的 JSON。

strict tool use 正是為了不讓這種問題發生而存在的。可是 **Amazon Bedrock 上 Sonnet 5 以及 Opus 4.6 之後的所有 Claude 模型都不支援 strict tool use**。最後為了維持住 JSON 的輸出格式，我在提示詞的最後加上一行規則提醒 JSON 的輸出格式，並且把 JSON 格式驗證失敗的訊息包裝成工具結果，連同格式錯誤的回應一起送回給模型、讓它重新生成一次。在 Sonnet 5 上，這兩種方法都發揮了作用。

## Bedrock 上的結構化輸出支援到第幾世代？

結構化輸出這項功能會把 JSON Schema 編譯成文法，讓模型在生成時只能選出文法允許的 token，這樣一來，輸出就不可能偏離要求的 JSON Schema。Bedrock 提供了兩種用法，可以分開也可以一起使用：

1. `strict: true` 加在工具定義上，讓模型依照該工具 `input_schema` 裡的 schema 填寫參數
2. `output_config.format` 自帶一份 JSON Schema，約束模型直接回答的 JSON，不需要經過工具

這兩種方法都能保證模型的 JSON 輸出格式，只是約束的對象不同。這兩種我都試過了，但在 Bedrock 上，Sonnet 5 兩種都不支援。

我把同一個最小請求送到 `ap-northeast-1` 的每一個 Claude 推論設定檔之後，**是否支援 strict tool use，在 4.6 世代之後出現了一道明顯的分界**：

```python title="strict_probe.py"
body = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 200,
    "messages": [{"role": "user", "content": "a に hello を入れて"}],
    "tools": [{"name": "t", "description": "x", "input_schema": schema, "strict": True}],
    "tool_choice": {"type": "tool", "name": "t"},
}
client.invoke_model(modelId=f"global.anthropic.claude-{m}", body=json.dumps(body))
```

| 模型 | Bedrock 上的 `strict: true` |
| --- | --- |
| Haiku 4.5、Sonnet 4.5、Opus 4.5 | 接受 |
| Sonnet 4.6、Opus 4.6 | 接受 |
| Opus 4.7、Opus 4.8、Opus 5、Opus 5.5 | `ValidationException` |
| Sonnet 5、Sonnet 5.5 | `ValidationException` |

每一次拒絕回傳的都是同一段錯誤訊息。`output_config.format` 在 Sonnet 5 上也以同樣的方式失敗了，而 Haiku 4.5 則回傳了符合要求 schema 的 JSON：

```text
ValidationException: tools.0.custom.strict: Extra inputs are not permitted
ValidationException: output_config.format: Extra inputs are not permitted
```

Sonnet 5 的模型卡也證實了這一點：在 Capabilities and Features 表格裡，不論是 `bedrock-runtime` 還是 `bedrock-mantle` 的 endpoint，Structured outputs 這一項都顯示 **Not Supported**。Fable 5 和 5.1 則完全無法測試，因為兩者都以 `data retention mode 'default' is not available for this model` 拒絕了請求。

*Figure — SupportMatrix: 同一批模型在 Bedrock 與 Anthropic 自家 API 上的比較。保證輸出符合 schema 的功能與強制 tool use，分別從不同的世代開始便不再支援。*

## 這個限制來自 Bedrock 還是 Anthropic？

**Anthropic 自家的 API 在 Sonnet 5 上是能夠使用結構化輸出的**。Anthropic 文件中的結構化輸出頁面把 `claude-sonnet-5`、`claude-opus-5-5` 和 `claude-sonnet-5-5` 列為支援的模型，同時註明在 Amazon Bedrock 上，只有少數幾個模型 (Opus 4.6、Sonnet 4.6、Sonnet 4.5、Opus 4.5 和 Haiku 4.5) 支援這項功能。

結構化輸出用的文法，是 Bedrock 根據請求裡的 JSON Schema 自己編譯出來的。Bedrock 文件的結構化輸出頁面在「Request workflow」裡寫著「Amazon Bedrock compiles the grammar, which may take up to a few minutes」。

編譯文法的這套機制是 Bedrock 自己維護的，所以**支援與否取決於部署方式，而不是模型本身**：同一個模型換了平台或推論設定檔，結果就可能不同。AWS 還警告，透過印度的推論設定檔 (`in.anthropic`) 呼叫 Claude Haiku 4.5 時不支援結構化輸出，而同一個模型透過其他推論設定檔都支援結構化輸出。

至於為什麼每一代新的 Claude 在 Bedrock 上推出時都沒有這項功能，以下**只是我的猜測，官方文件裡並沒有註明**，但最合理的解讀是：每個模型都得在 Bedrock 自己的文法引擎裡個別開啟，而 AWS 還沒有替 4.6 世代之後的模型開啟這項功能。AWS 沒有說明原因，也沒有給出時程，但我推測之後會逐漸開放支援。

### 在 Bedrock 上改用 Anthropic SDK

`anthropic[bedrock]` 提供了一個 `AnthropicBedrock` 用戶端，使用的是 Anthropic 原生的 Messages API 格式，看起來好像能規避 Bedrock 自家通用 API Converse 的限制。但它其實只是替請求簽章，然後送到**同一個 `bedrock-runtime` 的 InvokeModel endpoint**。我之前本來就是透過 `invoke_model` 向 Sonnet 5 送出 strict tool use 的請求，所以就算換了 `anthropic[bedrock]` 也行不通。

> **小心喔**
>
> **Anthropic 最新的模型不再接受強制 tool use**
>
> Anthropic 的官方文件寫到，使用 Opus 5.5、Sonnet 5.5、Fable 5.1 或 Mythos 5.1 時，如果把 `tool_choice` 設成 `any` 或 `tool` 會導致請求失敗，並建議改用 `auto` 搭配 strict tool use。因此強制 tool use 在這些模型上不論哪個平台都會失效。

## 為什麼強制 tool use 還是回傳了錯誤格式的 JSON？

`tool_choice: {"type": "tool"}` 保證的只是模型會呼叫這個工具，但並不會檢查呼叫的內容。Anthropic 的結構化輸出頁面也明白寫著，少了結構化輸出，「Claude can generate malformed JSON responses or invalid tool inputs」，其中也包括缺少必填欄位 (「Missing required fields」) 的情況。

我設計的工作流會把每個 batch 整理成由 `{"id", "text"}` 物件組成的 JSON 陣列，放進 user 訊息送出，並要求模型回傳符合 `{"id", "translated_text"}` 格式的 JSON。以其中一份 Word 文件為例，一個 batch 就有 63 個這種形式的物件。

只要模型在第一個物件把 key 寫成了 `text`，63 個物件的 key 就全都會寫成 `text`。**這些 `text` 欄位裡的值都是正確的英文譯文**，所以模型其實已經正確翻完了，只是把 key 的名稱標錯了而已。

出錯的比例會隨提示詞改變，而且翻譯那份 Word 文件時，隨時都有可能出現同樣的錯誤：

| 提示詞 | 所有 key 都變成 `text` 的呼叫次數 |
| --- | --- |
| 不加「譯文長度控制在原文 2 倍左右」的規則 | 30 次中 2 次 |
| 加上這條規則 (與 production 環境相同) | 40 次中 8 次 |

**在 production 環境的設定下，每五次就有一次出錯**，而用同一個提示詞直接重試，出錯的比例也一樣。

## 明確禁止 `text` 這個 key 的提示詞規則

第一個對策是在系統提示詞的最後，加上一行規則：用 `submit_translations` 回傳結果，每個物件的 key 必須是 `id` 和 `translated_text`，並且絕對不能在輸出裡使用輸入的 key `text`。

```text title="系統提示詞的最後一行"
- 結果は `submit_translations` ツールで返すこと。`translations` の各要素のキーは `id` と `translated_text` にする。入力のキー `text` を出力に使ってはいけない (訳文は `translated_text` に入れる)
```

```text title="中文翻譯 (未經測試)"
- 結果請用 `submit_translations` 工具回傳。`translations` 的每個元素，key 必須是 `id` 和 `translated_text`。不能在輸出裡使用輸入的 key `text` (譯文要放進 `translated_text`)
```

只寫出正確的 key、沒有提到 `text` 的版本，還是出錯了：

| 規則 | 所有 key 都變成 `text` 的呼叫次數 |
| --- | --- |
| 不加規則 | 40 次中 8 次 |
| 只要求寫正確的 key | 40 次中 4 次 |
| 要求寫出正確的 key，並明確禁止使用 `text` 這個 key | 40 次中 0 次 |

原本的提示詞有 4,885 個 token，加上這條規則之後，**每次呼叫多了 82 個輸入 token**。不過 40 次都沒出錯，並不代表出錯率就是 0：樣本只有 40 次，統計上只能推論真正的出錯率大約在 7.5% 以下。所以還需要第二道防線。

## 把錯誤包裝成 tool_result 回傳給模型

**不要重送同一個 batch，而是把出錯的回答連同錯誤訊息一起送回給模型**。我以前在一個 text-to-SQL 專案裡，就是這樣處理無效的 SQL：執行查詢、把資料庫的錯誤回傳給模型，讓模型參考錯誤訊息再寫一次。

Anthropic 的文件對工具呼叫也介紹了同樣的做法：用一個標示為錯誤的 `tool_result` 把對話接下去，Claude 就會再試著使用一次工具 (「continue the conversation forward with a `tool_result` that indicates the error, and Claude will try to use the tool again」)。

在 Bedrock 的 Converse API 上，做法是把出錯的 assistant 訊息原封不動地放回對話裡，再針對其中的 `toolUse` 附上一個錯誤訊息說明為什麼出錯：

```python title="Bedrock 轉接器"
bad_use = next(b["toolUse"] for b in bad_content if "toolUse" in b)
messages = [
    {"role": "user", "content": [{"text": user}]},
    {"role": "assistant", "content": bad_content},  # 原封不動放回收到的回應
    {"role": "user", "content": [{"toolResult": {
        "toolUseId": bad_use["toolUseId"],
        "content": [{"text": feedback}],
        "status": "error",
    }}]},
]
client.converse(modelId=model_id, system=[{"text": system}], messages=messages,
                inferenceConfig=inference_config, toolConfig=tool_config)
```

`feedback` 這段文字寫出了哪裡錯、該怎麼改：收到的 key 是 `['id', 'text']`、每個物件都需要 `id` 和 `translated_text`、所有物件都要重新提交一次。assistant 訊息的 `content` 必須原封不動地送回去，因為開啟 adaptive thinking 時，裡面可能帶著 `reasoningContent` 區塊，這些區塊也必須一起送回。

之前測試中 AI 給出的錯誤回應，8 件全都在第一次重試後就修好了。一次重試的輸入 token 數比原本的呼叫多了大約 72%，但花的時間基本一樣，都是 24 秒。

## 在寫入回應快取之前先完成重試

這條工作流會把每個模型回應存進 DynamoDB，key 是完整提示詞的雜湊值。這樣一來，SQS 重新傳遞同一個工作時，已經成功的 batch 就能直接從快取取回，不必再讓 AI 模型跑一遍。上面兩個對策都必須在**寫入快取之前**完成，否則快取可能會回傳之前儲存的錯誤回應，或是讓後面的 batch 都對不上快取。

如果快取在檢查格式之前就先儲存回應，就會連出錯的 batch 也存起來，**之後每次重新傳遞都會從快取取回這個錯誤的回應**。第 2 次和第 3 次 SQS 重新傳遞的日誌裡，都顯示同一個 key 命中了快取，接著又因為同一個 `ValueError` 失敗。只要在儲存之前、以及從快取讀出的時候都檢查格式，就能防止這個問題發生。

第二個陷阱是會改變提示詞的重試：把錯誤回傳給模型時，請求裡就多了出錯的回應和錯誤訊息。而且 Sonnet 5 只接受預設的 temperature 1.0，所以每重新呼叫一次，得到的譯文都會不一樣。

另外，每個 batch 的提示詞都會帶著前一個 batch 譯文的結尾當作脈絡。用改過的提示詞修好的 batch，會以改過的提示詞當 key 存進快取；但下一次重新傳遞時，工作流只會用原本的提示詞組出 key，所以永遠找不到之前對應的快取。這個 batch 只好重新翻譯一次，新的譯文又改變了下一個 batch 的脈絡，於是快取未命中就一路往後擴散。

*Figure — CacheCascade: 重試時改了提示詞，修好的 batch 就會存到另一個 key 底下，下一次重新傳遞時查不到，只好重新翻譯。這時重新翻譯出的譯文又和之前不同，導致後面每個 batch 的脈絡也跟著改變了。*

我在一份分成五個 batch 的模擬文件裡，讓 batch 1 出錯一次，並讓第 1 次傳遞在 batch 3 中斷。結果**第 2 次傳遞時，模型並沒有只處理 batch 3 和 4，而是重新處理了 batch 1 到 4**。

**把重試移到 Bedrock 轉接器裡面之後，兩個陷阱就一次解決了**。負責快取的那一層只會用原本的提示詞呼叫轉接器一次。轉接器負責驗證格式，必要時把錯誤回傳給模型重試，最後回傳格式正確的回應，或是仍然出錯的回應。快取只會把格式正確的回應存在原本的 key 底下。

*Figure — RetryBelowCache: 快取只會用原本的提示詞呼叫轉接器一次，驗證格式和把錯誤回傳給模型重試，都在轉接器裡完成。*

## 總結

| 問題 | 處理方式 |
| --- | --- |
| Bedrock 上的 Claude Sonnet 5 以及 Opus 4.6 之後的所有模型，都不支援 `strict: true` 和 `output_config.format` | Bedrock 這邊目前沒有辦法，要在自己的程式裡驗證 |
| 強制 tool use 把 `translated_text` 寫成 `text`，40 次中 8 次 | 明確禁止 `text` 的系統提示詞規則：40 次中 0 次 |
| 仍然出錯的呼叫 | 附上該用的 key，以 `toolResult` 錯誤回傳：8 件中 8 件修好 |
| 以提示詞為 key 的回應快取 | 儲存前先驗證，並在 Bedrock 轉接器裡重試，讓 key 維持原本的值 |

**Bedrock 上能不能用結構化輸出，取決於部署方式，而不是模型本身**，所以挑選模型之前，最好先到模型卡上確認。就算沒有這項功能，只要自己驗證輸出，並在出錯時把錯誤回傳給模型重試，也能得到相對穩定的輸出，代價只是出錯的時候 batch 需要多呼叫一次。

## 參考連結

- [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)
- [Claude API: Structured outputs (含支援模型清單與 Bedrock 上的提供狀況)](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)
- [Claude API: Strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use)
- [Claude API: Define tools (含強制 tool use 的限制)](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools)
- [Claude API: Handle tool calls (含以 is_error 處理錯誤)](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls)
