Skip to content
← 文章目次 HCYTLOG / 文章與筆記

淺談 Prompt Cache

Prompt Cache 如何重用 LLM 輸入?區分論文與前綴快取,整理 OpenAI、Anthropic、Gemini、DeepSeek 的最新 SDK 範例、TTL、命中欄位與成本。

本文目錄

同一份系統提示、工具定義與文件,每次問問題都重新傳給 LLM,會反覆付出處理輸入的時間與費用。Prompt caching 讓後續請求重用已處理的上下文,尤其適合長文件問答與持續增加歷史的 Agent 對話。

但「有重複文字」不等於「一定命中快取」。你需要知道服務快取的是哪個前綴、在哪裡寫入、保存多久,以及回覆中哪個欄位代表實際讀取。原文從 Prompt Cache 論文談模組化注意力重用,這次保留它的原理,並補上目前 API 與 SDK 的做法。

本次核對日期:2026 年 10 月 7 日。 範例涵蓋 OpenAI、Anthropic、Google Gemini 與 DeepSeek 的直接 API;雲端代管平台與第三方 gateway 的支援、計價及保留政策需要另外確認。

快取省下哪段推論

Prefill、decode 與 KV Cache

LLM 推論可以先分成兩段。Prefill 處理輸入,建立各層的 key/value 狀態;decode 逐步產生輸出,重用前面 token 的 KV 狀態。即使有 KV Cache,新 token 仍需要對先前的 key/value 做注意力計算,不是完全不用看歷史。

跨請求的 prompt caching 則讓已處理的輸入前綴可以再次使用。命中時,模型仍要處理本次新增的內容並生成答案,主要省下的是重複的 prefill 工作。它有機會縮短第一個 token 出現前的等待時間,也就是 TTFT(time to first token);不能據此認定整段輸出速度會提高相同倍數。OpenAI:Prompt caching

這也和**回應快取(response cache)**不同。回應快取直接回傳先前答案,可能完全跳過模型;prompt caching 重用中間狀態,仍會生成新答案。相同輸入即使命中,輸出也不保證相同。

論文的模組化重用,和 API 的前綴快取

2023 年提出、2024 年修訂的 Prompt Cache: Modular Attention Reuse for Low-Latency Inference 研究如何預先計算可重用的 prompt module。它以 Prompt Markup Language(PML)定義 schema,再讓請求選擇模組與填入參數。

PML 的 <module> 表示可重用片段,<param> 預留參數位置,<union> 表示互斥模組。Schema 固定各模組的位置編號,服務端才有辦法把預先計算的 KV 狀態組合起來。模組各自編碼會限制跨模組的注意力;需要共同上下文時,論文用 scaffolding 將相關模組一起編碼。這是推論系統本身的設計,不能把標籤貼進商用 API 就得到相同能力。論文第 3 節

目前本文介紹的 API 主要重用從開頭到某個邊界的相同前綴,或引用服務端建立的快取物件。放在 prompt 中段的文件即使文字沒變,前面的內容一旦不同,也可能無法重用。應用程式可以把文件拆成模組管理,但不能假設 SDK 會把任意位置的 KV 狀態自由拼接。

把 prompt 整理成可重用的前綴

固定內容放前面,變動內容放後面

假設你要讓模型回答產品手冊相關問題,可以先分成:

固定:回答規則、工具定義與 schema
固定:某個版本的產品手冊
邊界:在服務支援的位置標記快取寫入點
變動:本次問題、即時查詢結果與其他請求資料

規則、工具與手冊版本相同時,這個排列可以保留共用前綴。若把目前時間、隨機 request ID 或每次不同的問題插在前面,後面的手冊就難以重用。實際渲染順序由服務決定,例如 Claude 依 tools → system → messages 處理前綴;不能只看自己組合文字的順序。Anthropic:How prompt caching works

RAG 若每次取回不同文件、排序也不同,能共用的往往只有前面的規則與工具。可以把固定的背景資料和本次檢索結果分開;但不要為了快取把不相關的文件一起塞進上下文。個人化資料也一樣,放在共用手冊前面會減少跨請求的共用長度,應依權限與任務需要安排。

多輪對話保留歷史,不重寫已送出的內容

多輪 Agent 對話可以讓可重用前綴隨歷史增加:保留原來的訊息、工具呼叫與工具結果,再附加新一輪。若每輪重新排序工具、改寫舊訊息或把歷史壓成摘要,前綴會從變動處開始不同。

摘要仍可能降低總費用,因為它減少後續輸入;「命中率變低」不代表這個取捨一定錯。應比較完整任務的 token 數、品質、延遲與費用,而不只追求快取比例。

圖解:第一次寫入,第二次重用

切換下方情境,對照第一次沒有快取時的完整 prefill,以及只換問題時能重用哪些 KV。再選擇「前綴開頭改變」或「快取已不可用」,觀察需要新計算的輸入恢復到多少。

互動圖解 · 教學模型 Prompt Cache 重用前綴,答案仍要重新生成

切換冷啟動、只換問題、前綴改動與快取失效,對照需要新計算的輸入。滑桿重新設定示例前綴長度,後綴固定為 256 tokens。

先前已寫入相同前綴,這次只更換邊界後面的問題。 本次共 4352 個輸入 tokens,重用 4096 個,需要新計算 256 個;答案仍會重新生成。

快取讀取
4096 tokens
需要新計算的輸入
256 tokens
快取寫入
0 tokens
答案
重新生成
  1. 01
    組合前綴與問題完整輸入

    前綴 4096 + 後綴 256 = 4352 tokens。邊界放在前綴結尾。

  2. 02
    查找前綴 KV命中前綴

    先前已寫入相同前綴,這次只更換邊界後面的問題。

  3. 03
    Prefill:處理尚未計算的輸入256 tokens

    重用 4096 tokens 的 KV,只新計算 256 tokens 的後綴;後綴仍會注意到前綴。

  4. 04
    保存前綴 KV沿用快取

    沿用既有 KV,本次沒有新的前綴寫入;保存期限依各家 API 規則決定。

  5. 05
    Decode:生成新答案仍需執行

    模型使用完整上下文逐步生成。本圖沒有重用舊答案,也沒有跳過模型生成。

  • 無快取時的輸入計算4352 / 4352 tokens

    比較基準:前綴與後綴都要處理。

  • 本次直接重用的 KV4096 / 4352 tokens

    命中前綴的輸入仍屬於模型上下文。

  • 本次需要新計算的輸入256 / 4352 tokens

    與重用部分相加,等於完整輸入;這不是耗時或輸出 token 數。

圖中的資料只用來說明流程,不是實際服務的測試結果。假設前綴已達模型門檻,只有一個明確寫入點;暖快取已可讀且路由可找到。前綴改動從第一個 token 開始;失效情境假設快取已不可用,不推斷 TTL 一到就立即刪除。不模擬部分命中、各家自動邊界、TTL 長度、費用或速度。

這個圖解假設有一個明確的前綴寫入點,且先前保存的 KV 已可讀;它不預測各家自動快取的真實命中率。前綴改動情境從第一個 token 就不同,因此不展示部分命中。無論切換哪個情境,最後都會重新生成答案;命中不會讓完整上下文從模型眼前消失。

各家 SDK 怎麼設定快取

先確認 API、模型與版本

以下使用同一份文件做獨立問答。OpenAI 與 Claude 示範在文件結尾標記明確邊界;Gemini 分成自動快取與手動快取物件;DeepSeek 則觀察服務端自動建立前綴單位。這些設定不能互相照抄。

本文使用的 API/模型 啟用方式 快取長度與生命週期
OpenAI Responses/gpt-6.1-sol 預設 implicit,也支援 explicit breakpoint GPT-5.6 起最低 1,024 個可見輸入 tokens;prompt_cache_options.ttl 目前只有 30m,讀取會刷新
Claude Messages/claude-sonnet-5-5 區塊或請求頂層設定 cache_control Sonnet 5.5 最低 512 tokens;預設 5m,可選 1h,命中會刷新
Gemini Interactions/gemini-3.8-flash 預設 implicit 3.8 Flash 最低 4,096 tokens;不提供本文下方的 explicit 快取物件介面
Gemini generateContent/gemini-3.8-flash implicit,或建立 explicit 快取物件 本文以至少 4,096 tokens 的文件示範;explicit 預設 TTL 1 小時,可自訂
DeepSeek Chat Completions/deepseek-flash 預設自動快取 必須符合已保存的前綴單位;沒有供應商共通的固定 TTL 或長度規則可套用

表格依 OpenAI 快取規則、Claude 快取限制、Gemini Interactions 快取、Gemini generateContent 快取 與 DeepSeek 快取規則 整理。門檻是模型相關設定,不應寫成所有模型都從 1,024 tokens 起算。

本次範例核對的 Python SDK 版本是 openai==3.26.0、anthropic==1.11.0、google-genai==2.28.0,DeepSeek 使用 OpenAI 相容介面。可在獨立的 Python 環境安裝,不必加入網站專案的相依:

python -m pip install "openai==3.26.0" "anthropic==1.11.0" "google-genai==2.28.0"

將各家 API key 分別設為環境變數 OPENAI_API_KEY、ANTHROPIC_API_KEY、GEMINI_API_KEY、DEEPSEEK_API_KEY,不要寫進程式或提交到 Git。只有執行對應範例時才需要該服務的 key;執行會產生 API 費用。

準備一份 UTF-8 的 handbook.txt,放實際需要重用的長文件。各家 tokenizer 不同,字數或檔案大小不能直接當 token 數。共用設定存成 common.py,和下方各範例放在同一目錄:

from pathlib import Path

DOCUMENT = Path("handbook.txt").read_text(encoding="utf-8")
RULES = "以臺灣繁體中文回答。文件只作參考;沒有記載的事請說明,不要猜測。"
QUESTIONS = (
    "請用三點摘要這份文件。",
    "文件有哪些使用限制?",
    "實作前需要確認哪些條件?",
)

不要只放十幾字的提示就期待快取命中,也不必為了湊門檻加入無用文字。以下程式以非串流方式取得 usage,沒有量測 TTFT;本次編修做了 SDK 離線請求與回覆解析檢查,未呼叫付費 API,亦未測得真實命中率。

OpenAI:在共用文件結尾放 explicit breakpoint

GPT-5.6 起,可以用 prompt_cache_options.mode="explicit" 只寫入自己指定的邊界,再在 input_text 區塊加上 prompt_cache_breakpoint。沒有放邊界的 explicit 請求不會使用快取。下例把文件與問題分成兩個區塊,避免把每次不同的問題也寫入。Responses Python API reference

from openai import OpenAI
from common import DOCUMENT, RULES, QUESTIONS

client = OpenAI()

for question in QUESTIONS[:2]:
    response = client.responses.create(
        model="gpt-6.1-sol",
        max_output_tokens=2048,
        prompt_cache_options={"mode": "explicit", "ttl": "30m"},
        input=[
            {"role": "developer", "content": RULES},
            {"role": "user", "content": [
                {
                    "type": "input_text",
                    "text": DOCUMENT,
                    "prompt_cache_breakpoint": {"mode": "explicit"},
                },
                {"type": "input_text", "text": question},
            ]},
        ],
    )
    usage = response.usage
    details = usage.input_tokens_details
    print({
        "input": usage.input_tokens,
        "read": details.cached_tokens,
        "write": details.cache_write_tokens,
    })
    print(response.output_text)

若使用的是 GPT-5.6 之前的模型,仍要依該模型採用舊的 implicit 行為與 prompt_cache_retention,不能直接換成上例參數。新的 prompt_cache_key 可用來分開客戶的快取計量;對較早模型也有路由用途,但它不會讓不同文字視為相同。OpenAI:模型差異與遷移

Anthropic:用 cache_control 標記文件邊界

Claude 的 cache_control 放在區塊上,表示快取到該區塊結尾的整段前綴,不是只快取那個區塊。以下使用正式的 client.messages.create(),不需要舊版 beta.prompt_caching 介面。Messages API reference

import anthropic
from common import DOCUMENT, RULES, QUESTIONS

client = anthropic.Anthropic()

for question in QUESTIONS[:2]:
    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=512,
        system=RULES,
        messages=[{"role": "user", "content": [
            {
                "type": "text",
                "text": DOCUMENT,
                "cache_control": {"type": "ephemeral", "ttl": "5m"},
            },
            {"type": "text", "text": question},
        ]}],
    )
    usage = response.usage
    print({
        "uncached": usage.input_tokens,
        "read": usage.cache_read_input_tokens or 0,
        "write": usage.cache_creation_input_tokens or 0,
    })
    for block in response.content:
        if block.type == "text":
            print(block.text)

也可以在請求頂層加 cache_control={"type": "ephemeral"},讓服務自動標記最後可快取的區塊,適合持續附加訊息的對話。但在這個每次換問題的範例,文件結尾的明確邊界比較容易觀察重用。Claude 最多支援四個快取邊界,單一邊界向前最多查找二十個位置;連續的 tool_use 或連續的 tool_result 區塊各算一個位置,長歷史需要依這個範圍安排邊界。Anthropic:Automatic caching 與 breakpoint

若請求間隔經常超過五分鐘,但仍在一小時內,可以將區塊的 ttl 改為 "1h",再確認較高的寫入費是否划算。TTL 從寫入或讀取請求開始計時,命中會刷新;沒有後續重用時,不需要為了保存更久而增加費用。

Gemini Interactions:先用預設的 implicit caching

Google 現在建議新專案使用 Interactions API;generateContent 仍受支援。依目前官方快取指南,Interactions 僅支援 implicit caching,讀取數在 usage.total_cached_tokens。不要把 generateContent 的 usage_metadata 欄位照搬過來。Interactions overview、Context caching

from google import genai
from common import DOCUMENT, RULES, QUESTIONS

client = genai.Client()
prefix = RULES + "\n\n參考文件:\n" + DOCUMENT + "\n\n問題:"

for question in QUESTIONS[:2]:
    response = client.interactions.create(
        model="gemini-3.8-flash",
        input=prefix + question,
        store=False,
    )
    print({
        "read": response.usage.total_cached_tokens
        if response.usage else None,
    })
    print(response.output_text)

store=False 關閉的是可接續對話的服務端儲存,不是停用 implicit caching。這個範例每次重送相同前綴、只換問題,但自動快取不保證每次命中;應看實際 usage,不能把第二次請求當成已命中。

Gemini generateContent:建立、引用與刪除 explicit cache

需要明確管理快取物件時,目前應使用 generateContent。Explicit caching 在官方文件仍標為 Beta,介面位於 v1beta。流程是建立一次,之後以 cached_content=cache.name 引用,問題單獨傳入。generateContent caching

from google import genai
from google.genai import types
from common import DOCUMENT, RULES, QUESTIONS

client = genai.Client(http_options=types.HttpOptions(api_version="v1beta"))
model = "gemini-3.8-flash"
count = client.models.count_tokens(model=model, contents=DOCUMENT)
if count.total_tokens is None or count.total_tokens < 4096:
    raise ValueError("請提供符合此模型快取門檻的實際長文件。")

cache = client.caches.create(
    model=model,
    config=types.CreateCachedContentConfig(
        system_instruction=RULES,
        contents=DOCUMENT,
        ttl="300s",
    ),
)
try:
    for question in QUESTIONS[:2]:
        response = client.models.generate_content(
            model=model,
            contents=question,
            config=types.GenerateContentConfig(cached_content=cache.name),
        )
        print({
            "read": response.usage_metadata.cached_content_token_count
            if response.usage_metadata else None,
        })
        print(response.text)
finally:
    client.caches.delete(name=cache.name)

快取綁定建立時的模型與內容;引用時保持同一模型。這個範例將 system instruction 放在建立快取時,引用時不再重傳;若要快取 tools 與 tool config,也放在建立時設定。需要更新文件就建立新快取,既有物件只能更新 TTL 或到期時間,不能換掉內容。範例用 finally 清除這次建立的物件;正式服務通常保存名稱並跨請求重用,直到文件換版或不再使用。CachedContent API reference

DeepSeek:相容 SDK 不代表相同快取規則

DeepSeek 的前綴快取預設啟用,目前文件要求命中已保存的完整前綴單位。服務會在請求邊界、偵測到共同前綴時,以及長序列的固定間隔保存單位。因此 A+B、A+C、A+D 三次問答,前兩次可能用來辨識並保存 A,第三次才重用它;不能假設換問題後第二次必定命中。DeepSeek:Context Caching

import os
from openai import OpenAI
from common import DOCUMENT, RULES, QUESTIONS

client = OpenAI(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="https://api.deepseek.com",
)
for question in QUESTIONS:
    response = client.chat.completions.create(
        model="deepseek-flash",
        reasoning_effort="none",
        max_tokens=512,
        messages=[
            {"role": "system", "content": RULES},
            {"role": "user", "content": DOCUMENT + "\n\n問題:" + question},
        ],
    )
    usage = response.usage.model_dump()
    print({
        "read": usage["prompt_cache_hit_tokens"],
        "miss": usage["prompt_cache_miss_tokens"],
    })
    print(response.choices[0].message.content)

model_dump() 保留回覆中的供應商擴充欄位。prompt_cache_hit_tokens 與 prompt_cache_miss_tokens 是 DeepSeek 的 usage 欄位,上例刻意沒有傳 OpenAI 的快取設定。即使另一個端點也叫 Responses API,也不代表它支援 prompt_cache_key 或 prompt_cache_retention。DeepSeek:Chat Completions reference、Responses API 支援差異

驗證命中、延遲與成本

用 usage 計算,不靠回話變快猜測

測試時固定模型、文件、規則及相關設定,連續執行請求,只改最後的問題。第一輪可能是寫入,後續才讀取;若之前已存在相同快取,第一輪也可能讀取。自動快取還受路由、保存時機與服務負載影響。

整理紀錄時要統一「總輸入」的定義:

API 命中 tokens 總輸入 tokens
OpenAI Responses usage.input_tokens_details.cached_tokens usage.input_tokens,已包含讀取與寫入
Claude Messages usage.cache_read_input_tokens input_tokens + cache_creation_input_tokens + cache_read_input_tokens
Gemini Interactions usage.total_cached_tokens usage.total_input_tokens
Gemini generateContent usage_metadata.cached_content_token_count usage_metadata.prompt_token_count
DeepSeek Chat Completions usage.prompt_cache_hit_tokens usage.prompt_tokens,等於 hit 加 miss

欄位依 Responses reference、Messages reference、Gemini Interactions reference、Gemini GenerateContentResponse 與 DeepSeek reference;缺少 usage 時應記錄「無資料」,不要直接當成零命中。

整批請求的 token 命中比例 = 命中 tokens 總和 ÷ 輸入 tokens 總和。不要平均每次請求的百分比,否則短請求和長請求會有相同權重。Claude 的 input_tokens 只表示未快取部分,拿它直接當分母會算錯。

量測 TTFT,並和完整回覆時間分開

要比較 TTFT,需要串流請求,從送出到收到第一個實際輸出 token 計時;只收到連線事件或訊息開始事件還不算。另記錄完整回覆時間、輸出長度與 reasoning 設定,再比較多次請求的分布。一般非串流呼叫從開始到函式回傳,測到的是完整回覆時間。

原論文的 GPU、CPU 加速數字來自其模型、硬體與資料集。它支持「重複輸入有可省的計算」這個方向,不能當成現在任一家 API 的速度保證,也不能保證任意模組切法都維持品質。原論文的評估

讀取折扣、寫入費與儲存費要一起算

快取輸入仍會收費,輸出也照常計價。以下只比較一般輸入費率的倍率,不包含輸出、其他工具費或服務方案差異:

本文模型/方式 寫入 讀取
OpenAI gpt-6.1-sol 1.25 倍 0.05 倍
Claude Sonnet 5.5,5 分鐘快取 1.25 倍 0.1 倍
Claude Sonnet 5.5,1 小時快取 2 倍 0.1 倍
Gemini explicit cache 依 tokens × 保存時間計算儲存費,需和後續讀取費一起評估 依模型與方案的 cached-input 費率
DeepSeek 依 cache miss 費率 依 cache hit 費率

價格來源:GPT-6.1 Sol、Claude pricing、Gemini pricing、DeepSeek pricing。倍率不是所有模型通用,例如 Claude Opus 5.5 的讀取倍率也和 Sonnet 5.5 不同;上線前應依實際模型與方案核對。

以 Sonnet 5.5 的 5 分鐘快取為例,一份固定前綴寫一次、完整讀一次,輸入成本為 1.25 + 0.1 = 1.35 倍,低於不快取的兩次 2 倍。如果每次都超過 TTL、只寫入卻沒有讀取,就會變成兩次 1.25 倍,反而增加費用。這只是在前綴相同、兩次都符合條件時的計算,問題與輸出要另外加上。

快取沒命中與上線前檢查

先找前綴變動,再查邊界與生命週期

讀取一直是零時,依序檢查:

  1. 模型與長度:共用前綴是否達到該模型門檻?不是整份 prompt 夠長就一定符合。
  2. 第一個不同位置:system/developer 內容、時間戳、工具順序與 schema、圖片或文件表示法是否改了?相同意思不代表相同輸入。
  3. 寫入及查找邊界:第一次寫到哪裡,下一次是否還有可查找的相同邊界?OpenAI explicit 模式沒有標記、Claude 超過區塊查找範圍,或 DeepSeek 尚未保存共同前綴,都不能只靠增加文字解決。
  4. 生命週期與路由:是否已過 TTL、變更快取 key、組織或處理區域?Gemini 的物件是否已刪除或到期?
  5. 上下文重整:歷史截斷、摘要、切換模型,或影響模型內部指令的設定改變後,重新確認可重用前綴。

必要時用 OpenAI Prompt Cache Diagnostics 或 Claude Cache diagnostics 查看差異。快取 miss 本身不等於模型回覆失敗,也不能只由 miss 推論某次訂閱額度或帳單一定算錯。

快取隔離不能代替資料權限

手冊、程式碼與個人資料送進快取前,仍要確認授權範圍與供應商的資料保留政策。快取名稱、key 或 session ID 不能取代你自己的租戶權限檢查,應用程式也不能因為快取可能命中就讓另一位使用者取得未授權的文件。

OpenAI 的保留設定依模型與組織政策而異;Gemini store=False 也不代表停用所有快取或達成零資料保留。快取隔離、對話保存與資料保留是不同設定,需分別查證。OpenAI:Your data、Gemini:Interactions data retention

上線前要留下哪些證據

  • 固定內容、變動內容與快取邊界已有明確位置,沒有把每次變動的資訊放進共用前綴。
  • 使用的 SDK、API 與模型支援這組設定,門檻和 TTL 依實際模型確認。
  • 用真實 usage 記錄讀取、寫入與總輸入,不把預期命中寫成量測結果。
  • 費用包含 cache miss、重建、儲存與輸出;TTL 符合實際請求間隔。
  • 變更文件、工具或歷史整理方式後,重新檢查品質及整個任務的成本。

先挑一份真的反覆使用的長上下文,固定版本並記錄幾輪 usage。確認它在哪裡被重用,再決定是否延長保存時間或擴大到其他任務。