0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スキーマを変えるとプロンプトキャッシュが共有されなかったので、Bedrock の構造化出力をやめた話(2026年版)

0
Posted at

はじめに

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コネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ:https://gmo-connect.jp/contactus/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?