はじめに
GMOコネクトの永田です。
同じ長い資料を渡して、出力する JSON の形だけを変えながら Claude を何回か呼ぶ、というようなアプリを開発していました。プロンプトの前半(資料の部分)はどの呼び出しでも同じなので、プロンプトキャッシュが効いて、費用も待ち時間も減るだろうと思っていました。
ところが、構造化出力(JSON Schema で出力の形を指定する機能)で受け取っていると、スキーマを変えた呼び出しではキャッシュが効きませんでした😇
調べると、スキーマはプロンプトの前のほうに足されていました。スキーマを本文(依頼文の末尾)に自分で書く形に変えると、4つのスキーマで1つのキャッシュを共有できました。
この記事は、Amazon Bedrock(以下 Bedrock)の Converse API で、スキーマの2つの渡し方を比べ、トークン数・費用・速さを測った記録です。
先にまとめ
Claude Sonnet 5.5 に、同じ資料(約2万トークン)とスキーマの違う依頼を4回送ったときの比較です。
| 観点 | 構造化出力 | スキーマを本文に書く |
|---|---|---|
| キャッシュの共有 | 同じスキーマ同士だけ。4種なら4回書き込み | 4種で共有。書き込みは同時に始めた2本の分だけ |
| 4回の呼び出し(2本ずつ並べる。各3回の平均) | 78.7秒・0.388ドル | 54.4秒・0.280ドル |
| 答えを書く速さ(キャッシュなし、各3回の平均) | 124.5 トークン/秒 | 183.5 トークン/秒 |
出力をそのまま json.loads で読めた回数 |
15回中15回 | 27回中22回。残り5回もコードフェンスを外す処理で読めた |
| スキーマから外れた回数 | 15回中0回。API が形を守らせる | 27回中0回。守らせる仕組みは無い |
- 構造化出力では、スキーマから作った部分(392〜638トークン。本文に書いた同じスキーマの約2倍)が
cachePointより前に入り、スキーマごとに別のキャッシュになります。Sonnet 5.5・Haiku 5.5・Opus 5.5 で同じでした - スキーマを本文に書くと、4回の呼び出しで費用が約28%、時間が約31%減りました。費用の差は、ほぼキャッシュの書き込みの差です
- 同時に始めた呼び出しは、どちらも書き込みになりました。1本目の開始から8秒後に始めると、1本目がまだ書いている途中でも読み出せました
- 本文に書く形では、コードフェンスを外す処理と、スキーマでの検査を自分で持ちます
1. 何をどう測ったか
Converse API では、messages などの中に cachePoint を置くと、そこまでの部分がキャッシュされます(プロンプトキャッシュ。日本語の文書では「キャッシュチェックポイント」)。共通の資料の直後に cachePoint を置き、スキーマの渡し方だけを変えました。
def build(task: str, schema: dict, structured: bool) -> dict:
req = {
"modelId": "global.anthropic.claude-sonnet-5-5",
"system": [{"text": INSTRUCTIONS}],
}
if structured:
# 構造化出力: outputConfig でスキーマを渡す
req["outputConfig"] = {"textFormat": {
"type": "json_schema",
"structure": {"jsonSchema": {"schema": json.dumps(schema, ensure_ascii=False), "name": "report"}},
}}
else:
# 本文に書く: cachePoint より後ろの依頼文にスキーマを足す
task += ("\n\n# 出力の形式\n\n次の JSON Schema に沿った JSON オブジェクトだけを出力してください。\n\n"
+ json.dumps(schema, ensure_ascii=False))
req["messages"] = [{"role": "user", "content": [
{"text": REPORTS}, # 共通の資料(約2万トークン)
{"cachePoint": {"type": "default"}},
{"text": task}, # 呼び出しごとの依頼
]}]
return req
| 項目 | 内容 |
|---|---|
| 観測日 | 2026年10月16日 |
| 呼んだ場所 |
us-east-1 の Converse。速さだけ ConverseStream |
| モデル |
global.anthropic.claude-sonnet-5-5。スキーマとキャッシュの関係(2章)だけ、global.anthropic.claude-haiku-5-5 と global.anthropic.claude-opus-5-5 でも1回ずつ |
| 共通の資料 | 架空のカフェチェーン20店舗・14日分の日報280行(22,340文字)。乱数で作った |
| スキーマ | A まとめ、B 店舗ごとの集計(20件の配列)、C 出来事の分類、D 改善の提案。C・D は enum あり。どれも additionalProperties: false
|
| キャッシュの分け方 | 場面ごとにシステムプロンプトの先頭に違う目印を付け、前の場面のキャッシュを読まないようにした。TTL は既定の5分 |
| effort | 2章・4章は low(出力は64トークンで打ち切り。4章の1本目だけ3,000トークンまで)。3章・5章は medium |
| 単価(Sonnet 5.5、Global) | 100万トークンあたり、入力 2.00、キャッシュの書き込み(5分)2.50、読み出し 0.10、出力 10.00 ドル(Price List API) |
トークン数は、Converse の応答の usage で見ました。inputTokens はキャッシュ外の入力だけで、キャッシュの分は cacheWriteInputTokens(書き込み)と cacheReadInputTokens(読み出し)に分かれて返ります。
2. 構造化出力では、スキーマが cachePoint より前に入る
A → A → B → C → D の順に、1本ずつ呼びました(Sonnet 5.5 の1回目)。
| 渡し方 | A | A(2回目) | B | C | D |
|---|---|---|---|---|---|
| 構造化出力 | 書き込み 20,986 | 読み出し 20,986 | 書き込み 21,232 | 書き込み 21,139 | 書き込み 21,051 |
| 本文に書く | 書き込み 20,594 | 読み出し 20,594 | 読み出し 20,594 | 読み出し 20,594 | 読み出し 20,594 |
構造化出力では、同じスキーマなら読み出せ、スキーマが違うとそのたびに書き込みになりました。書き込みの大きさもスキーマごとに違います。Sonnet 5.5 の2回目、Haiku 5.5、Opus 5.5 でも、読み出しと書き込みの並びは同じでした。
構造化出力の書き込みから、本文に書く形の書き込み(20,594)を引くと、cachePoint より前に増えた分が分かります。
| スキーマ |
cachePoint より前に増えた分 |
本文に書いたスキーマの分 |
|---|---|---|
| A | 392 | 174 |
| B | 638 | 295 |
| C | 545 | 280 |
| D | 457 | 256 |
「本文に書いたスキーマの分」は、2つの渡し方の inputTokens の差で、説明の1文とスキーマの JSON の分です。構造化出力の inputTokens は依頼文の分(24〜76トークン)だけで、スキーマから作った部分はすべて cachePoint より前に数えられていました。大きさは、本文に書いた分の約2倍(1.8〜2.3倍)です。Haiku 5.5・Opus 5.5 でも、差は ±3 トークンでした。
Anthropic の Structured outputs のページに、この動きが書かれています。
When using structured outputs, Claude automatically receives an additional system prompt explaining the expected output format. This means:
- Your input token count is slightly higher
- The injected prompt costs you tokens like any other system prompt
- Changing the output_config.format parameter will invalidate any prompt cache for that conversation thread
Claude API では、スキーマからシステムプロンプトが作られて足されます。Bedrock のプロンプトキャッシュは、前の部分が変わると後ろのキャッシュも使えなくなります(プロンプトキャッシュ)。
Cache checkpoints are processed in this order: tools → system → messages. … changing content in an earlier section invalidates the cache for later sections
Anthropic の説明どおりスキーマが system に足されるなら、messages の中に cachePoint を置いても、その前の部分がスキーマごとに変わるので、キャッシュはスキーマごとに別になります。測った結果は、この説明と合っています。
Anthropic のページは Claude API の説明です。Bedrock の構造化出力とプロンプトキャッシュのページには、2つの関係は書かれていませんでした。構造化出力のページにある「キャッシュ」は、コンパイルした文法のキャッシュ("cached for 24 hours from first access")で、プロンプトキャッシュとは別のものです。
3. 4回の呼び出しで、費用は約28%、時間は約31%減った
A と B を同時に始め、終わった順に C・D を始める流れを、2つの渡し方で3回ずつ交互に測りました(Sonnet 5.5、effort medium)。読めない出力やスキーマに合わない出力は呼び直す形にしましたが、呼び直しは0回でした。
| 渡し方 | 全体(秒) | 書き込み | 読み出し | 出力 | 1回の費用(ドル) |
|---|---|---|---|---|---|
| 構造化出力 | 78.7(77.5〜80.2) | 84,407(4本とも) | 0 | 17,628 | 0.388(0.381〜0.391) |
| 本文に書く | 54.4(50.9〜57.7) | 41,191(同時に始めた A・B) | 41,191(後の C・D) | 17,059 | 0.280(0.271〜0.291) |
(3回の平均。括弧は最小〜最大)
費用の内訳は、構造化出力が書き込み約0.211ドル・出力約0.176ドル、本文に書く形が書き込み約0.103ドル・読み出し約0.004ドル・出力約0.171ドルです。下がった分は、ほぼキャッシュの書き込みの差です。割合は、資料の長さ、スキーマの種類の数、出力の量で変わります。
時間の差には、5章の出力の速さの差と、読み出しで入力の処理が軽くなる分が重なっています。この計測では切り分けていません。
4. 同時に始めた呼び出しは、どちらも書き込みになる
本文に書く形でも書き込みが2回あるのは、A と B を同時に始めたためです。同時に始めた2本は、2回測って2回とも両方が書き込みになりました。
1本目に長く書かせて(B、出力3,000トークンまで)、ずらして始めた呼び出しがいつから読み出せるかを見ました(本文に書く形、各2回)。
| 回 | B(0秒に開始) | A(4秒後) | C(8秒後) | D(12秒後) |
|---|---|---|---|---|
| 1回目 | 書き込み。16.7秒で終了 | 書き込み | 読み出し | 読み出し |
| 2回目 | 書き込み。16.4秒で終了 | 書き込み | 読み出し | 読み出し |
8秒後に始めた C は、B がまだ書いている途中でも読み出せました。1本目が終わるのを待つ必要はなさそうです。4秒後と8秒後のどこから読めるかは、2回の結果からは言えません。
5. 構造化出力は、答えを書く速さが約3分の2だった
同じ依頼(B)を、キャッシュなしの ConverseStream で3回ずつ交互に送りました(Sonnet 5.5、effort medium)。
| 渡し方 | 全体(秒) | 最初の文字まで(秒) | 答えを書く時間(秒) | 考えた分(トークン) | 答えを書く速さ(トークン/秒) | 空白を除いた JSON の速さ(文字/秒) |
|---|---|---|---|---|---|---|
| 構造化出力 | 57.8 | 44.0 | 13.8 | 5,638〜5,749 | 124.5(122.1〜126.4) | 205.2 |
| 本文に書く | 39.5 | 30.2 | 9.3 | 5,612〜6,198 | 183.5(178.2〜187.1) | 305.2 |
(3回の平均。括弧は最小〜最大)
- 答えを書く速さは、構造化出力が約3分の2でした。空白や改行を除いた JSON の文字数で割っても同じ割合なので、整形の空白の差ではありません
- 最初の文字までも約14秒長くかかりました。考えた内容(
reasoningContent)は最初の文字の直前にまとめて届くので、この時間には考える時間が入っています。考えた分のトークン数はほぼ同じなので、考える部分も遅いか、考える前に別の待ち時間があるかのどちらかです。この計測では切り分けられません - 3回とも同じくらい遅く、スキーマ B は2章で使っていたので、新しいスキーマで文法をコンパイルする待ち時間(Bedrock の文書では "may take up to a few minutes")ではなさそうです。理由は分かりません
- Sonnet 5.5 とスキーマ B だけの結果です。答えの部分のトークン数は、CountTokens が使えなかったので、その文だけを送った呼び出しの入力トークン数から数えました
6. 本文に書くと、出力の形は自分で確かめる
| 観点 | 構造化出力 | 本文に書く |
|---|---|---|
| 出力の形 | スキーマに沿うよう API が守らせる | 守らせる仕組みは無い |
そのまま json.loads で読めた回数 |
15回中15回 | 27回中22回 |
| フェンスを外す処理の後 | 処理は不要 | 27回中27回読めた |
| スキーマから外れた回数 | 15回中0回 | 27回中0回 |
| 書けるスキーマ |
minItems は0か1だけ。再帰、外部の $ref、minimum などの数値の制約、minLength などの文字列の制約は使えない |
制限は無い。守るかはモデル次第 |
| 受け取った後の検査 | 書けなかった制約の分 | スキーマ全体 |
| 新しいスキーマの初回 | 文法のコンパイルに数分かかることがある(公式文書。今回は測っていない) | 文法のコンパイルは無い |
(回数は3章・5章と、本文に書く形だけで A〜D を3回ずつ順に呼んだ12回の合計。どれも Sonnet 5.5)
そのまま読めなかった5回のうち、出力を保存していた1回は、先頭に ```json の行、末尾に ``` の行が付いていました。残りの4回(3章の流れ)は出力を保存していませんでしたが、フェンスがあれば外し、それでも読めなければ最初の { から最後の } までを読む処理で、4回とも読めました。
まとめ
- 構造化出力では、スキーマから作った部分(392〜638トークン)が
cachePointより前に入り、スキーマごとに別のキャッシュになりました。Sonnet 5.5・Haiku 5.5・Opus 5.5 で同じでした - スキーマを
cachePointより後ろの本文に書くと、4種のスキーマで1つのキャッシュを共有でき、4回の呼び出しで費用が約28%(0.388→0.280ドル)、時間が約31%(78.7→54.4秒)減りました - 構造化出力は、答えを書く速さが約3分の2で、最初の文字までも約14秒長くかかりました(Sonnet 5.5)
- 本文に書く形では、そのまま読めたのは27回中22回で、フェンスを外せば27回とも読めました。外す処理とスキーマでの検査は自分で持ちます
構造化出力では、スキーマから作った部分が自動で cachePoint より前に入ります。同じ資料をスキーマを変えて何回も使い回すとき、構造化出力のままではキャッシュを共有できません。共有したいなら、形を守らせる仕組みと引き換えに、スキーマを本文に書くことになります。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。