Amazon Bedrockのプロンプトキャッシング機能を検証してみた！実装のコツと落とし穴

:::note warn
一部公式ドキュメントと異なることを書いてます。自身がないので個人の検証結果として御覧ください。
:::

Bedrockのプロンプトキャッシュのドキュメント

https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html

Anthropicのプロンプトキャッシュのドキュメント

https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching

## はじめに

Amazon Bedrockで一般提供が始まったプロンプトキャッシング機能を実際に検証してみました。
公式ドキュメントには書かれていない挙動や、実装する際のコツをシェアします。

https://aws.amazon.com/blogs/machine-learning/effectively-use-prompt-caching-on-amazon-bedrock/

## プロンプトキャッシングって何？

簡単に言うと、よく使うプロンプトの一部をキャッシュしておいて、次回以降の処理を省略する機能です。公式発表では**コスト最大90%削減**、**レイテンシー最大85%改善**とかなり派手なことを言ってます。実際どうなのか検証してみました。

## 検証してわかった！実装のポイント

いろんなパターンで試してみて、公式ドキュメントには書かれていない重要な挙動がわかりました。

### キャッシュの評価方法

まず大事なのは、**キャッシュはTool、System、Messageを全部つなげて評価される**ということ。これ、実装する上でめちゃくちゃ重要です。

例えば、Toolの定義が短くて最小トークン数（Claude 3.7 Sonnetだと1,024トークン）に満たない場合、ToolにcachePointを付けても単独ではキャッシュされません。でも、ToolとSystemの両方にcachePointを付けて、合計で最小トークン数を超えれば、両方がキャッシュされます。

この仕組みを理解しておけば、短いコンポーネントでもキャッシュの恩恵を受けられるように工夫できます。

### キャッシュの一致条件と更新

キャッシュの一致判定はかなり厳しいです。**cachePointまでの完全一致**が必要で、Toolの定義が1トークンでも変わると、Tool、System、Messageすべて再キャッシュになります。

また、大事なのが、**既存のキャッシュに追加する形でcachePointを付けた場合の挙動**です。この場合、既にキャッシュされてる部分を除いた増分だけがキャッシュ対象になります。これを使えば、会話履歴を効率よくキャッシュしながら拡張していけます。

### クロスリージョンの動作

クロスリージョンの場合は、推測ですがそれぞれのリージョンでキャッシュされると思います。なので、複数ターンの処理の場合に、キャッシュされていないリージョンで処理した場合は再度キャッシュする動作になると思います。（あまり検証できず、正確でないかも）

### モデル別の違い

モデルによって挙動が違うこともわかりました：

**Amazon Nova**の場合：
- ユーザーメッセージでtextコンテンツがないと（ToolResultだけとか）エラーになる
- Toolは単体でキャッシュできないとドキュメントにはあるけど、SystemとMessageをキャッシュした後にToolだけ変えて送ると、なぜかcacheWriteInputTokensが出てくる。Tool単体じゃなくて他がキャッシュする際に合わせてキャッシュしてるみたい

**Anthropic Claude**の場合：
- Bedrockのドキュメントには「2つ目以降のcachePoint間のトークンもキャッシュ可能最小トークンを超える必要がある」って書いてあるけど、実際やってみるとそんな制限はなさそう
- Anthropicの公式ドキュメントにもそんな制限の記載はない

## 効果的なキャッシュポイントの配置方法

検証した結果、こんな感じでcachePointを配置するのが効果的でした：

1. **SystemプロンプトにcachePointを付ける**：システムプロンプトはあまり変えないから、最初にキャッシュするのが効果的
2. **Toolの定義にcachePointを付ける**：複雑なツール定義は再計算のコストが高いから、キャッシュするとかなり効く
3. **Messageの履歴にcachePointを付ける**：会話が進むにつれて、戦略的にcachePointを配置する

特にチャットボットみたいな会話型アプリだと、「後ろから2つ目のメッセージにcachePointを付けて前回までのキャッシュを使用」し、最後のユーザーメッセージの後にもcachePointを付けて「今回の会話をキャッシュする」というやり方が効果的でした。

ちなみに、Anthropicの公式ドキュメントには「最後のユーザーメッセージにだけcachePointをつければいい」って書いてあるんですが、実際やってみるとそんな動作はしませんでした。少なくともBedrockでClaudeを呼び出した場合は、「最後から２つ目」にもcachePointを付ける必要があります。

また、Bedrockのドキュメントには「2つ目以降のcachePoint間のトークン数もキャッシュ可能最小トークンを超える必要がある」って書いてあるけど、実際やってみるとそんな制限はなさそうです。


実装としては、こんな感じで、Messagesを逆順にして２つ処理し、また逆順に戻す感じです。（参考にしたのは[こちら](https://github.com/anthropics/anthropic-cookbook/blob/main/misc/prompt_caching.ipynb)）

```python
def get_messages_with_cache_point(messages_without_cache_point):
    messages_with_cache_point = []
    user_turns_processed = 0

    for message in reversed(messages_without_cache_point):
        m = copy.deepcopy(message)

        if message["role"] == "user" and user_turns_processed < 2:
            if (
                len([c for c in m["content"] if "text" in c]) > 0
            ):  # Do not add cachePoint if there is no Text block (this will cause an error in Nova models)
                m["content"].append({"cachePoint": {"type": "default"}})
                user_turns_processed += 1

        messages_with_cache_point.append(m)

    messages_with_cache_point.reverse()

    return messages_with_cache_point
```

---

:::note
以下の検証メモからQ Developer CLIにふくらませてもらったものを、調整して投稿いたしました。
:::

- キャッシュ対象が短いとキャッシュされない（モデルごとにキャッシュ可能な最小トークンが設定されている）
- キャッシュはTool、System、Messageをつなぎ合わせて評価される
- 例えばToolがキャッシュ可能最小トークンに満たない場合、ToolにcachePointをセットしてもTool部分だけではキャッシュされない
- ToolとSystemにcachePointをセットして、合計でキャッシュ可能最小トークンを超える場合は、Tool単体でキャッシュ可能最小トークンに達していなくても、ToolとSystemの両方がキャッシュされる
- 先頭からcachePointをつけた部分までキャッシュされる
- 最大4つまでのcachePointが指定可能
- キャッシュに存在するかのチェックは、cachePointをつけた部分までの完全一致で評価される
- 例えばToolが1トークンでも変わった場合、ToolもSystemもMessageもすべて再キャッシュになる（cacheWriteInputTokensとしてカウント）
- Messageの途中までキャッシュがあり、そのメッセージに追加する形でcachePointを付与した場合、すでにキャッシュされている分を除いて増分だけがキャッシュ対象（cacheWriteInputTokensとしてカウント）

- Novaの場合は、なぜかユーザーメッセージでtextコンテンツがない場合（ToolResultだけなど）、エラーになる
- NovaはToolがキャッシュ対象ではない（toolsにcachePointを付与するとエラー）だけど、SystemとMessageをキャッシュさせたあと、Toolだけ変更して再送信するとcacheWriteInputTokensが存在するので、Tool単体ではキャッシュしないけど他と合わせてキャッシュしているように見える
- Bedrockのドキュメントには以下の記述があり、２つ目以降もcachePoint間のトークンがキャッシュ可能最小トークンを超える必要がある旨記載があるが、Anthropicのドキュメントでは見つからず。動作的にもこの制限はなさそうに見える
    > That means that your first cache checkpoint can be defined after 1,024 tokens and your second cache checkpoint can be defined after 2,048 tokens.
- 


多分、期待する使い方としては、

- SystemにひとつcachePointを付与
- ToolにひとつcachePointを付与
- Messageのユーザーメッセージの後ろから２つにcachePointを付与

が良さそう。

Messageは、後ろから２つ目が前回キャッシュしたものを使用するためで、最後のユーザーメッセージは今回キャッシュする目的

Anthropicのドキュメントでは、「最後のユーザーメッセージにだけcachePointをつければいい」ように書いてあるが、そんな動作はしない。（少なくともBedrockでは）
以下のNotebookも最後のメッセージにつける実装になっている

https://github.com/anthropics/anthropic-cookbook/blob/main/misc/prompt_caching.ipynb