# Prompt caching thực chiến: sắp xếp prompt để trúng cache rồi đo tiền và độ trễ

> Chỉ một dấu thời gian đặt sai chỗ cũng làm hỏng cả cache. Hướng dẫn này giúp bạn sắp xếp lại prompt và chứng minh khoản tiết kiệm bằng chính số liệu usage của API.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-prompt-caching-giam-chi-phi-llm-cho-khach/

Với một prompt chứa cuốn sách dài 100.000 token, Anthropic đo được thời gian chờ token đầu tiên giảm từ 11,5 giây xuống 2,4 giây, còn chi phí giảm 90%. Hãng không cần mô hình mới để có kết quả đó. Họ chỉ cho hệ thống đọc lại phần prompt đã xử lý thay vì xử lý lại từ đầu.

Vấn đề là cache chỉ trúng khi phần đầu prompt giống hệt nhau. Thử hình dung một hệ thống ở phía khách hàng nhét ngày giờ hay tên người dùng lên dòng đầu system prompt. Khi đó, dù đã bật cache, request nào cũng trượt.

Hướng dẫn này đi qua sáu bước, từ đo baseline, sắp xếp lại prompt, đặt breakpoint, kiểm tra hit, cho tới quy đổi ra tiền. Cuối cùng bạn sẽ có một bảng trước/sau để đưa cho khách. Đó chính là thứ một FDE cần mang vào buổi review.

## Bạn sẽ dựng gì, cần chuẩn bị gì?

Ta sẽ viết một script Python nhỏ, hỏi đáp trên một tài liệu chính sách dài và không đổi. Script chạy hai chế độ có và không có cache, rồi ghi lại độ trễ cùng số token của từng request. Bạn cần API key của Anthropic, SDK Python và một file văn bản đủ dài, chẳng hạn quy trình nội bộ hoặc tài liệu sản phẩm.

Code dưới đây đã được rút gọn để dễ đọc. Tên model và cú pháp chính xác của `cache_control` nên đối chiếu lại với tài liệu prompt caching của Anthropic trước khi chạy thật.

## Bước 1: đo baseline trước khi động vào bất cứ thứ gì

Không có số "trước" thì cũng không có câu chuyện "sau". Hãy bọc lời gọi API, ghi thời gian và các trường usage.

```python
import time, anthropic
client = anthropic.Anthropic()
MODEL = "..."   # điền tên model bạn dùng, lấy từ tài liệu của Anthropic
DOC = open("policy.txt").read()   # phần tĩnh, không đổi giữa các request

def build_system(cached):
    system = [{"type": "text", "text": "Bạn là trợ lý tra cứu chính sách."},
              {"type": "text", "text": DOC}]
    if cached:
        system[-1]["cache_control"] = {"type": "ephemeral"}  # rút gọn
    return system

def ask(question, cached):
    t0 = time.perf_counter()
    r = client.messages.create(model=MODEL, max_tokens=300,
        system=build_system(cached),
        messages=[{"role": "user", "content": question}])
    u = r.usage
    return (time.perf_counter() - t0, u.input_tokens,
            u.cache_creation_input_tokens, u.cache_read_input_tokens)
```

Hàm `ask` đo tổng thời gian phản hồi, gồm cả thời gian sinh output. Benchmark của Anthropic lại đo thời gian tới token đầu tiên, nên muốn so trực tiếp với họ bạn cần thêm một biến thể streaming như dưới đây (bản rút gọn, đối chiếu cú pháp với tài liệu SDK).

```python
def ask_ttft(question, cached):
    t0 = time.perf_counter()
    ttft = None   # khởi tạo trước, phòng khi không có text nào trả về
    with client.messages.stream(model=MODEL, max_tokens=300,
            system=build_system(cached),
            messages=[{"role": "user", "content": question}]) as s:
        for _ in s.text_stream:
            ttft = time.perf_counter() - t0   # token đầu tiên đã về
            break
        u = s.get_final_message().usage
    if ttft is None:   # response không có text: ghi tổng thời gian và đánh dấu lại
        ttft = time.perf_counter() - t0
    return (ttft, u.input_tokens,
            u.cache_creation_input_tokens, u.cache_read_input_tokens)
```

Sau bước này, hãy chạy cả `ask(q, cached=False)` và `ask_ttft(q, cached=False)` với 10 câu hỏi rồi lưu kết quả. Bạn sẽ có tổng thời gian, thời gian tới token đầu tiên và số token để làm cột "trước". Ở các bước sau, chỉ cần đổi `cached=True` và chạy lại đúng hai hàm đó.

## Bước 2: thứ tự nào giúp trúng cache?

Theo tài liệu của Anthropic, cache bao phủ toàn bộ prompt theo đúng thứ tự `tools`, rồi `system`, rồi `messages`, tính tới hết block có gắn `cache_control`. Thay đổi ở tầng nào sẽ làm mất hiệu lực tầng đó cùng mọi tầng phía sau. Sửa một tool definition, kể cả chỉ đổi thứ tự hai tool, là system và messages cũng mất cache.

Vì thế, quy tắc sắp xếp chỉ có một: phần ổn định đặt trước, phần động đặt sau cùng. OpenAI khuyên điều tương tự, tức là đưa chỉ dẫn ổn định và tài liệu tham khảo dùng chung lên đầu. Nếu chuyển nhà cung cấp, bạn vẫn giữ được thói quen này.

**Điểm mấu chốt:** Cache chỉ trúng khi phần đầu prompt giống hệt nhau đến từng ký tự.

Thử hình dung một system prompt mở đầu bằng "Hôm nay là 25/10/2026, người dùng: Lan". Mỗi người dùng, mỗi ngày lại là một prefix mới. Hãy chuyển dòng đó xuống message của người dùng, đặt sau tài liệu tĩnh.

## Bước 3: đặt breakpoint ở đâu?

Anthropic khuyên đặt breakpoint ở block cuối cùng còn giống hệt nhau giữa các request. Mỗi request được dùng tối đa 4 breakpoint. Trong script trên, block đó là `DOC`, vì câu hỏi thay đổi liên tục và nằm trong `messages` sau nó.

Với hội thoại nhiều lượt, có thể suy ra một gợi ý từ hai quy tắc trên: các lượt cũ giữ nguyên giữa các request, nên chúng cũng là ứng viên cho một breakpoint thứ hai, trong giới hạn 4 breakpoint. Đây là cách áp dụng chứ không phải công thức có sẵn, vì vậy hãy kiểm chứng bằng usage ở bước 4 trước khi tin rằng nó hiệu quả.

## Bước 4: làm sao biết request đã trúng cache?

Chạy cùng một câu hỏi hai lần liên tiếp với `cached=True`. Request đầu tiên phải có `cache_creation_input_tokens` lớn hơn 0 và `cache_read_input_tokens` bằng 0. Đến request thứ hai, vẫn cùng prefix đó, `cache_read_input_tokens` phải lớn hơn 0.

Nếu lần thứ hai vẫn chỉ thấy số creation, nghĩa là prefix đã thay đổi đâu đó. Chỉ cần lệch một ký tự là cache tạo một entry mới. Hãy so hai prompt bằng diff, đừng so bằng mắt.

Tổng input của mỗi request bằng `cache_read_input_tokens + cache_creation_input_tokens + input_tokens`. Lấy số đọc chia cho tổng đó, bạn có tỉ lệ hit, và đây là con số đáng theo dõi trên dashboard.

## Bước 5: hâm nóng cache trước khi chạy song song

Một cái bẫy khi xử lý batch là bắn 50 request cùng một lúc rồi thấy request nào cũng ghi cache. Entry cache chỉ có sau khi response đầu tiên bắt đầu trả về, nên các request song song gửi cùng thời điểm đều trượt. Cách sửa là gửi một request trước, đợi nó bắt đầu trả lời, rồi mới mở song song.

Cũng cần để ý TTL. Mặc định cache sống 5 phút và được làm mới miễn phí mỗi lần trúng. Nếu traffic thưa hơn khoảng đó, bạn có thể chọn TTL 1 giờ, nhưng phải trả thêm phí ghi.

## Bước 6: quy ra tiền như thế nào?

Theo bảng giá của Anthropic, ghi cache với TTL 5 phút tốn 1,25 lần giá input cơ bản, TTL 1 giờ tốn 2 lần, còn đọc cache chỉ tốn 0,1 lần. Một số model mới có giá đọc còn thấp hơn. Hàm dưới đây tính chi phí theo đơn vị "giá input cơ bản", chưa tính token output.

```python
def cost_units(inp, write, read, write_rate=1.25):
    # write_rate=2.0 nếu dùng TTL 1 giờ; giá đọc 0.1 có thể thấp hơn ở model mới
    return inp * 1.0 + write * write_rate + read * 0.1
```

Thử một ví dụ giả định: tài liệu tĩnh dài 20.000 token, có 10 câu hỏi trong vòng 5 phút, bỏ qua phần câu hỏi ngắn. Không có cache thì tốn 10 × 20.000 = 200.000 đơn vị.

Có cache thì lần đầu ghi 20.000 × 1,25 = 25.000, chín lần sau đọc 9 × 20.000 × 0,1 = 18.000, cộng lại 43.000 đơn vị, tức giảm khoảng 78,5%.

Ví dụ này cũng cho thấy khi nào cache không có lời. Nếu mỗi prefix chỉ được dùng đúng một lần, bạn trả 1,25 lần mà không đọc lại lần nào, tức là lỗ 25%.

## Kết quả của bạn nên trông thế nào?

Mức cải thiện phụ thuộc vào workload. Benchmark của Anthropic ghi nhận hội thoại 10 lượt với system prompt dài giảm khoảng 75% độ trễ và 53% chi phí. Prompt many-shot dài 10.000 token giảm 31% độ trễ và 86% chi phí. Đừng hứa với khách con số 90% trước khi đo trên chính workload của họ.

| Chỉ số | Trước | Sau |
|---|---|---|
| Thời gian tới token đầu tiên (giây) | `ask_ttft(..., cached=False)` | `ask_ttft(..., cached=True)` |
| Tỉ lệ hit | 0% | read / tổng input |
| Chi phí input (đơn vị) | `cost_units` | `cost_units` |

## Với OpenAI thì khác gì?

OpenAI tự động cache, bạn không cần gắn breakpoint. Muốn kiểm tra hit thì đọc trường `cached_tokens`. Trường này chỉ đếm token đọc được tính tiền, không gồm token bị tính là ghi.

Tham số `prompt_cache_key` chỉ ảnh hưởng tới việc định tuyến request. Nó không ghim request vào một máy và cũng không bảo đảm sẽ trúng cache. Vì vậy cách kiểm chứng vẫn là con số trong usage.

## Những lỗi nào hay làm hỏng cache nhất?

Lỗi phổ biến nhất là để phần động như ngày giờ, tên người dùng hay ID nằm trước phần tĩnh, khiến prefix đổi theo từng request.

Gần giống vậy là sửa hoặc đổi thứ tự tool definition: vì `tools` đứng đầu, thay đổi ở đó kéo theo system và messages cũng mất cache.

Lỗi thứ ba là gắn `cache_control` lên một block vẫn còn thay đổi, thay vì block cuối cùng còn giống hệt nhau.

Ba lỗi còn lại nằm ở cách vận hành. Bắn batch song song khi cache chưa được hâm nóng thì request nào cũng trượt. Traffic thưa hơn TTL 5 phút làm cache hết hạn giữa hai lần gọi, còn prefix chỉ dùng một lần thì bạn trả phí ghi mà không đọc lại lần nào.

Với OpenAI, lỗi hay gặp là tin rằng đặt `prompt_cache_key` là đủ. Dù dùng nhà cung cấp nào, cách bắt cả sáu lỗi này vẫn chỉ có một: xem các trường usage sau mỗi thay đổi.

## Kỹ năng này xuất hiện ở khách hàng ra sao?

Ở phía khách hàng, chuyện thường bắt đầu bằng một câu như "hóa đơn API tháng này tăng gấp đôi" hoặc "chatbot trả lời chậm quá". Một FDE giỏi sẽ không đổi model ngay. Họ mở prompt ra, tìm thứ đang phá prefix, sắp xếp lại, rồi đưa bảng trước/sau ở trên cho khách.

Khi đọc job description, hãy để ý các cụm như "LLM cost optimization" hay "latency". Trong CV, viết kèm số đo được, chẳng hạn tỉ lệ hit tăng từ bao nhiêu lên bao nhiêu và chi phí input giảm bao nhiêu phần trăm, kèm cách bạn đo. Người phỏng vấn tin vào ba trường usage hơn là tin vào tính từ.

Lần tới khi mở một prompt dài, hãy đọc dòng đầu tiên trước. Nếu dòng đó thay đổi theo từng request, bạn đã thấy chỗ đầu tiên cần sửa.

**Thử ngay tuần này:**

- Ghi log usage và thời gian phản hồi của 20 request thật trong hệ thống hiện tại để có baseline trước khi bật cache.
- Rà prompt tìm mọi phần thay đổi theo request (ngày giờ, tên người dùng, ID) đang nằm trước phần tĩnh, rồi dời chúng xuống cuối.
- Tính tỉ lệ cache_read trên tổng input sau khi bật cache và ghi con số đó vào báo cáo hoặc CV.

## Nguồn

- [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)

- [Prompt caching with Claude](https://claude.com/blog/prompt-caching)

- [Prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching)

- [Prompt Caching Support in Spring AI with Anthropic Claude](https://spring.io/blog/2025/10/27/spring-ai-anthropic-prompt-caching-blog/)
