Bedrock 的結構化輸出只支援到 Claude 4.6 — 用一行提示詞與錯誤回傳守住 Sonnet 5 的 JSON 輸出
Amazon Bedrock 上 Claude Sonnet 5 以後的模型不支援 strict tool use。少了它之後,我靠著一行提示詞規則,並且以 tool_result 回傳錯誤後重試,讓模型的 JSON 輸出維持了正確的格式。

本頁目錄
引言
我負責的一條文件翻譯工作流,有一次 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 提供了兩種用法,可以分開也可以一起使用:
strict: true加在工具定義上,讓模型依照該工具input_schema裡的 schema 填寫參數output_config.format自帶一份 JSON Schema,約束模型直接回答的 JSON,不需要經過工具
這兩種方法都能保證模型的 JSON 輸出格式,只是約束的對象不同。這兩種我都試過了,但在 Bedrock 上,Sonnet 5 兩種都不支援。
我把同一個最小請求送到 ap-northeast-1 的每一個 Claude 推論設定檔之後,是否支援 strict tool use,在 4.6 世代之後出現了一道明顯的分界:
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:
ValidationException: tools.0.custom.strict: Extra inputs are not permittedValidationException: output_config.format: Extra inputs are not permittedSonnet 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 拒絕了請求。
這個限制來自 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] 也行不通。
為什麼強制 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。
- 結果は `submit_translations` ツールで返すこと。`translations` の各要素のキーは `id` と `translated_text` にする。入力のキー `text` を出力に使ってはいけない (訳文は `translated_text` に入れる)- 結果請用 `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 附上一個錯誤訊息說明為什麼出錯:
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 的脈絡,於是快取未命中就一路往後擴散。
我在一份分成五個 batch 的模擬文件裡,讓 batch 1 出錯一次,並讓第 1 次傳遞在 batch 3 中斷。結果第 2 次傳遞時,模型並沒有只處理 batch 3 和 4,而是重新處理了 batch 1 到 4。
把重試移到 Bedrock 轉接器裡面之後,兩個陷阱就一次解決了。負責快取的那一層只會用原本的提示詞呼叫轉接器一次。轉接器負責驗證格式,必要時把錯誤回傳給模型重試,最後回傳格式正確的回應,或是仍然出錯的回應。快取只會把格式正確的回應存在原本的 key 底下。
總結
| 問題 | 處理方式 |
|---|---|
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 需要多呼叫一次。



