[アップデート] Amazon BedrockでStructured Outputsがサポートされました
はじめに
こんにちは、スーパーマーケットが大好きなコンサル部の神野です。
皆さん、LLMの出力をアプリケーションで扱うとき、JSONパースに失敗して困った経験はありませんか?プロンプトで「JSON形式で出力してください」とお願いしても、余計な説明文が付いてきたり、フォーマットが微妙に崩れていたり・・・
そこで、以前Strands AgentsでStructured Outputの機能を活用して構造化するといった記事を書きました。
今回アップデートでBedrock自体にもStructured Outputsがサポートされました。(こちらはoutputsと複数形なんですね・・・)
早速試してきましょう!!
Structured Outputs
改めてStructured Outputsについて説明です。
この機能はモデルの出力を事前に定義したJSONスキーマに準拠させる機能です。
公式ドキュメントを確認すると、以下のように記載があります。
Structured outputs is a capability on Amazon Bedrock that ensures model responses conform to user-defined JSON schemas and tool definitions, reducing the need for custom parsing and validation mechanisms in production AI deployments.
公式ドキュメントのメリットを翻訳すると以下の通りです。
- スキーマ準拠の保証:プロンプトベースのアプローチで発生するエラーやリトライループを排除
- 開発の複雑さを軽減:カスタムパース・バリデーションロジックが不要に
- 運用コストの削減:失敗リクエストとリトライの減少
- 本番環境の信頼性向上:予測可能で機械可読な出力を必要とするAIアプリケーションを自信を持ってデプロイ可能
出力を受け取った後にJSONパースしてバリデーションして、失敗したらリトライして・・・という処理を自前で実装する必要がありました。今回Structured Outputsを使えば、そのあたりもサポートされているのは嬉しいですね。
補足ですが、Anthropic APIでは以前からStructured Outputsに対応していました。今回のアップデートは、Amazon Bedrock経由でClaudeなどのモデルを利用する際にも、この機能が使えるようになったというものです。
Strands Agentsなどのフレームワークからは利用できていたので、今までできなかったんだ・・・といった気づきにもなりました。
2つの利用方式
Structured Outputsには2つの利用方式があります。
JSON Schema output format
モデルのレスポンス全体を指定したJSONスキーマに準拠させる方式です。データ抽出やレポート生成など、構造化されたデータを直接取得したい場合に適しています。
こちらはイメージしやすいですね。シンプルに指定した形式にレスポンスを構造化させる形です。
Strict Tool Use
ツール使用(Function Calling)でStructured Outputsを活用する方式です。
例えば、天気を取得するツールでunitに"celsius"か"fahrenheit"のみを受け付けたい場合を考えます。
{
"name": "get_weather",
"strict": True, # これを追加
"inputSchema": {
"json": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
...
}
}
}
strict: trueを付けることで、ツール呼び出しの引数が必ずスキーマに準拠することが保証されます。上記の例だとunitは必ず"celsius"か"fahrenheit"のどちらかになります。
2つの方式の違い
簡単にまとめると、JSON Schema output formatはモデルの出力そのものを構造化したい場合、Strict Tool Useはツール呼び出しの引数を構造化したい場合に使います。用途に応じて使い分けましょう。
対応モデルとAPI
執筆時点での対応モデルは以下の通りです。
Anthropic
Claude Haiku 4.5 (
anthropic.claude-haiku-4-5-20251001-v1:0)Claude Sonnet 4.5 (anthropic.claude-sonnet-4-5-20250929-v1:0)Claude Opus 4.5 (anthropic.claude-opus-4-5-20251101-v1:0)Qwen
Qwen3 235B A22B 2507 (
qwen.qwen3-235b-a22b-2507-v1:0)Qwen3 32B (dense) (qwen.qwen3-32b-v1:0)Qwen3-Coder-30B-A3B-Instruct (qwen.qwen3-coder-30b-a3b-v1:0)Qwen3 Coder 480B A35B Instruct (qwen.qwen3-coder-480b-a35b-v1:0)Qwen3 Next 80B A3B (qwen.qwen3-next-80b-a3b)Qwen3 VL 235B A22B (qwen.qwen3-vl-235b-a22b)OpenAI
gpt-oss-120b (
openai.gpt-oss-120b-1:0)gpt-oss-20b (openai.gpt-oss-20b-1:0)GPT OSS Safeguard 120B (openai.gpt-oss-safeguard-120b)GPT OSS Safeguard 20B (openai.gpt-oss-safeguard-20b)DeepSeek
DeepSeek-V3.1 (
deepseek.v3-v1:0)Gemma 3 12B IT (
google.gemma-3-12b-it)Gemma 3 27B PT (google.gemma-3-27b-it)MiniMax
MiniMax M2 (
minimax.minimax-m2)Mistral AI
Magistral Small 2509 (
mistral.magistral-small-2509)Ministral 3B (mistral.ministral-3-3b-instruct)Ministral 3 8B (mistral.ministral-3-8b-instruct)Ministral 14B 3.0 (mistral.ministral-3-14b-instruct)Mistral Large 3 (mistral.mistral-large-3-675b-instruct)Voxtral Mini 3B 2507 (mistral.voxtral-mini-3b-2507)Voxtral Small 24B 2507 (mistral.voxtral-small-24b-2507)Moonshot AI
Kimi K2 Thinking (
moonshot.kimi-k2-thinking)NVIDIA
NVIDIA Nemotron Nano 12B v2 VL BF16 (
nvidia.nemotron-nano-12b-v2)NVIDIA Nemotron Nano 9B v2 (nvidia.nemotron-nano-9b-v2)
対応APIは以下の通りです。
- Converse API / ConverseStream API
- InvokeModel API / InvokeModelWithResponseStream API
Cross-Region推論やBatch推論も対応しています。
前提
本記事の検証環境は以下の通りです。
- Python 3.12
- uv
- AWS CLI設定済み(Bedrockへのアクセス権限あり)
- 使用モデル:Claude Haiku 4.5(
us.anthropic.claude-haiku-4-5-20251001-v1:0)
セットアップ
プロジェクトのセットアップを行います。
uv init
依存ライブラリをインストールします。
uv add boto3
これで準備完了です!
実装
ここからは実際にStructured Outputsを使った実装を見ていきます。
2パターン実装して、それぞれ動作確認を後で行います。
JSON Schema output format パターン
まずは、JSON Schema output formatパターンを試してみます。
非構造化テキストからプロジェクト情報を抽出する処理を実装していきました。
"""
Amazon Bedrock Structured Outputs - JSON Schemaパターン
Converse APIを使用して、モデルの出力をJSONスキーマに準拠させる
"""
import boto3
import json
# Bedrock Runtimeクライアントの作成
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")
# 使用するモデルID(Claude Haiku 4.5)
MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
# サンプルの非構造化テキスト
SAMPLE_TEXT = """
昨日の会議で新しいプロジェクトについて話し合いました。
プロジェクト名は「Operation Phoenix」で、来月の15日から開始予定です。
担当者は田中さん、鈴木さん、佐藤さんの3名で、予算は500万円です。
主な目標はレガシーシステムの刷新で、期間は6ヶ月を見込んでいます。
"""
# 抽出したいデータ構造を定義するJSONスキーマ
json_schema = {
"type": "object",
"properties": {
"project_name": {
"type": "string",
"description": "プロジェクトの名称"
},
"start_date": {
"type": "string",
"description": "プロジェクトの開始予定日"
},
"team_members": {
"type": "array",
"items": {"type": "string"},
"description": "プロジェクトメンバーのリスト"
},
"budget": {
"type": "string",
"description": "プロジェクトの予算"
},
"objective": {
"type": "string",
"description": "プロジェクトの主な目標"
},
"duration": {
"type": "string",
"description": "プロジェクトの予定期間"
}
},
"required": ["project_name", "start_date", "team_members", "budget", "objective", "duration"],
"additionalProperties": False
}
def extract_structured_data():
"""非構造化テキストから構造化データを抽出する"""
response = bedrock_runtime.converse(
modelId=MODEL_ID,
messages=[
{
"role": "user",
"content": [
{
"text": f"以下のテキストから情報を抽出してください:\n\n{SAMPLE_TEXT}"
}
]
}
],
# Structured Outputs の設定
outputConfig={
"textFormat": {
"type": "json_schema",
"structure": {
"jsonSchema": {
"schema": json.dumps(json_schema),
"name": "project_extraction",
"description": "プロジェクト情報の抽出"
}
}
}
}
)
# レスポンスからテキストを取得
output_text = response["output"]["message"]["content"][0]["text"]
return json.loads(output_text)
if __name__ == "__main__":
print("=" * 60)
print("Amazon Bedrock Structured Outputs - JSON Schema パターン")
print("=" * 60)
print(f"\n使用モデル: {MODEL_ID}")
print(f"\n--- 入力テキスト ---\n{SAMPLE_TEXT}")
print("\n--- 抽出結果 ---")
result = extract_structured_data()
print(json.dumps(result, indent=2, ensure_ascii=False))
Converse APIのoutputConfig.textFormatでJSON Schema output formatを指定しています。スキーマはstructure.jsonSchema.schemaに文字列として渡す形式になっています。
requiredとadditionalProperties: falseも忘れずに指定しておきましょう。
受け取りたい構造のスキーマを指定して、リクエストを送るだけなのでシンプルですね。
Strict Tool Use パターン
次に、Strict Tool Useパターンを試してみます。
strict: trueありなしで結果に違いが出るか比較検証してみました。
"""
strict: true ありなしの比較検証
"""
import boto3
import json
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
def create_tool(strict: bool):
"""strictフラグを指定してツール定義を作成"""
tool = {
"toolSpec": {
"name": "get_weather",
"description": "指定した都市の現在の天気を取得する",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "天気を取得する都市名"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度の単位"
}
},
"required": ["location", "unit"],
"additionalProperties": False
}
}
}
}
if strict:
tool["toolSpec"]["strict"] = True
return tool
def call_weather_tool(strict: bool, user_message: str):
"""天気ツールを呼び出す"""
response = bedrock_runtime.converse(
modelId=MODEL_ID,
messages=[
{
"role": "user",
"content": [{"text": user_message}]
}
],
toolConfig={
"tools": [create_tool(strict)],
"toolChoice": {"tool": {"name": "get_weather"}}
}
)
return response["output"]["message"]["content"][0]["toolUse"]["input"]
if __name__ == "__main__":
print("=" * 60)
print("strict: true ありなしの比較")
print("=" * 60)
# enumにない表現を使ったリクエスト
test_messages = [
"東京の天気をCで教えて",
"パリの気温をケルビンで知りたい",
"ニューヨークの天気、普通の単位で",
]
for msg in test_messages:
print(f"\n【リクエスト】{msg}")
print("-" * 40)
print("strict: false(指定なし):")
try:
result = call_weather_tool(strict=False, user_message=msg)
print(f" unit = {result.get('unit')}")
except Exception as e:
print(f" エラー: {e}")
print("strict: true:")
try:
result = call_weather_tool(strict=True, user_message=msg)
print(f" unit = {result.get('unit')}")
except Exception as e:
print(f" エラー: {e}")
unitにenum制約("celsius"か"fahrenheit"のみ)を設定しています。
strict: Trueを付けることで、必ずこのどちらかの値が返ることが保証されます。
動作確認
実際にスクリプトを実行してみます。
JSON Schema output format パターン
uv run structured_output_json_schema.py
============================================================
Amazon Bedrock Structured Outputs - JSON Schema パターン
============================================================
使用モデル: us.anthropic.claude-haiku-4-5-20251001-v1:0
--- 入力テキスト ---
昨日の会議で新しいプロジェクトについて話し合いました。
プロジェクト名は「Operation Phoenix」で、来月の15日から開始予定です。
担当者は田中さん、鈴木さん、佐藤さんの3名で、予算は500万円です。
主な目標はレガシーシステムの刷新で、期間は6ヶ月を見込んでいます。
--- 抽出結果 ---
{
"project_name": "Operation Phoenix",
"start_date": "来月の15日",
"team_members": [
"田中さん",
"鈴木さん",
"佐藤さん"
],
"budget": "500万円",
"objective": "レガシーシステムの刷新",
"duration": "6ヶ月"
}
おおお、非構造化テキストから情報がきれいに抽出されていますね!!
JSON形式で、定義したスキーマ通りに出力されていることが確認できました。
Strict Tool Use パターン
uv run strict_comparison.py
============================================================
strict: true ありなしの比較
============================================================
【リクエスト】東京の天気をCで教えて
----------------------------------------
strict: false(指定なし):
unit = celsius
strict: true:
unit = celsius
【リクエスト】パリの気温をケルビンで知りたい
----------------------------------------
strict: false(指定なし):
unit = celsius
strict: true:
unit = celsius
【リクエスト】ニューヨークの天気、普通の単位で
----------------------------------------
strict: false(指定なし):
unit = celsius
strict: true:
unit = fahrenheit
結果として、ほとんどのケースで違いは出ませんでした。モデルが賢いのでstrict: trueがなくてもスキーマに従おうとしてくれます。ニューヨークだけ違いが出ていて面白いですね。ニューヨークの普通 = fahrenheitと解釈してくれた感じですかね。
たまたま当てはまったではなく必ず従う保証が嬉しいのかなと思うので、スキーマ準拠が保証されることで、バリデーションロジックやリトライ処理を省略でき、シンプルで検証プロセスを排除できるのは良いことかと思います。この辺りは複雑なスキーマでも検証したいですね。
補足
additionalPropertiesの明示的な指定が必要
additionalPropertiesはJSON Schemaのプロパティで、定義されていない追加のプロパティを許可するかどうかを制御します。falseに設定すると、スキーマで定義したプロパティ以外は許可されなくなります。
このadditionalPropertiesを指定しないと以下のようなエラーが発生しました。
ValidationException: An error occurred (ValidationException) when calling the Converse operation:
The model returned the following errors: tools.0.custom: For 'object' type, 'additionalProperties' must be explicitly set to false
additionalPropertiesの明示的な指定が必須となっています。忘れずに設定しましょう。
APIやプロバイダーによるプロパティの違い
本記事ではConverse APIを使用しましたが、InvokeModel APIを使う場合やモデルプロバイダーによってプロパティ名が異なります。
JSON Schema output formatの場合
| API | プロパティ |
|---|---|
| Converse API | outputConfig.textFormat |
| InvokeModel(Anthropic Claude) | output_config.format |
| InvokeModel(Open-weightモデル) | response_format |
Strict Tool Useの場合
| API | プロパティ |
|---|---|
| Converse API | toolSpec.strict |
| InvokeModel(Anthropic Claude) | tools[].strict |
| InvokeModel(Open-weightモデル) | tools[].function.strict |
微妙に違うんですね。この辺りはそれぞれのモデルを使う際に注意したいですね。
詳細は公式ドキュメントにもあるので、ぜひこちらもご参照ください。
初回リクエストは少し遅くなることがある
Structured Outputsは、初回だけ内部的にスキーマの検証処理が走る関係で、レスポンスが少し遅く感じることがありました。ただ、同じスキーマを使い回すケースでは24時間キャッシュされる挙動もあるので、2回目以降のリクエストでは1回目よりも体験早く感じました。
おわりに
BedrockのAPIでも簡単に構造化出力できるようになって便利ですね!!
フォーマットを指定して推論させたいケースはぜひ活用していきましょう!
本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!







