Mục lục
- Mục tiêu bài học
- Vì sao cache LLM response
- 3 chiến lược cache
- Cài đặt Redis 7
- Exact match cache — implementation
- Khi nào không nên cache
- TTL strategies
- Cache invalidation
- Semantic cache với Redis Vector Search
- GPTCache — managed semantic cache
- LangChain cache integration
- Anthropic prompt caching (provider-level)
- Monitoring cache metrics
- Cache memory management và sizing
- Distributed cache patterns
- Common pitfalls
- Tóm tắt
- Bài tiếp theo
Mục tiêu bài học
Sau bài này bạn sẽ:
- ✅ Biết các lý do cache LLM response trong production
- ✅ Triển khai exact match cache với Redis và openai SDK 1.x
- ✅ Triển khai semantic cache với Redis Vector Search và embedding
- ✅ Chọn TTL phù hợp và xử lý cache invalidation
- ✅ Tích hợp cache vào LangChain qua
RedisCache/RedisSemanticCache - ✅ Hiểu Anthropic prompt caching hoạt động ở tầng provider
- ✅ Tránh các pitfall phổ biến trong LLM caching
Vì sao cache LLM response
Hai vấn đề chính khi gọi LLM API trực tiếp trong production:
- Chi phí: Pricing thường tính theo token. GPT-4o-mini khoảng $0.15/$0.60 per million input/output tokens (tháng 5/2026); GPT-4o khoảng $2.50/$10. Claude Opus 4 đắt hơn. Khi scale lên hàng triệu request/ngày, chi phí tăng tuyến tính.
- Latency: Mỗi LLM call mất 1–10 giây, tùy model và độ dài response. Với user-facing app, latency này thường không chấp nhận được nếu câu hỏi đã từng được hỏi trước.
Thực tế phần lớn production traffic có tỷ lệ query trùng lặp đáng kể:
- FAQ chatbot: cùng 50–200 câu hỏi phổ biến chiếm 60–80% tổng traffic.
- Document Q&A: user cùng hỏi về đoạn văn bản cố định.
- Code assistant: snippet generation lặp lại với cùng context.
Khi cache hit, cost = $0 và latency <5ms (chỉ là Redis lookup). Đây là lý do caching thường là optimization đầu tiên cần triển khai.
3 chiến lược cache
a) Exact match cache
Key = hash(prompt + model + params). Response được lưu và trả lại khi prompt giống y hệt.
- Ưu điểm: đơn giản, không có false positive, lookup O(1).
- Nhược điểm: hit rate thấp với open-ended conversation (user viết khác nhau dù cùng ý).
- Phù hợp: FAQ, template-based prompt, hệ thống có prompt tự động (không do user nhập tự do).
b) Semantic cache
Key = embedding của prompt. Match khi cosine similarity > threshold.
- Ưu điểm: bắt được các câu hỏi tương đồng dù viết khác nhau.
- Nhược điểm: phức tạp hơn, tốn chi phí embedding (nhỏ hơn LLM nhiều), có thể false positive nếu threshold quá thấp.
- Phù hợp: chatbot, support ticket, query có ngữ nghĩa đa dạng.
c) Prefix cache (provider-level)
System prompt + few-shot example cố định được cache bởi LLM provider. Không phải cache ở application layer — đây là tối ưu bên trong provider (ví dụ Anthropic prompt caching, xem mục 12). Application không cần tự implement, chỉ cần đánh dấu phần nào cần cache.
Cài đặt Redis 7
Chạy Redis 7 (alpine image nhỏ gọn) và cài Python client:
docker run -d --name redis-cache -p 6379:6379 redis:7-alpine
pip install redis openai
Verify kết nối:
import redis
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
print(r.ping()) # True
Để dùng semantic cache (Vector Search), Redis Stack cần được bật. Dùng image redis/redis-stack:latest thay vì redis:7-alpine:
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
pip install redis[hiredis] numpy openai
Exact match cache — implementation
Cache key bao gồm tất cả tham số ảnh hưởng đến output: messages, model, temperature. Dùng SHA-256 để tạo key ngắn gọn và collision-resistant.
import hashlib
import json
import redis
import openai
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
client = openai.OpenAI() # openai SDK 1.x
def cache_key(messages: list, model: str, temperature: float) -> str:
payload = json.dumps(
{"messages": messages, "model": model, "temperature": temperature},
sort_keys=True,
)
return f"llm:exact:{hashlib.sha256(payload.encode()).hexdigest()}"
def call_llm_cached(
messages: list,
model: str = "gpt-4o-mini",
temperature: float = 0,
ttl: int = 86400, # 24h
) -> dict:
key = cache_key(messages, model, temperature)
# Cache hit
cached = r.get(key)
if cached:
return json.loads(cached)
# Cache miss — gọi API
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
)
result = {
"content": response.choices[0].message.content,
"usage": response.usage.model_dump(),
"cached": False,
}
r.setex(key, ttl, json.dumps(result))
return result
Một vài lưu ý khi dùng đoạn code trên:
sort_keys=Truetrongjson.dumpsđảm bảo cùng dict tạo ra cùng hash dù thứ tự key khác nhau.decode_responses=Trueở Redis client trả về str thay vì bytes, không cần decode thủ công.temperature=0là default phù hợp nhất cho cache: deterministic output. Vớitemperature > 0, xem mục 6.r.setex(key, ttl, value)set và set TTL cùng lúc, atomic.
Race condition — double-spend problem
Khi 2 request đồng thời miss cache, cả 2 sẽ gọi LLM API. Tốn chi phí gấp đôi cho request đó. Với traffic thấp thì chấp nhận được; nếu cần lock:
import time
def call_llm_cached_with_lock(messages, model="gpt-4o-mini", temperature=0, ttl=86400):
key = cache_key(messages, model, temperature)
lock_key = f"{key}:lock"
cached = r.get(key)
if cached:
return json.loads(cached)
# Thử lấy lock trong 10s, tự expire sau 30s
acquired = r.set(lock_key, "1", nx=True, ex=30)
if not acquired:
# Đợi process khác hoàn thành rồi đọc lại
for _ in range(20):
time.sleep(0.5)
cached = r.get(key)
if cached:
return json.loads(cached)
# Fallback: gọi API dù không có cache
try:
response = client.chat.completions.create(
model=model, messages=messages, temperature=temperature
)
result = {
"content": response.choices[0].message.content,
"usage": response.usage.model_dump(),
}
r.setex(key, ttl, json.dumps(result))
return result
finally:
r.delete(lock_key)
Khi nào không nên cache
Không phải mọi LLM call đều nên cache. Các trường hợp sau cần cân nhắc hoặc bỏ qua cache:
- Temperature > 0: Response sẽ khác nhau mỗi lần. Cache sẽ trả về đúng 1 variant, loại bỏ diversity mà caller đang cố ý yêu cầu. Hoặc là giảm temperature về 0 khi cache, hoặc là bỏ qua cache cho use case này.
- Real-time data: Query về thời tiết, tin tức, giá cổ phiếu, trạng thái hệ thống — response phải fresh. Cache TTL rất ngắn (1–5 phút) hoặc không cache.
- Personalized response: Nếu response phụ thuộc user ID, session context, hay lịch sử riêng — cache key phải bao gồm user context, dễ bùng nổ key space.
- PII trong prompt: Nếu prompt chứa tên, email, số điện thoại của user, cache key (dù hash) vẫn là fingerprint của PII. Response cache có thể bị serve cho user khác nếu hash trùng (hiếm nhưng không phải zero). Xem mục pitfalls.
- Streaming response: Cache chunk-by-chunk phức tạp hơn. Thường cache toàn bộ accumulated response sau khi stream xong, replay từ cache như non-streaming.
TTL strategies
TTL (Time To Live) xác định bao lâu cache được coi là valid. Chọn sai TTL gây 2 vấn đề: TTL quá ngắn thì hit rate thấp; TTL quá dài thì trả về response lỗi thời.
| TTL | Use case | Ví dụ |
|---|---|---|
| 1h – 1 ngày | Query liên quan đến thông tin có thể thay đổi trong ngày | Tóm tắt news, phân tích sự kiện |
| 1 tuần – 1 tháng | FAQ, doc Q&A với nội dung ổn định | Hỏi về tính năng sản phẩm, policy |
| Không expire | Response xác định hoàn toàn bởi input không đổi | Dịch đoạn văn cố định, classification với prompt cố định |
| Conditional TTL | Dùng field trong response để quyết định TTL | Nếu model trả "uncertain" → TTL 1h; trả "definitive" → TTL 7 ngày |
Không expire trong Redis: r.set(key, value) không có ex/px parameter — key tồn tại cho đến khi bị xóa hoặc bị evict theo eviction policy (xem mục 14).
Cache invalidation
Khi nào cần invalidate:
- Upgrade model (vd từ
gpt-4osang phiên bản mới) — response chất lượng cao hơn nhưng cache vẫn trả response cũ. - Thay đổi system prompt, persona, hoặc few-shot example.
- Phát hiện cached response sai (hallucination) cần xóa ngay.
Pattern delete theo prefix
def invalidate_by_prefix(pattern: str = "llm:exact:*"):
"""Xóa tất cả key match pattern. Dùng SCAN thay KEYS để tránh block Redis."""
cursor = 0
deleted = 0
while True:
cursor, keys = r.scan(cursor, match=pattern, count=100)
if keys:
r.delete(*keys)
deleted += len(keys)
if cursor == 0:
break
return deleted
KEYS * block Redis cho đến khi quét xong toàn bộ keyspace — nguy hiểm với Redis lớn. SCAN trả về từng batch nhỏ, không block.
Version key
Thay vì xóa key cũ, bump version trong prefix:
MODEL_VERSION = "v3" # Tăng khi upgrade model hoặc system prompt
def cache_key_versioned(messages, model, temperature):
payload = json.dumps(
{"messages": messages, "model": model, "temperature": temperature},
sort_keys=True,
)
h = hashlib.sha256(payload.encode()).hexdigest()
return f"llm:{MODEL_VERSION}:exact:{h}"
Key cũ (v2, v1) sẽ tự expire theo TTL và không bao giờ được hit lại — không cần xóa thủ công.
Tag-based invalidation
Lưu danh sách key theo tag (vd "product_docs"), khi tài liệu thay đổi thì xóa tất cả key trong tag đó. Phức tạp hơn nhưng granular hơn pattern delete.
Semantic cache với Redis Vector Search
Redis Stack (từ v7.2 tích hợp RediSearch + RedisJSON) hỗ trợ vector similarity search. Đây là nền tảng cho semantic cache: lưu embedding của prompt, tìm entry gần nhất khi có query mới.
Tạo index một lần
import numpy as np
from redis.commands.search.field import VectorField, TextField
from redis.commands.search.indexDefinition import IndexDefinition, IndexType
from redis.commands.search.query import Query
EMBEDDING_DIM = 1536 # text-embedding-3-small
def create_semantic_index():
try:
r.ft("llm_semantic").dropindex(delete_documents=False)
except Exception:
pass # Index chưa tồn tại — bỏ qua
r.ft("llm_semantic").create_index(
fields=[
TextField("prompt"),
TextField("response"),
VectorField(
"embedding",
"HNSW",
{
"TYPE": "FLOAT32",
"DIM": EMBEDDING_DIM,
"DISTANCE_METRIC": "COSINE",
"M": 16, # HNSW parameter
"EF_CONSTRUCTION": 200,
},
),
],
definition=IndexDefinition(
prefix=["llm:semantic:"], index_type=IndexType.HASH
),
)
Embedding helper
def get_embedding(text: str) -> np.ndarray:
resp = client.embeddings.create(
model="text-embedding-3-small",
input=text,
)
return np.array(resp.data[0].embedding, dtype=np.float32)
Lookup và store
def semantic_lookup(prompt: str, threshold: float = 0.97) -> str | None:
"""
threshold=0.97 tương đương cosine similarity >= 0.97.
Redis trả COSINE distance (0 = identical, 2 = opposite),
nên distance <= (1 - threshold) là hit.
"""
emb = get_embedding(prompt).tobytes()
q = (
Query("*=>[KNN 1 @embedding $vec AS score]")
.return_fields("prompt", "response", "score")
.dialect(2)
)
results = r.ft("llm_semantic").search(q, {"vec": emb})
if results.docs:
score = float(results.docs[0].score)
if score <= (1 - threshold):
return results.docs[0].response
return None
def semantic_store(prompt: str, response: str):
emb = get_embedding(prompt).tobytes()
key = f"llm:semantic:{hashlib.sha256(prompt.encode()).hexdigest()}"
r.hset(
key,
mapping={"prompt": prompt, "response": response, "embedding": emb},
)
Lưu ý về threshold:
- 0.97–0.99: rất chặt, gần như exact; hit rate thấp nhưng ít false positive.
- 0.90–0.95: hợp lý cho FAQ; cần test thực tế với data của bạn.
- Dưới 0.85: rủi ro cao trả về response không liên quan.
GPTCache — managed semantic cache
GPTCache (GitHub: zilliztech/GPTCache) là thư viện wrap OpenAI SDK, tự động cache + lookup semantic mà không cần tự implement embedding + vector search.
pip install gptcache
from gptcache import cache
from gptcache.adapter import openai as gptcache_openai
from gptcache.embedding import Onnx
from gptcache.manager import CacheBase, VectorBase, get_data_manager
from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation
# Khởi tạo (chạy 1 lần khi startup)
onnx = Onnx()
data_manager = get_data_manager(
CacheBase("sqlite"), # metadata store
VectorBase("faiss", dimension=onnx.dimension), # vector store
)
cache.init(
embedding_func=onnx.to_embeddings,
data_manager=data_manager,
similarity_evaluation=SearchDistanceEvaluation(),
)
# Sau khi init, dùng gptcache_openai thay openai thông thường
response = gptcache_openai.ChatCompletion.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "FastAPI là gì?"}],
)
print(response["choices"][0]["message"]["content"])
GPTCache hỗ trợ nhiều backend: SQLite (development), Redis (production), Milvus (large scale). Thay VectorBase("faiss", ...) bằng VectorBase("redis", ...) khi cần distributed cache.
Hạn chế: GPTCache dùng API style cũ của openai SDK (trước 1.0). Với openai SDK 1.x, cần dùng adapter layer hoặc implement từ đầu như mục 9.
LangChain cache integration
LangChain 0.3.x có sẵn cache layer qua set_llm_cache(). Tất cả LLM call trong chain đi qua cache này tự động.
pip install langchain langchain-openai langchain-community
Exact match với RedisCache
import redis
from langchain.globals import set_llm_cache
from langchain_community.cache import RedisCache
from langchain_openai import ChatOpenAI
r = redis.Redis(host="localhost", port=6379, db=0)
set_llm_cache(RedisCache(redis_=r))
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Lần đầu: gọi API
response1 = llm.invoke("FastAPI là gì?")
# Lần thứ 2 cùng prompt: lấy từ Redis, không gọi API
response2 = llm.invoke("FastAPI là gì?")
Semantic với RedisSemanticCache
from langchain_community.cache import RedisSemanticCache
from langchain_openai import OpenAIEmbeddings
set_llm_cache(
RedisSemanticCache(
redis_url="redis://localhost:6379",
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
score_threshold=0.2, # LangChain dùng L2 distance, không phải cosine
)
)
# Các câu tương đồng ngữ nghĩa sẽ được cache
response = llm.invoke("FastAPI dùng để làm gì?")
Lưu ý: score_threshold trong RedisSemanticCache là L2 distance threshold, không phải cosine similarity — giá trị nhỏ nghĩa là gần hơn. Giá trị mặc định 0.2 thường hơi chặt; thử 0.3–0.5 nếu hit rate quá thấp với data của bạn. Đọc source code để xác nhận vì convention này có thể thay đổi theo version.
Anthropic prompt caching (provider-level)
Từ tháng 8/2024, Anthropic hỗ trợ prompt caching native. Khác với application-level cache (cache toàn bộ response), đây là cache ở tầng KV (key-value attention cache) bên trong model. Phần được cache không bị tính phí input token lần 2.
Pricing (Claude Sonnet 4, tháng 5/2026):
- Cache write: 1.25× base input price.
- Cache read: 0.1× base input price — tiết kiệm 90%.
- Break-even: đọc lại 2 lần trở lên là có lợi.
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT = "..." * 500 # System prompt dài, ít thay đổi
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}, # Đánh dấu để cache
}
],
messages=[{"role": "user", "content": "Câu hỏi của user"}],
)
# Kiểm tra cache status
usage = response.usage
print(f"cache_creation_input_tokens: {usage.cache_creation_input_tokens}")
print(f"cache_read_input_tokens: {usage.cache_read_input_tokens}")
Điều kiện để cache có hiệu quả:
- Prefix được cache phải >= 1024 token (Claude Sonnet/Haiku) hoặc >= 2048 token (Claude Opus).
- Cache TTL của Anthropic là 5 phút (ephemeral). Nếu không gọi lại trong 5 phút, cache expire.
- Phần thay đổi (user message) phải nằm sau phần cache.
OpenAI cũng có context caching trong một số model (Gemini 1.5 Pro đã có từ lâu). Anthropic và Google là hai provider có explicit prompt caching API tính đến giữa 2026.
Monitoring cache metrics
Ba metric cốt lõi:
- Hit rate = hits / (hits + misses). Target thực tế: 30–60% cho app có mixed traffic; FAQ-heavy app có thể đạt 70–80%.
- Latency với/không cache: Cache hit nên <10ms (Redis lookup); API call thường 1–10s.
- Cost saved: = hits × average_token_count × token_price.
Prometheus counter
from prometheus_client import Counter, Histogram
CACHE_HITS = Counter(
"llm_cache_hits_total",
"Total LLM cache hits",
["cache_type"], # "exact" | "semantic"
)
CACHE_MISSES = Counter(
"llm_cache_misses_total",
"Total LLM cache misses",
["cache_type"],
)
LLM_LATENCY = Histogram(
"llm_call_duration_seconds",
"LLM call latency",
["source"], # "cache" | "api"
buckets=[0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
)
def call_llm_cached_instrumented(messages, model="gpt-4o-mini", temperature=0, ttl=86400):
import time
key = cache_key(messages, model, temperature)
t0 = time.perf_counter()
cached = r.get(key)
if cached:
CACHE_HITS.labels(cache_type="exact").inc()
LLM_LATENCY.labels(source="cache").observe(time.perf_counter() - t0)
return json.loads(cached)
CACHE_MISSES.labels(cache_type="exact").inc()
t1 = time.perf_counter()
response = client.chat.completions.create(
model=model, messages=messages, temperature=temperature
)
LLM_LATENCY.labels(source="api").observe(time.perf_counter() - t1)
result = {
"content": response.choices[0].message.content,
"usage": response.usage.model_dump(),
}
r.setex(key, ttl, json.dumps(result))
return result
Ngoài Prometheus, Redis có sẵn stats qua redis-cli INFO stats:
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
# keyspace_hits:12450
# keyspace_misses:3210
Cache memory management và sizing
Ước lượng RAM
1 cached response (content + usage metadata, JSON serialized) ≈ 3–10 KB, trung bình ~5 KB.
Ví dụ: 100,000 request/ngày, hit rate 40%, unique query 60,000 entry × 5 KB = 300 MB. Redis 1 GB thoải mái.
Với semantic cache, thêm embedding vector: 1536 float32 × 4 bytes = 6 KB/entry riêng cho vector. Tổng ~11 KB/entry.
Cấu hình Redis maxmemory
# redis.conf hoặc docker run -e
maxmemory 2gb
maxmemory-policy allkeys-lru
Các eviction policy phổ biến:
allkeys-lru: xóa key ít dùng nhất, áp dụng cho mọi key — phù hợp nhất cho cache.volatile-lru: chỉ xóa key có TTL — an toàn hơn nhưng có thể fail nếu không có key nào có TTL.noeviction(default): trả error khi đầy bộ nhớ — nguy hiểm cho cache.
# Kiểm tra memory hiện tại
redis-cli INFO memory | grep used_memory_human
Distributed cache patterns
Chọn topology Redis phù hợp với scale:
| Topology | Mô tả | Phù hợp |
|---|---|---|
| Single Redis | 1 instance, đơn giản nhất | Development, startup nhỏ; SPOF (Single Point of Failure) |
| Redis Sentinel | 1 primary + N replica + Sentinel process monitor | Production cần HA; auto failover trong ~30s |
| Redis Cluster | Sharding tự động, scale horizontal | Khi data > RAM của 1 node; traffic > 100k req/s |
| Managed (Upstash, ElastiCache, Memorystore) | Cloud-managed, serverless pricing (Upstash) | Không muốn manage infra; Upstash phù hợp cho low-traffic với serverless pricing |
Kết nối Redis Sentinel từ Python:
from redis.sentinel import Sentinel
sentinel = Sentinel(
[("sentinel-host-1", 26379), ("sentinel-host-2", 26379)],
socket_timeout=0.1,
)
# Sentinel tự resolve primary
master = sentinel.master_for("mymaster", socket_timeout=0.1, decode_responses=True)
master.set("key", "value")
Common pitfalls
- Response chứa timestamp hoặc random data. LLM đôi khi thêm "As of [date]..." vào response kể cả khi không được yêu cầu. Cache sẽ trả response cũ với ngày sai. Thiết kế system prompt để tránh timestamp không cần thiết trong output.
-
Quên TTL. Không đặt TTL cho key → cache không bao giờ expire → đầy RAM → eviction xóa ngẫu nhiên hoặc Redis trả error nếu dùng
noeviction. -
Cache key không bao gồm model version. Khi upgrade từ
gpt-4o-2024-11-20sang phiên bản mới hơn mà không bump version trong key, cache vẫn trả response cũ. Bao gồm model string đầy đủ vào payload khi hash. - Semantic cache threshold quá thấp. Ví dụ threshold=0.80: "giá iPhone 15 là bao nhiêu" và "iPhone 15 Pro cấu hình thế nào" có thể có similarity 0.85 — response sai. Test thực tế với negative examples trước khi chọn threshold.
- Cache full prompt với PII. Nếu prompt chứa "Tên tôi là Nguyễn Văn A, tôi muốn hỏi về...", cache key là hash của chuỗi có PII. Dù hash không reversible, response được lưu vào cache có thể được phục vụ cho user khác nếu hash trùng (cực kỳ hiếm nhưng trong GDPR context cần xem xét). Mask PII khỏi cache key, hoặc không cache prompt có PII.
-
Sai serializer.
json.dumpskhông xử lýdatetime,UUID, hay Pydantic model.response.usage.model_dump()trả dict thuần — OK. Nhưng nếu bạn tự thêm field có kiểu không serializable, code sẽ raise exception và fail silently nếu cache write nằm trong try/except rộng. - Race condition (double-spend). 2 request đồng thời miss cache → cả 2 gọi LLM. Xem implementation với distributed lock ở mục 5.
-
Dùng
KEYSpattern scan.r.keys("llm:*")block Redis cho đến khi hoàn thành. Với Redis có hàng triệu key, điều này gây downtime. Dùngr.scan_iter()hoặcr.scan()thay thế.
Tóm tắt
- ✅ Exact match cache: hash(messages + model + params) → response; đơn giản, zero false positive, hit rate tùy use case.
- ✅ Semantic cache: embedding + Redis Vector Search; bắt query tương đồng; threshold cần test thực tế.
- ✅ Chỉ cache khi
temperature=0và response không phụ thuộc real-time hoặc PII. - ✅ TTL: FAQ dài ngày → 7–30 ngày; news-related → 1h–1 ngày; immutable → không expire.
- ✅ Invalidation: dùng version key hoặc SCAN-based pattern delete, tránh KEYS.
- ✅ Anthropic prompt caching tiết kiệm 90% cost cho phần repeated prefix ≥ 1024 token.
- ✅ Redis maxmemory +
allkeys-lruđể tránh OOM. - ✅ Monitor hit rate, latency, cost saved qua Prometheus counter.
