Bedrock の構造化出力は Claude 4.6 で止まる — Sonnet 5 の JSON をプロンプト 1 行とエラーの差し戻しで守る

Amazon Bedrock は Claude Sonnet 5 で strict tool use を拒否します。プロンプトの 1 行と tool_result によるエラーの差し戻しで、翻訳の JSON を正しい形に保ちました。

白いカードに、六角形の輪郭と小さな円で終わる回路の線からなる白い線描きの脳を配したティール色の角丸正方形のAmazon Bedrockのアイコンと、濃紺のAmazon Bedrockの文字
目次

はじめに

私が関わっている文書翻訳のパイプラインで、バッチが ValueError: 応答要素の形式が不正: ['id', 'text'] で失敗するようになりました。応答の要素のキーが違う、というエラーです。Claude Sonnet 5 には、{id, translated_text} を返すスキーマのツールで答えるよう強制していました。ところが一部の呼び出しで、入力を写したように、すべてのキーを text にして返していました。

strict tool use はこれを起こさないための機能です。しかし、Amazon Bedrock は Sonnet 5 と、Opus 4.6 より後のすべての Claude モデルで strict tool use を拒否します。JSON の形を守ったのは、プロンプトに足した 1 行のルールと、検証のエラーをツールの結果としてモデルに返す出し直しでした。Sonnet 5 では、どちらも効果がありました。

Bedrock で構造化出力はどこまで使えるか

構造化出力は、JSON Schema を文法にコンパイルし、その文法が許すトークンだけをモデルにサンプリングさせる機能です。そのため、出力がスキーマを外れることがありません。Bedrock では 2 つの形で使え、どちらか一方だけでも、同じリクエストで両方を指定しても構いません。strict: true はツール定義に付け、呼び出しの引数をそのツールの input_schema に従わせます。output_config.format は自前の JSON Schema を持ち、ツールを介さずにモデルが本文として返す JSON の回答を制約します。どちらも保証は同じで、制約する対象が違うだけです。Sonnet 5 では両方を試しました。

ap-northeast-1 のすべての Claude の推論プロファイルに同じ最小限のリクエストを送ると、4.6 世代を境にモデルがきれいに分かれました。

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.5ValidationException
Sonnet 5、Sonnet 5.5ValidationException

拒否はどれも同じメッセージでした。output_config.format も Sonnet 5 では同じように失敗し、Haiku 4.5 はスキーマどおりの JSON を返しました。

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

Sonnet 5 のモデルカードも同じことを書いています。Capabilities and Features の表では、Structured outputs が bedrock-runtime と bedrock-mantle の両方のエンドポイントで Not Supported になっています。Fable 5 と 5.1 は試すこともできませんでした。どちらも data retention mode 'default' is not available for this model でリクエストを拒否しました。

Claude のモデルとプラットフォーム別の 構造化出力と強制 tool use6 つの Claude モデル群と 3 つの列の表。Bedrock では strict tool use が Haiku 4.5 と 4.5・4.6 世代の Sonnet・Opus で通り、Opus 4.7 以降と Sonnet 5・5.5 で拒否される。Anthropic API ではすべての群が 構造化出力に対応する。強制 tool_choice は Sonnet 5.5 と Opus 5.5 以外で使える。本件で使う Sonnet 5 は、強制 tool use は両方で使えるが、構造化出力は Anthropic API でしか使えない。モデルBedrockstrict / output_configAnthropic API構造化出力Anthropic API強制 tool_choiceHaiku 4.5✓ 対応✓ 対応✓ 対応Sonnet 4.5 · Opus 4.5✓ 対応✓ 対応✓ 対応Sonnet 4.6 · Opus 4.6✓ 対応✓ 対応✓ 対応Opus 4.7 · 4.8 · 5✗ 拒否✓ 対応✓ 対応Sonnet 5本件で使用✗ 拒否✓ 対応✓ 対応Sonnet 5.5 · Opus 5.5✗ 拒否✓ 対応✗ 拒否
同じモデルを Bedrock と Anthropic の API で比べた表です。スキーマの保証と強制 tool use は、それぞれ別の世代で使えなくなります。

制約は Bedrock 側か、Anthropic 側か

Anthropic 自身の API では、Sonnet 5 でも構造化出力が使えます。構造化出力のページは、対応モデルとして 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 独自の基盤です。そのため、対応はモデルごとではなく、提供先 (プラットフォームや推論プロファイル) ごとに決まります。同じページには、構造化出力が「aren’t supported for Claude Haiku 4.5 through the India (in.anthropic) inference profile」という注意もあります。Haiku 4.5 は、ほかの推論プロファイルではどこでも使えるモデルです。

新しい Claude の世代が構造化出力なしで Bedrock に来る理由は、文書には書かれておらず、ここからは推測です。2 つの引用から最も素直に読めるのは、モデルごとに Bedrock 独自の文法エンジンで有効にする必要がある、ということです。そう読むと、4.6 世代より新しいモデルではそれがまだ済んでいないことになります。AWS は理由も時期も示していません。ただ、今後は順次対応されていくと推測しています。

Bedrock で Anthropic の SDK を使う場合

anthropic[bedrock] には、Messages API の形式でリクエストを送る AnthropicBedrock クライアントがあります。Bedrock 独自の、モデルを問わない会話 API である Converse を迂回する手段に見えますが、このクライアントはリクエストに署名して、同じ bedrock-runtime の InvokeModel エンドポイントに送ります。上の検証も invoke_model で同じ形のリクエストを送っており、Bedrock が拒否したのはまさにそのリクエストです。

強制 tool use でもキーが違った理由

tool_choice: {"type": "tool"} が保証するのは、モデルがツールを呼ぶことだけです。呼び出しの中身までは見ません。Anthropic の構造化出力のページも、構造化出力を使わない場合は「Claude can generate malformed JSON responses or invalid tool inputs」と明記しており、その例に「Missing required fields」を挙げています。

このパイプラインは、各バッチを {"id", "text"} のオブジェクトの JSON 配列として user メッセージに入れて送ります。Word の文書 1 つで 63 個です。返してほしいのは {"id", "translated_text"} です。

モデルが最初の要素で text と書くと、63 個すべてが text になりました。値はきちんとした英訳でした。つまりモデルは翻訳を済ませていて、ラベルだけを間違えていました。

崩れる率はプロンプトによって変わり、その Word の文書 1 つでいつでも再現できました。

プロンプトすべてのキーが text になった呼び出し
「訳文の長さは原文の 2.0 倍程度」のルールなし30 回中 2 回
このルールあり (本番と同じ)40 回中 8 回

本番の設定では 5 回に 1 回崩れました。同じプロンプトでそのまま出し直しても、崩れる率は同じです。

禁止するキーを名指しするプロンプトのルール

1 つ目の対策は、システムプロンプトの末尾、ほかのすべての指示の後ろに足す 1 行です。結果は submit_translations で返すこと、キーは id と translated_text にすること、入力のキー text は使わないことを書いています。

システムプロンプトの最終行
- 結果は `submit_translations` ツールで返すこと。`translations` の各要素のキーは `id` と `translated_text` にする。入力のキー `text` を出力に使ってはいけない (訳文は `translated_text` に入れる)

正しいキーだけを書き、text に触れない版では、まだ崩れました。

ルールすべてのキーが text になった呼び出し
なし40 回中 8 回
正しいキーだけ40 回中 4 回
正しいキーと、text の禁止40 回中 0 回

このルールのコストは 1 回あたり入力 82 トークンで、元のプロンプトは 4,885 トークンです。ただ、40 回中 0 回だからといって、崩れる率が 0 とは言えません。40 回の試行から言えるのは、本当の率がおよそ 7.5% 未満だということまでです。そのため、すり抜ける呼び出しに備えて 2 段目の守りが要ります。

エラーを tool_result として返す

同じバッチを送り直すのではなく、崩れた答えをエラーと一緒に返します。私は text-to-SQL のプロジェクトで、無効な SQL をこう扱っていました。クエリを実行し、データベースのエラーを返し、それを見たうえでモデルにもう一度書かせる、というやり方です。

Anthropic もツール呼び出しについて同じ手を説明しており、「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 にエラーで答えます。

bedrock adapter
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 の文には、何が違っていて、どうすればよいかを書きます。受け取ったキーは ['id', 'text'] だったこと、各要素には id と translated_text が必要なこと、すべての要素をもう一度提出することです。assistant の content は 1 バイトも変えずに返してください。adaptive thinking が有効だと reasoningContent のブロックが含まれることがあり、それも一緒に返す必要があるからです。

上の測定で実際に崩れた応答は、8 件すべてが 1 回目の出し直しで直りました。出し直しの呼び出しは、元の呼び出しより入力トークンが約 72% 多くかかり、所要時間はどちらも 24 秒でした。

応答キャッシュに書き込む前に出し直しを終える

このパイプラインは、完成したプロンプトのハッシュをキーにして、モデルの応答を DynamoDB にキャッシュします。そのため、再配信されたジョブは成功済みのバッチをキャッシュから読み直せるので、料金をもう一度払わずに済みます。上の 2 つの対策は、どちらもキャッシュに書き込む前に終わらせる必要があります。そうしないと、キャッシュが対策を打ち消してしまいます。

形を確かめる前に応答を保存するキャッシュは、崩れたバッチも保存してしまい、再配信のたびにそれを返します。2 回目と 3 回目の SQS の配信は、どちらも同じキーでのキャッシュヒットをログに出し、同じ ValueError で失敗しました。保存の前と読み出しのときに形を確かめれば、これは防げます。

2 つ目の落とし穴は、プロンプトを変えて出し直すことです。エラーをモデルに返すと、崩れた応答とエラーメッセージがリクエストに加わります。さらに Sonnet 5 は既定の 1.0 以外の temperature を受け付けないので、もう一度呼ぶたびに別の訳が返ります。

しかも、各バッチのプロンプトには、直前のバッチの訳文の末尾がコンテキストとして入っています。別のプロンプトで直したバッチは、次の配信では二度と組み立てられないキーで保存されるので、キャッシュに当たりません。そして新しく作り直した訳文が後ろのバッチのプロンプトを変え、キャッシュの外れが次々と後ろへ広がっていきます。

プロンプトを変えた再依頼は、後ろのバッチのキャッシュをすべて外す5 つのバッチが 2 段に並ぶ。1 回目の配信では、バッチ 0 を保存し、バッチ 1 が崩れて別のプロンプトで再依頼され再依頼のキーに保存され、バッチ 2 を保存し、バッチ 3 が Bedrock のエラーで失敗し、バッチ 4 には届かない。2 回目の配信では、バッチ 0 はキャッシュに当たり、バッチ 1 は元のキーを組んで外れて新しい訳を作り、バッチ 2 から 4 はコンテキストが変わって外れる。バッチ 0バッチ 1バッチ 2バッチ 3バッチ 41 回目の配信保存崩れて、別のプロンプトで再依頼再依頼のキーに保存保存Bedrock のエラー配信が終わる未到達2 回目の配信キャッシュに当たる元のキー外れて新しい訳コンテキストが変わり外れるコンテキストが変わり外れるコンテキストが変わり外れるSQS がジョブを再配信し、各バッチはプロンプトを組み直してキーを引く各プロンプトは直前のバッチの訳文を含むので、訳が 1 つ変わると後ろのキーがすべて変わる
プロンプトを変えて直したバッチは、次の配信が探さない場所に保存されます。新しく作った訳文が、同じ流れの後ろのバッチすべてのコンテキストを変えます。

5 バッチの模擬文書で、バッチ 1 を 1 回だけ崩し、1 回目の配信がバッチ 3 で落ちる場合を再現しました。すると 2 回目の配信は、バッチ 3 と 4 だけでなく、バッチ 1 から 4 までモデルを呼びました。

出し直しを Bedrock のアダプターの中に移すと、2 つの落とし穴がまとめて解消します。キャッシュのラッパーは、元のプロンプトでアダプターを 1 回呼びます。アダプターは形を確かめ、必要ならエラーを返して出し直し、正しい応答か崩れた応答のどちらかを返します。キャッシュが保存するのは正しい応答だけで、キーは元のキーです。

Bedrock アダプターが形を検証し、必要ならエラーをモデルに返して出し直す。応答キャッシュには形が正しい結果だけを保存するTranslator は応答キャッシュを呼ぶ。キャッシュのキーは元のプロンプトのハッシュで、形が正しい応答だけを保存する。その中に Bedrock アダプターがある。アダプターは Converse を呼んで形を確かめ、正しければキャッシュへ返す。不正なら tool_result のエラーとしてモデルに返し、1 回まで呼び直す。Translator応答キャッシュキー: 元のプロンプトのハッシュ形が正しい応答だけを保存するBedrock アダプターConverse の呼び出し形を確かめるtool_result のエラーとしてモデルに返す正しい応答正しい不正1 回までキャッシュへ返す
キャッシュから見えるのは、元のプロンプトでの 1 回の呼び出しだけです。形の確認とエラーの往復は、アダプターの中で行います。

まとめ

問題対処
Bedrock が Claude Sonnet 5 と Opus 4.6 より後のすべてのモデルで strict: true と output_config.format を拒否するBedrock 側ではまだ手がないので、自分のコードで検証する
強制 tool use で translated_text が text になる (40 回中 8 回)text の禁止を名指しするシステムプロンプトのルール: 40 回中 0 回
それでも崩れる呼び出し使うべきキーを添えて toolResult のエラーとして返す: 8 件中 8 件が直った
プロンプトをキーにした応答キャッシュ保存の前に検証し、出し直しはモデルのアダプターの中で行ってキーを元のままにする

Bedrock の構造化出力が使えるかどうかは提供先ごとに決まるので、モデルを選ぶ前にモデルカードで確かめるのが確実です。使えない場合でも、エラーをモデルに返すバリデーターを置けば、出し直しが要るバッチで 1 回呼び出しが増えるだけで、保証の大部分を取り戻せます。

参考リンク

この記事をシェア