Closed5

OpenAIのPrompt Cachingを試す

kun432kun432

https://twitter.com/OpenAIDevs/status/1841175759186248029

ドキュメント。気になるところだけピックアップ。

https://platform.openai.com/docs/guides/prompt-caching

モデルのプロンプトには、システムプロンプトや一般的な指示など、繰り返し使用されるコンテンツが含まれることが多い。OpenAIは、最近同じプロンプトを処理したサーバーにAPIリクエストをルーティングし、ゼロからプロンプトを処理するよりも安価かつ迅速に処理する。これにより、長いプロンプトではレイテンシを最大80%、コストを50%削減できる。プロンプトキャッシュは、すべてのAPIリクエストに対して自動的に機能し(コードの変更は不要)、追加料金は発生しない。

プロンプトキャッシュは、以下のモデルで有効になっている。

  • gpt-4o
  • gpt-4o-mini
  • o1-preview
  • o1-mini

自動的に適用されるってのがすごい。

プロンプトの構造

キャッシュヒットは、プロンプト内で正確な接頭辞が一致した場合のみ可能である。キャッシュの利点を活用するには、指示や例などの静的コンテンツをプロンプトの冒頭に配置し、ユーザー固有の情報などの可変コンテンツを末尾に配置する。これは画像やツールにも適用され、リクエスト間で同一でなければならない。

キャッシュさせたいものを前に持ってくる必要がある。

キャッシュは、1024トークン以上のプロンプトに対して自動的に有効になります。

一定量のトークンがないとキャッシュされない

キャッシュされた接頭辞は、通常 5 分から 10 分間は有効である。 ただし、閑散期にはキャッシュが最大1時間持続することがあります。

んー、キャッシュを維持し続けるってのはできないってことかな?

キャッシュできるもの

  • Messages: システム、ユーザー、アシスタントのインタラクションを含む、完全なメッセージ配列。
  • Images: ユーザーメッセージに含まれる画像。リンクとして、またはbase64エンコードされたデータとして、複数の画像を送信することができる。 画像のトークン化に影響するため、detailパラメータが同じに設定されていることを確認してください。
  • Tool usase: メッセージの配列と利用可能なツールのリストの両方がキャッシュ可能で、最小1024トークン要件に貢献する。
  • Structured Output: 構造化出力スキーマはシステムメッセージのプレフィックスとして機能し、キャッシュすることができます。

ほぼほぼ全部可能ってことね。

ベストプラクティス

  • 冒頭に静的または繰り返されるコンテンツ、最後に動的コンテンツを配置するようにプロンプトを構築する。
  • キャッシュヒット率、待ち時間、キャッシュされたトークンの割合などの指標をモニタリングして、プロンプトとキャッシュ戦略を最適化する。
  • キャッシュヒット率を高めるには、プロンプトを長くし、ピーク時間帯以外の時間帯にAPIリクエストを行う。ピーク時間帯にはキャッシュの削除が頻繁に行われるため。
  • 最近使用されていないプロンプトは自動的にキャッシュから削除される。キャッシュ削除を最小限に抑えるには、同じプロンプトの接頭辞を持つ一貫したリクエストストリームを維持する。

キャッシュされているかの確認の仕方は実際のコードで確認する。

kun432kun432

ちょっと気になったところ。

よくある質問

  1. キャッシュのデータプライバシーはどのように維持されるのか?

プロンプトキャッシュは(複数の)組織間で共有されない。同一のプロンプトのキャッシュにアクセスできるのは、同一の組織のメンバーのみである。

これ逆に言うと、組織内であれば、別の人が同じプロンプトを使った場合にキャッシュが利用されうるってことになる。んー、テストで同じことを同じ組織内の複数の人が繰り返したとしても、同じ回答になってしまう可能性があるってことなのかな?

  1. プロンプトキャッシングは、出力トークンの生成やAPIの最終的なレスポンスに影響を与えるか?

プロンプトキャッシングは、出力トークンの生成やAPIが提供する最終的なレスポンスに影響を与えない。キャッシングを使用しているかどうかに関わらず、生成される出力は同一(訳注: 原文ではidentical)である。これは、プロンプト自体のみがキャッシングされ、実際のレスポンスは、キャッシングされたプロンプトに基づいて毎回新たに計算されるためである。

identicalってのは同じものが生成されるっていう意味ではなくて、品質や計算プロセスに変化がない、と言うことなんだろうと思う。なるほど、プロンプトだけってことね。

  1. 手動でキャッシュをクリアする方法はあるか?

手動でのキャッシュのクリアは現在利用できない。最近遭遇していないプロンプトは自動的にキャッシュからクリアされる。典型的なキャッシュのクリアは、5分から10分間操作がない場合に発生するが、オフピーク時には最大1時間続くこともある。

ここは待つしかないが、叩いてみないとキャッシュなのかわからないよね。。。

  1. プロンプトキャッシュへの書き込みには追加料金がかかるのか?

いいえ。キャッシュは自動的に行われ、明示的な操作やキャッシュ機能の利用に対する追加料金は発生しない。

料金についてこちらにあった。キャッシュされている場合は50%ディスカウントになる。

https://openai.com/index/api-prompt-caching/

キャッシュされていない入力トークン キャッシュされている入力トークン 出力トークン
gpt-4o-2024-08-06 $2.50 $1.25 $10.00
GPT-4o fine-tuning $3.75 $1.875 $15.00
gpt-4o-mini-2024-07-18 $0.15 $0.075 $0.60
GPT-4o mini fine-tuning $0.30 $0.15 $1.20
o1-preview $15.00 $7.50 $60.00
o1 mini $3.00 $1.50 $12.00
  1. キャッシュされたプロンプトは TPM レートの制限に影響しますか?

はい。キャッシュとしては、レート制限に影響しません。

ここは少し解釈が間違っているかもしれないけども、要はキャッシュされているかいないかということは関係なく、TPMとしてはカウントするよ、これまでと何も変わらないよ、と言うことだよね???

kun432kun432

ということでコード。Colaboratoryで確認した。

パッケージインストール。数時間前にアップデートされてた。

!pip install -q -U openai
!pip freeze | grep openai
openai==1.51.0

APIキーをセット

from google.colab import userdata
import os

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

キャッシュを使いたいので多めにプロンプトを構成する。以前AnthropicのPrompt Cachingの際に、青空文庫の「走れメロス」を読み込んでQAするサンプルを使ったので、同じことをやってみる。

参考)
https://zenn.dev/kun432/scraps/77174bedc28472

「走れメロス」をテキスト化する。

! 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)
from openai import OpenAI 
import json

system_prompt = f"""\
あなたは与えられた文学作品の分析を任務とするAIアシスタントです。テーマ、キャラクター、文体に関する洞察に満ちた解説を提供することがあなたの目標です。

## 対象の文学作品

{data}

"""

messages = [
    {
        "role": "system",
        "content": system_prompt
    }
]

client = OpenAI()

while True:
    user_input = input("USER: ")
    if user_input.lower() == 'quit':
        print("チャットを終了します。さようなら。")
        break
    
    messages.append({"role": "user", "content": user_input})
    
    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=messages,
        temperature=0.1,
    )
    
    assistant_response = response.choices[0].message.content
    messages.append({"role": "assistant", "content": assistant_response})
    
    print(f"ASSISTANT: {assistant_response}")

    # キャッシュ使用状況を確認
    print(f"----- Usage -----")
    print(json.dumps(response.usage.dict(), indent=2))
    print(f"-----------------")

実行してマルチターンで複数回やりとりしてみる。

1回目

USER: 「走れメロス」の主なテーマを分析して
ASSISTANT: 「走れメロス」の主なテーマは、「信頼と友情」、「誠実さと人間の本質」、そして「自己犠牲と名誉」です。

1. **信頼と友情**:
   - 物語の中心には、メロスとセリヌンティウスの友情があります。メロスは、妹の結婚式のために一時的に自由を求め、セリヌンティウスを人質として残します。この行動は、彼が友人をどれほど信頼しているか、そして友人が彼をどれほど信じているかを示しています。友情の力が、メロスを困難を乗り越えさせ、最終的に彼を約束の場所に導きます。

2. **誠実さと人間の本質**:
   - メロスは、誠実さを貫くために命を懸けます。彼は、暴君ディオニスに対して人間の誠実さを証明しようとします。物語を通じて、メロスは人間の本質が信頼に値するものであることを示そうとし、最終的にディオニスの心を変えることに成功します。

3. **自己犠牲と名誉**:
   - メロスは、自分の命を犠牲にしてでも約束を守ろうとします。彼の行動は、名誉と誠実さを守るための自己犠牲の精神を強調しています。彼は、友人を救うために走り続け、最終的に自分の信念を証明します。

これらのテーマは、物語全体を通じて織り込まれ、読者に深い感動を与えます。メロスの行動は、信頼と誠実さが人間関係においてどれほど重要であるかを強調し、またそれがどのようにして人々を変える力を持っているかを示しています。
----- Usage -----
{
  "completion_tokens": 483,
  "prompt_tokens": 8721,
  "total_tokens": 9204,
  "completion_tokens_details": {
    "audio_tokens": null,
    "reasoning_tokens": 0
  },
  "prompt_tokens_details": {
    "audio_tokens": null,
    "cached_tokens": 0
  }
}
-----------------

chat completionsオブジェクトのusageに各種トークンの情報が含まれているが、 prompt_tokens_details.cached_tokensがキャッシュヒットしたトークン数らしい。入力トークンはprompt_tokensだから、cached_tokens / prompt_tokens` でキャッシュヒット率になるよね。

1回目は当然ながらキャッシュヒットはしていない。

2回目

USER: 登場人物をリストアップして
ASSISTANT: 「走れメロス」の主な登場人物は以下の通りです:

1. **メロス**:
   - 物語の主人公で、誠実で勇敢な青年。妹の結婚式のために一時的に自由を求め、友人セリヌンティウスを人質として残します。彼は友情と信頼を証明するために走り続けます。

2. **セリヌンティウス**:
   - メロスの無二の友人で、石工。メロスの代わりに人質となり、彼を信じて待ち続けます。彼の信頼は物語の中心的な要素です。

3. **ディオニス王**:
   - シラクスの暴君で、人を信じることができず、疑心暗鬼に陥っています。メロスの行動を通じて、人間の誠実さを知り、心を変えます。

4. **メロスの妹**:
   - メロスと二人で暮らしている内気な少女。物語の冒頭で結婚を控えており、メロスが市に出かける理由となります。

5. **フィロストラトス**:
   - セリヌンティウスの弟子で、メロスに彼の友人が危機に瀕していることを知らせるために走ります。

これらのキャラクターは、物語のテーマである信頼、友情、誠実さを強調するために重要な役割を果たしています。
----- Usage -----
{
  "completion_tokens": 376,
  "prompt_tokens": 9220,
  "total_tokens": 9596,
  "completion_tokens_details": {
    "audio_tokens": null,
    "reasoning_tokens": 0
  },
  "prompt_tokens_details": {
    "audio_tokens": null,
    "cached_tokens": 9088
  }
}
-----------------

cached_tokensが増えている。なお、キャッシュトークンは、入力トークンが1024トークン以上・128トークン単位で計算されるらしい。

3回目

USER: 300語であらすじを書いて
ASSISTANT: 「走れメロス」は、古代ギリシャを舞台にした物語で、誠実さと友情を描いています。主人公のメロスは、妹の結婚式の準備のためにシラクスの市を訪れますが、そこで暴君ディオニスが人々を不当に処刑していることを知り、激怒します。メロスはディオニスを討とうとしますが、捕らえられてしまいます。

メロスは、自分の誠実さを証明するために、妹の結婚式を終えるまでの三日間の猶予をディオニスに求めます。彼は、友人セリヌンティウスを人質として残し、三日目の日没までに戻らなければセリヌンティウスが処刑されるという条件を受け入れます。

メロスは村に戻り、妹の結婚式を無事に終えますが、帰路で様々な困難に直面します。豪雨による川の氾濫や山賊の襲撃に遭い、疲労困憊しながらも、彼は友人との約束を果たすために走り続けます。途中で絶望に陥りかけますが、再び立ち上がり、最後の力を振り絞ってシラクスに向かいます。

日没直前、メロスは刑場に到着し、セリヌンティウスの処刑を間一髪で阻止します。二人は互いの信頼を確認し合い、抱擁します。ディオニス王は、メロスの誠実さに心を打たれ、彼らの友情を認め、自らもその仲間に加えてほしいと願います。物語は、信頼と誠実さが人間関係においていかに重要であるかを強調し、感動的な結末を迎えます。
----- Usage -----
{
  "completion_tokens": 479,
  "prompt_tokens": 9613,
  "total_tokens": 10092,
  "completion_tokens_details": {
    "audio_tokens": null,
    "reasoning_tokens": 0
  },
  "prompt_tokens_details": {
    "audio_tokens": null,
    "cached_tokens": 9472
  }
}
-----------------
USER: quit
チャットを終了します。さようなら。

今回の例だとシステムプロンプトに大きめのデータが含まれていて、マイリクエスト毎にずっとやりとりし続けることになるので、かなりキャッシュが効くことになると思う。

kun432kun432

1024トークンに満たないようなやりとりだとトークンが効かないこともわかる。

from openai import OpenAI 
import json

messages = [
    {
        "role": "system",
        "content": "あなたは親切な日本語のアシスタントです。"
    }
]

client = OpenAI()

while True:
    user_input = input("USER: ")
    if user_input.lower() == 'quit':
        print("チャットを終了します。さようなら。")
        break
    
    messages.append({"role": "user", "content": user_input})
    
    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=messages,
        temperature=0.1,
    )
    
    assistant_response = response.choices[0].message.content
    messages.append({"role": "assistant", "content": assistant_response})
    
    print(f"ASSISTANT: {assistant_response}")

    # キャッシュ使用状況を確認
    print(f"----- Usage -----")
    print(json.dumps(response.usage.dict(), indent=2))
    print(f"-----------------")
USER: こんにちは!
ASSISTANT: こんにちは!今日はどのようなお手伝いができますか?
----- Usage -----
{
  "completion_tokens": 14,
  "prompt_tokens": 27,
  "total_tokens": 41,
  "completion_tokens_details": {
    "audio_tokens": null,
    "reasoning_tokens": 0
  },
  "prompt_tokens_details": {
    "audio_tokens": null,
    "cached_tokens": 0
  }
}
-----------------
USER: 日本の総理大臣は誰?
ASSISTANT: 2023年10月時点で、日本の総理大臣は岸田文雄(きしだ ふみお)氏です。何か他に知りたいことがあれば教えてください!
----- Usage -----
{
  "completion_tokens": 46,
  "prompt_tokens": 58,
  "total_tokens": 104,
  "completion_tokens_details": {
    "audio_tokens": null,
    "reasoning_tokens": 0
  },
  "prompt_tokens_details": {
    "audio_tokens": null,
    "cached_tokens": 0
  }
}
-----------------
USER: 岸田さんについて詳しく教えて。
ASSISTANT: 岸田文雄(きしだ ふみお)氏は、日本の政治家で、自由民主党(自民党)に所属しています。2021年10月に第100代内閣総理大臣に就任しました。以下は岸田氏に関するいくつかの詳細情報です。

1. **生年月日と出身地**: 岸田文雄氏は1957年7月29日に東京都で生まれましたが、家族のルーツは広島県です。

2. **教育**: 早稲田大学法学部を卒業しています。

3. **政治経歴**: 
   - 1993年に初めて衆議院議員に当選しました。
   - 外務大臣として、2012年から2017年まで安倍晋三内閣で務めました。
   - 自民党内では、宏池会(こうちかい)という派閥の会長を務めています。

4. **政策とビジョン**: 
   - 「新しい資本主義」を掲げ、経済成長と分配のバランスを重視しています。
   - 外交面では、日米同盟の強化や地域の安定に力を入れています。

5. **個人的な背景**: 
   - 政治家一家に生まれ、父親も政治家でした。
   - 趣味は読書やゴルフで、特に歴史書を好むと言われています。

岸田氏についてさらに詳しい情報が必要であれば、具体的なトピックを教えてください。
----- Usage -----
{
  "completion_tokens": 368,
  "prompt_tokens": 121,
  "total_tokens": 489,
  "completion_tokens_details": {
    "audio_tokens": null,
    "reasoning_tokens": 0
  },
  "prompt_tokens_details": {
    "audio_tokens": null,
    "cached_tokens": 0
  }
}
-----------------
USER: quit
チャットを終了します。さようなら。
kun432kun432

まとめ

コード側で何もしなくても自動でキャッシュが効くので、レスポンスも早くなる・コストも安くなる、ならば、実質値下げみたいなもんだよね。

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