Claude Media
BedrockのClaudeで構造化出力を使う — 3つの入口とスキーマ上限の違い

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.formatstrict tool useの置き場所tools[].strictスキーマの型JSONオブジェクト
ConverseJSON outputsの置き場所outputConfig.textFormatstrict tool useの置き場所toolSpec.strictスキーマの型JSON文字列
Messages API(bedrock-mantle)JSON outputsの置き場所output_config.formatstrict 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 APIBedrock
省略可能なプロパティの合計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のエンドポイントについての記載ではありません。

迷ったときの選び方

入口とモデルの選択は、次のように整理できます。

どの入口でも、移行前にlint_schemaを通し、初回のコンパイル待ちを織り込んだタイムアウトに直しておけば、Claude APIでは起きなかった詰まり方を避けられます。

まとめ

BedrockのClaudeは、InvokeModel・Converse・Messages APIのどの入口でも構造化出力に対応しています。違いは3つに絞れます。スキーマの渡し方が入口ごとに違うこと、Claude APIより上限が厳しいこと、初回に文法のコンパイル待ちが入ることです。13個以上の任意項目を持つスキーマと、additionalPropertiesの書き漏れを先に直すのが最短の準備になります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. platform.claude.com/docs/en/build-with-claude/structured-outputs
  2. platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock
  3. platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy
  4. docs.aws.amazon.com/bedrock/latest/userguide/structured-output.html
  5. aws.amazon.com/blogs/machine-learning/structured-outputs-for-anthropic-cl…
この記事を共有:XはてブLinkedIn