La nostra fattura API LLM cresceva del 30% mese su mese. Il traffico stava aumentando, ma non così velocemente. Quando ho analizzato i nostri log delle question, 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?” stavano tutti raggiungendo il nostro LLM separatamente, generando risposte quasi identiche, ciascuno sostenendo costi API completi.
La memorizzazione nella cache della corrispondenza esatta, la prima soluzione ovvia, ha catturato solo il 18% di queste chiamate ridondanti. La stessa domanda semantica, formulata in modo diverso, ha aggirato completamente la cache.
Quindi, ho implementato la memorizzazione nella cache semantica in base al significato delle question, non al modo in cui sono formulate. Dopo l’implementazione, la nostra percentuale di riscontri nella cache è aumentata al 67%, riducendo i costi dell’API LLM del 73%. Ma per arrivarci è necessario risolvere i problemi che le implementazioni ingenue non riescono a cogliere.
Perché la memorizzazione nella cache della corrispondenza esatta non è sufficiente
La memorizzazione nella cache tradizionale utilizza il testo della question come chiave di cache. Funziona quando le question sono identiche:
# Caching della corrispondenza esatta
chiave_cache = hash(testo_query)
se cache_key nella cache:
restituire la cache[cache_key]
Ma gli utenti non formulano le domande in modo identico. La mia analisi di 100.000 question di produzione ha rilevato:
-
Solo il 18% erano duplicati esatti delle question precedenti
-
47% erano semanticamente simili alle question precedenti (stesso intento, formulazione diversa)
-
35% erano domande davvero nuove
Quel 47% rappresentava un enorme risparmio sui costi che ci mancava. Ogni question semanticamente simile ha attivato una chiamata LLM completa, generando una risposta quasi identica a quella che avevamo già calcolato.
Architettura della cache semantica
La memorizzazione nella cache semantica sostituisce le chiavi basate su testo con la ricerca di somiglianza basata sull’incorporamento:
classe SemanticCache:
def __init__(self, embedding_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, question: str) -> Opzionale[str]:
“””Restituisce la risposta memorizzata nella cache se esiste una question semanticamente simile.”””
query_embedding = self.embedding_model.encode(question)
# Trova la question memorizzata nella cache più simile
corrispondenze = self.vettore_store.search(query_embedding, top_k=1)
se corrisponde e corrisponde[0].somiglianza >= self.soglia:
cache_id = corrisponde[0].id
restituisce self.response_store.get(cache_id)
ritorno Nessuno
def set(self, question: str, risposta: str):
“””Memorizza nella cache la coppia query-risposta.”””
query_embedding = self.embedding_model.encode(question)
cache_id = generate_id()
self.vettore_store.add(cache_id, query_embedding)
self.response_store.set(cache_id, {
‘interroga’: interrogazione,
‘risposta’: risposta,
‘timestamp’: datetime.utcnow()
})
L’intuizione chiave: invece di eseguire l’hashing del testo della question, incorporo le question nello spazio vettoriale e trovo le question memorizzate nella cache entro una soglia di somiglianza.
Il problema della soglia
La soglia di somiglianza è il parametro critico. Impostalo su un valore troppo alto e perderai riscontri cache validi. Impostalo su un valore troppo basso e restituirai risposte sbagliate.
La nostra soglia iniziale di 0,85 sembrava ragionevole; L’85% di cose simili dovrebbero essere “la stessa domanda”, giusto?
Sbagliato. A 0,85, abbiamo ottenuto riscontri nella cache come:
Queste sono domande numerous con risposte numerous. Restituire la risposta memorizzata nella cache sarebbe errato.
Ho scoperto che le soglie ottimali variano in base al tipo di question:
|
Tipo di question |
Soglia ottimale |
Motivazione |
|
Domande in stile FAQ |
0,94 |
Necessaria alta precisione; le risposte sbagliate danneggiano la fiducia |
|
Ricerche di prodotti |
0,88 |
Maggiore tolleranza per le partite vicine |
|
Domande di supporto |
0,92 |
Equilibrio tra copertura e precisione |
|
Question transazionali |
0,97 |
Tolleranza agli errori molto bassa |
Ho implementato soglie specifiche per il tipo di question:
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, question: str) -> float:
query_type = self.query_classifier.classify(question)
return self.thresholds.get(query_type, self.thresholds[‘default’])
def get(self, question: str) -> Opzionale[str]:
soglia = self.get_threshold(question)
query_embedding = self.embedding_model.encode(question)
corrispondenze = self.vettore_store.search(query_embedding, top_k=1)
se corrisponde e corrisponde[0].somiglianza >= soglia:
return self.response_store.get(matchs[0].id)
ritorno Nessuno
Metodologia di ottimizzazione della soglia
Non potevo regolare le soglie alla cieca. Avevo bisogno di una verità fondamentale su quali coppie di question fossero effettivamente “le stesse”.
La nostra metodologia:
Passaggio 1: Coppie di question di esempio. Ho campionato 5.000 coppie di question a vari livelli di somiglianza (0,80-0,99).
Passaggio 2: Etichettatura umana. Gli annotatori hanno etichettato ciascuna coppia come “stesso intento” O “intento diverso.” Ho utilizzato tre annotatori per coppia e ho ottenuto la maggioranza dei voti.
Passaggio 3: Calcolare curve di precisione/richiamo. Per ogni soglia abbiamo calcolato:
-
Precisione: degli accessi alla cache, quale frazione aveva lo stesso intento?
-
Ricordiamo: delle coppie con lo stesso intento, quale frazione abbiamo memorizzato nella cache?
def compute_precision_recall(coppie, etichette, soglia):
“””Precisione del calcolo e richiamo alla soglia di somiglianza information.”””
previsioni = [1 if pair.similarity >= threshold else 0 for pair in pairs]
veri_positivi = somma(1 for p, l in zip(previsioni, etichette) se p == 1 e l == 1)
falsi_positivi = somma (1 for p, l in zip(previsioni, etichette) se p == 1 e l == 0)
falsi_negativi = somma(1 for p, l in zip(previsioni, etichette) 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: selezionare la soglia in base al costo degli errori. Per le domande frequenti sulle domande frequenti in cui le risposte errate danneggiano la fiducia, ho ottimizzato la precisione (la soglia di 0,94 ha fornito una precisione del 98%). Per le question di ricerca in cui perdere un riscontro nella cache costa solo denaro, ho ottimizzato per il richiamo (soglia 0,88).
Sovraccarico di latenza
La memorizzazione nella cache semantica aggiunge latenza: è necessario incorporare la question ed effettuare una ricerca nell’archivio dei vettori prima di sapere se chiamare LLM.
Le nostre misurazioni:
|
Operazione |
Latenza (p50) |
Latenza (p99) |
|
Incorporamento delle question |
12 ms |
28 ms |
|
Ricerca vettoriale |
8 ms |
19 ms |
|
Ricerca cache totale |
20 ms |
47 ms |
|
Chiamata API LLM |
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, l’overhead di 47ms è accettabile.
Tuttavia, i mancati risultati della cache ora richiedono 20 ms in più rispetto a prima (incorporamento + ricerca + chiamata LLM). Con la nostra percentuale di successo del 67%, i conti funzionano favorevolmente:
Miglioramento netto della latenza del 65% insieme alla riduzione dei costi.
Invalidazione della cache
Le risposte memorizzate nella cache diventano out of date. Le modifiche alle informazioni sui prodotti, l’aggiornamento delle politiche e la risposta corretta di ieri diventano la risposta sbagliata di oggi.
Ho implementato tre strategie di invalidazione:
-
TTL basato sul tempo
Scadenza semplice in base al tipo di contenuto:
TTL_BY_CONTENT_TYPE = {
‘prezzi’: timedelta(ore=4), # Cambia frequentemente
‘politica’: timedelta(giorni=7), # Cambia raramente
“info_prodotto”: timedelta(giorni=1), # Aggiornamento quotidiano
‘general_faq’: timedelta(giorni=14), # Molto stabile
}
-
Invalidazione basata sugli eventi
Quando i dati sottostanti cambiano, invalida le voci della cache correlate:
classe CacheInvalidator:
def on_content_update(self, content_id: str, content_type: str):
“””Invalida le voci della cache relative al contenuto aggiornato.”””
# Trova le question 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(affected_queries))
-
Rilevamento della stagnazione
Per le risposte che potrebbero diventare out of date senza eventi espliciti, ho implementato controlli periodici di aggiornamento:
def check_freshness(self, cached_response: dict) -> bool:
“””Verificare che la risposta memorizzata nella cache sia ancora valida.”””
# Esegui nuovamente la question rispetto ai dati correnti
fresh_response = self.generate_response(cached_response[‘query’])
# Confronta la somiglianza semantica delle risposte
cached_embedding = self.embed(cached_response[‘response’])
fresh_embedding = self.embed(fresh_response)
somiglianza = coseno_similarità(cached_embedding, fresh_embedding)
# Se le risposte divergono in modo significativo, invalidare
se somiglianza < 0,90:
self.cache.invalidate(cached_response[‘id’])
restituire Falso
restituisce Vero
Eseguiamo quotidianamente controlli di aggiornamento 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
Dopo tre mesi di produzione:
|
Metrico |
Prima |
Dopo |
Modifica |
|
Percentuale di riscontri nella cache |
18% |
67% |
+272% |
|
Costi dell’API LLM |
$ 47.000 al mese |
$ 12,7K al mese |
-73% |
|
Latenza media |
850 ms |
300 ms |
-65% |
|
Tasso di falsi positivi |
N / A |
0,8% |
— |
|
Reclami dei clienti (risposte errate) |
Linea di base |
+0,3% |
Aumento minimo |
Il tasso di falsi positivi dello 0,8% (question 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 period appena al di sopra del limite ma le intenzioni differivano leggermente.
Insidie da evitare
Non utilizzare un’unica soglia globale. Diversi tipi di question hanno una tolleranza diversa per gli errori. Ottimizza le soglie per categoria.
Non saltare il passaggio di incorporamento in caso di riscontri nella cache. Potresti essere tentato di ignorare l’overhead di incorporamento quando si restituiscono risposte memorizzate nella cache, ma è necessario l’incorporamento per la generazione della chiave di cache. Il sovraccarico è inevitabile.
Non dimenticare l’invalidazione. La memorizzazione nella cache semantica senza strategia di invalidazione porta a risposte out of date che minano la fiducia degli utenti. Costruisci l’invalidazione fin dal primo giorno.
Non memorizzare tutto nella cache. Alcune question non devono essere memorizzate nella cache: risposte personalizzate, informazioni urgenti, conferme transazionali. Costruisci regole di esclusione.
def dovrebbe_cache(self, question: str, risposta: str) -> bool:
“””Determinare se la risposta deve essere memorizzata nella cache.””
# Non memorizzare nella cache le risposte personalizzate
se self.contains_personal_info(risposta):
restituire Falso
# Non memorizzare nella cache informazioni urgenti
se self.is_time_sensitive(question):
restituire Falso
# Non memorizzare nella cache le conferme delle transazioni
se self.is_transactional(question):
restituire Falso
restituisce Vero
Punti chiave
Il caching semantico è un modello pratico per il controllo dei costi LLM che cattura i mancati caching della ridondanza con corrispondenza esatta. Le sfide principali sono l’ottimizzazione delle soglie (utilizzare soglie specifiche per il tipo di question basate sull’analisi di precisione/richiamo) e l’invalidazione della cache (combinare TTL, rilevamento basato su eventi e obsolescenza).
Con una riduzione dei costi del 73%, questa è stata la nostra ottimizzazione con il ROI più elevato per i sistemi LLM di produzione. La complessità dell’implementazione è moderata, ma l’ottimizzazione della soglia richiede un’attenzione particolare per evitare il degrado della qualità.
Sreenivasa Reddy Hulebeedu Reddy è un ingegnere informatico capo.
Benvenuto nella comunità VentureBeat!
Il nostro programma di visitor posting è il luogo in cui gli esperti tecnici condividono approfondimenti e forniscono approfondimenti neutrali e non conferiti su intelligenza artificiale, infrastruttura dati, sicurezza informatica e altre tecnologie all’avanguardia che plasmano il futuro dell’impresa.
Per saperne di più dal nostro programma di publish per gli ospiti e dai un’occhiata al nostro linee guida se sei interessato a contribuire con un tuo articolo!













