Prompt Caching 與 Batch:成本砍半
第 6 課學完 Streaming 和多模態之後,你的系統已經能即時回應、讀圖、讀 PDF。但如果把這套東西推上生產環境,第一個月帳單拿到手可能會嚇一跳。API 費用的殺手通常不是單次呼叫,而是兩個壞習慣:一是「每次都把同一份長文件重新送進去」,二是「幾千筆離線任務全部即時跑、但其實根本不需要即時」。這堂課把這兩個坑填掉。
用真實數字來感受問題規模:假設你用 claude-sonnet-4-6 做一個 RAG 問答系統,每次問答都把同一份 10,000 token 的產品手冊貼進 system prompt,一天有 500 個使用者各問 5 個問題。光是 system prompt 的重複輸入費,一天就是 500 × 5 × 10,000 ÷ 1,000,000 × $3.00 = $75,一個月就是 $2,250——什麼功能都沒做,光貼文件就燒這麼多錢。Prompt Caching 可以把這個數字砍到 $250 以內。
這堂學什麼
- Prompt Caching 的運作原理:前綴比對、cache 斷點、TTL 怎麼算
- cache write / cache read 的計費差異,以及何時「寫貴讀省」划算
- Batch API 的工作流程、50% 折扣的正確適用場景
- Python 和 JavaScript 的完整可跑範例,含 usage 欄位解讀與成本計算
- 兩者組合策略,以及真實帳單對比的完整計算
觀念一:Prompt Caching 怎麼運作
Claude 每次收到 request,都要把所有輸入的 token 從頭計算一遍——這叫做「context processing」。如果你的 system prompt 有 10,000 個 token、或每次都把同一份合約文件貼進去,這 10,000 個 token 的費用就會一筆一筆疊加,完全是重複付費。
Prompt Caching 的做法是讓你標記「這段內容請快取」。第一次呼叫是 cache write:Anthropic 計算並儲存這段前綴的中間狀態。之後只要你送進來的前綴完全一樣,API 就直接回傳 cache read——跳過重新計算,費用大幅下降。這個「完全一樣」很關鍵:快取比對的是位元組級別的精確比對,前綴只要差一個字元,就無法命中。

計費結構(以 claude-sonnet-4-6 為例)
| 動作 | 費用(每百萬 token) | 說明 |
|---|---|---|
| 一般輸入 | $3.00 | 沒有 caching 的基準 |
| Cache write(5 分鐘 TTL) | $3.75 | 首次寫入貴 25% |
| Cache write(1 小時 TTL) | $6.00 | 首次寫入貴 100% |
| Cache read(命中) | $0.30 | 只要基準的 10%,省 90% |
| 輸出 | $15.00 | 輸出費用不受 caching 影響 |
其他模型的 cache read 價格:claude-haiku-4-5 的 cache read 是 $0.10/MTok,claude-opus-4-8 是 $0.50/MTok,比例都是基準輸入的 10%。
快取有最短存活時間(TTL)。預設是 5 分鐘,每次 cache read 成功都會重置計時器,換句話說只要持續有人問,5 分鐘 TTL 的快取可以活很久。選用 1 小時 TTL 的話 cache write 費用翻倍,但適合不常呼叫、每次間隔超過 5 分鐘的場景,例如批次離線任務或低流量的夜間服務。
什麼時候划算? 計算很簡單。5 分鐘 TTL:cache write 比普通輸入貴 25%,只要同一段快取被讀取超過 1.25 次就開始省錢。1 小時 TTL:cache write 是普通輸入的 2 倍,超過 2 次才回本。對話系統的高頻呼叫幾乎瞬間就超過門檻;批次任務則要事先估算每份文件會被幾個 request 使用。

一個 request 最多四個 cache 斷點
你可以在一個 request 的不同位置各放一個 cache_control 標記,最多 4 個。這讓你能分層快取:例如把「通用 system prompt」放一個斷點、「這個客戶的歷史對話紀錄」放第二個斷點、「本次要分析的文件」放第三個斷點。每一層都是獨立的快取,命中各自算費用。實際上大多數情境用 1–2 個斷點就夠了。
觀念二:Batch API 怎麼運作
Batch API 讓你把一大批 request 打包成一個工作送出,Anthropic 在後台非同步處理,通常 1 小時內完成。代價是你沒辦法即時拿到結果——但因此省下整整 50% 的輸入和輸出費用。這個折扣是直接打在帳單上,不需要任何特別設定,只要透過 Batch API 送就自動適用。
每個批次最多可以包含 100,000 個 request,最多攜帶 256MB 的資料。Batch 有 24 小時的處理期限,超過就會被自動取消。
適合 Batch API 的場景:
- 離線資料分析:把 5,000 筆客戶評論一次分類,明天看結果沒問題
- 大量文件摘要:每晚跑前一天的業務報告,隔天早上進辦公室就有摘要
- 測試集評估:定期跑 benchmark 看模型表現
- 資料標注:微調前的大量樣本處理
不適合 Batch API 的場景:任何需要即時回應的互動。使用者在等你的聊天機器人回答,這個時候用 Batch 就是把人晾在那裡。

觀念三:組合策略——最高省 95%
兩者可以同時使用。以 claude-sonnet-4-6 為例,如果你有一個每天跑 3,000 筆、每筆都包含同一份 20,000 token 說明文件的批次任務:
| 策略 | 輸入費用估算(每天) | 較基準節省 |
|---|---|---|
| 無任何優化 | 3,000 × 20,000 ÷ 1M × $3.00 = $180 | — |
| 只用 Batch API | $180 × 0.5 = $90 | 50% |
| 只用 Prompt Caching | 首次 write $0.075 + 2,999 次 read $18.0 ≈ $18.08 | 90% |
| Batch + Cache read | ($0.075 × 0.5) + (2,999 × 20k ÷ 1M × $0.30 × 0.5) ≈ $9.04 | 95% |
從 $180 壓到 $9:帳單縮水 95%。這不是理論值——只要任務符合「有重複前綴、不需要即時」這兩個條件,就能實現。

手把手實戰
Step 1. 啟用 Prompt Caching,觀察 cache write 與 read
在需要快取的區塊最後加上 "cache_control": {"type": "ephemeral"}。以下範例用一份模擬的長合約文件:
import anthropic
client = anthropic.Anthropic()
# 模擬一份超長文件(真實情境可能是讀取 PDF 轉成的文字)
LONG_DOCUMENT = "這是合約條款全文..." + ("內容重複填充..." * 400)
def ask_about_contract(question: str):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是一位合約審閱助理,請根據使用者問題分析以下合約內容。",
},
{
"type": "text",
"text": LONG_DOCUMENT,
"cache_control": {"type": "ephemeral"}, # 快取斷點放在這裡
},
],
messages=[{"role": "user", "content": question}],
)
u = response.usage
print(f"問題: {question[:20]}...")
print(f" cache_write: {u.cache_creation_input_tokens}")
print(f" cache_read: {u.cache_read_input_tokens}")
print(f" input: {u.input_tokens}")
print(f" output: {u.output_tokens}")
return response.content[0].text
# 第一次:cache write(寫入快取,稍貴)
ask_about_contract("這份合約的違約條款在哪裡?")
# 第二次:cache read(命中快取,省 90%)
ask_about_contract("交貨期限是多少天?")
# 第三次:仍然 cache read
ask_about_contract("付款方式是什麼?")
執行後你會看到:第一次 cache_creation_input_tokens > 0、cache_read_input_tokens == 0。第二次起則反過來,cache_read_input_tokens > 0,代表省錢生效了。只要每次問問題的間隔在 5 分鐘以內,快取就會持續命中。
Step 2. 同樣功能的 JavaScript 版本
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const LONG_DOCUMENT = "合約條款全文...".padEnd(30000, "內容");
async function askAboutContract(question) {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: [
{
type: "text",
text: "你是一位合約審閱助理,請根據使用者問題分析以下合約內容。",
},
{
type: "text",
text: LONG_DOCUMENT,
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: question }],
});
const u = response.usage;
console.log(`write: ${u.cache_creation_input_tokens} | read: ${u.cache_read_input_tokens}`);
return response.content[0].text;
}
await askAboutContract("違約條款在哪裡?"); // cache write
await askAboutContract("交貨期限多少天?"); // cache read ✓
Step 3. 使用 1 小時 TTL(適合慢速批次場景)
如果你的任務間隔超過 5 分鐘,例如批次任務在後台排隊等待 30 分鐘才開始處理,5 分鐘 TTL 的快取可能在批次跑完前就過期了。這時改用 1 小時 TTL:
{
"type": "text",
"text": LONG_DOCUMENT,
"cache_control": {"type": "ephemeral", "ttl": "1h"}, # 1 小時 TTL
}
使用 1 小時 TTL 時,cache write 費用是 Sonnet 4.6 的 $6.00/MTok(基準的 2 倍)。只要在 1 小時內讀超過 2 次就回本。批次任務的每份文件通常對應多個分析問題,很容易超過這個門檻。
Step 4. 送出 Batch 任務
假設你要把 1,000 筆客服評論做情緒分類,不需要即時結果:
import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
client = anthropic.Anthropic()
feedbacks = [
{"id": "fb_001", "text": "商品很快到,包裝也很好,非常滿意!"},
{"id": "fb_002", "text": "客服沒有回應,等了三天都沒消沒息。"},
{"id": "fb_003", "text": "品質普通,跟預期差一點點。"},
# ... 更多筆,實際情況可能有幾千筆
]
SYSTEM_PROMPT = (
"你是客服分析師。請將以下回饋分類為「正面」、「負面」或「中性」,"
"只回傳一個詞,不要加其他說明。"
)
# 建立批次 request 清單
requests = [
Request(
custom_id=fb["id"], # 一定要設,取結果時靠它對應
params=MessageCreateParamsNonStreaming(
model="claude-haiku-4-5", # 分類用 Haiku 就夠,更省錢
max_tokens=10,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": fb["text"]}],
),
)
for fb in feedbacks
]
# 送出批次(立刻拿到 batch_id,但結果還沒好)
batch = client.messages.batches.create(requests=requests)
print(f"Batch ID: {batch.id}")
print(f"狀態: {batch.processing_status}") # in_progress
print(f"請求數量: {batch.request_counts.processing}")
# 重要:把 batch.id 存起來,程式重啟後還能查詢
with open("batch_id.txt", "w") as f:
f.write(batch.id)
注意:custom_id 是你自己取的識別碼。批次結果的回傳順序不保證和送入時一樣,一定要靠 custom_id 對應,不能靠位置。
Step 5. 輪詢狀態並取回結果
Batch 不是即時的,送出後要等。下面示範簡單的輪詢迴圈,實際生產環境建議改用獨立的 cron job,避免程式 crash 導致結果遺失:
import time
batch_id = open("batch_id.txt").read().strip()
# 持續輪詢直到完成
while True:
status = client.messages.batches.retrieve(batch_id)
counts = status.request_counts
print(
f"狀態: {status.processing_status} | "
f"完成: {counts.succeeded} | "
f"錯誤: {counts.errored} | "
f"處理中: {counts.processing}"
)
if status.processing_status == "ended":
break
time.sleep(30) # 每 30 秒查一次
# 取回每筆結果
results = {}
for entry in client.messages.batches.results(batch_id):
if entry.result.type == "succeeded":
label = entry.result.message.content[0].text.strip()
results[entry.custom_id] = label
else:
# 記錄失敗的 request,稍後可以重跑
results[entry.custom_id] = f"ERROR:{entry.result.error.type}"
for fb_id, label in results.items():
print(f"{fb_id}: {label}")
Step 6. Batch API JavaScript 版本
import Anthropic from "@anthropic-ai/sdk";
import fs from "fs";
const client = new Anthropic();
async function runFeedbackBatch(feedbacks) {
const requests = feedbacks.map((fb) => ({
custom_id: fb.id,
params: {
model: "claude-haiku-4-5",
max_tokens: 10,
system: "你是客服分析師。請將以下回饋分類為「正面」、「負面」或「中性」,只回傳一個詞。",
messages: [{ role: "user", content: fb.text }],
},
}));
const batch = await client.messages.batches.create({ requests });
console.log("Batch ID:", batch.id);
fs.writeFileSync("batch_id.txt", batch.id);
// 輪詢
let current = batch;
while (current.processing_status !== "ended") {
await new Promise((r) => setTimeout(r, 30000));
current = await client.messages.batches.retrieve(batch.id);
const c = current.request_counts;
console.log(`完成: ${c.succeeded} | 錯誤: ${c.errored} | 處理中: ${c.processing}`);
}
// 取結果
const results = {};
for await (const entry of await client.messages.batches.results(batch.id)) {
results[entry.custom_id] =
entry.result.type === "succeeded"
? entry.result.message.content[0].text.trim()
: `ERROR:${entry.result.error.type}`;
}
return results;
}
Step 7. 終極組合:Batch + Prompt Caching
如果批次裡每筆 request 都包含同一份長文件,把 cache_control 標上去——Batch API 和 Prompt Caching 完全相容:
REFERENCE_DOC = "產品規格書全文..." * 100 # 模擬 20,000 token 文件
requests = [
Request(
custom_id=f"q_{i}",
params=MessageCreateParamsNonStreaming(
model="claude-sonnet-4-6",
max_tokens=256,
system=[
{
"type": "text",
"text": "你是產品評測助理,根據以下規格書回答問題。",
},
{
"type": "text",
"text": REFERENCE_DOC,
# Batch 任務用 1h TTL,避免排隊等待期間快取過期
"cache_control": {"type": "ephemeral", "ttl": "1h"},
},
],
messages=[{"role": "user", "content": question}],
),
)
for i, question in enumerate(questions)
]
batch = client.messages.batches.create(requests=requests)
批次跑完後,查每筆結果的 usage.cache_read_input_tokens——如果大部分都大於 0,代表組合策略生效了。
帳單對比:同任務開關 caching 的計算
以下是一個具體情境:用 claude-sonnet-4-6 回答同一份 8,000 token 說明文件的問題,50 個使用者各問 1 個問題。
| 沒有 caching | 有 caching(5分鐘 TTL) | |
|---|---|---|
| system prompt 輸入 | 50 × 8,000 = 400,000 tokens | — |
| cache write | — | 第 1 次:8,000 tokens |
| cache read | — | 第 2–50 次:49 × 8,000 = 392,000 tokens |
| 輸入費用 | 400,000 ÷ 1M × $3.00 = $1.200 | 8,000 ÷ 1M × $3.75 + 392,000 ÷ 1M × $0.30 = $0.148 |
| 節省 | — | 88% |
實際上 50 個使用者在 5 分鐘內都問到的可能性很高——這是保守估算,現實中命中率往往更好。
要在程式裡追蹤這個數字,可以在每次呼叫後記錄 usage:
def log_usage_cost(response, task_name: str):
u = response.usage
# claude-sonnet-4-6 計費(2026 年 7 月)
cost = (
u.input_tokens * 3.00 / 1_000_000
+ u.cache_creation_input_tokens * 3.75 / 1_000_000
+ u.cache_read_input_tokens * 0.30 / 1_000_000
+ u.output_tokens * 15.00 / 1_000_000
)
print(
f"[{task_name}] "
f"write={u.cache_creation_input_tokens:,} "
f"read={u.cache_read_input_tokens:,} "
f"out={u.output_tokens:,} "
f"cost=${cost:.5f}"
)
把這個 function 嵌進你的每個 API 呼叫,就有免費的成本監控。cache_read_input_tokens / (cache_read_input_tokens + cache_creation_input_tokens) 就是你的快取命中率,這個比例愈高、省得愈多。

常見坑
坑 1:cache_control 放錯位置,每次都是 write
症狀:每次呼叫都看到 cache_creation_input_tokens > 0、cache_read_input_tokens 永遠是 0。費用沒有下降,甚至稍微變貴了。
原因幾乎都是快取前綴每次不完全一樣:
- 把
cache_control標在messages裡的 user message,而 user message 每次都不同,當然無法命中 - system 的靜態文字前面插入了動態內容,例如當下的日期、使用者 ID、session 流水號——這讓前綴每次都不一樣
解法:所有靜態、不變的內容放進 system 並加 cache_control。動態的、每次都不同的部分一律放進 messages。用 cache_read_input_tokens > 0 當作快取命中的健康指標。
坑 2:Batch 結果有 errored 但沒被察覺
批次整體狀態顯示 ended,但 request_counts.errored > 0——代表有些 request 失敗了。如果你只看整體狀態就跳過,這些失敗的 request 和它們的資料就無聲無息地消失了。
# 你以為全部成功,實際上有 37 筆失敗
request_counts.succeeded = 963
request_counts.errored = 37
解法:取結果時一定要逐筆檢查 entry.result.type,遇到 "errored" 就記錄到失敗清單,之後重跑:
failed_ids = []
for entry in client.messages.batches.results(batch_id):
if entry.result.type == "errored":
print(f"失敗: {entry.custom_id} | {entry.result.error.type}")
failed_ids.append(entry.custom_id)
常見的 error.type:invalid_request(你的 request 格式有問題,要修再重送)、overloaded(伺服器繁忙,可以直接重試)。
坑 3:Batch 任務的 batch_id 搞丟了
送出批次後,如果你沒有把 batch.id 存下來,程式重啟就找不回來了——因為批次是非同步的,你需要拿著 ID 去查結果。最糟糕的情況是批次其實跑完了,但你不知道要去哪裡取。
解法:送出後立刻把 batch.id 持久化儲存(資料庫、檔案都行)。萬一真的找不到 ID,可以用 batches.list() 列出最近的批次:
# 找出所有還在跑或已完成的批次
for b in client.messages.batches.list():
print(f"ID: {b.id} | 狀態: {b.processing_status} | 建立時間: {b.created_at}")
坑 4:把即時任務改成 Batch,使用者在等結果
有人為了省 50%,把所有 API 呼叫都換成 Batch。但聊天機器人、即時分析這些場景,使用者點送出就要等你回覆——Batch 可能要幾十分鐘才給結果,這樣的使用者體驗是災難。
判斷標準很清楚:「使用者或系統在這一次操作的當下需要等待這個結果嗎?」是的話就用普通 Messages API;不需要即時才用 Batch。離線批次分析、隔天早上才看的報告、測試集評估——這些都適合。

作業
把第 3 課的多輪對話系統加上 caching:把 system prompt 標上
cache_control,連問三個問題,印出每次的cache_read_input_tokens——確認第二、三次都有命中。自己模擬一個批次情境:準備 10 句話,用 Batch API 送出分類任務,等結果,用
custom_id對應輸出。刻意在其中一筆的 request 把max_tokens設成-1(非法值,會觸發invalid_request錯誤),觀察result.type == "errored"時的錯誤訊息長什麼樣。進階作業:把作業 1 和作業 2 組合——帶長文件、同時使用 Prompt Caching(1h TTL)的批次任務,確認結果裡有
cache_read_input_tokens > 0。在你的 API 呼叫流程裡加上
log_usage_cost函式,跑 20 次問答,計算你的快取命中率。
下一課預告
成本控制做好了,整個 Claude API 的核心功能你都學完了:呼叫、選模型、對話管理、結構化輸出、Tool Use、Streaming、省錢優化。第 8 課是本課程的壓軸:綜合實戰:客服 API 服務上線。我們會從零開始,把這七堂課學過的技術組裝成一個真實的客服後端:多輪對話加上 Tool Use 查訂單狀態、Streaming 即時輸出、Prompt Caching 讓 FAQ 文件只讀入一次、最後部署到能接真實流量的環境。讀者買的是整合能力,最後一課就是把這個能力完整走一遍。