Closed5

AnthropicのPrompt Cachingを試す

kun432kun432

https://www.anthropic.com/news/prompt-caching

要約

Prompt caching with Claude

Anthropic APIで、プロンプトキャッシングが公開ベータ版として利用可能になりました。長いプロンプトのコストを最大90%、レイテンシーを最大85%削減できます。

主な特徴:

  • Claude 3.5 SonnetとClaude 3 Haikuで利用可能(Claude 3 Opusは近日対応)
  • 大量のコンテキストを一度送信し、後続のリクエストで参照可能

使用例:

  • 会話型エージェント: 長い指示やアップロードされた文書を含む延長された会話のコストとレイテンシーを削減。
  • コーディングアシスタント: コードベースの要約版をプロンプトに保持することで、オートコンプリートやコードベースQ&Aを改善。
  • 大規模文書処理: 画像を含む完全な長文資料をレスポンスレイテンシーを増加させずにプロンプトに組み込み。
  • 詳細な指示セット: 広範な指示、手順、例のリストを共有し、Claudeの応答を微調整。開発者は通常プロンプトに少数の例を含めますが、プロンプトキャッシングを使用することで、多様な高品質の出力例を数十個含めることで、さらに優れたパフォーマンスを得ることが可能。
  • エージェントによる検索とツール使用: 複数回のツール呼び出しと反復的な変更を含むシナリオのパフォーマンスを向上。各ステップは通常新しいAPI呼び出しを必要とします。
  • 書籍、論文、ドキュメント、ポッドキャスト原稿、その他の長文コンテンツとの対話: 文書全体をプロンプトに埋め込み、ユーザーが質問できるようにすることで、任意の知識ベースを活性化。

ユースケース別

ユースケース キャッシングなしのレイテンシー(最初のトークンまでの時間) キャッシングありのレイテンシー(最初のトークンまでの時間) コスト削減
本との対話 (100,000トークンのキャッシュされたプロンプト) [1] 11.5秒 2.4秒 (-79%) -90%
多数事例によるプロンプト (10,000トークンのプロンプト) [1:1] 1.6秒 1.1秒 (-31%) -86%
複数ターンの会話 (長いシステムプロンプトを持つ10ターンの会話) [2] 約10秒 約2.5秒 (-75%) -53%

価格設定:

  • キャッシュへの書き込み: 基本入力トークン価格の125%
  • キャッシュからの読み取り: 基本入力トークン価格の10%

モデル別価格 (100万トークンあたり):

  • Claude 3.5 Sonnet
    • 入力: $3
    • キャッシュ書き込み: $3.75
    • キャッシュ読み取り: $0.30
    • 出力: $15
  • Claude 3 Opus (近日対応)
    • 入力: $15
    • キャッシュ書き込み: $18.75
    • キャッシュ読み取り: $1.50
    • 出力: $75
  • Claude 3 Haiku
    • 入力: $0.25
    • キャッシュ書き込み: $0.30
    • キャッシュ読み取り: $0.03
    • 出力: $1.25

事例

Notionがプロンプトキャッシングを導入し、Notion AIの速度向上とコスト削減を実現。

脚注
  1. Claude 3.5 Sonnetを使用し、キャッシュされたプロンプトの後に100-200トークンの動的指示を含めて測定。 ↩︎ ↩︎

  2. Claude 3.5 Sonnetを使用し、5000トークンのシステムプロンプト、約100トークンのユーザーメッセージ、Claudeからの約2000トークンの応答で測定。コスト削減は会話全体で測定され、レイテンシー削減は中央値のメッセージについて報告されています。 ↩︎

kun432kun432

ドキュメント

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

Colaboratoryでやってみる。公式のサンプルは文学作品のコンテンツをキャッシュすると仮定して書いてあるので、青空文庫の「走れメロス」を使ってやってみようと思う。

https://www.aozora.gr.jp/cards/000035/card1567.html

以下を参考にさせてもらった。
https://qiita.com/Ninagawa123/items/af7d704680fba63914e8

! wget https://www.aozora.gr.jp/cards/000035/files/1567_ruby_4948.zip
! unzip 1567_ruby_4948.zip
import re

with open('hashire_merosu.txt', 'r', encoding='SJIS') as f:
    data = f.read()
    data = re.sub(r'-------------------------------------------------------.*?-------------------------------------------------------\n', '', data, flags=re.DOTALL)
    data = re.sub(r'[#地から1字上げ[\s\S]*$', '', data)
    data = re.sub("\n\u3000", "", data)
    data = re.sub("《[^》]+》", "", data)
print(data[:100])
print("〜")
print(data[-100:])
print(f"({len(data)})")
走れメロス
太宰治

メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては
〜
気をきかせて教えてやった。
「メロス、君は、まっぱだかじゃないか。早くそのマントを着るがいい。この可愛い娘さんは、メロスの裸体を、皆に見られるのが、たまらなく口惜しいのだ。」勇者は、ひどく赤面した。

(9862)

パッケージインストール

!pip install anthropic
!pip freeze | egrep -i "anthropic"
anthropic==0.34.0

APIキーをセット

from google.colab import userdata
import os

os.environ["ANTHROPIC_API_KEY"] = userdata.get('ANTHROPIC_API_KEY')

ではまずはプロンプトキャッシングを使わない場合。

import anthropic
import time

client = anthropic.Anthropic()

system_message = [
    {
        "type": "text",
        "text": "あなたは文学作品の分析を任務とするAIアシスタントです。テーマ、キャラクター、文体に関する洞察に満ちた解説を提供することがあなたの目標です。"
    },
    {
        "type": "text", 
        "text": "<LITERALY_WORK>\n" + data + "\n</LITERALY_WORK>",
    }
]

messages = []

print("チャットを開始します。やめるときは'quit'と入力してください。")
while True:
    user_input = input("========== USER ==========\n")
    if user_input.lower() == 'quit':
        print("チャットを終了します。さようなら。")
        break
    
    messages.append({"role": "user", "content": user_input})
    
    start_time = time.time()
    response = client.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=1024,
        temperature=0,
        system=system_message,
        messages=messages
    )
    end_time = time.time()
    response_time = end_time - start_time
    
    assistant_response = response.content[0].text.strip()
    messages.append({"role": "assistant", "content": assistant_response})
    
    print("========== ASSISTANT ==========\n" + assistant_response + "\n")
    print(f"ResponseTime: {response_time:.2f} secs")
    print(f"Usage: {response.usage.dict()}")

こんな感じでチャットしてみた。

チャットを開始します。やめるときは'quit'と入力してください。
========== USER ==========
「走れメロス」の主なテーマを分析して
========== ASSISTANT ==========
「走れメロス」の主なテーマは以下のように分析できます。

1. 信頼と誠実さ
- メロスは友人セリヌンティウスを人質に立てることを約束し、必ず期限までに戻ってくると誓う。この信頼関係が物語の核となっている。

2. 正義と善良さの勝利
- メロスは暴君ディオニスの残虐な行為に立ち向かい、自らの命を懸けて正義を貫く。最終的に善良さが勝利する。

3. 人間性の探求
- メロスの内面の葛藤や変化が描かれ、人間の弱さや強さ、善悪の複雑さが描かれている。

4. 友情の絆
- メロスとセリヌンティウスの深い友情が物語の中心にある。二人の絆は試練に耐え、最終的に強化される。

5. 生きる意味の探求
- メロスは自身の命を賭して正義を貫くが、その背景には生きる意味を見出そうとする姿勢がある。

以上のように、「走れメロス」は信頼、正義、人間性、友情、生きる意味といった普遍的なテーマを扱っており、人間の内面を深く掘り下げた作品といえる。

ResponseTime: 4.08 secs
Usage: {'input_tokens': 10488, 'output_tokens': 412}
========== USER ==========
登場人物をリストアップして
========== ASSISTANT ==========
「走れメロス」に登場する主な人物は以下の通りです。

1. メロス
- 主人公。村の牧人で、正義感と誠実さを持つ人物。

2. セリヌンティウス
- メロスの親友で、石工をしている。メロスを信じ続ける。

3. ディオニス
- 暴君。人々を疑い、無実の者を次々と殺害する。

4. メロスの妹
- メロスの大切な家族。メロスの誇りを受け継ぐ。

5. 花婿の牧人
- メロスの妹の婿となる人物。

6. フィロストラトス
- セリヌンティウスの弟子。メロスの到着が遅れていることを知らせる。

7. 群衆
- 物語の最後に登場し、メロスとセリヌンティウスの友情を称える。

このように、メロスとセリヌンティウスが中心的な人物であり、ディオニスが対極的な存在として描かれている。その他の登場人物も、物語の展開に重要な役割を果たしている。

ResponseTime: 3.97 secs
Usage: {'input_tokens': 10915, 'output_tokens': 349}
========== USER ==========
300語であらすじを書いて
========== ASSISTANT ==========
以下のように、「走れメロス」の300語程度のあらすじを書きました。

村の牧人メロスは、妹の結婚式の準備のため、隣町のシラクスに向かう。しかし町は異様に静かで、人々は恐怖に怯えていた。老人から、暴君ディオニスが無実の人々を次々と殺害していると聞かされる。メロスは激怒し、ディオニスを倒すべく王城に乗り込むが、捕らえられてしまう。

ディオニスはメロスに、三日の猶予を与えて妹の結婚式に出席させ、その後処刑すると提案する。メロスは友人のセリヌンティウスを人質に立て、約束を守ると誓う。

メロスは必死に走り続け、困難に立ち向かうが、ついに疲れ果ててしまう。しかし、最後の力を振り絞り、ついに王城に到着する。しかし、すでにセリヌンティウスは磔刑に処されようとしていた。

メロスは必死に叫び、自分こそが処刑されるべき人間だと訴える。ディオニスはこの誠実さに感動し、二人の友情を称えて赦免する。メロスとセリヌンティウスは抱き合って涙を流し、ディオニスも二人の仲間に加わる。

この物語は、信頼と誠実さの勝利を描いた感動的な作品である。人間の弱さと強さ、善悪の複雑さを深く掘り下げながら、普遍的なテーマを描き出している。

ResponseTime: 4.64 secs
Usage: {'input_tokens': 11280, 'output_tokens': 514}
========== USER ==========
quit
チャットを終了します。さようなら。

次にプロンプトキャッシュを有効にした場合。こちらから送信するメッセージは上と全く同じとする。

import anthropic
import time

client = anthropic.Anthropic()

system_message = [
    {
        "type": "text",
        "text": "あなたは文学作品の分析を任務とするAIアシスタントです。テーマ、キャラクター、文体に関する洞察に満ちた解説を提供することがあなたの目標です。"
    },
    {
        "type": "text", 
        "text": "<LITERALY_WORK>\n" + data + "\n</LITERALY_WORK>",
        "cache_control": {"type": "ephemeral"}  # キャッシュを有効化
    }
]

messages = []

print("チャットを開始します。やめるときは'quit'と入力してください。")
while True:
    user_input = input("========== USER ==========\n")
    if user_input.lower() == 'quit':
        print("チャットを終了します。さようなら。")
        break
    
    messages.append({"role": "user", "content": user_input})
    
    start_time = time.time()
    # ベータのエンドポイントを使用する(anthropic-beta: prompt-caching-2024-07-31)
    response = client.beta.prompt_caching.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=1024,
        temperature=0,
        system=system_message,
        messages=messages
    )
    end_time = time.time()
    response_time = end_time - start_time

    assistant_response = response.content[0].text.strip()
    messages.append({"role": "assistant", "content": assistant_response})
    
    print("========== ASSISTANT ==========\n" + assistant_response + "\n")
    print(f"ResponseTime: {response_time:.2f} secs")
    print(f"Usage: {response.usage.dict()}")

キャッシュに書き込む入力トークン数はcache_creation_input_tokens、キャッシュから読み出された入力トークン数はcache_read_input_tokensで参照できる。

結果

チャットを開始します。やめるときは'quit'と入力してください。
========== USER ==========
「走れメロス」の主なテーマを分析して
========== ASSISTANT ==========
「走れメロス」の主なテーマは以下のように分析できます。

1. 信頼と誠実さ
- メロスは友人セリヌンティウスを人質に立てることを約束し、必ず期限までに戻ってくると誓う。この信頼関係が物語の核となっている。

2. 正義と善良さの勝利
- メロスは暴君ディオニスの残虐な行為に立ち向かい、自らの命を懸けて正義を貫く。最終的に善良さが勝利する。

3. 人間性の探求
- メロスの内面の葛藤や変化が描かれ、人間の弱さや強さ、善悪の複雑さが描かれている。

4. 友情の絆
- メロスとセリヌンティウスの深い友情が物語の中心にある。二人の絆は試練に耐え、最終的に強化される。

5. 生きる意味の探求
- メロスは自身の命を賭して正義を貫くが、その背景には生きる意味を見出そうとする姿勢がある。

以上のように、「走れメロス」は信頼、正義、人間性、友情、生きる意味といった普遍的なテーマを扱っており、人間の内面を深く掘り下げた作品といえる。

ResponseTime: 4.04 secs
Usage: {'cache_creation_input_tokens': 10463, 'cache_read_input_tokens': 0, 'input_tokens': 25, 'output_tokens': 412}
========== USER ==========
登場人物をリストアップして
========== ASSISTANT ==========
「走れメロス」に登場する主な人物は以下の通りです。

1. メロス
- 主人公。村の牧人で、正義感と誠実さを持つ人物。

2. セリヌンティウス
- メロスの親友で、石工をしている。メロスを信じ続ける。

3. ディオニス
- 暴君。人々を疑い、無実の者を次々と殺害する。

4. メロスの妹
- メロスの大切な家族。メロスの誇りを受け継ぐ。

5. 花婿の牧人
- メロスの妹の婿となる人物。

6. フィロストラトス
- セリヌンティウスの弟子。メロスの到着が遅れていることを知らせる。

7. 群衆
- 物語の最後に登場し、メロスとセリヌンティウスの友情を称える。

このように、メロスとセリヌンティウスが中心的な人物であり、ディオニスが対極的な存在として描かれている。その他の登場人物も、物語の展開に重要な役割を果たしている。

ResponseTime: 3.60 secs
Usage: {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 10463, 'input_tokens': 452, 'output_tokens': 349}
========== USER ==========
300語であらすじを書いて
========== ASSISTANT ==========
以下のように、「走れメロス」の300語程度のあらすじを書きました。

村の牧人メロスは、妹の結婚式の準備のため、隣町のシラクスに向かう。しかし町は異様に静かで、人々は恐怖に怯えていた。老人から、暴君ディオニスが無実の人々を次々と殺害していると聞かされる。メロスは激怒し、ディオニスを倒すべく王城に乗り込むが、捕らえられてしまう。

ディオニスはメロスに、三日の猶予を与えて妹の結婚式に出席させ、その後処刑すると提案する。メロスは友人のセリヌンティウスを人質に立て、約束を守ると誓う。

メロスは必死に走り続け、困難に立ち向かうが、ついに疲れ果ててしまう。しかし、最後の力を振り絞り、ついに王城に到着する。しかし、すでにセリヌンティウスは磔刑に処されようとしていた。

メロスは必死に叫び、自分こそが処刑されるべき人間だと訴える。ディオニスはこの誠実さに感動し、二人の友情を称えて赦免する。メロスとセリヌンティウスは抱き合って涙を流し、ディオニスも二人の仲間に加わる。

この物語は、信頼と誠実さの勝利を描いた感動的な作品である。人間の弱さと強さ、善悪の複雑さを深く掘り下げながら、普遍的なテーマを描き出している。

ResponseTime: 5.19 secs
Usage: {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 10463, 'input_tokens': 817, 'output_tokens': 514}
========== USER ==========
quit
チャットを終了します。さようなら。

ということで比較してみる

プロンプトキャッシュなし

会話ターン 入力トークン レスポンス時間(秒)
1回目 10488 412
2回目 10925 3.97
3回目 11280 4.64

総入力トークン: 32693

プロンプトキャッシュあり

会話ターン 入力トークン キャッシュ作成入力トークン キャッシュ読み込み入力トークン レスポンス時間(秒)
1回目 25 10463 0 4.04
2回目 452 0 10463 3.60
3回目 817 0 10463 5.19

ポイントは

  • キャッシュ書き込みトークンは、ベース入力トークンより25%高い。
  • キャッシュ・リード・トークンはベース入力トークンより90%安い

というところになるので、プロンプトキャッシュ有効時の入力トークン数を相対的に計算するとこうかな。

入力トークン: 817
キャッシュ作成入力トークン: 10463 * 1.25 ≒ 13079
キャッシュ読み込み入力トークン: 20926 * 0.1 ≒ 2093
総入力トークン: 15989

3ターンでも半分ぐらいになるってことかー。

レスポンス時間はこれぐらいだと誤差レベルだと思う。ボリュームがもっと大きければ違いがでてくるんだろうと思う。

kun432kun432

注意すべき点とか

プロンプトキャッシュはプロンプト全体を見る

つまり、toolssystemはもちろんmessagesも。cache_controlで指定したブロックが対象になる

自分が書いたサンプルでは、システムプロンプトしかキャッシュ対象にしていないけども、毎回の送受信メッセージに"cache_control": {"type": "ephemeral"}を指定すれば会話履歴も全部キャッシュされるので常に指定しておいても良いってことになるのかなーと思ったけど、ここは後述。

キャッシュの有効期間は5分で、キャッシュされたコンテンツが使用されるたびに更新される

最初見たときは5分でキャッシュの有効期間が切れてしまうのかなと思ったけど、継続的にLLMとのやりとりが続く限りはキャッシュの効果がずっと期待できると考えて良さそう。

キャッシュ可能なプロンプトの最小長に満たない場合はキャッシュされない

Claude 3.5 Sonnet/Claude 3 Opus では1024トークン、Claude 3.0 Haikuは2048トークンがキャッシュ可能な最小トークン数となる。これに満たない場合はcache_controlが指定されていてもキャッシュされない。

システムプロンプトがでかい、みたいなパターンはまあわかりやすいのだけども。システムプロンプトがそれほどでもない場合はどう考えればいいのかな?例えば、単に会話履歴をキャッシュするみたいなケースだと、

  • system+messagesが最小トークンに満たない場合はキャッシュされない
  • 会話履歴が溜まって、最小トークンを超えだしたらキャッシュされる

というような感じで考えてもよいかな?

cache_controlパラメータを使用すると、最大4つのキャッシュブレークポイントを定義でき、異なる再利用可能なセクションを個別にキャッシュすることができる

この「最大4つのキャッシュブレークポイント」ってのが意味がわからなかった。公式ドキュメントの一番下にある"Prompt Caching examples"を見てみた。

まず、"Large Content caching example"。

response = client.beta.prompt_caching.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "あなたは、法律文書の分析を任務とするAIアシスタントです。"
        },
        {
            "type": "text",
            "text": "以下は、複雑な法的合意の全文です。: [実際はここに50ページの法的契約書の全文を挿入する]",
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[
        {
            "role": "user",
            "content": "What are the key terms and conditions in this agreement?"
        }
    ]
)

この場合、法律文書全文の1ブロックでキャッシュが指定されていて、ペルソナ設定部分はキャッシュされないように思えるが、説明を見る限りはシステムメッセージ全体がキャッシュされる、というふうに読める。

次に"Caching tool definition"。

response = client.beta.prompt_caching.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "指定した場所の現在の天気を取得する",
            "input_schema": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "市と州、例えば、 サンフランシスコ、CA"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度の単位、'celsius' または 'fahrenheit'"
                    }
                },
                "required": ["location"]
            },
        },
        # many more tools
        {
            "name": "get_time",
            "description": "指定したタイムゾーンにおける現在時刻を取得する",
            "input_schema": {
                "type": "object",
                "properties": {
                    "timezone": {
                        "type": "string",
                        "description": "IANAのタイムゾーン名、例えばAmerica/Los_Angeles"
                    }
                },
                "required": ["timezone"]
            },
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[
        {
            "role": "user",
            "content": "ニューヨークの天気と時刻は?"
        }
    ]
)

こちらの例でも、最後のツールだけキャッシュが有効になっているように思えるが、全てのツールがキャッシュ対象になるみたい。

最後の"Continuing a multi-turn conversation"。

response = client.beta.prompt_caching.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "...長いシステムプロンプト",
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[
        # ...ここまでの長い会話
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "こんにちは、太陽系についてもっと教えて?",
                    "cache_control": {"type": "ephemeral"}
                }
            ]
        },
        {
            "role": "assistant",
            "content": "もちろん!太陽系は、太陽の周りを回る天体の集合体です。太陽系は8つの惑星、多数の衛星、小惑星、彗星、その他の天体から構成されています。太陽に最も近い順に並べると、水星、金星、地球、火星、木星、土星、天王星、海王星となります。それぞれの惑星には独自の特性や特徴があります。太陽系について、特に知りたいことはありますか?"
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "火星について詳しく教えて。",
                    "cache_control": {"type": "ephemeral"}
                }
            ]
        }
    ]
)

これ説明見てもよくわからない。

システムプロンプトで1つ使っているよね。で、会話履歴の中でこの書き方だと2つ使うってことになる?となると、後続の会話履歴はどうなるのか?

cache_controlパラメータは、システムメッセージに静的プレフィックスの一部として指定するために配置されます。

ここはわかる。

会話履歴(過去のメッセージ)はメッセージ配列に含まれています。最後のターンには、フォローアップを継続するためにキャッシュコントロールのマークが付けられています。2番目に最後のユーザーメッセージには、キャッシュコントロールパラメータでキャッシュのマークが付けられており、このチェックポイントでは前のキャッシュから読み取ることができます。

このアプローチは、同じ情報を繰り返し処理することなく、進行中の会話の文脈を維持するのに役立ちます。

各リクエストについて:

  • input_tokens: 最小限になります
  • cache_creation_input_tokens: 新しいアシスタントとユーザーのターンを含みます
  • cache_read_input_tokens: 前回のターンまでの会話を含みます。

ここがよくわからない。

ということで、実際にやってみた

import anthropic
import time

client = anthropic.Anthropic()

system_message = [
    {
        "type": "text",
        "text": "あなたは文学作品の分析を任務とするAIアシスタントです。テーマ、キャラクター、文体に関する洞察に満ちた解説を提供することがあなたの目標です。"
    },
    {
        "type": "text", 
        "text": "<LITERALY_WORK>\n" + data + "\n</LITERALY_WORK>",
        "cache_control": {"type": "ephemeral"}  # キャッシュを有効化
    }
]

messages = []

print("チャットを開始します。やめるときは'quit'と入力してください。")
while True:
    user_input = input("========== USER ==========\n")
    if user_input.lower() == 'quit':
        print("チャットを終了します。さようなら。")
        break
    
    messages.append({
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": user_input,
                "cache_control": {"type": "ephemeral"}  # ユーザメッセージをキャッシュ
            }
        ]
    })
    
    start_time = time.time()
    response = client.beta.prompt_caching.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=1024,
        temperature=0,
        system=system_message,
        messages=messages
    )
    end_time = time.time()
    response_time = end_time - start_time

    assistant_response = response.content[0].text.strip()
    messages.append({"role": "assistant", "content": assistant_response})
    
    print("========== ASSISTANT ==========\n" + assistant_response + "\n")
    print(f"ResponseTime: {response_time:.2f} secs")
    print(f"Usage: {response.usage.dict()}")

結果。ちょっとやり取りは一部省略する。

チャットを開始します。やめるときは'quit'と入力してください。
========== USER ==========
「走れメロス」の主なテーマを分析して
========== ASSISTANT ==========
「走れメロス」の主なテーマは以下のように分析できます。
(snip)

ResponseTime: 4.38 secs
Usage: {'cache_creation_input_tokens': 10484, 'cache_read_input_tokens': 0, 'input_tokens': 4, 'output_tokens': 412}
========== USER ==========
登場人物をリストアップして
========== ASSISTANT ==========
「走れメロス」に登場する主な人物は以下の通りです。
(snip)

ResponseTime: 3.45 secs
Usage: {'cache_creation_input_tokens': 427, 'cache_read_input_tokens': 10484, 'input_tokens': 4, 'output_tokens': 349}
========== USER ==========
300語であらすじを書いて
========== ASSISTANT ==========
以下のように、「走れメロス」の300語程度のあらすじを書きました。
(snip)

ResponseTime: 4.90 secs
Usage: {'cache_creation_input_tokens': 365, 'cache_read_input_tokens': 10911, 'input_tokens': 4, 'output_tokens': 514}
========== USER ==========
登場人物の関係性をER図にしてみて
BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'A maximum of 4 blocks with cache_control may be provided. Found 5.'}}

なるほど、システムで1つ、会話のターンごとに単純にキャッシュコントロールを指定すると4ターン目で最大ブロック数を超えてエラーになると。

んー、単純な感じではなさそう。

kun432kun432

プロンプトキャッシュのnotebookが用意されていて、そこにマルチターンの会話履歴の増分キャッシュみたいなサンプルコードがあった。

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

class ConversationHistory:
    def __init__(self):
        # 会話のやり取りを保存するための空のリストを初期化
        self.turns = []

    def add_turn_assistant(self, content):
        # 会話履歴にアシスタントのターンを追加
        self.turns.append({
            "role": "assistant",
            "content": [
                {
                    "type": "text",
                    "text": content
                }
            ]
        })

    def add_turn_user(self, content):
        # 会話履歴にユーザーのターンを追加
        self.turns.append({
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": content
                }
            ]
        })

    def get_turns(self):
        # 特定のフォーマットで会話のやり取りを取得
        result = []
        user_turns_processed = 0
        # 順番を逆順で繰り返し
        for turn in reversed(self.turns):
            if turn["role"] == "user" and user_turns_processed < 2:
                # Add the last two user turns with ephemeral cache control
                # 最後の2つのユーザーのターンにcache_controlを追加
                result.append({
                    "role": "user",
                    "content": [
                        {
                            "type": "text",
                            "text": turn["content"][0]["text"],
                            "cache_control": {"type": "ephemeral"}
                        }
                    ]
                })
                user_turns_processed += 1
            else:
                # 他のターンをそのまま追加
                result.append(turn)
        # ターンを元の順番に戻す
        return list(reversed(result))

# 会話履歴を初期化
conversation_history = ConversationHistory()

# 書籍の内容を含むシステムメッセージ
# 注:'book_content'はコードの別の場所で定義する必要がある。
system_message = f"<file_contents> {book_content} </file_contents>"

# シミュレーション用の事前定義された質問
questions = [
    "What is the title of this novel?",
    "Who are Mr. and Mrs. Bennet?",
    "What is Netherfield Park?",
    "What is the main theme of this novel?"
]

def simulate_conversation():
    for i, question in enumerate(questions, 1):
        print(f"\nTurn {i}:")
        print(f"User: {question}")
        
        # 会話履歴にユーザー入力を追加
        conversation_history.add_turn_user(question)

        # パフォーマンス測定の開始時間を記録
        start_time = time.time()

        # アシスタントにAPIコール
        response = client.messages.create(
            model=MODEL_NAME,
            extra_headers={
              "anthropic-beta": "prompt-caching-2024-07-31"
            },
            max_tokens=300,
            system=[
                {"type": "text", "text": system_message, "cache_control": {"type": "ephemeral"}},
            ],
            messages=conversation_history.get_turns(),
        )

        # 終了時間を記録
        end_time = time.time()

        # アシスタントの回答を抽出
        assistant_reply = response.content[0].text
        print(f"Assistant: {assistant_reply}")

        # トークンの使用状況を出力
        input_tokens = response.usage.input_tokens
        output_tokens = response.usage.output_tokens
        input_tokens_cache_read = getattr(response.usage, 'cache_read_input_tokens', '---')
        input_tokens_cache_create = getattr(response.usage, 'cache_creation_input_tokens', '---')
        print(f"User input tokens: {input_tokens}")
        print(f"Output tokens: {output_tokens}")
        print(f"Input tokens (cache read): {input_tokens_cache_read}")
        print(f"Input tokens (cache write): {input_tokens_cache_create}")

        # 経過時間を計算して出力
        elapsed_time = end_time - start_time

        # 入力プロンプトのキャッシュの割合を計算
        total_input_tokens = input_tokens + (int(input_tokens_cache_read) if input_tokens_cache_read != '---' else 0)
        percentage_cached = (int(input_tokens_cache_read) / total_input_tokens * 100 if input_tokens_cache_read != '---' and total_input_tokens > 0 else 0)

        print(f"{percentage_cached:.1f}% of input prompt cached ({total_input_tokens} tokens)")
        print(f"Time taken: {elapsed_time:.2f} seconds")

        # アシスタントの応答を会話履歴に追加
        conversation_history.add_turn_assistant(assistant_reply)

# シミュレーションされた会話を実行
simulate_conversation()

なるほど、どうやら自分が書いたサンプルでは、ユーザーメッセージを組み立てる際にキャッシュコントロールを有効にして、それをそのまま会話履歴に追加してからまるっと送信していたのだけど、それだとキャッシュコントロールのブロック数がどんどん増えてしまう。

messages
[{'role': 'user',
  'content': [{'type': 'text',
    'text': '「走れメロス」の主なテーマを分析して',
    'cache_control': {'type': 'ephemeral'}}]},
 {'role': 'assistant',
  'content': '「走れメロス」の主なテーマは以下のように分析できます。\n\n1. 信頼と誠実さ\n- メロスは友人セリヌンティウスを人質に立てることを約束し、必ず期限までに戻ってくると誓う。この信頼関係が物語の核となっている。\n\n2. 正義と善良さの勝利\n- メロスは暴君ディオニスの残虐な行為に立ち向かい、自らの命を懸けて正義を貫く。最終的に善良さが勝利する。\n\n3. 人間性の探求\n- メロスの内面の葛藤や変化が描かれ、人間の弱さや強さ、善悪の複雑さが描かれている。\n\n4. 友情の絆\n- メロスとセリヌンティウスの深い友情が物語の中心にある。二人の絆は試練に耐え、最終的に強化される。\n\n5. 生きる意味の探求\n- メロスは自身の命を賭して正義を貫くが、その背景には生きる意味を見出そうとする姿勢がある。\n\n以上のように、「走れメロス」は信頼、正義、人間性、友情、生きる意味といった普遍的なテーマを扱っており、人間の内面を深く掘り下げた作品といえる。'},
 {'role': 'user',
  'content': [{'type': 'text',
    'text': '登場人物をリストアップして',
    'cache_control': {'type': 'ephemeral'}}]},
 {'role': 'assistant',
  'content': '「走れメロス」に登場する主な人物は以下の通りです。\n\n1. メロス\n- 主人公。村の牧人で、正義感と誠実さを持つ人物。\n\n2. セリヌンティウス\n- メロスの親友で、石工をしている。メロスを信じ続ける。\n\n3. ディオニス\n- 暴君。人々を疑い、無実の者を次々と殺害する。\n\n4. メロスの妹\n- メロスの大切な家族。メロスの誇りを受け継ぐ。\n\n5. 花婿の牧人\n- メロスの妹の婿となる人物。\n\n6. フィロストラトス\n- セリヌンティウスの弟子。メロスの到着が遅れていることを知らせる。\n\n7. 群衆\n- 物語の最後に登場し、メロスとセリヌンティウスの友情を称える。\n\nこのように、メロスとセリヌンティウスが中心的な人物であり、ディオニスが対極的な存在として描かれている。その他の登場人物も、物語の展開に重要な役割を果たしている。'},
 {'role': 'user',
  'content': [{'type': 'text',
    'text': '300語であらすじを書いて',
    'cache_control': {'type': 'ephemeral'}}]},
 {'role': 'assistant',
  'content': '以下のように、「走れメロス」の300語程度のあらすじを書きました。\n\n村の牧人メロスは、妹の結婚式の準備のため、隣町のシラクスに向かう。しかし町は異様に静かで、人々は恐怖に怯えていた。老人から、暴君ディオニスが無実の人々を次々と殺害していると聞かされる。メロスは激怒し、ディオニスを倒すべく王城に乗り込むが、捕らえられてしまう。\n\nディオニスはメロスに、三日の猶予を与えて妹の結婚式に出席させ、その後処刑すると提案する。メロスは友人のセリヌンティウスを人質に立て、約束を守ると誓う。\n\nメロスは必死に走り続け、困難に立ち向かうが、ついに疲れ果ててしまう。しかし、最後の力を振り絞り、ついに王城に到着する。しかし、すでにセリヌンティウスは磔刑に処されようとしていた。\n\nメロスは必死に叫び、自分こそが処刑されるべき人間だと訴える。ディオニスはこの誠実さに感動し、二人の友情を称えて赦免する。メロスとセリヌンティウスは抱き合って涙を流し、ディオニスも二人の仲間に加わる。\n\nこの物語は、信頼と誠実さの勝利を描いた感動的な作品である。人間の弱さと強さ、善悪の複雑さを深く掘り下げながら、普遍的なテーマを描き出している。'},
 {'role': 'user',
  'content': [{'type': 'text',
    'text': '登場人物の関係性をER図にしてみて',
    'cache_control': {'type': 'ephemeral'}}]}]

送信時に、最近のユーザーメッセージ2件分のブロックだけキャッシュコントロールを有効にしないとダメってことか。

ということで書き直してみた。

import anthropic
import time

client = anthropic.Anthropic()

system_message = [
    {
        "type": "text",
        "text": "あなたは文学作品の分析を任務とするAIアシスタントです。テーマ、キャラクター、文体に関する洞察に満ちた解説を提供することがあなたの目標です。"
    },
    {
        "type": "text", 
        "text": "<LITERALY_WORK>\n" + data + "\n</LITERALY_WORK>",
        "cache_control": {"type": "ephemeral"}  # キャッシュを有効化
    }
]

messages = []

print("チャットを開始します。やめるときは'quit'と入力してください。")
while True:
    user_input = input("========== USER ==========\n")
    if user_input.lower() == 'quit':
        print("チャットを終了します。さようなら。")
        break
    
    messages.append({
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": user_input,
            }
        ]
    })

    # 最近2件のユーザーメッセージのみキャッシュコントロールを有効化する
    result = []
    user_turns_processed = 0
    for msg in reversed(messages):
        if msg["role"] == "user" and user_turns_processed < 2:
            result.append({
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": msg["content"][0]["text"],
                        "cache_control": {"type": "ephemeral"}
                    }
                ]
            })
            user_turns_processed += 1
        else:
            result.append(msg)
    messages_to_send = list(reversed(result))

    start_time = time.time()
    # ベータのエンドポイントを使用する(anthropic-beta: prompt-caching-2024-07-31)
    response = client.beta.prompt_caching.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=1024,
        temperature=0,
        system=system_message,
        messages=messages_to_send
    )
    end_time = time.time()
    response_time = end_time - start_time

    assistant_response = response.content[0].text.strip()
    messages.append({"role": "assistant", "content": assistant_response})
    
    print("========== ASSISTANT ==========\n" + assistant_response + "\n")
    print(f"ResponseTime: {response_time:.2f} secs")
    print(f"Usage: {response.usage.dict()}")

結果。やり取りは一部省略。

チャットを開始します。やめるときは'quit'と入力してください。
========== USER ==========
「走れメロス」の主なテーマを分析して
========== ASSISTANT ==========
「走れメロス」の主なテーマは以下のように分析できます。
(snip)

ResponseTime: 4.37 secs
Usage: {'cache_creation_input_tokens': 10484, 'cache_read_input_tokens': 0, 'input_tokens': 4, 'output_tokens': 412}
========== USER ==========
登場人物をリストアップして
========== ASSISTANT ==========
「走れメロス」に登場する主な人物は以下の通りです。
(snip)

ResponseTime: 3.36 secs
Usage: {'cache_creation_input_tokens': 427, 'cache_read_input_tokens': 10484, 'input_tokens': 4, 'output_tokens': 349}
========== USER ==========
300語であらすじを書いて
========== ASSISTANT ==========
以下のように、「走れメロス」の300語程度のあらすじを書きました。
(snip)

ResponseTime: 5.37 secs
Usage: {'cache_creation_input_tokens': 365, 'cache_read_input_tokens': 10911, 'input_tokens': 4, 'output_tokens': 514}
========== USER ==========
登場人物の関係性をER図にしてみて
========== ASSISTANT ==========
「走れメロス」の登場人物の関係性をER図で表すと以下のようになります。
(snip)

ResponseTime: 4.54 secs
Usage: {'cache_creation_input_tokens': 535, 'cache_read_input_tokens': 11276, 'input_tokens': 4, 'output_tokens': 511}
========== USER ==========
起こった出来事を箇条書きで書いてみて
========== ASSISTANT ==========
「走れメロス」に描かれた主な出来事を箇条書きで以下のように整理しました。
(snip)

ResponseTime: 3.67 secs
Usage: {'cache_creation_input_tokens': 533, 'cache_read_input_tokens': 11811, 'input_tokens': 4, 'output_tokens': 430}
========== USER ==========
quit
チャットを終了します。さようなら。

これならば、systemで1つ、toolsで1つ、messagesで2つ、合計4つのブロックで収まると。

保持する会話履歴と送信する会話履歴を分けて考える必要があるってことね。

kun432kun432

所感

トークン数大きくなりがち、かつ、毎回送信するシステムプロンプトやツール定義なんかは、簡単にできて効果も高いと思うので、ここだけでもやる価値は大きい。

会話履歴のところはちょっとわかりにくかったけども、サンプルのnotebookが参考になった。こちらも会話が長くなればなるほど効果は高くなるはずなので、活用したい。

このスクラップは4ヶ月前にクローズされました