BedrockのClaudeで構造化出力を使う — 3つの入口とスキーマ上限の違い
Amazon BedrockのClaudeで使える構造化出力を、InvokeModel・Converse・Messages APIの3入口に分けて書き方を示し、Claude APIと違うスキーマ上限や初回遅延を示します。
BedrockのClaudeで構造化出力を使う — 3つの入口とスキーマ上限の違い
Amazon BedrockのClaudeでも、構造化出力(JSON outputsとstrict tool use)は使えます。JSON Schemaに沿った応答を返させるoutput_config.formatと、ツール入力を検証するstrict: trueの両方が対象です。ただしBedrockには入口が3つあり、スキーマの渡し方がそれぞれ違います。しかもClaude APIより上限が厳しい項目があり、そのまま持ち込むと400になることがあります。
Bedrockでは構造化出力を使えるのか
使えます。対応プラットフォームはClaude API、Claude Platform on AWS、Amazon Bedrock、Google Cloud、Microsoft Foundryで、Amazon Bedrockは一般提供(ga)の扱いです。
モデルの範囲は、AWSの機械学習ブログが最も広く書いています。構造化出力はAnthropicのすべてのClaudeモデルで、bedrock-runtimeとbedrock-mantleの両エンドポイントから使えます。条件は、そのモデルがそのエンドポイントで提供されていることです。
注意点が1つあります。地域をまたぐ推論プロファイルの一部は、モデルによっては構造化出力に対応しません。例はClaude Haiku 4.5をインド向け(in.anthropic)プロファイルで呼ぶ場合で、対応しません。モデル単体の対応表だけで判断せず、使うプロファイルのモデルカードも見ておく必要があります。
新しい入口(Claude in Amazon Bedrock)と従来の入口(InvokeModelとConverse)のどちらの機能一覧にも構造化出力が載っているので、新旧どちらの入口でも使える前提で構いません。
3つの入口でスキーマの渡し方が変わる
Bedrockの構造化出力は、呼び出しAPIによって置く場所と型が変わります。
| 入口 | JSON outputsの置き場所 | strict tool useの置き場所 | スキーマの型 |
|---|---|---|---|
| InvokeModel(Claude) | JSON outputsの置き場所output_config.format | strict tool useの置き場所tools[].strict | スキーマの型JSONオブジェクト |
| Converse | JSON outputsの置き場所outputConfig.textFormat | strict tool useの置き場所toolSpec.strict | スキーマの型JSON文字列 |
Messages API(bedrock-mantle) | JSON outputsの置き場所output_config.format | strict tool useの置き場所tools[].strict | スキーマの型JSONオブジェクト |
Converseだけがスキーマを文字列で受けます。ここでjson.dumpsを忘れると、型エラーで弾かれます。InvokeModelとMessages APIはClaude APIの形に近く、スキーマはそのままオブジェクトで渡します。
AWSのドキュメントには、古いoutput_formatフィールドは非推奨でoutput_config.formatを使うとあります。Claude APIの側も同じ移行を案内しています。ベータヘッダーに頼った古いサンプルを見つけたら、output_configに直してから動かすのが安全です。
InvokeModelで書く
InvokeModelはAnthropic形式のリクエスト本文を受けます。サンプルをもとにした例です。
import json
import boto3
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan_interest": {"type": "string"},
"demo_requested": {"type": "boolean"},
},
"required": ["name", "email", "plan_interest", "demo_requested"],
"additionalProperties": False,
}
response = bedrock.invoke_model(
modelId="global.anthropic.claude-sonnet-5-5",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 512,
"messages": [{"role": "user", "content": "問い合わせメール本文…"}],
"output_config": {"format": {"type": "json_schema", "schema": schema}},
}),
)
body = json.loads(response["body"].read())
if body["stop_reason"] == "end_turn":
text = next(b["text"] for b in body["content"] if b["type"] == "text")
result = json.loads(text)modelIdはglobal.anthropic.claude-sonnet-5-5の形です。リージョンごとに使えるモデルとプロファイルの種類は、Bedrock東京リージョンのClaudeモデル一覧で選べます。
stop_reasonを見てからパースしているのには理由があります。拒否やmax_tokensで止まった応答は、スキーマに合わない可能性があるためです。
Converseで書く
ConverseではoutputConfig.textFormatの下に、jsonSchemaとしてスキーマ文字列と名前を入れます。
response = bedrock.converse(
modelId="global.anthropic.claude-sonnet-5-5",
messages=[{"role": "user", "content": [{"text": "問い合わせメール本文…"}]}],
outputConfig={
"textFormat": {
"type": "json_schema",
"structure": {
"jsonSchema": {
"name": "lead_extraction",
"description": "問い合わせメールから営業情報を抜き出す",
"schema": json.dumps(schema),
}
},
}
},
)schemaがjson.dumps(schema)の文字列になっている点が、InvokeModelとの違いです。
Messages API(bedrock-mantle)で書く
新しい入口はhttps://bedrock-mantle.{region}.api.aws/anthropic/v1/messagesを呼ぶ形です。Anthropic SDKのAnthropicBedrockMantleクライアントで呼べ、モデルIDはanthropic.接頭辞だけのanthropic.claude-sonnet-5のような形になっています。構造化出力の指定はClaude APIと同じoutput_config.formatで、リクエストの形を変える必要がありません。
strict tool useはツール定義に1行足す
ツール入力を検証したいときは、ツール定義にstrict: trueを足します。InvokeModelとMessages APIではtools[]の要素に、ConverseではtoolSpecの中に置きます。
tool = {
"name": "get_weather",
"description": "指定した場所の天気を返す",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location", "unit"],
"additionalProperties": False,
},
}JSON outputsとstrict tool useは、独立して使えるほか、同じリクエストで併用できます。strict: trueは入力がスキーマに合うことを保証しますが、保証しない範囲もあります(strict tool useの保証範囲)。
Claude APIとBedrockで違うスキーマ上限とキャッシュ
Claude APIで動いていたスキーマをBedrockへ移すとき、数字が食い違う項目があります。400になるのは「省略可能なプロパティが12個を超える」ときと「additionalPropertiesの書き漏れ」の2つだけで、残りは運用上の差です。
| 項目 | Claude API | Bedrock |
|---|---|---|
| 省略可能なプロパティの合計 | Claude API24個 | Bedrock12個 |
| 文法キャッシュの保持 | Claude API最後の利用から24時間 | Bedrock最初のアクセスから24時間 |
| 初回コンパイルのタイムアウト/所要時間 | Claude APIタイムアウトは180秒 | Bedrock所要時間は最大で約90秒 |
| クライアントの読み取りタイムアウト | Claude API記載なし | Bedrock120秒以上を推奨 |
union型(anyOf・型配列)のプロパティは、どちらも16個までで差はありません。コンパイルの行は比べている量が違い、Claude APIの値は打ち切りまでの時間、Bedrockの値は実際にかかる時間の目安です。
最も踏みやすいのは省略可能なプロパティの数です。Claude APIでは24個まで通っていたスキーマが、Bedrockでは13個目でSchema is too complexになります。しかもBedrockは文法のコンパイル中にこの上限を検査するため、エラーが返るまでに約90秒かかることがあります。
落とし穴
additionalPropertiesの書き忘れは400になる
Bedrockの構造化出力では、スキーマ内のすべてのオブジェクトでadditionalPropertiesを明示的にfalseにする必要があります。Claude APIでも同じ要件です。省略した場合も、false以外の値の場合も、400のValidationExceptionが返ります。ネストしたオブジェクトや$defsの中身も対象です。
13個以上の項目を扱いたいとき
省略可能なプロパティが12個を超えるなら、項目をrequiredに入れたうえでnull許容の型配列にします。
{ "memo": { "type": ["string", "null"] } }モデルは埋められない項目にnullを返します。ただし型配列はunion型として数えられるため、この方法で扱えるのは最大16個です。13〜16個ならこの置き換えで収まり、それ以上ならスキーマを分割するほうが現実的です。
送信前にスキーマを数える
上限は、送ってから90秒待って初めて分かるのでは遅すぎます。次の関数で、additionalPropertiesの漏れ、省略可能なプロパティ数、union型の数を手元で数えられます。
def lint_schema(schema, optional_limit=12, union_limit=16):
problems, optional, union = [], 0, 0
def walk(node, path):
nonlocal optional, union
if not isinstance(node, dict):
return
if node.get("type") == "object" or "properties" in node:
if node.get("additionalProperties") is not False:
problems.append(f"{path}: additionalProperties を false にする")
required = set(node.get("required", []))
for name, sub in node.get("properties", {}).items():
if name not in required:
optional += 1
if isinstance(sub, dict) and (
isinstance(sub.get("type"), list) or "anyOf" in sub
):
union += 1
walk(sub, f"{path}.{name}")
if "items" in node:
walk(node["items"], f"{path}[]")
for key in ("$defs", "definitions"):
for name, sub in node.get(key, {}).items():
walk(sub, f"{path}#{name}")
for sub in node.get("anyOf", []) + node.get("allOf", []):
walk(sub, path)
walk(schema, "$")
if optional > optional_limit:
problems.append(f"省略可能なプロパティが {optional} 個(上限 {optional_limit})")
if union > union_limit:
problems.append(f"union 型のプロパティが {union} 個(上限 {union_limit})")
return problemsルートとaddressオブジェクトのどちらにもadditionalPropertiesを書いていないスキーマ(addressはstreetとcityを持つ)を渡すと、次の2件が返ります。
[
"$: additionalProperties を false にする",
"$.address: additionalProperties を false にする"
]オブジェクト値のプロパティも、requiredになければ省略可能として数えられます。上の関数も、requiredにない名前をすべて数えています。
スキーマに書けない制約
Bedrockが受け付けるのはJSON Schema Draft 2020-12の部分集合です。非対応は次のとおりです。
- 再帰的なスキーマ
- 外部の
$ref(内部参照だけ可) minimum・maximum・multipleOfなどの数値制約minLength・maxLengthなどの文字列制約minItemsの0と1以外の値
patternの正規表現は、先読みなどのlookaroundと後方参照を除いた部分集合です。無効な式は「Invalid regex in pattern field.」の400になります。defaultは受け付けられますが強制されません。モデルへのヒントにとどまり、値は保証されないため、defaultを持つプロパティも省略可能として数えられます。
範囲の制約をスキーマで表せないぶん、値の範囲チェックはアプリ側に残ります。enumの大文字小文字も保証されません。照合は大文字小文字を区別せずに行うのが安全です。
初回リクエストの遅延とタイムアウト
新しいスキーマの初回リクエストでは、Bedrockが文法をコンパイルします。コンパイルには最大で約90秒かかるため、クライアントの読み取りタイムアウトは120秒以上にします。boto3の既定のままだと、初回だけ切れる構成になりかねません。
from botocore.config import Config
bedrock = boto3.client(
"bedrock-runtime",
region_name="us-east-1",
config=Config(read_timeout=120),
)コンパイル済みの文法はアカウント単位で24時間キャッシュされ、AWS管理キーで暗号化されます。スキーマの構造を変えるとキャッシュは無効になりますが、nameやdescriptionだけの変更なら残ります。デプロイ直後に最初の1件を流して温めておけば、利用者が初回の待ち時間に当たらずに済みます。
併用できない機能と止まり方
使えない組み合わせは次のとおりです。
- Citations: 有効にすると400が返ります。Bedrockでも、Claude APIと同じです。理由と回避の構成はCitationsとStructured outputsの併用不可にあります
- メッセージのprefilling: JSON outputsとは併用できません
構造化出力を指定しても、スキーマ通りにならない場面は残ります。拒否(stop_reason: "refusal")では200が返り、出力はスキーマに合わないことがあります。max_tokensに達した応答は途中で切れたJSONになります。応答トークンの上限は、完全なJSONが収まる大きさにしておきます。
バッチ推論と、地域をまたぐ推論(クロスリージョン推論)は、追加設定なしで構造化出力が使えます。ただしクロスリージョンはbedrock-runtime側の話で、bedrock-mantleのエンドポイントについての記載ではありません。
迷ったときの選び方
入口とモデルの選択は、次のように整理できます。
- 既存のboto3コードがInvokeModelなら、
output_config.formatを足す。スキーマはオブジェクトのまま渡す - Converse中心のコードなら、
outputConfig.textFormatにする。スキーマは文字列にする - Claude APIのコードをなるべく変えずに移したいなら、
bedrock-mantleのMessages APIが近い(BedrockとGoogle Cloudの機能比較で入口ごとの違いを確かめられる) - AWS上でBedrock以外の経路も候補なら、Claude Platform on AWSへの移行が判断材料になる
- スキーマを型定義から生成するならStructured outputs SDKの型定義比較、制限の全体像ならStructured outputsのJSON Schema制限を見る
どの入口でも、移行前にlint_schemaを通し、初回のコンパイル待ちを織り込んだタイムアウトに直しておけば、Claude APIでは起きなかった詰まり方を避けられます。
まとめ
BedrockのClaudeは、InvokeModel・Converse・Messages APIのどの入口でも構造化出力に対応しています。違いは3つに絞れます。スキーマの渡し方が入口ごとに違うこと、Claude APIより上限が厳しいこと、初回に文法のコンパイル待ちが入ることです。13個以上の任意項目を持つスキーマと、additionalPropertiesの書き漏れを先に直すのが最短の準備になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- platform.claude.com/docs/en/build-with-claude/structured-outputs
- platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock
- platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy
- docs.aws.amazon.com/bedrock/latest/userguide/structured-output.html
- aws.amazon.com/blogs/machine-learning/structured-outputs-for-anthropic-cl…