# Bedrock の構造化出力が Claude Sonnet 5 で使えるようになった — 強制 tool use をやめたら thinking が動き出した

> Amazon Bedrock が Claude Sonnet 5 で outputConfig.textFormat を受け付けるようになりました。強制 tool use から置き換えると JSON の形は直りましたが、thinking を切る必要がありました。

- Source: https://oharu121.com/ja/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 モデルで構造化出力に対応したと発表したので、この回避策を外せるかどうかを確かめました。

外せました。プロンプトのルールを外すと、実際のバッチで強制 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 はこのフィールドを読んでおり、黙って捨ててはいません。

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

Converse はスキーマを `json.dumps()` で JSON 文字列にしてから渡す必要があり、InvokeModel は Python の dict をそのまま受け取ります。そのため、**2 つのリクエストの形はそのまま入れ替えられません**。

```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`）の 3 つがあります。ツール定義に付ける `strict: true` は、そのツールが呼ばれたときに、引数が必ずツールのスキーマに合うようにします。`outputConfig.textFormat` はツールを介さずに、モデルが返す JSON そのものを指定したスキーマに合わせます。

*Figure — ThreeKnobs: 呼び出しを強制できるのは toolChoice だけです。strict と textFormat は JSON をスキーマに合わせるもので、strict はツールの引数、textFormat はモデルの応答が対象です。*

`strict` と `textFormat` は、構造化出力を構成する 2 つの機能です。どちらも制約付きデコーディングを使います。Bedrock がスキーマを文法にコンパイルし、モデルはその文法が許すトークンしかサンプリングできません。`toolChoice` はそれより古い機能で、構造化出力には含まれません。

*Figure — ConstrainedDecoding: Bedrock はスキーマを文法にコンパイルし、文法が許さないトークンをすべて除外します。図中のトークンは説明用の例です。*

### 強制 tool use と構造化出力は両立しないのか？

両立します。私は、構造化出力はツールとして実装されていて、強制 tool use と組み合わせないと効かないと思い込んでいました。実際には、構造化出力そのものはツールではなく、モデルが選べるトークンを制限する仕組みで、使い方は 2 つあります。`strict` はツール定義に付ける属性で、モデルがそのツールを呼んだときだけ引数を制限します。`textFormat` はリクエストに直接書き、応答全体を制限します。

前回の記事の回避策は、まさにこの思い込みの上に成り立っていました。回答のスキーマをそのまま入力スキーマにしたツールを定義し、`toolChoice: {"tool": {"name": ...}}` で強制し、その引数を読むというものです。**強制で保証されるのは呼び出しだけで、その中身は一切確認されません**。前回、キーが `text` で返ってきたのはそのためです。

`strict` は、従来の強制 tool use に欠けていた部分をちょうど補います。強制 tool use はモデルがこのダミーのツールを必ず呼ぶことを保証し、`strict` はモデルがダミーのツールに入れる input が、ツールに定義したスキーマに必ず合うことを保証します。つまり、ダミーのツールを実行して結果を得るのではなく、モデルがダミーのツールに入れた input から、スキーマに合った JSON をそのまま受け取ります。

*Figure — FakeTool: どちらもツールを渡し、モデルに引数を埋めさせます。ふつうのツールはそのあと実行されますが、ダミーのツールは実行されず、引数そのものが答えになります。*

Sonnet 5 は `strict` と強制 tool use を両方含むリクエストを受け付け、毎回スキーマに合った結果を返しました。一方、`textFormat` はツールとは独立した機能で、ツールの一部ではありません。そのため、**ツールがまったく要らず**、呼び出しを強制する必要もありません。

### toolChoice の auto・any・tool の意味

`toolChoice` の値は、モデルが `tools` の一覧をどう使うかを決めます。

1. `{"auto": {}}`：どのツールを呼ぶかをモデルが決めます。ツールを選ばず、テキストで直接答えることもできます。`toolChoice` を省いたときは、この値が既定になります。

   ```python
   tool_config = {
       "tools": [get_weather, get_time],
       "toolChoice": {"auto": {}},  # どちらのツールを呼んでも、テキストだけで答えてもよい
   }
   ```

2. `{"any": {}}`：一覧のツールを少なくとも 1 つ必ず呼びます。テキストは生成しません。

   ```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 を呼ぶ
   }
   ```

後の 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」(単独でも、同じリクエストで両方を一緒にも使える) 仕組みだと説明しています。

*Figure — StrictVsTextFormat: strict はスキーマを実行されないダミーのツールに載せ、答えは tool_use の input として返ってきます。textFormat はスキーマをリクエストに載せ、答えは応答そのものです。*

| | `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 回実行しました。

*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 回は、どれもすべてのキーが `text` になっていました。前回の記事で追いかけたのと同じ崩れ方です。S1 は最小の変更でこれを直せます。しかし、**Sonnet 5.5 と Opus 5.5 は強制 tool use そのものを拒否します**。

```text
tool_choice: type "tool" and "any" are not supported for this model.
```

A1 が唯一失敗した 1 回では、モデルは 2 秒で出力 75 トークンだけを生成し、空の `translations` 配列を渡してツールを呼びました。空の配列もスキーマには合うので、**`strict` はこれを通してしまいました**。最終的に私は T1 を選びました。A1 は auto なので、モデルがツールを呼ばないこともありえますが、T1 にはそもそもツールがないので、その心配がありません。

### 構造化出力でもコード側に残る確認

スキーマの保証には範囲があり、パイプラインの Bedrock アダプターは、その外側を 1 つずつ確認し続けています。

1. **`max_tokens` で途切れたレスポンスや拒否のレスポンスは、スキーマに合わないことがあります**。Anthropic の構造化出力のページに、両方についてそう書かれています。Converse には `refusal` の停止理由がないので、アダプターは `stopReason` が `end_turn` のときだけ応答を読みます。
2. **要素の数と id は制約されません**。`minItems`（配列に最低いくつの要素が必要か）は 0 か 1 しか受け付けず、63 個を必須にはできません。A1 の空の配列が通ったのもこのためで、入力の id との照合は残しています。
3. **すべてのオブジェクトに `"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 は、そのたびに例外を出さずに再送しました。そのため、**タイムアウトした試行はどれも課金され、どれも失敗としてログに残りませんでした**。

*Figure — ThinkingLatency: 143 セグメントと 80 セグメントの 2 つのバッチを、強制 tool use、thinking ありの textFormat、thinking なしの textFormat で比べたものです。thinking ありでは、143 セグメントのバッチが 120 秒のタイムアウトを 5 回超え、6 回目の試行でようやく終わりました。*

遅いバッチを、タイムアウト 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` で渡します。

```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 バッチで試したところ、**すべてのレスポンスがスキーマに合い、すべての 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 を呼ぶ前にアダプターが拒否するようにしました。

```python title="bedrock adapter"
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](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)
