Tối ưu chi phí LLM API: Prompt caching, batching và cắt giảm token không cần thiết

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

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-haiku thay vì claude-sonnet
  • Giới hạn max_tokens sá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 ý:

  1. Token reduction trước: Nén prompt, xóa whitespace thừa — không cần code thêm gì, làm ngay được
  2. Thêm prompt caching: System prompt trên 1024 token? Thêm cache_control là xong
  3. 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.

Share: