🍆

Amazon BedrockのClaude Sonnet 4でプロンプトキャッシュがかかったりかからなかったりする原因の調査

に公開

概要

先日、Bedrockを利用した小説翻訳アプリを作成しました。
https://zenn.dev/pesi/articles/7bb34b30673b8d

Bedrockはプロンプトキャッシュ機能に対応しており、長文のプロンプトなどを一時的にキャッシュすることが可能です。
一字一句まで完璧に一致している必要がありますが、プロンプトに毎回同じ定型文などを入れている場合、5分間の間に再度同じプロンプトを実行するとコストやレイテンシーの面でメリットがあります。
https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/prompt-caching.html

小説の翻訳では毎回同じ内容の長い原文を送っているため、プロンプトキャッシュを利用しない手はありません。
システムプロンプトに毎回小説の原文を含めてプロンプトキャッシュを有効にすることで、プロンプトキャッシュを活用しています。

プロンプトイメージ
"system": [ 
    {
        "text": `<novel>タグで小説の原文を渡します。AIは翻訳家として小説の翻訳に関するタスクを行います。<novel>【ここに翻訳前の小説全文が入る】</novel>`
    },
    {
        "cachePoint": {
            "type": "default"
        }
    }
],

コストでいうと、フランツ・カフカの『断食芸人』をドイツ語から日本語に訳す場合、キャッシュなしで$1程度、キャッシュありで$0.5程度となり、かなり安くなっている計算です。

しかし、プロンプトキャッシュが同じ小説の翻訳でもかかったりかからなかったり、なかなか動作が安定しません。
そのため、原因の調査を行いました。

調査

BedrockではログをS3に出力することが可能です。
リクエスト内容やレスポンス内容が含まれたデータがjson形式で出力されるようになっています。

キャッシュがかかっている場合と、かかっていない場合についてそれぞれログを見ていきます。
inputブロックがbedrockに渡した入力、outputブロックがbedrockからのレスポンスです。
正常に動作すれば "system" 内のプロンプト、主に小説原文の部分にプロンプトキャッシュがかかってきます。

ログ内の "usage" ブロックで以下のどちらかが0以外になっていればキャッシュがかかっています。

  • cacheReadInputTokens
  • cacheWriteInputTokens
キャッシュがかかっている場合
{
    "timestamp": "2025-07-24T15:04:11Z",
    "accountId": "xxxxxxxxxxxx",
    "identity": {
        "arn": "arn:aws:iam::xxxxxxxxxxxx:user/xxxxxxxxxxxxx"
    },
    "region": "ap-northeast-1",
    "requestId": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "operation": "Converse",
    "modelId": "apac.anthropic.claude-sonnet-4-20250514-v1:0",
    "input": {
        "inputContentType": "application/json",
        "inputBodyJson": {
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {
                            "text": "小説の原文をドイツ語から日本語で140字から500字程度に要約してください。必要があれば小説のタイトルである断食芸人も要約の参考にしてください。小説の原文とタイトルの情報だけを要約の参考にしてください。改行は<br>タグで記載してください。"
                        }
                    ]
                }
            ],
            "system": [
                {
                    "text": "<novel>【ここに翻訳前の小説全文が入る】</novel>"
                },
                {
                    "cachePoint": {
                        "type": "default"
                    }
                }
            ],
            "inferenceConfig": {
                "maxTokens": 40000,
                "temperature": 0.0
            }
        },
        "inputTokenCount": 106,
        "cacheReadInputTokenCount": 0,
        "cacheWriteInputTokenCount": 7707
    },
    "output": {
        "outputContentType": "application/json",
        "outputBodyJson": {
            "output": {
                "message": {
                    "role": "assistant",
                    "content": [
                        {
                            "text": "断食芸人の物語は、かつて人々の注目を集めた断食パフォーマンスの興隆と衰退を描いている。主人公の断食芸人は檻の中で何日も断食を続け、多くの観客が見物に訪れていた。しかし時代が変わり、人々の関心は他の娯楽に移っていく。<br><br>芸人はサーカスに身を寄せるが、そこでも忘れ去られた存在となり、動物小屋の近くに置かれた檻で誰にも気づかれることなく断食を続ける。最期に監督に発見された時、芸人は「自分に合う食べ物を見つけられなかった。もし見つけていたら、騒ぎを起こすことなく皆と同じように食べていただろう」と告白して息を引き取る。その後、檻には若い豹が入れられ、生命力に満ちた豹は観客たちを魅了した。"
                        }
                    ]
                }
            },
            "stopReason": "end_turn",
            "metrics": {
                "latencyMs": 8474
            },
            "usage": {
                "inputTokens": 106,
                "cacheReadInputTokens": 0,
                "cacheWriteInputTokens": 7707,
                "outputTokens": 303,
                "totalTokens": 8116
            }
        },
        "outputTokenCount": 303
    },
    "inferenceRegion": "ap-northeast-1",
    "schemaType": "ModelInvocationLog",
    "schemaVersion": "1.0"
}
{
    "timestamp": "2025-07-24T15:04:20Z",
    "accountId": "xxxxxxxxxxxx",
    "identity": {
        "arn": "arn:aws:iam::xxxxxxxxxxxx:user/xxxxxxxxxxxxxxxx"
    },
    "region": "ap-northeast-1",
    "requestId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "operation": "Converse",
    "modelId": "apac.anthropic.claude-sonnet-4-20250514-v1:0",
    "input": {
        "inputContentType": "application/json",
        "inputBodyJson": {
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {
                            "text": "小説の原文から主要なキャラクターを抽出し、日本語で2-3行程度で紹介してください。フォーマットは\"人物:説明\"という形にして、それ以外の文章を出力してはいけません。人物ごとに<br><br>で改行してください。"
                        }
                    ]
                }
            ],
            "system": [
                {
                    "text": "<novel>タグで小説の原文を渡します。AIは翻訳家として小説の翻訳に関するタスクを行います。<novel>【ここに翻訳前の小説全文が入る】</novel>"
                },
                {
                    "cachePoint": {
                        "type": "default"
                    }
                }
            ],
            "inferenceConfig": {
                "maxTokens": 40000,
                "temperature": 0.0
            }
        },
        "inputTokenCount": 96,
        "cacheReadInputTokenCount": 7707,
        "cacheWriteInputTokenCount": 0
    },
    "output": {
        "outputContentType": "application/json",
        "outputBodyJson": {
            "output": {
                "message": {
                    "role": "assistant",
                    "content": [
                        {
                            "text": "断食芸人:断食のパフォーマンスを行う芸人。断食を芸術として捉え、自分の技術に誇りを持っているが、観客に真に理解されることはない。最後まで断食を続け、本当に美味しいと思える食べ物を見つけられなかったと告白して死ぬ。<br><br>\n\n興行師:断食芸人のマネージャー的存在。断食芸人の興行を取り仕切り、観客への説明や演出を行う。断食芸人の真意を理解せず、商業的な成功のみを重視している。<br><br>\n\n監視人:断食芸人が本当に断食しているかを見張る役割の人々。主に肉屋が選ばれることが多い。断食芸人の誠実さを疑い、隠れて食事をしているのではないかと疑っている。<br><br>\n\nサーカスの監督:断食芸人を雇ったサーカスの責任者。最後に断食芸人の檻を発見し、死にゆく断食芸人と会話を交わす人物。断食芸人の最期の告白を聞く重要な役割を果たす。"
                        }
                    ]
                }
            },
            "stopReason": "end_turn",
            "metrics": {
                "latencyMs": 9390
            },
            "usage": {
                "inputTokens": 96,
                "cacheReadInputTokens": 7707,
                "cacheWriteInputTokens": 0,
                "outputTokens": 365,
                "totalTokens": 8168
            }
        },
        "outputTokenCount": 365
    },
    "inferenceRegion": "ap-northeast-1",
    "schemaType": "ModelInvocationLog",
    "schemaVersion": "1.0"
}
キャッシュがかかっていない場合
{
    "timestamp": "2025-07-24T14:46:12Z",
    "accountId": "xxxxxxxxxxxx",
    "identity": {
        "arn": "arn:aws:iam::xxxxxxxxxxxx:user/xxxxxxxxxxxxxx"
    },
    "region": "ap-northeast-1",
    "requestId": "xxxxxxxxxxxxxxxxxxxxxxxx",
    "operation": "Converse",
    "modelId": "apac.anthropic.claude-sonnet-4-20250514-v1:0",
    "input": {
        "inputContentType": "application/json",
        "inputBodyJson": {
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {
                            "text": "小説の原文を内容にしたがってフィンランド語で自然な段落ごとに分割してください。各段落は小説の原文の意味やストーリーのまとまりを考慮して分けてください。出力はJSON配列で、各要素が1つの段落テキストとなるようにしてください。小説の原文に説明や余計な文章の追加、削除は行わないで、純粋なJSON配列のみを出力してください。"
                        }
                    ]
                }
            ],
            "system": [
                {
                    "text": "<novel>【ここに翻訳前の小説全文が入る】</novel>"
                },
                {
                    "cachePoint": {
                        "type": "default"
                    }
                }
            ],
            "inferenceConfig": {
                "maxTokens": 40000,
                "temperature": 0.0
            }
        },
        "inputTokenCount": 29849,
        "cacheReadInputTokenCount": 0,
        "cacheWriteInputTokenCount": 0
    },
    "output": {
        "outputContentType": "application/json",
        "outputBodyJson": {
            "output": {
                "message": {
                    "role": "assistant",
                    "content": [
                        {
                            "text": "【出力された値】"
                        }
                    ]
                }
            },
            "stopReason": "end_turn",
            "metrics": {
                "latencyMs": 962168
            },
            "usage": {
                "inputTokens": 29849,
                "cacheReadInputTokens": 0,
                "cacheWriteInputTokens": 0,
                "outputTokens": 29892,
                "totalTokens": 59741
            }
        },
        "outputTokenCount": 29892
    },
    "inferenceRegion": "ap-northeast-2",
    "schemaType": "ModelInvocationLog",
    "schemaVersion": "1.0"
}

キャッシュがかかっていない場合には "inferenceRegion"、つまり推論処理を行ったリージョンがap-northeast-2のソウルリージョンになっています。
上記処理はいずれも東京リージョンに対してリクエストしているのですが、BedrockのClaude Sonnet 4ではクロスリージョン推論というものが有効になっており、必ずしも東京リージョンで処理が行われるのではなく、近隣のリージョンを含めて処理が実行されます。

ソウルリージョンがプロンプトキャッシュ未対応といったような情報は見当たらず、なぜソウルリージョンで推論が行われた場合のみキャッシュがかからないのかは不明ですが、少なくとも自分の環境では2025/07/25現在ソウルリージョンでキャッシュがかからない状況です。

(料金表ではソウルリージョンにもキャッシュの項目があるので未対応ということはない気がしますが、、、)

対応策

理由は不明ですが、とりあえずクロスリージョン推論によってソウルリージョンに処理が振られてしまうことが原因らしいことは分かりました。
さすがにusリージョンであればプロンプトキャッシュ未対応ということはないだろうという推測の元、リクエスト先をus-east1に変更し、推論プロファイルIDも "apac.anthropic.claude-sonnet-4-20250514-v1:0" から "us.anthropic.claude-sonnet-4-20250514-v1:0" に変更します。

変更後のログは以下の通りです。

usリージョン実行時ログ
{
    "timestamp": "2025-07-24T15:53:35Z",
    "accountId": "xxxxxxxxxxxx",
    "identity": {
        "arn": "arn:aws:iam::xxxxxxxxxxxx:user/xxxxxxxxxxxxxx"
    },
    "region": "us-east-1",
    "requestId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "operation": "Converse",
    "modelId": "us.anthropic.claude-sonnet-4-20250514-v1:0",
    "input": {
        "inputContentType": "application/json",
        "inputBodyJson": {
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {
                            "text": "小説の原文をフィンランド語から日本語で140字から500字程度に要約してください。必要があれば小説のタイトルである外套も要約の参考にしてください。小説の原文とタイトルの情報だけを要約の参考にしてください。改行は<br>タグで記載してください。"
                        }
                    ]
                }
            ],
            "system": [
                {
                    "text": "<novel>タグで小説の原文を渡します。AIは翻訳家として小説の翻訳に関するタスクを行います。<novel>【ここに翻訳前の小説全文が入る】</novel>"
                },
                {
                    "cachePoint": {
                        "type": "default"
                    }
                }
            ],
            "inferenceConfig": {
                "maxTokens": 40000,
                "temperature": 0.0
            }
        },
        "inputTokenCount": 104,
        "cacheReadInputTokenCount": 0,
        "cacheWriteInputTokenCount": 29705
    },
    "output": {
        "outputContentType": "application/json",
        "outputBodyJson": {
            "output": {
                "message": {
                    "role": "assistant",
                    "content": [
                        {
                            "text": "ペテルブルクのある役所で働く下級官吏アカーキー・アカーキエヴィチは、書類の清書だけを生きがいとする孤独な男だった。彼の古いマントは修理不可能なほど傷んでおり、仕立屋に新しいマントを作るよう勧められる。<br><br>アカーキーは必死に節約して資金を貯め、ついに美しい新しいマントを手に入れる。同僚たちも彼を祝福し、初めて社交の場に招かれた。しかし帰り道、強盗に新しいマントを奪われてしまう。<br><br>警察も高官も彼の訴えを真剣に取り合わず、絶望したアカーキーは病に倒れて死んでしまう。その後、ペテルブルクの街には官吏の幽霊が現れ、人々からマントを奪い取るという噂が広まった。幽霊は特に彼を冷たく扱った高官からマントを奪い、ようやく成仏したという。"
                        }
                    ]
                }
            },
            "stopReason": "end_turn",
            "metrics": {
                "latencyMs": 34953
            },
            "usage": {
                "inputTokens": 104,
                "cacheReadInputTokens": 0,
                "cacheWriteInputTokens": 29705,
                "outputTokens": 333,
                "totalTokens": 30142
            }
        },
        "outputTokenCount": 333
    },
    "inferenceRegion": "us-west-2",
    "schemaType": "ModelInvocationLog",
    "schemaVersion": "1.0"
}
{
    "timestamp": "2025-07-24T15:54:11Z",
    "accountId": "xxxxxxxxxxxx",
    "identity": {
        "arn": "arn:aws:iam::xxxxxxxxxxxx:user/xxxxxxxxxxxxxxxx"
    },
    "region": "us-east-1",
    "requestId": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
    "operation": "Converse",
    "modelId": "us.anthropic.claude-sonnet-4-20250514-v1:0",
    "input": {
        "inputContentType": "application/json",
        "inputBodyJson": {
            "messages": [
                {
                    "role": "user",
                    "content": [
                        {
                            "text": "小説の原文から主要なキャラクターを抽出し、日本語で2-3行程度で紹介してください。フォーマットは\"人物:説明\"という形にして、それ以外の文章を出力してはいけません。人物ごとに<br><br>で改行してください。"
                        }
                    ]
                }
            ],
            "system": [
                {
                    "text": "<novel>タグで小説の原文を渡します。AIは翻訳家として小説の翻訳に関するタスクを行います。<novel>【ここに翻訳前の小説全文が入る】</novel>"
                },
                {
                    "cachePoint": {
                        "type": "default"
                    }
                }
            ],
            "inferenceConfig": {
                "maxTokens": 40000,
                "temperature": 0.0
            }
        },
        "inputTokenCount": 96,
        "cacheReadInputTokenCount": 0,
        "cacheWriteInputTokenCount": 29705
    },
    "output": {
        "outputContentType": "application/json",
        "outputBodyJson": {
            "output": {
                "message": {
                    "role": "assistant",
                    "content": [
                        {
                            "text": "アカーキー・アカーキエヴィチ・バシュマチキン:ある役所で写字係として働く小柄で薄毛の下級官吏。新しい外套を手に入れることが人生最大の喜びとなるが、強盗に奪われ、その後病死する物語の主人公。<br><br>\n\nペトローヴィチ:片目の仕立て屋で元農奴。アカーキーの古い外套の修理を断り、新しい外套を作ることを提案する。酒好きで気難しい性格だが、腕は確かな職人。<br><br>\n\n権勢ある人物:将軍の地位にある高級官僚。アカーキーが外套の件で助けを求めに来た際、厳しく叱責して追い返す。後にアカーキーの亡霊に外套を奪われる。<br><br>\n\n部長の助手:アカーキーの新しい外套完成を祝って宴会を開く同僚の役人。物語中盤でアカーキーを自宅のパーティーに招待する。"
                        }
                    ]
                }
            },
            "stopReason": "end_turn",
            "metrics": {
                "latencyMs": 41934
            },
            "usage": {
                "inputTokens": 96,
                "cacheReadInputTokens": 0,
                "cacheWriteInputTokens": 29705,
                "outputTokens": 335,
                "totalTokens": 30136
            }
        },
        "outputTokenCount": 335
    },
    "inferenceRegion": "us-east-2",
    "schemaType": "ModelInvocationLog",
    "schemaVersion": "1.0"
}

上記ログを見ると、推論処理を行った "inferenceRegion" が "us-east-2" と "us-west-2" にまたがっていますが、どちらでもプロンプトキャッシュがかかっています。
しかし、リージョン間でキャッシュは共有されないのか、別のリージョンに処理が割り振られると都度キャッシュ書き込みが発生してしまうようです。

上記より、usリージョンであれば、クロスリージョンで処理が実行されても、それぞれのリージョンではちゃんとキャッシュがかかることがわかりました。
usリージョンでも、連続で同じリージョンを引けないとキャッシュを有効に活用はできないですが、少なくともソウルリージョンのようにキャッシュが書き込まれすらしないということはありませんでした。

公式ドキュメントなどから原因を解明できたわけではありませんが、少なくとも現状の動作結果を見た感じ、プロンプトキャッシュがかかったりかからなかったりする場合にはクロスリージョン推論周りを疑ってみるといいかもしれません。

Discussion