Chi phí API leo thang — chuyện không của riêng ai
Tháng trước, mình nhận hóa đơn API từ nhà cung cấp LLM và giật mình: $340 chỉ trong 30 ngày, dù ứng dụng chưa có nhiều user. Sau vài tiếng ngồi phân tích log, mình phát hiện 2 thủ phạm chính: hơn 60% token đến từ system prompt lặp đi lặp lại trong mỗi request, và hàng trăm request nhỏ gọi đơn lẻ thay vì được gom batch.
Claude, GPT-4, Gemini hay bất kỳ LLM nào tính phí theo token — câu chuyện này sẽ đến với bạn sớm hay muộn. Mình sẽ chia sẻ 3 kỹ thuật đã giúp cắt khoảng 55% chi phí, không ảnh hưởng chất lượng output.
3 nguyên nhân chính khiến chi phí API tăng vọt
LLM API tính phí theo số token — input cộng output. Trong thực tế, có 3 thứ hay ngốn token nhất:
- System prompt dài lặp lại: Mỗi request đều gửi system prompt y chang, nhưng bạn vẫn trả tiền đầy đủ cho phần đó mỗi lần.
- Request nhỏ lẻ tẻ: Gọi API từng cái một thay vì gom lại — bỏ phí discount của batch API.
- Token “rác” trong prompt: Whitespace thừa, hướng dẫn lặp, context không cần thiết. Tất cả đều lên hóa đơn.
Kỹ thuật 1: Prompt Caching — chỉ trả tiền cho phần thay đổi
Ý tưởng đơn giản: nhà cung cấp “nhớ” phần prompt không đổi giữa các request. Lần đầu gửi, bạn trả phí bình thường. Lần sau, phần đã cache chỉ tốn 10% giá gốc (với Anthropic). Nghe nhỏ, nhưng với system prompt 2000 token mà gọi 10.000 lần/ngày thì khoản tiết kiệm rất đáng kể.
Anthropic Claude hỗ trợ caching từ Claude 3.5+. Chỉ cần thêm cache_control vào phần muốn cache:
import anthropic
client = anthropic.Anthropic()
# System prompt dài (ví dụ: 2000 token) — phần muốn cache
system_prompt = """
Bạn là chuyên gia phân tích tài chính với 15 năm kinh nghiệm...
[toàn bộ nội dung hướng dẫn dài ở đây]
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"} # Đánh dấu cache
}
],
messages=[
{"role": "user", "content": "Phân tích báo cáo tài chính Q1 này..."}
]
)
# Kiểm tra cache hits
usage = response.usage
print(f"Cache read tokens: {usage.cache_read_input_tokens}")
print(f"Cache write tokens: {usage.cache_creation_input_tokens}")
print(f"Regular input tokens: {usage.input_tokens}")
Request tiếp theo dùng cùng system prompt, phần đó được đọc từ cache thay vì tính lại. Cache tồn tại tối thiểu 5 phút, tự refresh mỗi lần dùng. Với chatbot hoặc AI assistant có nhiều user cùng chung system prompt dài — tiết kiệm có thể lên 80–90% input token.
Khi nào nên dùng prompt caching?
- System prompt trên 1024 token (ngưỡng cache tối thiểu của Anthropic)
- Nhiều request dùng chung prefix dài — ví dụ: toàn bộ documentation làm context
- RAG với fixed knowledge base — cache phần documents, chỉ thay user query mỗi lần
Kỹ thuật 2: Batching — gom request để tiết kiệm chi phí
Thay vì gọi API 1000 lần cho 1000 items, Batch API cho phép gửi tất cả cùng lúc và nhận kết quả bất đồng bộ sau. Anthropic Message Batches API chấp nhận tối đa 10.000 request/batch, giá chỉ bằng 50% so với gọi đơn lẻ. Đánh đổi duy nhất: không có kết quả realtime.
import anthropic
import json
client = anthropic.Anthropic()
# Danh sách cần xử lý
items_to_process = [
{"id": "item_001", "text": "Phân loại cảm xúc: 'Sản phẩm tuyệt vời!'"},
{"id": "item_002", "text": "Phân loại cảm xúc: 'Giao hàng chậm quá.'"},
{"id": "item_003", "text": "Phân loại cảm xúc: 'Bình thường, không đặc biệt.'"},
# ... thêm hàng trăm/nghìn item
]
# Tạo batch request
requests = [
{
"custom_id": item["id"],
"params": {
"model": "claude-haiku-4-5-20251001", # Model rẻ hơn cho task đơn giản
"max_tokens": 50, # Giới hạn output ngắn
"messages": [{"role": "user", "content": item["text"]}]
}
}
for item in items_to_process
]
# Gửi batch
batch = client.messages.batches.create(requests=requests)
print(f"Batch ID: {batch.id}")
print(f"Status: {batch.processing_status}")
# Lưu batch_id để query sau (batch có thể mất vài phút đến vài giờ)
with open("batch_ids.json", "a") as f:
json.dump({"batch_id": batch.id, "count": len(requests)}, f)
# Script riêng để lấy kết quả khi batch hoàn thành
client = anthropic.Anthropic()
batch_id = "msgbatch_xxx" # ID từ lúc tạo
batch = client.messages.batches.retrieve(batch_id)
if batch.processing_status == "ended":
results = {}
for result in client.messages.batches.results(batch_id):
if result.result.type == "succeeded":
results[result.custom_id] = result.result.message.content[0].text
print(f"Xử lý xong: {len(results)} items")
Tips khi dùng Batch API
- Dùng model nhỏ cho task đơn giản (classification, extraction) —
claude-haikuthay vìclaude-sonnet - Giới hạn
max_tokenssát output thực tế — đừng set 4096 khi chỉ cần 50 token - Thiết kế ứng dụng async nếu không cần realtime: nightly processing, data pipeline
Kỹ thuật 3: Cắt giảm token không cần thiết
Kỹ thuật đơn giản nhất, nhưng hay bị bỏ qua nhất. Mình từng review một codebase và tìm thấy prompt có hơn 500 token chỉ là whitespace, comment thừa và hướng dẫn lặp lại. 500 token × hàng triệu request = không nhỏ chút nào.
3.1 Nén system prompt
# BAD — dài dòng, nhiều whitespace thừa (~120 token)
system_prompt_bad = """
Bạn là một trợ lý AI thông minh và hữu ích.
Bạn sẽ giúp người dùng trả lời các câu hỏi.
Khi trả lời, bạn cần:
- Trả lời rõ ràng và đầy đủ
- Sử dụng ngôn ngữ dễ hiểu
- Không nói những điều không chắc chắn
"""
# GOOD — súc tích, cùng ý nghĩa, tiết kiệm ~70% token (~35 token)
system_prompt_good = "Trợ lý IT hữu ích. Trả lời ngắn gọn, chính xác. Thừa nhận khi không chắc."
3.2 Cắt bớt conversation history
Chatbot với lịch sử trò chuyện dài là bẫy token phổ biến. Người dùng chat 2 tiếng, mỗi request gửi toàn bộ lịch sử — đến cuối buổi mỗi message tốn gấp 10–20 lần ban đầu. Chỉ giữ N tin nhắn gần nhất là đủ:
def trim_conversation_history(messages: list, max_messages: int = 10) -> list:
"""Giữ tối đa N tin nhắn gần nhất để giảm token."""
if len(messages) <= max_messages:
return messages
# Luôn giữ tin đầu tiên (thường là context quan trọng)
return [messages[0]] + messages[-(max_messages - 1):]
def count_tokens_estimate(text: str) -> int:
"""Ước tính token: ~3 ký tự = 1 token cho nội dung mixed VI/EN."""
return len(text) // 3
# Kiểm tra trước khi gửi
conversation = load_conversation_history(user_id)
total_tokens = sum(count_tokens_estimate(m["content"]) for m in conversation)
if total_tokens > 50_000: # Ngưỡng cảnh báo
conversation = trim_conversation_history(conversation, max_messages=6)
3.3 Dùng đúng model cho đúng task
Mình hay thấy mọi người mặc định dùng model mạnh nhất cho mọi thứ. Nhưng phân loại cảm xúc một đoạn text không cần Opus. Bảng tham khảo nhanh:
- Classification, sentiment, extraction đơn giản: Claude Haiku (~20x rẻ hơn Opus)
- Viết content, summarization, Q&A thông thường: Claude Sonnet (điểm ngọt chất lượng/giá)
- Reasoning phức tạp, code review, phân tích chuyên sâu: Claude Opus hoặc Sonnet mới nhất
Đo lường để biết đang tối ưu đúng hướng
Tối ưu mà không đo lường là làm mò. Mình track 3 metric: tổng token/request, cache hit rate, và estimated cost/feature. Thêm logging từ ngày đầu — đừng đợi đến khi hóa đơn “sốc” mới bắt đầu:
def log_api_usage(response) -> dict:
usage = response.usage
cache_read = getattr(usage, 'cache_read_input_tokens', 0)
cache_write = getattr(usage, 'cache_creation_input_tokens', 0)
total_input = usage.input_tokens + cache_read + cache_write
cache_hit_rate = cache_read / total_input if total_input > 0 else 0
# Giá Claude Sonnet 4.6 (kiểm tra giá thực tế trên trang Anthropic)
cost = (
usage.input_tokens * 3 / 1_000_000 # Regular input: $3/MTok
+ cache_write * 3.75 / 1_000_000 # Cache write: $3.75/MTok
+ cache_read * 0.30 / 1_000_000 # Cache read: $0.30/MTok
+ usage.output_tokens * 15 / 1_000_000 # Output: $15/MTok
)
print(f"Cost: ${cost:.4f} | Cache hit: {cache_hit_rate:.1%} | Output: {usage.output_tokens} tok")
return {"cost": cost, "cache_hit_rate": cache_hit_rate}
Kết quả và thứ tự ưu tiên triển khai
Sau 3 kỹ thuật trên, bill API mình giảm từ $340 xuống ~$150/tháng — gần 56% — chất lượng output không đổi, latency còn cải thiện nhờ batch processing.
Nếu bạn mới bắt đầu tối ưu, đây là thứ tự mình gợi ý:
- Token reduction trước: Nén prompt, xóa whitespace thừa — không cần code thêm gì, làm ngay được
- Thêm prompt caching: System prompt trên 1024 token? Thêm
cache_controllà xong - Chuyển sang batch: Khi có use case không cần realtime — data processing, content generation hàng loạt
Thêm monitoring từ ngày đầu dự án. Đặt alert khi vượt ngưỡng chi phí. Review token usage định kỳ theo từng feature. Ba thói quen nhỏ này — khi scale lên — tiết kiệm nhiều hơn bất kỳ kỹ thuật tối ưu nào.
