La nostra fattura API LLM aumentava del 30% mese su mese. Il traffico stava aumentando, ma non così velocemente. Quando ho analizzato i nostri log delle query, ho scoperto il vero problema: gli utenti pongono le stesse domande in modi diversi.
"Qual è la vostra politica di restituzione?" "Come posso restituire qualcosa?"E "Posso ottenere un rimborso?" tutti raggiungevano il nostro LLM separatamente, producendo risposte quasi identiche e ciascuno incorrendo nell’intero costo API.
La prima soluzione definitiva, la memorizzazione nella cache delle corrispondenze esatte, ha rilevato solo il 18% di queste chiamate indesiderate. La stessa domanda semantica, formulata in modo diverso, ha aggirato completamente la cache.
Pertanto, ho implementato la memorizzazione nella cache semantica in base al significato delle query, non a come vengono espresse. Dopo aver implementato questa funzionalità, la nostra percentuale di riscontri nella cache è aumentata al 67% e abbiamo ridotto i costi dell’API LLM del 73%. Ma per arrivarci è necessario risolvere problemi che le pratiche ingenue trascurano.
Perché la memorizzazione nella cache delle corrispondenze esatte non riesce?
La memorizzazione nella cache tradizionale utilizza il testo della query come chiave di cache. Funziona quando le query sono le stesse:
# Caching della corrispondenza esatta
chiave_cache = hash(testo_query)
Se cache_key è nella cache:
restituire la cache (cache_key)
Tuttavia, gli utenti non formulano le domande nello stesso modo. La mia analisi di 100.000 query di produzione ha rilevato:
-
Solo il 18% erano copie esatte delle query precedenti
-
47% semanticamente simile alle query precedenti (stesso intento, diversa formulazione)
-
35% Erano domande davvero nuove.
Questo 47% rappresentava un notevole risparmio sui costi che non ci era sfuggito. Ogni query semanticamente simile ha attivato una chiamata LLM completa e ha prodotto quasi la stessa risposta calcolata in precedenza.
Architettura della cache semantica
La memorizzazione nella cache semantica sostituisce le chiavi basate su testo con la ricerca per somiglianza basata sull’incorporamento:
classe SemanticCache:
def __init__(self, deploy_model, similarity_threshold=0.92):
self.modello_incorporamento = modello_incorporamento
self.soglia = soglia_somiglianza
self.vettore_store = VectorStore() # FAISS, Pigna ecc.
self.response_store = ResponseStore() # Redis, DynamoDB ecc.
def get(self, query: str) -> Opzionale(str):
"""Restituisce la risposta memorizzata nella cache se esiste una query semanticamente simile."""
query_embedding = self.embedding_model.encode(query)
# Trova la query memorizzata nella cache più simile
corrispondenze = self.vettore_store.search(query_embedding, top_k=1)
se corrisponde e corrisponde(0).somiglianza >= self.soglia:
corrisponde a cache_id = (0).id
restituisce self.response_store.get(cache_id)
Nessun ritorno
def set(self, query: str, risposta: str):
"""Memorizza nella cache la coppia query-risposta."""
query_embedding = self.embedding_model.encode(query)
cache_id = create_id()
self.vettore_store.add(cache_id, query_embedding)
self.response_store.set(cache_id, {
‘interroga’: interrogazione,
‘risposta’: risposta,
‘timestamp’: datetime.utcnow()
})
Idea di base: invece di eseguire l’hashing del testo della query, inserisco le query nello spazio vettoriale e trovo le query memorizzate nella cache entro una soglia di somiglianza.
problema della soglia
La soglia di somiglianza è il parametro critico. Se lo imposti troppo alto perderai riscontri cache validi. Se lo imposti troppo basso otterrai risposte errate.
La nostra soglia iniziale di 0,85 sembrava ragionevole; Deve essere simile all’85%. "stessa domanda," Giusto?
Sbagliato. A 0,85 abbiamo ottenuto riscontri nella cache come questi:
-
Domanda: "Come posso cancellare il mio abbonamento?"
-
Memorizzato nella cache: "Come posso annullare il mio ordine?"
-
Somiglianza: 0,87
Queste sono domande diverse con risposte diverse. Sarebbe sbagliato restituire la risposta memorizzata nella cache.
Ho scoperto che le soglie ottimali variano in base al tipo di query:
|
Tipo di query |
Soglia ottimale |
Motivo |
|
Domande in stile FAQ |
0,94 |
È necessaria un’elevata precisione; Le risposte sbagliate danneggiano la fiducia |
|
Ricerche di prodotti |
0,88 |
Maggiore tolleranza per le partite ravvicinate |
|
Domande di supporto |
0,92 |
Equilibrio tra copertura e precisione |
|
Query transazionali |
0,97 |
Tolleranza agli errori molto bassa |
Ho implementato soglie specifiche del tipo di query:
classe AdaptiveSemanticCache:
def __init__(self):
self.soglie = {
“Domande frequenti”: 0,94,
‘cerca’: 0,88,
‘supporto’: 0,92,
‘transazionale’: 0,97,
‘predefinito’: 0,92
}
self.query_classificatore = QueryClassificatore()
def get_threshold(self, query: str) -> float:
query_type = self.query_classifier.classify(query)
return self.thresholds.get(query_type, self.thresholds(‘default’))
def get(self, query: str) -> Opzionale(str):
soglia = self.get_threshold(query)
query_embedding = self.embedding_model.encode(query)
corrispondenze = self.vettore_store.search(query_embedding, top_k=1)
se corrisponde e corrisponde(0).somiglianza >= soglia:
restituisce self.response_store.get(matches(0).id)
Nessun ritorno
Metodologia di aggiustamento della soglia
Non potevo regolare le soglie alla cieca. Avevo bisogno della verità fondamentale su quali coppie di query effettivamente "lo stesso."
La nostra metodologia:
Passaggio 1: Coppie di query di esempio. Ho campionato 5.000 coppie di query a vari livelli di somiglianza (0,80-0,99).
Passaggio 2: Etichettatura umana. Gli annotatori hanno etichettato ciascuna coppia come segue: "stessa intenzione" O "intenzione diversa." Ho usato tre spiegatori per coppia e ho ottenuto la maggioranza dei voti.
Passaggio 3: Calcolare le curve di precisione/richiamo. Per ciascuna soglia abbiamo calcolato:
-
Precisione: quale parte degli accessi alla cache aveva lo stesso scopo?
-
Ricorda: quale porzione di coppie con lo stesso intento abbiamo memorizzato nella cache?
def compute_precision_recall(coppie, etichette, soglia):
"""Calcolare la precisione e il richiamo a una determinata soglia di somiglianza."""
previsioni = (1 se coppia.somiglianza >= soglia, 0 altrimenti per coppie in coppie)
veri_positivi = somma(1 for p, l(previsioni, etichette) in zip se p == 1 e l == 1)
falsi_positivi = somma (1 for p, l(previsioni, etichette) in zip se p == 1 e l == 0)
falsi_negativi = somma (1 for p, l(previsioni, etichette) in zip se p == 0 e l == 1)
precisione = veri_positivi / (veri_positivi + falsi_positivi) se (veri_positivi + falsi_positivi) > 0 altrimenti 0
richiamo = veri_positivi / (veri_positivi + falsi_negativi) se (veri_positivi + falsi_negativi) > 0 altrimenti 0
restituzione precisione, richiamo
Passaggio 4: scegli la soglia in base al costo degli errori. Per le domande FAQ in cui le risposte errate minano la fiducia, ho ottimizzato la sensibilità (una soglia di 0,94 ha dato una sensibilità del 98%). Ho ottimizzato il richiamo per le query di ricerca in cui i risultati della cache mancanti costano solo denaro (soglia 0,88).
ritardare il carico
La memorizzazione nella cache semantica aumenta la latenza: è necessario inserire la query ed eseguire la ricerca nell’archivio dei vettori prima di sapere se chiamare LLM o meno.
Le nostre misurazioni:
|
Operazione |
Ritardo (pag.50) |
Ritardo (pag. 99) |
|
Posizionamento delle query |
12 ms |
28 ms |
|
Ricerca vettoriale |
8 ms |
19 ms |
|
Ricerca cache totale |
20 ms |
47 ms |
|
Chiamata API principale |
850 ms |
2400 ms |
L’overhead di 20 ms è trascurabile rispetto alla chiamata LLM da 850 ms che evitiamo in caso di riscontri nella cache. Anche a P99, 47ms di sovraccarico sono accettabili.
Tuttavia, i fallimenti nella cache ora richiedono 20 ms in più rispetto a prima (inserimento + ricerca + chiamata principale). I calcoli danno risultati positivi per il nostro tasso di successo del 67%:
-
Prima: 100% delle query × 850 ms = 850 ms in media
-
Dopo: (33% × 870 ms) + (67% × 20 ms) = 287 ms + 13 ms = 300 ms in media
Miglioramento del 65% della latenza netta insieme alla riduzione dei costi.
Invalidazione della cache
Le risposte memorizzate nella cache diventano obsolete. Le informazioni sui prodotti cambiano, le politiche vengono aggiornate e la risposta giusta di ieri diventa la risposta sbagliata di oggi.
Ho implementato tre strategie di override:
-
TTL basato sul tempo
Data di scadenza semplice per tipo di contenuto:
TTL_BY_CONTENT_TYPE = {
‘prezzi’: timedelta(ora=4), # Cambia frequentemente
‘politica’: timedelta(giorno=7), # Cambia raramente
‘info_prodotto’: timedelta(giorno=1), # Aggiornamento quotidiano
‘general_sss’: timedelta(giorno=14), # Molto stabile
}
-
Override basato sugli eventi
Invalida le voci della cache rilevanti quando i dati sottostanti cambiano:
classe CacheInvalidator:
def on_content_update(self, content_id: str, content_type: str):
"""Invalida le voci della cache relative al contenuto aggiornato."""
# Trova le query memorizzate nella cache che fanno riferimento a questo contenuto
interessati_queries = self.find_queries_referencing(content_id)
per query_id in interessati_queries:
self.cache.invalidate(query_id)
self.log_invalidation(content_id, len(infected_queries))
-
Rilevamento della stagnazione
Ho implementato controlli periodici di aggiornamento per le risposte che potrebbero diventare obsolete senza eventi chiari:
def check_freshness(self, cache_response: dict) -> bool:
"""Verificare che la risposta memorizzata nella cache sia ancora valida."""
# Esegui nuovamente la query con i dati esistenti
fresh_response = self.generate_response(cached_response(‘query’))
# Confronta la somiglianza semantica delle risposte
cached_embedding = self.embed(cached_response(‘risposta’))
fresh_embedding = self.embed(fresh_response)
somiglianza = coseno_similarità(cached_embedding, fresh_embedding)
# Sostituisci se le risposte differiscono in modo significativo
Se somiglianza < 0,90:
self.cache.invalidate(cached_response(‘id’))
svolta sbagliata
restituire vero
Eseguiamo controlli di aggiornamento giornalieri su un campione di voci memorizzate nella cache, rilevando i livelli di obsolescenza che TTL e l’invalidazione basata sugli eventi non rilevano.
Risultati della produzione
Tre mesi dopo la produzione:
|
Metrico |
Prima |
Dopo |
Per cambiare |
|
Percentuale di riscontri nella cache |
18% |
67% |
+272% |
|
Costi dell’API principale |
47 mila dollari/mese |
$ 12,7 mila al mese |
-73% |
|
Ritardo medio |
850 ms |
300 ms |
-65% |
|
Tasso di falsi positivi |
Nessuno |
0,8% |
— |
|
Reclami dei clienti (risposte errate) |
Base |
+0,3% |
Incremento minimo |
Un tasso di falsi positivi dello 0,8% (query in cui abbiamo restituito una risposta memorizzata nella cache semanticamente errata) rientrava nei limiti accettabili. Questi casi si sono verificati principalmente ai limiti della nostra soglia, dove la somiglianza era appena sopra il limite ma l’intento era leggermente diverso.
Insidie da evitare
Non utilizzare un’unica soglia generale. Diversi tipi di query hanno una tolleranza diversa per gli errori. Imposta soglie per categoria.
Non saltare la fase di iniezione per gli hit della cache. Potresti voler saltare l’overhead dell’iniezione quando restituisci le risposte memorizzate nella cache, ma hai bisogno dell’iniezione per generare la chiave della cache. Il sovraccarico è inevitabile.
Non dimenticare di eseguire l’override. La memorizzazione nella cache semantica senza una strategia di invalidazione porta a risposte obsolete che minano la fiducia degli utenti. Integra l’override fin dal primo giorno.
Non memorizzare tutto nella cache. Alcune query non devono essere memorizzate nella cache: risposte personalizzate, informazioni urgenti, conferme di transazioni. Crea regole di esclusione.
def Dovrebbe_cache(self, query: str, risposta: str) -> bool:
"""Determina se la risposta deve essere memorizzata nella cache.""
# Non memorizzare nella cache le risposte personalizzate
se self.contains_personal_info(risposta):
svolta sbagliata
# Non memorizzare nella cache informazioni sensibili al tempo
se self.is_time_sensitive(query):
svolta sbagliata
# Non memorizzare nella cache le conferme delle transazioni
se self.is_transactional(query):
svolta sbagliata
restituire vero
Punti chiave
Il caching semantico è un modello pratico per il controllo dei costi LLM che cattura gli errori di caching della corrispondenza esatta della ridondanza. Le sfide principali sono l’ottimizzazione delle soglie (utilizzando soglie specifiche del tipo di query basate sull’analisi di precisione/richiamo) e l’invalidazione della cache (combinando TTL, basato su eventi e rilevamento di obsolescenza).
Con una riduzione dei costi del 73%, questa è stata la nostra più alta ottimizzazione del ROI per i sistemi LLM di produzione. La complessità dell’implementazione è moderata, ma la definizione delle soglie deve essere effettuata con attenzione per evitare un degrado della qualità.
Sreenivasa Reddy Hulebeedu Reddy è un ingegnere informatico leader.














