成本控制:省錢是一門工程藝術
本篇是「Claude API & Agent SDK 完全指南」系列的第 13 / 15 篇。你可以從系列總覽開始閱讀,也可以直接接著看本文。
我曾經有一個月收到 $2,341 的 Anthropic 帳單。
那個時候,我在做一個內部知識庫的 RAG 系統,大概有三十幾個人在用。$2,341 除以 35 人,每個人每個月大概 $67——這個成本在企業環境裡看起來還好,但我的老闆覺得太貴了。
他說:「這個系統有沒有辦法變成 $500 以內?」
我花了兩週,把費用降到了 $287。這一章我要告訴你我是怎麼做到的。
為什麼成本優化是 AI 應用的核心工程問題
很多開發者把成本優化當成「後期才需要考慮的事」。這是個錯誤。
AI 應用的成本結構跟傳統軟體完全不同。傳統 SaaS 的邊際成本接近零——多一個用戶,你的 AWS 帳單大概增加幾塊錢。但 AI 應用的邊際成本是線性的甚至是超線性的:用戶量增加一倍,API 費用大概也增加一倍;如果你的系統設計讓每次查詢消耗更多 token(例如累積越來越長的對話歷史),費用增長甚至會超過用戶量增長。
成本不只影響利潤,還影響產品設計決策。當你知道每次 API 呼叫的成本,你會更謹慎地設計什麼時候要呼叫 API、呼叫什麼等級的模型、要不要快取。不理解成本的工程師,容易設計出用起來爽但賣不起錢的系統。
Token 成本的全貌
很多人只知道「輸入 token 比輸出 token 便宜」,但 Anthropic 的計費實際上有四個維度:
Input Tokens(輸入 token):你傳給模型的所有文字——system prompt、用戶訊息、工具結果、對話歷史。這是最大宗的費用來源。
以 claude-3-5-sonnet-20241022 為例,定價是 $3 / 1M tokens(2026 年初的定價,請以 Anthropic 官方定價頁面為準)。
Output Tokens(輸出 token):模型生成的文字——回覆內容、思考鏈(如果啟用)、工具呼叫參數。輸出 token 比輸入貴,Sonnet 是 $15 / 1M tokens——整整貴了五倍。這意味著讓模型少說一點話,比讓你少說一點話更有效益。
Cache Read Tokens(快取讀取 token):Prompt Caching 命中時,快取部分的 token 計費為 $0.30 / 1M——只有標準輸入的十分之一。快取是成本優化最強力的武器。
Cache Write Tokens(快取寫入 token):建立快取時,需要支付 $3.75 / 1M(比標準輸入稍貴)。但只要快取被讀取 1 次,就已大幅回本(寫入多付的 $0.75,對比讀取省下的 $2.70)。
另外還有 Tool Use 的 token——工具的描述(tools 參數中的 JSON)也算 input tokens。如果你有很多工具,工具描述本身就可能佔用 1000-3000 tokens,每次呼叫都在計費。
用這個表格來做成本估算:
| 類型 | claude-3-5-haiku | claude-3-5-sonnet | claude-opus-4 |
|---|---|---|---|
| Input | $0.80/1M | $3/1M | $15/1M |
| Output | $4/1M | $15/1M | $75/1M |
| Cache Read | $0.08/1M | $0.30/1M | $1.50/1M |
| Cache Write | $1/1M | $3.75/1M | $18.75/1M |
(以上為撰寫時的定價,請參考官方 pricing 頁面)
模型選擇策略:用最便宜能完成任務的模型
這是最直接的成本優化策略,也最容易被忽視。
我見過很多系統,不管什麼任務都用 Sonnet——分類一個客服問題?Sonnet。判斷情感是正面還是負面?Sonnet。把一段文字翻譯成英文?Sonnet。
Haiku 的定價是 Sonnet 的 1/4 左右。如果你用 Haiku 能做的事,就不要用 Sonnet。
我的模型選擇框架:
用 Haiku(最便宜)的場景:
- 分類任務(這封信是垃圾郵件嗎?這個問題屬於哪個類別?)
- 簡單的資料提取(從文字中提取名字、日期、金額)
- 格式轉換(把 JSON 轉成 CSV)
- 快速的是/否判斷
用 Sonnet(中等)的場景:
- 一般的對話和問答
- 中等複雜的程式碼生成
- 文件摘要
- 大多數的 RAG 應用
用 Opus(最貴)的場景:
- 複雜的多步驟推理
- 需要深度分析的長文件
- 高風險的決策(法律文件分析、財務建議)
- 你需要最高品質且預算不是問題
在實際應用中,我推薦分層模型策略(Model Routing):
import anthropic
client = anthropic.Anthropic()
def classify_query(user_message: str) -> str:
"""用 Haiku 分類查詢的複雜度,決定用什麼模型回答"""
response = client.messages.create(
model="claude-3-5-haiku-20241022", # 用 Haiku 做分類
max_tokens=10,
messages=[{"role": "user", "content": f"""
判斷以下查詢的複雜度:
"{user_message}"
只回答 "simple"、"medium" 或 "complex":
- simple:直接的事實查詢、簡單的是/否問題
- medium:需要一些推理、綜合多個資訊
- complex:需要深度分析、多步驟推理、高風險決策
"""}]
)
return response.content[0].text.strip().lower()
def route_and_answer(user_message: str, context: str) -> str:
"""根據查詢複雜度選擇適當的模型"""
complexity = classify_query(user_message)
model_map = {
"simple": "claude-3-5-haiku-20241022",
"medium": "claude-3-5-sonnet-20241022",
"complex": "claude-opus-4-5",
}
model = model_map.get(complexity, "claude-3-5-sonnet-20241022")
print(f"使用模型: {model} (複雜度: {complexity})")
response = client.messages.create(
model=model,
max_tokens=1024,
messages=[
{"role": "user", "content": f"Context: {context}\n\nQuestion: {user_message}"}
]
)
return response.content[0].text
這個策略讓你的系統自動把簡單問題路由到便宜的模型,複雜問題才用昂貴的模型。根據我的經驗,80% 的查詢屬於 simple 或 medium,只有 20% 需要 Sonnet 等級。光是這個策略就能把成本降低 40-60%。
Prompt Caching:最強力的成本武器
Prompt Caching 是 Anthropic 在 2024 年推出的功能,我認為它是迄今為止最重要的成本優化工具。
核心概念:如果你的 prompt 有一部分是固定的(system prompt、背景文件、工具描述),Anthropic 可以把這部分快取起來。下次請求如果包含相同的前綴,直接讀快取,費用只有標準輸入的十分之一。
快取的觸發條件:
- 前綴必須超過 1024 tokens(小於這個值不值得快取)
- 使用
cache_control: {"type": "ephemeral"}標記快取斷點 - 快取有效時間:5 分鐘(5 分鐘沒有相同前綴的請求就過期)
import anthropic
client = anthropic.Anthropic()
# 假設這是你的知識庫文件(幾千 tokens)
knowledge_base = """
[你的公司知識庫內容,可能有 10,000+ tokens...]
這裡是關於產品的說明...
這裡是常見問題解答...
這裡是退款政策...
[更多內容...]
"""
def answer_with_caching(user_question: str) -> anthropic.Message:
"""使用 Prompt Caching 的 RAG 系統"""
return client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是公司的客服 AI 助手。請根據以下知識庫內容回答用戶問題。"
},
{
"type": "text",
"text": knowledge_base,
"cache_control": {"type": "ephemeral"} # 標記這裡是快取斷點
}
],
messages=[
{"role": "user", "content": user_question}
]
)
# 第一次請求:需要寫入快取(稍貴)
response1 = answer_with_caching("退款政策是什麼?")
usage1 = response1.usage
print(f"Cache creation: {usage1.cache_creation_input_tokens} tokens")
print(f"Cache read: {usage1.cache_read_input_tokens} tokens") # 第一次:0
# 第二次請求(5 分鐘內):命中快取
response2 = answer_with_caching("如何申請退貨?")
usage2 = response2.usage
print(f"Cache read: {usage2.cache_read_input_tokens} tokens") # 第二次:命中快取!
# 知識庫的 tokens 現在按 $0.30/1M 計費,而不是 $3/1M
如何最大化快取命中率:
-
把穩定的內容放在前面,動態的內容放在後面。 快取是前綴匹配——只要前面相同,後面不同也算命中。
-
快取斷點要謹慎放置。 如果你有多個可快取的段落,每個段落都加
cache_control,但注意每個斷點都需要前面的整個前綴完全匹配。 -
對話歷史的快取策略。 在長對話中,把累積的對話歷史也標記為快取:
def chat_with_caching(
messages: list[dict],
new_user_message: str,
system_prompt: str,
knowledge_base: str
) -> str:
"""長對話中的快取策略"""
# 建立新的訊息列表
all_messages = messages.copy()
# 對最後一條 assistant 訊息加快取標記(代表到目前為止的對話歷史)
if all_messages and all_messages[-1]["role"] == "assistant":
last_msg = all_messages[-1].copy()
if isinstance(last_msg["content"], str):
last_msg["content"] = [
{
"type": "text",
"text": last_msg["content"],
"cache_control": {"type": "ephemeral"}
}
]
all_messages[-1] = last_msg
# 加入新的用戶訊息
all_messages.append({"role": "user", "content": new_user_message})
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=[
{"type": "text", "text": system_prompt},
{
"type": "text",
"text": knowledge_base,
"cache_control": {"type": "ephemeral"}
}
],
messages=all_messages
)
return response.content[0].text
我的目標是讓 cache hit rate 超過 80%——也就是說,80% 以上的請求中,大部分 input tokens 都來自快取。這個數字對一個活躍的服務(每分鐘有多個請求)是可達到的。
Output Length 控制
輸出 token 比輸入貴 5 倍,控制輸出長度是重要的成本槓桿。
設定合理的 max_tokens。 很多開發者直接設 4096 或更高,但如果你的用例只需要幾百 token 的回覆,這樣做不只浪費(模型可能輸出比需要多的內容),也讓用戶等待更長時間。
根據用例設定 max_tokens:
- 客服問答:512-1024
- 文件摘要:1024-2048
- 長文章生成:2048-4096
- 程式碼生成:視複雜度而定
在 prompt 中明確要求簡潔:
system_prompt = """
你是客服助手。回答用戶問題時:
- 直接給出答案,不要有前言和後記
- 使用條列式格式
- 回答控制在 200 字以內
- 如果問題複雜,告訴用戶可以進一步詢問,而不是一次說完所有內容
"""
一個設計良好的 prompt,讓模型知道「夠了就停」,不要追求詳細。對多數用戶來說,精簡的回覆其實比詳細的回覆更好——AI 的「詳細」往往包含大量廢話。
Batch API:節省 50% 的懶人救星
如果你的任務不需要即時回應(例如批次處理文件、離線分析),用 Batch API 可以省下 50% 的費用。
Batch API 的工作方式:你一次送出多個請求,Anthropic 在 24 小時內處理完,費用是標準 API 的一半。
import anthropic
import json
client = anthropic.Anthropic()
# 準備批次請求
requests = []
documents = ["文件 1 的內容...", "文件 2 的內容...", "文件 3 的內容..."]
for i, doc in enumerate(documents):
requests.append({
"custom_id": f"doc-{i}",
"params": {
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 512,
"messages": [
{
"role": "user",
"content": f"請摘要以下文件(100 字以內):\n\n{doc}"
}
]
}
})
# 送出批次請求
batch = client.messages.batches.create(requests=requests)
print(f"Batch ID: {batch.id}")
print(f"狀態: {batch.processing_status}")
# 等待完成(可以隔幾個小時再查)
import time
while True:
batch_status = client.messages.batches.retrieve(batch.id)
if batch_status.processing_status == "ended":
break
print(f"還在處理中... ({batch_status.request_counts.processing} 個請求)")
time.sleep(60)
# 取得結果
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
print(f"{result.custom_id}: {result.result.message.content[0].text}")
else:
print(f"{result.custom_id}: 失敗 - {result.result.error}")
適合 Batch API 的場景:
- 每晚批次生成隔天的報告
- 離線的文件分類和標記
- 測試時跑大量的 evaluation
- 定期更新快取的知識庫摘要
Context Window 管理:不要無限累積對話歷史
這是很多對話 AI 應用的常見問題:每輪對話都把完整的歷史塞進去,越說越長,費用越來越高。
正確的做法是有策略地管理對話歷史:
from anthropic import Anthropic
client = Anthropic()
class ConversationManager:
def __init__(self, max_history_tokens: int = 4000):
self.messages = []
self.max_history_tokens = max_history_tokens
def estimate_tokens(self, messages: list) -> int:
"""粗略估算 token 數(1 token ≈ 4 個英文字元 / 2 個中文字元)"""
total_chars = sum(
len(m["content"]) if isinstance(m["content"], str)
else sum(len(c.get("text", "")) for c in m["content"])
for m in messages
)
return total_chars // 3 # 保守估算
def trim_history(self):
"""保留最近的對話,確保不超過 token 限制"""
while len(self.messages) > 2 and self.estimate_tokens(self.messages) > self.max_history_tokens:
# 移除最早的一組對話(user + assistant)
self.messages = self.messages[2:]
def chat(self, user_message: str, system_prompt: str) -> str:
self.messages.append({"role": "user", "content": user_message})
self.trim_history()
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=system_prompt,
messages=self.messages
)
assistant_message = response.content[0].text
self.messages.append({"role": "assistant", "content": assistant_message})
return assistant_message
def summarize_and_reset(self, system_prompt: str):
"""當對話太長,先讓 AI 做摘要,然後重置歷史"""
if len(self.messages) < 4:
return
# 用 Haiku 做對話摘要(便宜)
summary_response = client.messages.create(
model="claude-3-5-haiku-20241022",
max_tokens=512,
messages=[
{
"role": "user",
"content": f"請用 200 字摘要以下對話的重點:\n\n{json.dumps(self.messages, ensure_ascii=False)}"
}
]
)
summary = summary_response.content[0].text
# 重置歷史,只保留摘要
self.messages = [
{
"role": "user",
"content": f"[對話摘要:{summary}]\n\n繼續我們的對話。"
},
{
"role": "assistant",
"content": "好的,我了解之前的對話內容。請繼續。"
}
]
監控成本:每個功能都應該知道它花了多少錢
你不能優化你不監控的東西。我的做法是在每次 API 呼叫後記錄 token 使用量:
import anthropic
from dataclasses import dataclass
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
@dataclass
class APICallMetrics:
feature: str
model: str
input_tokens: int
output_tokens: int
cache_read_tokens: int
cache_creation_tokens: int
timestamp: datetime
def estimated_cost_usd(self) -> float:
"""估算這次呼叫的美金成本"""
pricing = {
"claude-3-5-haiku-20241022": {
"input": 0.80/1_000_000,
"output": 4.00/1_000_000,
"cache_read": 0.08/1_000_000,
"cache_write": 1.00/1_000_000,
},
"claude-3-5-sonnet-20241022": {
"input": 3.00/1_000_000,
"output": 15.00/1_000_000,
"cache_read": 0.30/1_000_000,
"cache_write": 3.75/1_000_000,
},
}
p = pricing.get(self.model, pricing["claude-3-5-sonnet-20241022"])
return (
self.input_tokens * p["input"] +
self.output_tokens * p["output"] +
self.cache_read_tokens * p["cache_read"] +
self.cache_creation_tokens * p["cache_write"]
)
def track_api_call(feature: str, response: anthropic.Message) -> APICallMetrics:
"""從 API 回應中提取並記錄 metrics"""
usage = response.usage
metrics = APICallMetrics(
feature=feature,
model=response.model,
input_tokens=usage.input_tokens,
output_tokens=usage.output_tokens,
cache_read_tokens=getattr(usage, 'cache_read_input_tokens', 0),
cache_creation_tokens=getattr(usage, 'cache_creation_input_tokens', 0),
timestamp=datetime.now()
)
logger.info(
"api_call",
extra={
"feature": feature,
"model": response.model,
"cost_usd": metrics.estimated_cost_usd(),
"cache_hit_rate": metrics.cache_read_tokens / (metrics.input_tokens or 1),
}
)
return metrics
把這些 metrics 送到你的監控系統(Datadog、Grafana、或簡單的 Google Sheets),你就能看到:哪個功能最貴、快取命中率如何、模型選擇是否合適。
真實案例:從 $2000 降到 $287
回到這章開頭的那個 RAG 系統。這是我具體做了什麼:
問題診斷(第一週)
首先我加入了 token tracking,看清楚費用來自哪裡:
- 70% 的費用來自 input tokens(主要是知識庫文件每次都重新傳)
- 20% 來自 output tokens(模型的回覆比必要的長)
- 10% 來自 Sonnet 用於所有查詢(包括只需要 Haiku 的簡單問題)
修正 1:實施 Prompt Caching(最大效益)
知識庫文件大約 15,000 tokens,每次查詢都要傳一次。加入 cache 之後,命中率達到 85%——這 15,000 tokens 中的 85% 從 $3/1M 降到 $0.30/1M。
光這一項,input tokens 費用降了 75%。
修正 2:模型路由(次大效益)
分析後發現,60% 的查詢是簡單的事實查詢(「退款期限是幾天?」「服務時間是?」),完全不需要 Sonnet。改用 Haiku 後,這 60% 的查詢費用降到原來的 20%。
修正 3:Output length 控制
修改 system prompt,明確要求簡潔回覆,平均 output tokens 從 420 降到 180。
修正 4:對話歷史截斷
原本每輪對話都傳完整歷史,改為最多保留 6 輪(避免 context 無限增長)。
最終結果:
| 項目 | 修改前 | 修改後 | 降幅 |
|---|---|---|---|
| 知識庫 input tokens | $1,200 | $220 | -82% |
| Output tokens | $650 | $280 | -57% |
| 模型費用 | $491 | 包含在上面 | - |
| 總計 | $2,341 | $287 | -88% |
兩個最重要的 takeaway:
- Prompt Caching 是 ROI 最高的優化,如果你有重複的大型 prompt,先把這個做好。
- 測量再優化,不要猜——先加入 monitoring,搞清楚費用來自哪裡,再針對性地優化。
省了錢之後,下一個問題是:怎麼讓這個省錢的系統在生產環境穩定跑?錯誤處理、Rate Limit、可觀測性——這些是我們下一章要解決的問題。