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 モデルで構造化出力に対応したと発表したので、この回避策を外せるかどうかを確かめました。
外せました。プロンプトのルールを外すと、実際のバッチで強制 tool use は 20 回中 5 回崩れ、outputConfig.textFormat は 1 回も崩れませんでした。ただし、切り替えて元の速さを保てたのは thinking を切ったときだけでした。強制 tool use が Sonnet 5 に thinking をまったくさせていなかったためで、それがなくなると、143 セグメントのバッチ 1 つが 47 秒から 731 秒になりました。
今は Bedrock がモデルの生成中にスキーマを強制するので、リクエストは、Sonnet 5.5 と Opus 5.5 が拒否する強制 tool use に頼らずに済みます。
Bedrock が Claude Sonnet 5 で受け付けるようになったもの
Bedrock で呼べた Claude のモデルは、どれも回答に JSON Schema を指定できるようになっていました。Claude Code が ap-northeast-1 の global.anthropic.* 推論プロファイルに、モデルごとに小さなリクエストを 1 つずつ送りました。ランタイム API は 2 つとも試しています。Bedrock のモデルを問わない会話 API である Converse と、Anthropic 独自のリクエストボディをそのまま渡す InvokeModel です。
| モデル | 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 | 試せず | 試せず |
プロンプトでは、JSON ではない普通の文章での回答、スキーマにない enum の値、余分なキーをわざと求めました。それでも、通ったレスポンスはすべてスキーマに合っていました。Fable はアカウントの設定の問題で使えなかったため、構造化出力に対応しているかどうかは確かめていません。Sonnet 5 のモデルカードでも、Structured outputs が Supported に載るようになっています。
わざと壊したスキーマ {"type": "bogus"} は拒否されました。つまり Bedrock はこのフィールドを読んでおり、黙って捨ててはいません。
ValidationException: The json schema definition at outputConfig.textFormat.structure.jsonSchema is invalid.Converse はスキーマを json.dumps() で JSON 文字列にしてから渡す必要があり、InvokeModel は Python の dict をそのまま受け取ります。そのため、2 つのリクエストの形はそのまま入れ替えられません。
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)の 3 つがあります。ツール定義に付ける strict: true は、そのツールが呼ばれたときに、引数が必ずツールのスキーマに合うようにします。outputConfig.textFormat はツールを介さずに、モデルが返す JSON そのものを指定したスキーマに合わせます。
strict と textFormat は、構造化出力を構成する 2 つの機能です。どちらも制約付きデコーディングを使います。Bedrock がスキーマを文法にコンパイルし、モデルはその文法が許すトークンしかサンプリングできません。toolChoice はそれより古い機能で、構造化出力には含まれません。
強制 tool use と構造化出力は両立しないのか?
両立します。私は、構造化出力はツールとして実装されていて、強制 tool use と組み合わせないと効かないと思い込んでいました。実際には、構造化出力そのものはツールではなく、モデルが選べるトークンを制限する仕組みで、使い方は 2 つあります。strict はツール定義に付ける属性で、モデルがそのツールを呼んだときだけ引数を制限します。textFormat はリクエストに直接書き、応答全体を制限します。
前回の記事の回避策は、まさにこの思い込みの上に成り立っていました。回答のスキーマをそのまま入力スキーマにしたツールを定義し、toolChoice: {"tool": {"name": ...}} で強制し、その引数を読むというものです。強制で保証されるのは呼び出しだけで、その中身は一切確認されません。前回、キーが text で返ってきたのはそのためです。
strict は、従来の強制 tool use に欠けていた部分をちょうど補います。強制 tool use はモデルがこのダミーのツールを必ず呼ぶことを保証し、strict はモデルがダミーのツールに入れる input が、ツールに定義したスキーマに必ず合うことを保証します。つまり、ダミーのツールを実行して結果を得るのではなく、モデルがダミーのツールに入れた input から、スキーマに合った JSON をそのまま受け取ります。
Sonnet 5 は strict と強制 tool use を両方含むリクエストを受け付け、毎回スキーマに合った結果を返しました。一方、textFormat はツールとは独立した機能で、ツールの一部ではありません。そのため、ツールがまったく要らず、呼び出しを強制する必要もありません。
toolChoice の auto・any・tool の意味
toolChoice の値は、モデルが tools の一覧をどう使うかを決めます。
-
{"auto": {}}:どのツールを呼ぶかをモデルが決めます。ツールを選ばず、テキストで直接答えることもできます。toolChoiceを省いたときは、この値が既定になります。tool_config = {"tools": [get_weather, get_time],"toolChoice": {"auto": {}}, # どちらのツールを呼んでも、テキストだけで答えてもよい} -
{"any": {}}:一覧のツールを少なくとも 1 つ必ず呼びます。テキストは生成しません。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 を呼ぶ}
後の 2 つが強制 tool use で、モデルは必ずツールを使い、テキストで直接答えることはできません。
小さな検証では、Opus 4.7、Opus 4.8、Haiku 5.5 が stopReason: end_turn でテキストで答え、ツールを呼びませんでした。strict と auto の組み合わせでは、スキーマが効くのは呼び出しが起きた場合だけです。
strict と textFormat は同じものか?
仕組みは同じで、対象が違います。AWS は 2 つを、「can be used independently or together in the same request」(単独でも、同じリクエストで両方を一緒にも使える) 仕組みだと説明しています。
strict: true | outputConfig.textFormat | |
|---|---|---|
| 書く場所 | ツール定義の中 | リクエストのトップレベル |
| 制約する対象 | モデルがダミーのツールの input に入れる JSON | モデルがテキストで答えるときの出力 |
| 効くタイミング | モデルがそのツールを呼ぶと決めたとき | モデルがツールを呼ばず、テキストで直接答えるとき(ツールを渡していなければ毎回) |
2 つを同じリクエストに入れても動き、それぞれが自分の担当する出力だけを制約します。Sonnet 5 に、strict 付きのツール、toolChoice: auto、textFormat をすべて指定したリクエストを 30 回送りました。返ってきたのは毎回ツール呼び出しかテキストのどちらか一方で、両方が同時に返ることはありませんでした。ただし auto では、モデルはほとんどの場合ツールを呼び、ツールを使わないよう明示した 10 回でも 4 回はツールを呼びました。つまり、モデルがツールを呼ばないと決めたときにだけ textFormat が効きます。
strict と textFormat は別々の経路を通ります。モデルがツールを呼ぶと決めたときは strict が、ツールを呼ばずテキストで直接答えるときは textFormat が効きます。どちらの経路でも、結果はスキーマに合います。ツール呼び出し 24 回もテキストの応答 6 回も、すべてそれぞれのスキーマに合っていました。
同じ JSON を求める 3 つのリクエストを実際のバッチで比べる
強制 tool use + strict、strict + auto、textFormat の 3 つの組み合わせを試しました。Sonnet 5 と Sonnet 5.5 の両方で崩れず、しかもツールを呼ぶかどうかの判断をモデルに残さなかったのは textFormat だけでした。
テストのバッチは 63 セグメントです。崩れる余地を残すため、text を禁止キーとして名指しするプロンプトのルールは外しました。各リクエストを 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 回は、どれもすべてのキーが text になっていました。前回の記事で追いかけたのと同じ崩れ方です。S1 は最小の変更でこれを直せます。しかし、Sonnet 5.5 と Opus 5.5 は強制 tool use そのものを拒否します。
tool_choice: type "tool" and "any" are not supported for this model.A1 が唯一失敗した 1 回では、モデルは 2 秒で出力 75 トークンだけを生成し、空の translations 配列を渡してツールを呼びました。空の配列もスキーマには合うので、strict はこれを通してしまいました。最終的に私は T1 を選びました。A1 は auto なので、モデルがツールを呼ばないこともありえますが、T1 にはそもそもツールがないので、その心配がありません。
構造化出力でもコード側に残る確認
スキーマの保証には範囲があり、パイプラインの Bedrock アダプターは、その外側を 1 つずつ確認し続けています。
max_tokensで途切れたレスポンスや拒否のレスポンスは、スキーマに合わないことがあります。Anthropic の構造化出力のページに、両方についてそう書かれています。Converse にはrefusalの停止理由がないので、アダプターはstopReasonがend_turnのときだけ応答を読みます。- 要素の数と id は制約されません。
minItems(配列に最低いくつの要素が必要か)は 0 か 1 しか受け付けず、63 個を必須にはできません。A1 の空の配列が通ったのもこのためで、入力の id との照合は残しています。 - すべてのオブジェクトに
"additionalProperties": falseが必要です。ほかの値を指定すると、Bedrock は 400 を返します。
強制 tool use をやめたら adaptive thinking が動き出した
強制 tool use が Sonnet 5 に thinking をさせておらず、textFormat にしたことで再び thinking するようになりました。adaptive thinking は Sonnet 5 での Claude の既定の動作です。回答の前に推論するかどうか、どれだけ推論するかをモデルがリクエストごとに決め、推論のトークンは maxTokens に含まれます。
textFormat で 1 つの文書をまるごと翻訳したところ、ほとんどのバッチは以前と同じ時間で終わりましたが、3 つは明らかに遅くなりました。最も遅いバッチは合計 6 回試行されました。そのうち 5 回が 120 秒の read_timeout を超えたためです。boto3 の下で動く AWS SDK である botocore は、そのたびに例外を出さずに再送しました。そのため、タイムアウトした試行はどれも課金され、どれも失敗としてログに残りませんでした。
遅いバッチを、タイムアウト 300 秒、再送なしの設定で、それぞれさらに 3 回ずつ流すと、原因を切り分けられました。
| リクエスト | 所要時間 | 停止理由 | 出力トークン | 推論が入った実行 |
|---|---|---|---|---|
| 強制 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 回 |
所要時間、停止理由、出力トークンは最も遅い 2 バッチ(計 6 回)だけを集計し、最後の列は 3 バッチの全 9 回を集計しています。
JSON そのものの長さはどのモードでも同じで、1 バッチでおよそ 13,000 文字でした。つまり、増えたトークンは推論の分です。143 セグメントのバッチは、文法がコンパイルされてキャッシュされたずっと後の 2 回目の通し実行でも同じように遅かったので、コンパイルが遅れの原因ではありません。
原因は、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 を切る必要があります。
thinking を切る設定と、それを拒否するモデル
thinking: {"type": "disabled"} を送ると、textFormat は強制 tool use のときの数字に戻りました。Converse では、Anthropic 固有のフィールドを additionalModelRequestFields で渡します。
client.converse( # … additionalModelRequestFields={"thinking": {"type": "disabled"}}, outputConfig={"textFormat": {"type": "json_schema", "structure": {"jsonSchema": { "schema": json.dumps(schema), "name": "translations", }}}},)23 バッチで試したところ、すべてのレスポンスがスキーマに合い、すべての id が返ってきました。最も長い呼び出しは 45 秒で、再送は 1 回もありませんでした。パイプラインの評価スイートでの翻訳品質も、強制 tool use 版の実行ごとのばらつきの範囲に収まりました。
この設定は、すべてのモデルが受け付けるわけではありません。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 ブロックとして返ってきます。ドキュメントの説明どおりなら、このパイプラインのようにツールを使わず 1 回だけ呼ぶリクエストでは、両者に違いはないはずです。
パイプラインはモデル ID を実行時に SSM Parameter Store から読むので、再デプロイなしで Sonnet 5.5 に切り替えられます。そして切り替えると、その後のバッチはすべて 400 で失敗します。そこで私は、thinking を切った状態で確かめていないモデル ID を、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}" )このエラーは、Bedrock の 400 が延々と続く形ではなく、モデル ID を含んだ 1 つのメッセージとして表に出ます。モデルを追加するには、先に測定が必要になります。
まとめ
| 問題 | 対処 |
|---|---|
| 強制 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"}。強制 tool use のときの所要時間とトークン数に戻る |
max_tokens での途切れ、拒否、要素の数、id | end_turn のときだけ読み、id の照合を残す |
disabled を拒否するモデルへの切り替え | Bedrock を呼ぶ前にモデル ID 付きで失敗させる許可リスト |
Bedrock の Claude Sonnet 5 は、応答に JSON Schema を指定できるようになりました。ただ、強制 tool use から離れると、モデルが thinking するかどうかも変わります。そのため、切り替える前に、JSON の形だけでなく、いちばん重いバッチで所要時間と出力トークンを測ってください。
参考リンク
- 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 の制限を含む)

