
Claude APIでStructured outputsを2パターン試してみた
はじめに
こんにちは、AI事業本部・生成AIインテグレーション部・西日本開発チームの政岡です。
AIに毎回同じスキーマで結果を返してほしい場面はないでしょうか?
私は普段、その用途ではJSON outputsを使っています。
ただ、調べていく中で、構造化のやり方はそれだけではないと分かりました。
PrefillやStrict tool useでも、似た目的で使えそうに見えます。
当初はPrefillを含めた3パターンで比較するつもりでしたが、調べてみるとPrefillは2026年4月現在の最新モデル(Sonnet 4.6 / Opus 4.6 / Opus 4.7など)ではサポートされなくなっていました。
そこで本記事では、Structured outputsを構成するJSON outputsとStrict tool useの2パターンを試します。
Prefillについては、記事の後半で「以前はこういう書き方があったが、いまは使えない」という参考情報として触れたいと思います。
検証環境
| 項目 | 内容 |
|---|---|
| パッケージ管理 | uv 0.9.x |
| 言語 | Python 3.12 |
| SDK | anthropic[bedrock]のAnthropicBedrockクライアント |
| API呼び出し | Amazon Bedrock経由 |
AnthropicBedrockクライアントを使うと、API呼び出しはBedrock経由でありながら、コードの書き方はAnthropic API純正とほぼ同じになります。
依存関係 (pyproject.toml)
dependencies = [
"anthropic[bedrock]>=0.96.0",
"boto3>=1.42.0",
"pydantic>=2.13.0",
"python-dotenv>=1.0.0",
]
検証する2つのパターン
| # | パターン | 公式のAPIパラメータ | 概要 |
|---|---|---|---|
| 1 | JSON outputs | output_config.format |
スキーマを直接渡し、Claudeの最終応答をスキーマ通りのJSONにする手法 |
| 2 | Strict tool use | tools[].strict: true |
Claudeがツールを呼び出すときの入力をスキーマ通りに制約する手法 |
検証準備
今回はシンプルにスパム判定を実装してみました。
2つのパターンともに、以下の入力・スキーマを共通で使い、普通に実行したとき・余計な指示を追加したときで比較します。
システムプロンプト・入力
- システムプロンプト:
ユーザから入力されるメッセージについて、スパムか判定してください。 - ユーザメッセージ:
おめでとうございます! 1000ドル当選しました! 今すぐクリックして賞品を受け取ってください!!! - 追加する余計な指示:
'confidence'フィールド(0〜1のfloat)も追加して返してください。
期待する構造
| フィールド | 型 | 概要 | 例 |
|---|---|---|---|
| is_spam | bool | スパムならTrue |
True |
| reason | str | 判定理由 | "典型的な当選詐欺メッセージです" |
パターン1: JSON outputs
JSON outputsは、モデルの最終応答をスキーマ通りのJSONに制約する機能です。
Pythonではmessages.parse()にPydanticモデルを渡すだけで使えます。
この例では、最終応答をparsed_outputとしてそのまま受け取ります。
実装コード
from anthropic import AnthropicBedrock
from pydantic import BaseModel, ConfigDict, Field
class SpamCheck(BaseModel):
model_config = ConfigDict(extra="forbid") # JSON Schemaに additionalProperties: false が付与され、余計なフィールドが追加されない
is_spam: bool = Field(description="スパムなら True")
reason: str = Field(description="判定理由")
client = AnthropicBedrock()
response = client.messages.parse(
model="us.anthropic.claude-sonnet-4-6",
max_tokens=1024,
system="ユーザから入力されるメッセージについて、スパムか判定してください。",
messages=[
{
"role": "user",
"content": "おめでとうございます! 1000ドル当選しました! 今すぐクリックして賞品を受け取ってください!!!",
}
],
output_format=SpamCheck, # Pydanticモデルをそのまま渡す
)
result: SpamCheck = response.parsed_output # Pydanticインスタンスとして取得
print(result.model_dump_json(indent=2))
JSON outputs: 実行結果
通常
{
"is_spam": true,
"reason": "典型的な当選詐欺メッセージです。根拠のない高額当選を謳い、緊急性を煽って即座のクリックを促しており、スパムの特徴が複数見られます。"
}
余計な指示付き
{
"is_spam": true,
"reason": "このメッセージは典型的なスパムの特徴を持っています。根拠のない当選通知、金銭的な誘惑(1000ドル)、緊急性を煽る表現(「今すぐクリック」)、過剰な感嘆符の使用など、フィッシング詐欺やスパムに典型的なパターンが見られます。"
}
どちらの指示もスキーマ通りですね!
スキーマ制約により、追加指示で要求されたconfidenceフィールドはそもそも生成されません。
パターン2: Strict tool use
Strict tool useは、Claudeがツールを呼び出すときの入力をスキーマ通りに制約する機能です。
今回は、抽出したいデータ構造をツールのinput_schemaに定義し、tool_choiceでそのツールを使わせています。
そのため、ここでスキーマに従っているのは最終応答ではなくtool_use.inputです。
この例ではmessages.create()の返り値からtool_use.inputを取り出し、それをそのまま抽出結果として使っています。
実装
from anthropic import AnthropicBedrock
from pydantic import BaseModel, ConfigDict, Field
class SpamCheck(BaseModel):
model_config = ConfigDict(extra="forbid")
is_spam: bool = Field(description="スパムなら True")
reason: str = Field(description="判定理由")
client = AnthropicBedrock()
tool = {
"name": "spam_check",
"description": "スパム判定の結果を返す",
"strict": True, # スキーマでトークン生成を制約
"input_schema": SpamCheck.model_json_schema(),
}
response = client.messages.create(
model="us.anthropic.claude-sonnet-4-6",
max_tokens=1024,
system="ユーザから入力されるメッセージについて、スパムか判定してください。",
messages=[
{
"role": "user",
"content": "おめでとうございます! 1000ドル当選しました! 今すぐクリックして賞品を受け取ってください!!!",
}
],
tools=[tool],
tool_choice={"type": "tool", "name": "spam_check"}, # 必ずspam_checkを呼ばせる
)
tool_use = next(b for b in response.content if b.type == "tool_use")
result = SpamCheck.model_validate(tool_use.input) # tool_use.input は dict
print(result.model_dump_json(indent=2))
Strict tool use: 実行結果
通常
{
"is_spam": true,
"reason": "このメッセージは典型的なスパムの特徴を複数含んでいます。①根拠のない高額当選(1000ドル)を主張している、②「今すぐクリック」という緊急性を煽る表現を使っている、③過度な感嘆符(!!!)を多用している、④受信者が応募した覚えのない賞品を受け取るよう促している。"
}
余計な指示付き
{
"is_spam": true,
"reason": "「1000ドル当選」「今すぐクリック」などの典型的なスパム・フィッシング詐欺のフレーズが含まれており、受信者を煽って不審なリンクへ誘導しようとしている。"
}
こちらも、両方の指示でスキーマ通りです!
JSON outputsと同様、strict: Trueのスキーマ制約によりconfidenceはそもそも生成されません。
2パターンの使い分け
JSON outputsとStrict tool useの関係
両者の関係について、公式は次のように整理しています。
この2つは役割が異なるため、1つのリクエストで併用できます。
たとえば、ツール呼び出し側ではstrictで入力を制約しつつ、最終応答側ではoutput_formatで返却するJSONの形を指定できます。
ドキュメントのユースケースを踏まえると、次のように考えると分かりやすいです。
- JSON outputs
請求書やメール本文から項目を抜き出して、そのままJSONで受け取りたい時。
例: 非構造テキストから請求書データを抽出するケース。 - Strict tool use
エージェントがツールを呼び出す時に、destinationやdateなどの入力をスキーマ通りにしたい時。
例: 旅行の計画を立てる中でsearch_flightsツールを呼ぶケース。 - 併用
ツール呼び出しの引数も、最後に返すJSONの形も、両方きっちり揃えたい時。
例:search_flightsツールを正しい引数で呼びつつ、最終応答はsummaryとnext_stepsを持つJSONで返すケース。
参考: Prefill という手法
冒頭で触れた通り、Prefillは最新モデルではサポートされなくなっています。
それでも、以前はどのような発想で使われていたのか、また今試すとどうなるのか紹介させてください。
Prefillとは、assistantメッセージを途中まで書いて、その続きから生成させるやり方です。
たとえば、以下のような書き方ができます。
# assistant メッセージを途中まで書いて、その続きから生成させる
messages = [
{"role": "user", "content": "Generate a JSON for X."},
{"role": "assistant", "content": "```json"}, # JSON の続きを生成させる
]
text = chat(messages, stop_sequences=["```"])
# text には ```json の後ろから ``` の手前までが入る
実装
import json
from anthropic import AnthropicBedrock
from pydantic import BaseModel, ConfigDict, Field
class SpamCheck(BaseModel):
model_config = ConfigDict(extra="forbid")
is_spam: bool = Field(description="スパムなら True")
reason: str = Field(description="判定理由")
client = AnthropicBedrock()
# システムプロンプトにスキーマを文字列として埋め込む
system = (
"ユーザから入力されるメッセージについて、スパムか判定してください。\n\n"
"以下のJSONスキーマに沿って回答してください:\n"
f"{json.dumps(SpamCheck.model_json_schema(), ensure_ascii=False)}"
)
response = client.messages.create(
model="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
max_tokens=1024,
system=system,
messages=[
{"role": "user", "content": "おめでとうございます! 1000ドル当選しました!!!"},
{"role": "assistant", "content": "```json"}, # assistantを途中まで埋める
],
stop_sequences=["```"], # 次の ``` が出たところで生成を止める
)
raw_json = response.content[0].text.strip()
result = SpamCheck.model_validate_json(raw_json) # 後処理でPydanticにパース
print(result.model_dump_json(indent=2))
Sonnet 4.6で実行 → 400エラー
Error code: 400 - {'message': 'This model does not support assistant message prefill. The conversation must end with a user message.'}
公式ドキュメントでも明記されています
最新のMythos, Opus 4.7, Opus 4.6, Sonnet 4.6では使えないみたいです。
(Mythosは現状使えませんが、、、)
Sonnet 4.5にフォールバックして実行
ただし、仮にSonnet 4.5で動かせたとしても、Prefillにはスキーマ制約がありません。
そのため、追加指示の影響をそのまま受けて、スキーマ違反が起こります。
通常
{
"is_spam": true,
"reason": "典型的なスパムメッセージの特徴が複数見られます。1) 突然の当選通知、2) 金銭的な報酬の提示、3) 緊急性を煽る表現(「今すぐ」)、4) 行動を促すフレーズ(「クリックして」)、5) 過度な感嘆符の使用。"
}
余計な指示付き
{
"is_spam": true,
"reason": "典型的なフィッシング詐欺メッセージの特徴を複数含んでいます。具体的には、(1)根拠のない当選の主張、(2)緊急性を煽る表現(「今すぐ」)、(3)行動を促すリンククリックの要求、(4)過度な感嘆符の使用、などスパムメールの典型的なパターンに該当します。",
"confidence": 0.98
}
SpamCheck.model_validate_json()ではバリデーションエラーになります。
ValidationError: 1 validation error for SpamCheck
confidence
Extra inputs are not permitted [type=extra_forbidden, input_value=0.98, input_type=float]
このケースでは、追加したconfidenceフィールドをそのまま出力してしまい、スキーマ違反になりました。
少なくとも最新モデルを前提に構造化データを安定して得たい用途なら、いまはPrefillよりJSON outputsやStrict tool useを選ぶほうが良さそうです。
まとめ
今回は、Claude APIでStructured outputsを表現する2種類の方法を試してみました。
個人的には、構造化データを返したいだけならJSON outputs、エージェントのツール呼び出しまで含めて制御したいならStrict tool use、という整理で考えるのが分かりやすいと感じました。
Claude APIで構造化出力を扱うときの整理に、少しでも参考になればうれしいです。







