mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
310 lines
10 KiB
Markdown
310 lines
10 KiB
Markdown
---
|
||||
|
|
name: llm-caching
|
|||
|
|
description: Implement multi-layer LLM caching with exact match, semantic similarity, and provider-side prompt caching. Reduce API costs by 30–70%, cut latency, and improve throughput using Redis, GPTCache, and provider caching APIs.
|
|||
|
|
license: MIT
|
|||
|
|
metadata:
|
|||
|
|
author: devops-skills
|
|||
|
|
version: "1.0"
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# LLM Caching
|
|||
|
|
|
|||
|
|
Cut LLM costs and latency with exact match, semantic, and provider-side caching layers.
|
|||
|
|
|
|||
|
|
## When to Use This Skill
|
|||
|
|
|
|||
|
|
Use this skill when:
|
|||
|
|
- The same or similar queries are asked repeatedly (FAQ bots, support tools)
|
|||
|
|
- LLM API costs are growing and you need immediate savings
|
|||
|
|
- Serving high request volumes where repeated queries cause bottlenecks
|
|||
|
|
- Implementing prompt caching for long system prompts (Anthropic/OpenAI)
|
|||
|
|
- Building offline-capable AI features that need response persistence
|
|||
|
|
|
|||
|
|
## Caching Layers
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Request → Exact Cache → Semantic Cache → Provider Cache → LLM API
|
|||
|
|
↓ hit ↓ hit ↓ hit
|
|||
|
|
instant ~5ms 50-80% cheaper
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Layer 1: Exact Match Cache (Redis)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import hashlib
|
|||
|
|
import json
|
|||
|
|
import redis
|
|||
|
|
from openai import OpenAI
|
|||
|
|
|
|||
|
|
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
|
|||
|
|
client = OpenAI()
|
|||
|
|
|
|||
|
|
def build_cache_key(model: str, messages: list, temperature: float) -> str:
|
|||
|
|
"""Deterministic key from request parameters."""
|
|||
|
|
payload = json.dumps({
|
|||
|
|
"model": model,
|
|||
|
|
"messages": messages,
|
|||
|
|
"temperature": temperature,
|
|||
|
|
}, sort_keys=True)
|
|||
|
|
return f"llm:exact:{hashlib.sha256(payload.encode()).hexdigest()}"
|
|||
|
|
|
|||
|
|
def cached_completion(model: str, messages: list, temperature: float = 0.0,
|
|||
|
|
ttl: int = 3600) -> dict:
|
|||
|
|
key = build_cache_key(model, messages, temperature)
|
|||
|
|
|
|||
|
|
# Check cache
|
|||
|
|
if cached := r.get(key):
|
|||
|
|
return json.loads(cached)
|
|||
|
|
|
|||
|
|
# Call API
|
|||
|
|
response = client.chat.completions.create(
|
|||
|
|
model=model, messages=messages, temperature=temperature
|
|||
|
|
)
|
|||
|
|
result = response.model_dump()
|
|||
|
|
|
|||
|
|
# Cache result (only cache deterministic responses)
|
|||
|
|
if temperature == 0.0:
|
|||
|
|
r.setex(key, ttl, json.dumps(result))
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Layer 2: Semantic Cache (GPTCache)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from gptcache import cache, Config
|
|||
|
|
from gptcache.adapter import openai
|
|||
|
|
from gptcache.embedding import Onnx
|
|||
|
|
from gptcache.manager import CacheBase, VectorBase, get_data_manager
|
|||
|
|
from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation
|
|||
|
|
|
|||
|
|
# Configure GPTCache with Qdrant backend
|
|||
|
|
def init_gptcache(cache_obj, llm: str):
|
|||
|
|
onnx = Onnx() # local embedding model
|
|||
|
|
data_manager = get_data_manager(
|
|||
|
|
CacheBase("redis"), # metadata store
|
|||
|
|
VectorBase("qdrant",
|
|||
|
|
host="localhost",
|
|||
|
|
port=6333,
|
|||
|
|
collection_name=f"llm-cache-{llm}",
|
|||
|
|
dimension=onnx.dimension),
|
|||
|
|
)
|
|||
|
|
cache_obj.init(
|
|||
|
|
embedding_func=onnx.to_embeddings,
|
|||
|
|
data_manager=data_manager,
|
|||
|
|
similarity_evaluation=SearchDistanceEvaluation(),
|
|||
|
|
config=Config(similarity_threshold=0.80), # 80% similarity = cache hit
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
cache.set_openai_key()
|
|||
|
|
init_gptcache(cache, "gpt-4o-mini")
|
|||
|
|
|
|||
|
|
# Now openai calls are automatically cached
|
|||
|
|
response = openai.ChatCompletion.create(
|
|||
|
|
model="gpt-4o-mini",
|
|||
|
|
messages=[{"role": "user", "content": "What is machine learning?"}],
|
|||
|
|
)
|
|||
|
|
# Second call with similar question ("Explain machine learning") → cache hit
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Custom Semantic Cache (Production-Grade)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from sentence_transformers import SentenceTransformer
|
|||
|
|
from qdrant_client import QdrantClient
|
|||
|
|
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, Range
|
|||
|
|
import numpy as np
|
|||
|
|
import uuid
|
|||
|
|
import time
|
|||
|
|
|
|||
|
|
embed_model = SentenceTransformer("BAAI/bge-small-en-v1.5") # fast, 33M params
|
|||
|
|
qdrant = QdrantClient("http://localhost:6333")
|
|||
|
|
|
|||
|
|
CACHE_COLLECTION = "semantic-cache"
|
|||
|
|
SIMILARITY_THRESHOLD = 0.88
|
|||
|
|
CACHE_TTL_SECONDS = 86400 # 24h
|
|||
|
|
|
|||
|
|
# Create collection once
|
|||
|
|
qdrant.create_collection(
|
|||
|
|
collection_name=CACHE_COLLECTION,
|
|||
|
|
vectors_config=VectorParams(size=384, distance=Distance.COSINE),
|
|||
|
|
on_disk_payload=True,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def semantic_cache_lookup(query: str, model: str) -> str | None:
|
|||
|
|
embedding = embed_model.encode(query).tolist()
|
|||
|
|
results = qdrant.query_points(
|
|||
|
|
collection_name=CACHE_COLLECTION,
|
|||
|
|
query=embedding,
|
|||
|
|
query_filter=Filter(must=[
|
|||
|
|
FieldCondition(key="model", match={"value": model}),
|
|||
|
|
FieldCondition(key="expires_at", range=Range(gte=time.time())),
|
|||
|
|
]),
|
|||
|
|
limit=1,
|
|||
|
|
score_threshold=SIMILARITY_THRESHOLD,
|
|||
|
|
)
|
|||
|
|
if results.points:
|
|||
|
|
return results.points[0].payload["response"]
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def semantic_cache_store(query: str, response: str, model: str):
|
|||
|
|
embedding = embed_model.encode(query).tolist()
|
|||
|
|
qdrant.upsert(
|
|||
|
|
collection_name=CACHE_COLLECTION,
|
|||
|
|
points=[PointStruct(
|
|||
|
|
id=str(uuid.uuid4()),
|
|||
|
|
vector=embedding,
|
|||
|
|
payload={
|
|||
|
|
"query": query,
|
|||
|
|
"response": response,
|
|||
|
|
"model": model,
|
|||
|
|
"created_at": time.time(),
|
|||
|
|
"expires_at": time.time() + CACHE_TTL_SECONDS,
|
|||
|
|
},
|
|||
|
|
)],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def smart_llm_call(query: str, model: str = "gpt-4o-mini") -> dict:
|
|||
|
|
# 1. Semantic lookup
|
|||
|
|
if cached_response := semantic_cache_lookup(query, model):
|
|||
|
|
return {"response": cached_response, "source": "semantic_cache", "cost": 0}
|
|||
|
|
|
|||
|
|
# 2. LLM call
|
|||
|
|
response = client.chat.completions.create(
|
|||
|
|
model=model,
|
|||
|
|
messages=[{"role": "user", "content": query}],
|
|||
|
|
)
|
|||
|
|
text = response.choices[0].message.content
|
|||
|
|
cost = litellm.completion_cost(response)
|
|||
|
|
|
|||
|
|
# 3. Store in cache
|
|||
|
|
semantic_cache_store(query, text, model)
|
|||
|
|
|
|||
|
|
return {"response": text, "source": "llm_api", "cost": cost}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Layer 3: Provider-Side Prompt Caching
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# Anthropic — cache long system prompts (saves 90% on cached input tokens)
|
|||
|
|
import anthropic
|
|||
|
|
|
|||
|
|
client = anthropic.Anthropic()
|
|||
|
|
|
|||
|
|
# Long system prompt — mark for caching
|
|||
|
|
SYSTEM_PROMPT = open("knowledge-base.txt").read() # e.g., 50k tokens
|
|||
|
|
|
|||
|
|
def call_with_prompt_cache(user_question: str) -> str:
|
|||
|
|
response = client.messages.create(
|
|||
|
|
model="claude-sonnet-4-6",
|
|||
|
|
max_tokens=1024,
|
|||
|
|
system=[
|
|||
|
|
{"type": "text", "text": "You are a helpful assistant."},
|
|||
|
|
{
|
|||
|
|
"type": "text",
|
|||
|
|
"text": SYSTEM_PROMPT,
|
|||
|
|
"cache_control": {"type": "ephemeral"}, # cache this block
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
messages=[{"role": "user", "content": user_question}],
|
|||
|
|
)
|
|||
|
|
# Log cache efficiency
|
|||
|
|
usage = response.usage
|
|||
|
|
cache_savings = usage.cache_read_input_tokens * 0.9 # 90% discount on cached
|
|||
|
|
print(f"Cache hits: {usage.cache_read_input_tokens} tokens "
|
|||
|
|
f"(saved ~${cache_savings * 3.0 / 1_000_000:.4f})")
|
|||
|
|
return response.content[0].text
|
|||
|
|
|
|||
|
|
# OpenAI — automatic for repeated prefixes (≥1,024 tokens)
|
|||
|
|
# No code change needed; cached tokens appear in usage.prompt_tokens_details
|
|||
|
|
response = client.chat.completions.create(
|
|||
|
|
model="gpt-4o-mini",
|
|||
|
|
messages=[
|
|||
|
|
{"role": "system", "content": LONG_SYSTEM_PROMPT}, # auto-cached
|
|||
|
|
{"role": "user", "content": user_question},
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
cached = response.usage.prompt_tokens_details.cached_tokens
|
|||
|
|
print(f"OpenAI cached {cached} tokens")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Cache Warming
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def warm_cache(common_queries: list[str], model: str):
|
|||
|
|
"""Pre-populate cache with known frequent queries."""
|
|||
|
|
import asyncio
|
|||
|
|
from openai import AsyncOpenAI
|
|||
|
|
|
|||
|
|
aclient = AsyncOpenAI()
|
|||
|
|
|
|||
|
|
async def warm_single(query: str):
|
|||
|
|
if not semantic_cache_lookup(query, model):
|
|||
|
|
response = await aclient.chat.completions.create(
|
|||
|
|
model=model,
|
|||
|
|
messages=[{"role": "user", "content": query}],
|
|||
|
|
)
|
|||
|
|
text = response.choices[0].message.content
|
|||
|
|
semantic_cache_store(query, text, model)
|
|||
|
|
print(f"Warmed: {query[:50]}...")
|
|||
|
|
|
|||
|
|
await asyncio.gather(*[warm_single(q) for q in common_queries])
|
|||
|
|
|
|||
|
|
# Warm on startup
|
|||
|
|
import asyncio
|
|||
|
|
asyncio.run(warm_cache(FREQUENT_QUERIES, "gpt-4o-mini"))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Cache Metrics
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from prometheus_client import Counter, Histogram
|
|||
|
|
|
|||
|
|
cache_hits = Counter("llm_cache_hits_total", "Cache hits", ["cache_layer", "model"])
|
|||
|
|
cache_misses = Counter("llm_cache_misses_total", "Cache misses", ["model"])
|
|||
|
|
cache_savings_usd = Counter("llm_cache_savings_usd_total", "USD saved by cache", ["model"])
|
|||
|
|
|
|||
|
|
# Use in your smart_llm_call function
|
|||
|
|
if source == "semantic_cache":
|
|||
|
|
cache_hits.labels(cache_layer="semantic", model=model).inc()
|
|||
|
|
cache_savings_usd.labels(model=model).inc(estimated_cost)
|
|||
|
|
else:
|
|||
|
|
cache_misses.labels(model=model).inc()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Redis Configuration for LLM Caching
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# redis.conf tuning for LLM cache workload
|
|||
|
|
maxmemory 8gb
|
|||
|
|
maxmemory-policy allkeys-lru # evict least-recently-used when full
|
|||
|
|
save "" # disable persistence (cache is ephemeral)
|
|||
|
|
appendonly no
|
|||
|
|
tcp-keepalive 60
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Common Issues
|
|||
|
|
|
|||
|
|
| Issue | Cause | Fix |
|
|||
|
|
|-------|-------|-----|
|
|||
|
|
| Low cache hit rate | Threshold too strict | Lower `SIMILARITY_THRESHOLD` to 0.82–0.85 |
|
|||
|
|
| Stale cached responses | Long TTL | Use topic-specific TTLs; invalidate on data updates |
|
|||
|
|
| Cache serving wrong answers | Threshold too loose | Raise threshold or add model-name filtering |
|
|||
|
|
| Redis OOM | No eviction policy | Set `maxmemory` + `allkeys-lru` |
|
|||
|
|
| Slow semantic lookup | Large cache collection | Add payload index on `model` + `expires_at` |
|
|||
|
|
|
|||
|
|
## Best Practices
|
|||
|
|
|
|||
|
|
- Start with exact cache — zero cost, instant wins for identical queries.
|
|||
|
|
- Semantic threshold of 0.88–0.92 balances hit rate vs. accuracy; tune with your data.
|
|||
|
|
- Set per-model TTLs: longer for stable knowledge (1 week), shorter for news/events (1 hour).
|
|||
|
|
- Always filter by model name in semantic cache — different models give different answers.
|
|||
|
|
- Log cache hit rate as a KPI; target 30%+ for FAQ-style applications.
|
|||
|
|
|
|||
|
|
## Related Skills
|
|||
|
|
|
|||
|
|
- [llm-cost-optimization](../llm-cost-optimization/) - Full cost strategy
|
|||
|
|
- [llm-gateway](../../infrastructure/networking/llm-gateway/) - Gateway-level caching
|
|||
|
|
- [vector-database-ops](../../infrastructure/databases/vector-database-ops/) - Qdrant setup
|
|||
|
|
- [agent-observability](../agent-observability/) - Cache metrics dashboards
|