Caching semantico con gli embedding
Crei una cache semantica che recuperi risposte memorizzate per query semanticamente simili ma non identiche, confrontando gli embedding delle query con una cache degli embedding delle richieste precedenti.
Caching semantico con gli embedding è una lezione AI Engineering Academy gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Engineering Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Engineering Academy include 4 lezioni in totale.
Il limite del caching esatto
Il caching esatto è utile solo quando gli utenti inviano richieste identiche byte per byte. Nella realtà, gli utenti formulano la stessa domanda in modi diversi: «Come posso annullare il mio abbonamento?», «Qual è la procedura per disiscriversi?» e «Posso interrompere il mio piano?» esprimono tutti la stessa intenzione, ma producono chiavi della cache diverse. Il caching esatto non riconosce nessuna di queste varianti. Il caching semantico risolve il problema confrontando query simili invece di query identiche, aumentando notevolmente i tassi di cache hit.
Come funziona il caching semantico
Una cache semantica memorizza l'embedding di ogni query memorizzata nella cache insieme alla risposta memorizzata. Quando arriva una nuova query, ne calcoli l'embedding e cerchi nella cache una query già vista con un'elevata similarità coseno. Se la query memorizzata più simile supera una soglia di similarità (in genere 0,95 o superiore), restituisci la risposta memorizzata nella cache. Se non trovi alcuna corrispondenza, chiami l'LLM, memorizzi la nuova risposta e aggiungi l'embedding della nuova query all'indice della cache per le ricerche future.
# Semantic cache flow
# 1. New query arrives: 'How do I cancel my subscription?'
# 2. Embed it: embed_query = embed('How do I cancel my subscription?')
# 3. Search cache index for nearest cached query embedding
# 4. Find cached: 'What is the process to unsubscribe?' (similarity=0.97)
# 5. 0.97 >= threshold (0.95) → cache HIT, return cached response
# 6. If 0.82 < threshold → cache MISS, call LLM, cache result, add embedding to indexCache semantica in memoria con NumPy
Per applicazioni di piccole dimensioni o prototipi, implementi il caching semantico in memoria utilizzando NumPy per calcolare la similarità coseno. Memorizzi gli embedding delle query nella cache in un array 2D e le risposte in un elenco parallelo. Per ogni nuova query, calcoli la similarità coseno tra il nuovo embedding e tutti gli embedding memorizzati nella cache, quindi restituisca la corrispondenza più vicina se supera la soglia.
import numpy as np
from openai import OpenAI
client = OpenAI()
class InMemorySemanticCache:
def __init__(self, threshold: float = 0.95):
self.threshold = threshold
self.embeddings = [] # list of np.ndarray
self.responses = [] # list of str
self.queries = [] # list of str (for inspection)
def _embed(self, text: str) -> np.ndarray:
resp = client.embeddings.create(model='text-embedding-3-small', input=text)
return np.array(resp.data[0].embedding)
def get(self, query: str) -> str | None:
if not self.embeddings:
return None
q_emb = self._embed(query)
cache_matrix = np.array(self.embeddings)
# Cosine similarity: dot product of normalized vectors
norms = np.linalg.norm(cache_matrix, axis=1)
q_norm = np.linalg.norm(q_emb)
sims = (cache_matrix @ q_emb) / (norms * q_norm + 1e-8)
best_idx = int(np.argmax(sims))
if sims[best_idx] >= self.threshold:
print(f'[SEMANTIC HIT] sim={sims[best_idx]:.3f} matched: {self.queries[best_idx]!r}')
return self.responses[best_idx]
return None
def set(self, query: str, response: str):
emb = self._embed(query)
self.embeddings.append(emb)
self.responses.append(response)
self.queries.append(query)Cache semantica con Redis e Pinecone
Per la cache semantica in produzione, archiviare gli embedding delle query in un database vettoriale per eseguire rapidamente una ricerca approssimata dei vicini più prossimi e archiviare le risposte in Redis usando un ID univoco come chiave. Quando arriva una nuova query, cercare nel database vettoriale la query memorizzata nella cache più vicina, recuperare la risposta da Redis usando l'ID nei metadati del vettore e restituirla, il tutto senza chiamare l'LLM.
import redis
from pinecone import Pinecone
import hashlib
r = redis.Redis(decode_responses=True)
pc = Pinecone(api_key='YOUR_KEY')
index = pc.Index('semantic-cache')
SIMILARITY_THRESHOLD = 0.95
def semantic_cache_get(query: str) -> str | None:
q_emb = embed(query) # from earlier lesson
results = index.query(vector=q_emb, top_k=1, include_metadata=True)
if not results.matches:
return None
best = results.matches[0]
if best.score >= SIMILARITY_THRESHOLD:
response_key = best.metadata.get('response_key')
return r.get(response_key)
return None
def semantic_cache_set(query: str, response: str):
q_emb = embed(query)
entry_id = hashlib.sha256(query.encode()).hexdigest()[:16]
response_key = f'sem_cache_resp:{entry_id}'
r.setex(response_key, 86400, response) # 24h TTL
index.upsert(vectors=[{
'id': entry_id,
'values': q_emb,
'metadata': {'query': query[:200], 'response_key': response_key},
}])GPTCache: una cache semantica pronta all'uso
GPTCache è una libreria open source che implementa la cache semantica per le applicazioni basate su LLM. Avvolge il client OpenAI, crea automaticamente gli embedding delle query, verifica una cache basata sulla similarità vettoriale e, in caso di mancata corrispondenza, ricorre all'API reale. Supporta diversi archivi vettoriali (FAISS, Milvus, Redis) e diversi modelli di embedding pronti all'uso, offrendo un modo rapido per aggiungere la cache semantica a un'applicazione esistente.
# pip install gptcache
from gptcache import cache
from gptcache.adapter import openai
from gptcache.embedding import OpenAI as EmbeddingOpenAI
from gptcache.manager import CacheBase, VectorBase, get_data_manager
from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation
# Configure GPTCache with FAISS vector store
cache.init(
embedding_func=EmbeddingOpenAI().to_embeddings,
data_manager=get_data_manager(
CacheBase('sqlite'),
VectorBase('faiss', dimension=1536),
),
similarity_evaluation=SearchDistanceEvaluation(),
)
# Now use the wrapped openai client — caching is transparent
response = openai.ChatCompletion.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': 'What is RAG?'}],
)Scegliere la soglia di similarità
La soglia di similarità è l'iperparametro più importante per la cache semantica. Se è troppo alta (0.99+), si perdono la maggior parte delle varianti parafrasate. Se è troppo bassa (0.85-), si restituiscono risposte errate memorizzate nella cache per query diverse ma superficialmente simili. Convalidare empiricamente la soglia campionando coppie di query e verificando che le query sopra la soglia abbiano davvero la stessa risposta attesa. Valori tipici: 0.92-0.97 per domande e risposte fattuali, 0.98+ per la generazione di codice, dove piccole differenze sono molto importanti.
def validate_threshold(cache, query_pairs_with_labels):
'''
query_pairs_with_labels: list of (query1, query2, should_match: bool)
'''
true_pos = true_neg = false_pos = false_neg = 0
for q1, q2, should_match in query_pairs_with_labels:
e1, e2 = embed(q1), embed(q2)
sim = cosine_similarity(e1, e2)
matched = sim >= cache.threshold
if should_match and matched: true_pos += 1
elif not should_match and not matched: true_neg += 1
elif not should_match and matched: false_pos += 1
else: false_neg += 1
precision = true_pos / (true_pos + false_pos) if (true_pos + false_pos) else 0
recall = true_pos / (true_pos + false_neg) if (true_pos + false_neg) else 0
print(f'Precision: {precision:.3f}, Recall: {recall:.3f}')Ambito della cache semantica: il system prompt è importante
Un dettaglio fondamentale: la cache semantica deve tenere conto del system prompt. Due query utente identiche producono risposte diverse se il system prompt è diverso (personas diverse, basi di conoscenza diverse, formati di risposta diversi). Includere sempre il system prompt nell'input per l'embedding oppure creare namespace separati nella cache per ogni system prompt. Un approccio ordinato consiste nell'applicare un hash al system prompt e usarlo come prefisso del namespace della cache.
import hashlib
def make_semantic_cache_namespace(system_prompt: str) -> str:
return 'sc:' + hashlib.md5(system_prompt.encode()).hexdigest()[:8]
def semantic_cache_get_namespaced(system_prompt: str, user_query: str) -> str | None:
namespace = make_semantic_cache_namespace(system_prompt)
q_emb = embed(user_query)
# Search only within this namespace
results = index.query(
vector=q_emb,
top_k=1,
filter={'namespace': namespace},
include_metadata=True,
)
if results.matches and results.matches[0].score >= SIMILARITY_THRESHOLD:
return r.get(results.matches[0].metadata['response_key'])
return NoneAnalisi del tasso di riscontro della cache semantica
Dopo aver implementato la cache semantica, analizzare i tassi di riscontro suddivisi per cluster di query. Usare direttamente gli embedding delle query memorizzate nella cache: raggrupparli con K-means e calcolare il tasso di riscontro per ogni cluster. I cluster con un tasso elevato rappresentano temi di domande comuni, per i quali la cache offre i maggiori vantaggi. I cluster con un tasso basso, composti da query uniche e diversificate, potrebbero non trarre alcun beneficio dalla cache e si potrebbero escludere per ridurre le dimensioni dell'indice e i costi degli embedding.
from sklearn.cluster import KMeans
import numpy as np
def analyze_cache_clusters(cache, n_clusters=10):
if len(cache.embeddings) < n_clusters:
print('Not enough cache entries to cluster')
return
matrix = np.array(cache.embeddings)
kmeans = KMeans(n_clusters=n_clusters, n_init=10, random_state=42)
labels = kmeans.fit_predict(matrix)
from collections import Counter
cluster_sizes = Counter(labels)
print('Query clusters by size:')
for cluster_id, count in cluster_sizes.most_common():
representative = cache.queries[labels.tolist().index(cluster_id)]
print(f' Cluster {cluster_id}: {count} queries, e.g. {representative!r}')Considerazioni sulla sicurezza della cache semantica
La cache semantica introduce un rischio per la privacy: se l'utente A pone una domanda sensibile, la relativa risposta potrebbe essere restituita all'utente B che pone una domanda simile. Questo è accettabile per le basi di conoscenza pubbliche, ma non per le applicazioni con dati specifici dell'utente o contenuti sensibili. Applicare un isolamento rigoroso dei namespace per utente o organizzazione e valutare l'esclusione completa dalla cache delle query che corrispondono a modelli come quelli delle informazioni personali.
import re
PII_PATTERNS = [
r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', # phone numbers
r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b', # emails
r'\b\d{9}\b', # SSN-like
]
def should_cache(query: str) -> bool:
for pattern in PII_PATTERNS:
if re.search(pattern, query, re.IGNORECASE):
return False # do not cache queries with PII
return True
def secure_semantic_completion(user_id: str, query: str) -> str:
if should_cache(query):
cached = semantic_cache_get_namespaced(f'user:{user_id}', query)
if cached:
return cached
result = call_llm_api(query) # actual API call
if should_cache(query):
semantic_cache_set_namespaced(f'user:{user_id}', query, result)
return resultCombinare la cache esatta e quella semantica
La strategia di caching più efficiente utilizza sia la cache esatta sia quella semantica in una gerarchia a due livelli. Controllare prima la cache esatta (più rapida e senza costi per gli embedding) e restituire immediatamente il risultato in caso di riscontro. Se non c'è riscontro nella cache esatta, controllare la cache semantica (che richiede una chiamata all'API di embedding). Se non c'è riscontro nemmeno nella cache semantica, chiamare l'LLM. Questo ordine riduce al minimo sia la latenza sia il costo per richiesta.
async def two_tier_cached_completion(messages: list[dict], model: str = 'gpt-4o-mini') -> str:
user_query = messages[-1].get('content', '')
system_prompt = messages[0].get('content', '') if messages and messages[0]['role'] == 'system' else ''
# Tier 1: exact cache (instant, free)
exact_key = make_cache_key(messages, model, temperature=0.0)
exact_cached = await async_r.get(exact_key)
if exact_cached:
return json.loads(exact_cached)
# Tier 2: semantic cache (one embedding call ~5ms)
sem_result = semantic_cache_get_namespaced(system_prompt, user_query)
if sem_result:
# Backfill exact cache to avoid embedding next time
await async_r.setex(exact_key, 3600, json.dumps(sem_result))
return sem_result
# Tier 3: actual LLM call
response = await async_client.chat.completions.create(
model=model, messages=messages, temperature=0.0
)
result = response.choices[0].message.content
await async_r.setex(exact_key, 3600, json.dumps(result))
semantic_cache_set_namespaced(system_prompt, user_query, result)
return resultRiscaldamento della cache per l'avvio a freddo
Una cache semantica vuota non offre alcun vantaggio finché non viene popolata. Per le applicazioni con modelli di traffico prevedibili, pre-riscaldare la cache all'avvio creando gli embedding e memorizzando nella cache le risposte alle domande più frequenti ricavate dai log storici delle query. In questo modo si elimina il periodo di avvio a freddo, durante il quale ogni utente delle prime ore di operatività riceve un mancato riscontro nella cache e genera il costo completo dell'API.
async def warm_semantic_cache(faq_list: list[dict], system_prompt: str):
print(f'Warming cache with {len(faq_list)} FAQ entries...')
for entry in faq_list:
cached = semantic_cache_get_namespaced(system_prompt, entry['question'])
if cached:
print(f' Already cached: {entry["question"][:50]}')
continue
# Generate and cache the response
response = await async_client.chat.completions.create(
model='gpt-4o-mini',
messages=[
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': entry['question']},
],
temperature=0.0,
)
answer = response.choices[0].message.content
semantic_cache_set_namespaced(system_prompt, entry['question'], answer)
print(f' Cached: {entry["question"][:50]}')
print('Cache warming complete')Verifica rapida
Verifichi la Sua comprensione della cache semantica trattata in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che la cache semantica abbina query simili ma non identiche confrontando gli embedding delle query con un archivio vettoriale di embedding di query memorizzati nella cache; la soglia di similarità controlla il compromesso tra tasso di riscontro e correttezza della risposta; e la creazione di namespace per il system prompt impedisce riscontri errati nella cache tra contesti diversi. Un'architettura a due livelli che controlla la cache esatta prima di quella semantica riduce al minimo sia la latenza sia i costi degli embedding. Ora sfrutteremo la memorizzazione automatica nella cache dei prefissi dei prompt integrata in OpenAI.
Domande Frequenti
La lezione «Caching semantico con gli embedding» è gratuita?
Sì — il testo completo di «Caching semantico con gli embedding» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Engineering Academy, passa a CoddyKit PRO. Il corso AI Engineering Academy include 4 lezioni in totale.
Cosa imparerò in «Caching semantico con gli embedding»?
Crei una cache semantica che recuperi risposte memorizzate per query semanticamente simili ma non identiche, confrontando gli embedding delle query con una cache degli embedding delle richieste prece… Eserciti AI Engineering Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare AI Engineering Academy?
Non è richiesta alcuna esperienza precedente. AI Engineering Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.
Quanto tempo richiede la lezione «Caching semantico con gli embedding»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione AI Engineering Academy?
Sì. Ogni lezione AI Engineering Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Caching esatto con Redis
- Caching semantico con gli embedding
- Caching dei prefissi dei prompt con OpenAI
- Batching, routing dei modelli e dashboard dei costi