Onze LLM API-factuur groeide maand-op-maand met 30%. Het verkeer nam toe, maar niet zo snel. Toen ik onze zoekopdrachtlogboeken analyseerde, ontdekte ik het echte probleem: gebruikers stellen dezelfde vragen op verschillende manieren.
“Wat is uw retourbeleid?”, “Hoe kan ik iets retourneren?” en “Kan ik mijn geld terugkrijgen?” bereikten allemaal afzonderlijk onze LLM en genereerden vrijwel identieke reacties, waarbij elk de volledige API-kosten met zich meebracht.
Exact-match caching, de voor de hand liggende eerste oplossing, kon slechts 18% van deze redundante oproepen opvangen. Dezelfde semantische vraag, anders geformuleerd, ging volledig voorbij aan de cache.
Daarom heb ik semantische caching geïmplementeerd op basis van wat zoekopdrachten betekenen, niet hoe ze zijn geformuleerd. Na de implementatie ervan steeg ons cachetrefferpercentage naar 67%, waardoor de LLM API-kosten met 73% daalden. Maar om daar te komen, zijn problemen nodig die naïeve implementaties over het hoofd zien.
Waarom exacte match-caching tekortschiet
Bij traditionele caching wordt querytekst gebruikt als cachesleutel. Dit werkt als zoekopdrachten identiek zijn:
# Caching met exacte overeenkomsten
cache_key = hash(query_tekst)
als cache_key in cache:
retour cache(cache_key)
Maar gebruikers formuleren vragen niet op dezelfde manier. Mijn analyse van 100.000 productiequery’s vond het volgende:
-
Slechts 18% waren exacte duplicaten van eerdere zoekopdrachten
-
47% waren semantisch vergelijkbaar met eerdere zoekopdrachten (dezelfde intentie, andere bewoording)
-
35% waren werkelijk nieuwe vragen
Die 47% vertegenwoordigde enorme kostenbesparingen die we misten. Elke semantisch vergelijkbare zoekopdracht veroorzaakte een volledige LLM-aanroep, waardoor een antwoord ontstond dat vrijwel identiek was aan het antwoord dat we al hadden berekend.
Semantische caching-architectuur
Semantische caching vervangt op tekst gebaseerde sleutels door op insluitingen gebaseerde overeenkomsten op te zoeken:
klasse SemantischeCache:
def __init__(zelf, embedding_model, gelijkenisdrempel=0,92):
self.embedding_model = embedding_model
self.threshold = gelijkenis_drempel
self.vector_store = VectorWinkel() # FAISS, Dennenappel, enz.
self.response_store = ResponseStore() # Redis, DynamoDB, enz.
def get(self, query: str) -> Optioneel(str):
“””Retourneer het in de cache opgeslagen antwoord als er een semantisch vergelijkbare zoekopdracht bestaat.”””
query_embedding = self.embedding_model.encode(query)
# Vind de meest vergelijkbare in de cache opgeslagen zoekopdracht
komt overeen met = self.vector_store.search(query_embedding, top_k=1)
if komt overeen met en komt overeen met(0).similarity >= self.threshold:
cache_id = komt overeen met(0).id
return self.response_store.get(cache_id)
retour Geen
def set(zelf, vraag: str, antwoord: str):
“””Cachequery-antwoordpaar.”””
query_embedding = self.embedding_model.encode(query)
cache_id = genereer_id()
self.vector_store.add(cache_id, query_embedding)
self.response_store.set(cache_id, {
‘vraag’: vraag,
‘reactie’: reactie,
’tijdstempel’: datetime.utcnow()
})
Het belangrijkste inzicht: in plaats van de tekst van de zoekopdracht te hashen, sluit ik zoekopdrachten in de vectorruimte in en vind ik in de cache opgeslagen zoekopdrachten binnen een gelijkenisdrempel.
Het drempelprobleem
De gelijkenisdrempel is de kritische parameter. Als u deze te hoog instelt, mist u geldige cachehits. Als u het te laag instelt, krijgt u verkeerde antwoorden.
Onze aanvankelijke drempel van 0,85 leek redelijk; 85% vergelijkbaar zou “dezelfde vraag” moeten zijn, toch?
Fout. Bij 0,85 kregen we cachehits zoals:
Dit zijn verschillende vragen met verschillende antwoorden. Het retourneren van het in de cache opgeslagen antwoord zou onjuist zijn.
Ik ontdekte dat optimale drempels variëren per querytype:
|
Zoektype |
Optimale drempel |
Reden |
|
Vragen in FAQ-stijl |
0,94 |
Hoge precisie nodig; Verkeerde antwoorden schaden het vertrouwen |
|
Zoeken naar producten |
0,88 |
Meer tolerantie voor bijna-matches |
|
Ondersteuningsvragen |
0,92 |
Balans tussen dekking en nauwkeurigheid |
|
Transactionele vragen |
0,97 |
Zeer lage tolerantie voor fouten |
Ik heb querytype-specifieke drempels geïmplementeerd:
klasse AdaptiveSemanticCache:
def __init__(zelf):
zelf.drempels = {
‘veelgestelde vragen’: 0.94,
‘zoeken’: 0,88,
‘ondersteuning’: 0,92,
’transactioneel’: 0,97,
‘standaard’: 0,92
}
self.query_classifier = QueryClassifier()
def get_threshold(self, query: str) -> float:
query_type = self.query_classifier.classify(query)
return self.thresholds.get(query_type, self.thresholds(‘standaard’))
def get(self, query: str) -> Optioneel(str):
drempel = self.get_threshold(query)
query_embedding = self.embedding_model.encode(query)
komt overeen met = self.vector_store.search(query_embedding, top_k=1)
if komt overeen met en match(0).similarity >= drempelwaarde:
return self.response_store.get(matches(0).id)
retour Geen
Methodologie voor drempelafstemming
Ik kon drempels niet blindelings afstemmen. Ik had grondwaarheid nodig over welke vraagparen eigenlijk ‘hetzelfde’ waren.
Onze methodologie:
Stap 1: Voorbeeldqueryparen. Ik heb 5.000 zoekparen op verschillende gelijkenisniveaus (0,80-0,99) onderzocht.
Stap 2: Menselijke etikettering. Annotators bestempelden elk paar als “dezelfde bedoeling” of “andere bedoeling.” Ik gebruikte drie annotators per paar en nam een meerderheidsstemming.
Stap 3: Bereken precisie/recall-curven. Voor elke drempel berekenden we:
-
Precisie: Welk deel van de cachehits had dezelfde bedoeling?
-
Bedenk: welk deel van de paren met dezelfde bedoelingen hebben we in de cache opgeslagen?
def compute_precision_recall(paren, labels, drempelwaarde):
“””Bereken precisie en herinner aan een gegeven gelijkenisdrempel.”””
voorspellingen = (1 als paar.similariteit >= drempel anders 0 voor paar in paren)
true_positives = som(1 voor p, l in zip(voorspellingen, labels) als p == 1 en l == 1)
false_positives = som(1 voor p, l in zip(voorspellingen, labels) als p == 1 en l == 0)
false_negatives = som(1 voor p, l in zip(voorspellingen, labels) als p == 0 en l == 1)
precisie = waar_positieven / (waar_positieven + onwaar_positieven) if (waar_positieven + onwaar_positieven) > 0 anders 0
herinneren = waar_positieven / (waar_positieven + onwaar_negatieven) if (waar_positieven + onwaar_negatieven) > 0 anders 0
terugkeerprecisie, terugroepen
Stap 4: Selecteer een drempel op basis van de kosten van fouten. Voor veelgestelde vragen waarbij verkeerde antwoorden het vertrouwen schaden, heb ik geoptimaliseerd voor precisie (drempel van 0,94 gaf 98% nauwkeurigheid). Voor zoekopdrachten waarbij het missen van een cachehit alleen maar geld kost, heb ik geoptimaliseerd voor terugroepen (drempelwaarde van 0,88).
Latency-overhead
Semantische caching voegt latentie toe: u moet de query insluiten en het vectorarchief doorzoeken voordat u weet of u de LLM moet aanroepen.
Onze metingen:
|
Operatie |
Latentie (p50) |
Latentie (p99) |
|
Query-insluiting |
12 ms |
28 ms |
|
Vector zoeken |
8ms |
19 ms |
|
Totaal cache-opzoeken |
20 ms |
47 ms |
|
LLM API-oproep |
850 ms |
2400 ms |
De overhead van 20 ms is verwaarloosbaar vergeleken met de LLM-oproep van 850 ms die we vermijden bij cachehits. Zelfs bij p99 is de overhead van 47 ms acceptabel.
Cache-missers duren nu echter 20 ms langer dan voorheen (insluiten + zoeken + LLM-oproep). Bij ons hitpercentage van 67% pakt de berekening gunstig uit:
Netto latentieverbetering van 65% naast de kostenverlaging.
Cache-invalidatie
In het cachegeheugen opgeslagen reacties blijven verouderd. Productinformatie verandert, beleidsupdates en het juiste antwoord van gisteren wordt het verkeerde antwoord van vandaag.
Ik heb drie valideringsstrategieën geïmplementeerd:
-
Op tijd gebaseerde TTL
Eenvoudige vervaldatum op basis van inhoudstype:
TTL_BY_CONTENT_TYPE = {
‘prijzen’: timedelta(uren=4), # Verandert regelmatig
‘beleid’: timedelta(dagen=7), # Veranderingen zelden
‘product_info’: timedelta(dagen=1), # Dagelijks vernieuwen
‘general_faq’: timedelta(dagen=14), # Zeer stabiel
}
-
Op gebeurtenissen gebaseerde ongeldigverklaring
Wanneer onderliggende gegevens veranderen, maakt u gerelateerde cachegegevens ongeldig:
klasse CacheInvalidator:
def on_content_update(self, content_id: str, content_type: str):
“””Cachegegevens met betrekking tot bijgewerkte inhoud ongeldig maken.”””
# Zoek in de cache opgeslagen zoekopdrachten die naar deze inhoud verwijzen
beïnvloed_queries = self.find_queries_referencing(content_id)
voor query_id in beïnvloede_queries:
self.cache.invalidate(query_id)
self.log_invalidation(content_id, len(beïnvloede_queries))
-
Detectie van veroudering
Voor reacties die zonder expliciete gebeurtenissen verouderd zouden kunnen raken, heb ik periodieke versheidscontroles geïmplementeerd:
def check_freshness(self, cached_response: dict) -> bool:
“””Controleer of het in de cache opgeslagen antwoord nog steeds geldig is.”””
# Voer de query opnieuw uit op basis van de huidige gegevens
fresh_response = self.generate_response(cached_response(‘query’))
# Vergelijk semantische gelijkenis van antwoorden
cached_embedding = self.embed(cached_response(‘reactie’))
fresh_embedding = zelf.embed(fresh_response)
gelijkenis = cosinus_similarity(cached_embedding, verse_embedding)
# Als de antwoorden aanzienlijk uiteenlopen, maak deze dan ongeldig
als gelijkenis
self.cache.invalidate(cached_response(‘id’))
terugkeer Vals
terugkeer Waar
We voeren dagelijks controles uit op de versheid van een aantal in de cache opgeslagen vermeldingen, waarbij we verouderde gegevens opsporen die TTL en op gebeurtenissen gebaseerde invalidatie missen.
Productieresultaten
Na drie maanden in productie:
|
Metrisch |
Voor |
Na |
Wijziging |
|
Cachehitpercentage |
18% |
67% |
+272% |
|
LLM API-kosten |
$ 47.000/maand |
$ 12,7K/maand |
-73% |
|
Gemiddelde latentie |
850 ms |
300 ms |
-65% |
|
Vals-positief percentage |
N.v.t |
0,8% |
— |
|
Klachten van klanten (foute antwoorden) |
Basislijn |
+0,3% |
Minimale stijging |
Het fout-positieve percentage van 0,8% (query’s waarbij we een in de cache opgeslagen antwoord retourneerden dat semantisch onjuist was) lag binnen aanvaardbare grenzen. Deze gevallen deden zich voornamelijk voor aan de grenzen van onze drempel, waar de gelijkenis net boven de grens lag, maar de intentie enigszins verschilde.
Valkuilen om te vermijden
Gebruik geen enkele globale drempel. Verschillende querytypen hebben verschillende toleranties voor fouten. Stem drempels per categorie af.
Sla de stap voor het insluiten van cachehits niet over. U zou in de verleiding kunnen komen om de insluitingsoverhead over te slaan bij het retourneren van in de cache opgeslagen antwoorden, maar u hebt de insluiting nodig voor het genereren van cachesleutels. De overhead is onvermijdelijk.
Vergeet de ongeldigverklaring niet. Semantische caching zonder invalidatiestrategie leidt tot verouderde reacties die het vertrouwen van gebruikers ondermijnen. Creëer invalidatie vanaf dag één.
Cache niet alles. Sommige zoekopdrachten mogen niet in de cache worden opgeslagen: gepersonaliseerde antwoorden, tijdgevoelige informatie, transactiebevestigingen. Stel uitsluitingsregels op.
def Should_cache(self, query: str, respons: str) -> bool:
“”Bepaal of het antwoord in de cache moet worden opgeslagen.””
# Bewaar geen gepersonaliseerde antwoorden in het cachegeheugen
als self.contains_personal_info(antwoord):
terugkeer Vals
# Bewaar geen tijdgevoelige informatie in het cachegeheugen
if self.is_time_sensitive(query):
terugkeer Vals
# Bewaar transactiebevestigingen niet in de cache
als self.is_transactional(query):
terugkeer Vals
terugkeer Waar
Belangrijkste afhaalrestaurants
Semantische caching is een praktisch patroon voor LLM-kostenbeheersing dat caching-missers met exacte match van redundantie opvangt. De belangrijkste uitdagingen zijn het afstemmen van drempels (gebruik querytypespecifieke drempels op basis van precisie-/herinneringsanalyse) en cache-invalidatie (combineer TTL, op gebeurtenissen gebaseerde detectie en detectie van veroudering).
Met een kostenreductie van 73% was dit onze hoogste ROI-optimalisatie voor productie-LLM-systemen. De implementatiecomplexiteit is gematigd, maar het afstemmen van de drempel vereist zorgvuldige aandacht om kwaliteitsverlies te voorkomen.
Sreenivasa Reddy Hulebeedu Reddy is een hoofdsoftware-ingenieur.
Welkom bij de VentureBeat-community!
In ons gastpostprogramma delen technische experts inzichten en bieden ze neutrale, niet-gevestigde diepgaande inzichten over AI, data-infrastructuur, cyberbeveiliging en andere geavanceerde technologieën die de toekomst van het bedrijfsleven vormgeven.
Lees meer uit ons gastpostprogramma — en bekijk ons richtlijnen als u geïnteresseerd bent om een eigen artikel bij te dragen!



