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:

  1. 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

}

  1. 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))

  1. 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.

Collegamento alla fonte