Semantisches Caching für LLM-Agenten mit Redis: Kosteneinsparung und Verzögerungsreduzierung
Wie semantisches Caching die Kosten und Latenzzeit von LLMs verringert: Embeddings, Schwellenwerte, Redis und RedisVL, Präzisionsanpassung sowie das Caching von Unterfragen innerhalb von Agentenworkflows.
Quellen: Semantic Caching for AI Agents: How to Make Your LLM Apps Faster and Cheaper
Aufrufe an leistungsstarke LLMs kosten echtes Geld und sind außerdem langsam. Jeder, der beobachtet hat, wie die Rechnungen für eine API nach dem Einsatz eines Agents vor echten Nutzern anstiegen, kennt diese Spannung: Die Modelle, die am besten antworten, sind gleichzeitig die teuersten und langsamsten. Modelle der GPT-4-Klasse können mehrere Dollar pro tausend Anfragen kosten, und agierende Workflows verschlimmern die Situation, da eine Nutzeranfrage bis zu 8 bis 20 Modelaufrufe auslösen kann.
Semantisches Caching zielt direkt auf diese Kosten ab, wird aber von den meisten Teams untergenutzt. Es lässt sich leicht mit Redis implementieren.
Warum herkömmliche Caches versagen
Ein reguläres Cache betrachtet zwei Anfragen als identisch nur dann, wenn ihre Bytes übereinstimmen. Das funktioniert bei Datenbankabfragen und HTTP-Anfragen, bricht aber bei natürlicher Sprache zusammen. Stellen Sie sich einen Support-Agenten vor, der folgende Anfragen von drei verschiedenen Nutzern erhält:
- Ein Kunde fragt, wie er einen Rückzahlungsantrag stellen kann
- Ein weiterer verlangt sein Geld zurück
- Ein Dritter möchte wissen, was die Rückerstattungspolitik vorsieht
Ein Cache auf Byte-Ebene erkennt drei unverwandte Eingaben und zahlt für drei Aufrufe des LLM. Ein Mensch würde sagen, alle drei wollen dieselbe Antwort. Ein semantischer Cache arbeitet hingegen mit Bedeutungen statt mit Zeichen.
Der Mechanismus in vier Schritten
- Embedden Sie die eingehende Anfrage mithilfe eines Embedding-Modells und wandeln Sie sie in einen hochdimensionalen Vektor um.
- Vergleichen Sie diesen Vektor mit jeder gespeicherten Eintragung unter Verwendung der Kosinus-Distanz oder eines ähnlichen Maßes.
- Falls der nächstgelegene Eintrag innerhalb des von Ihnen festgelegten Distanzschwellenwerts liegt, zählen Sie ihn als Treffer und geben Sie die gespeicherte Antwort umgehend zurück.
- Falls nichts nah genug ist, leiten Sie die Anfrage durch Ihren RAG-Pipeline oder das LLM weiter und speichern Sie anschließend die Frage sowie die Antwort für das nächste Mal.
Paraphrasierte Fragen teilen sich letztendlich die gleichen Antworten. Wenn der Cache sich füllt, erreicht das Modell nur noch ein immer kleinerer Anteil des Traffics.
Eine minimale Version von Grund auf
Bevor man eine Produktionsbibliothek verwendet, ist es hilfreich, die einzelnen Komponenten zu betrachten. Beginnen wir mit den Imports:
from sentence_transformers import SentenceTransformer
import numpy as np
Dann fügen Sie eine kleine FAQ-Sammlung ein und schreiben Sie eine Nachbarnsuche:
model = SentenceTransformer("all-mpnet-base-v2")# Embed your FAQ dataset
faq_questions = ["How do I get a refund?", "Where is my order?", ...]
faq_embeddings = model.encode(faq_questions)def cosine_distance(a, b):
return 1 - np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))def check_cache(query: str, threshold: float = 0.3):
query_embedding = model.encode(query)
distances = [cosine_distance(query_embedding, e) for e in faq_embeddings]
best_idx = np.argmin(distances)
best_distance = distances[best_idx]
if best_distance < threshold:
return faq_answers[best_idx] # Cache hit
return None # Cache miss
Man versucht eine Testfrage dazu, wie viele Tage es dauert, bis ein Rückzahlungsbetrag ankommt – die Suche liefert dabei die gespeicherte FAQ zu Rückzahlungen als den am nächsten liegenden Eintrag mit einer Kosinus-Distanz von etwa 0,33. Allein anhand gemeinsamer Wörter wurde nichts gefunden; die Vektoren liegen einfach in der Nähe voneinander.
Ausführung auf Redis
Die Testversion verfügt weder über Persistenz noch über Ablaufdaten, weder über Trennung der Nutzer noch über eine optimierte Vektorsuche. Für alles, was Kunden betrifft, benötigt man eine echte Datenbank. Redis zusammen mit dem Open-Source-RedisVL SDK stellt all diese Funktionen bereits aus der Box bereit.
from redisvl.extensions.cache.llm import SemanticCache
from redisvl.utils.vectorize import HFTextVectorizer
Wählen Sie ein Embedding-Modell aus, das für diese Aufgabe entwickelt wurde, und erstellen Sie anschließend den Cache:
# Load a cache-optimized embedding model
vectorizer = HFTextVectorizer("redis/langcache-embed-v1")# Create the cache
cache = SemanticCache(
name="customer_support_cache",
vectorizer=vectorizer,
redis_client=redis_client,
distance_threshold=0.3
)# Set TTL (time to live) — keeps cache fresh
cache.set_ttl(86400) # 24 hours
Das langcache-embed-v1-Modell ist ein open-weight-Embedding-Modell, das speziell für semantisches Caching feinabgestimmt wurde, wodurch es in dieser Aufgabe in der Regel bessere Ergebnisse liefert als allgemein einsetzbare Embedder.
Drei Produktfunktionen sind im Redis-Route enthalten:
- TTL. Die Einträge verfallen von selbst, sodass veraltete Antworten nicht weiter bestehen bleiben, wenn sich das Produkt ändert.
- Namespacing. Teams, Benutzer oder Mieter können jeweils einen isolierten Cache innerhalb einer einzigen Redis-Instanz haben.
- Optimierte Vektorabfrage. Die HNSW-Indizierung ermöglicht auch bei großen Datenmengen schnelle Abfragen nach den nächstgelegenen Elementen.
Vier Werte, auf die man achten sollte
Den Cache aufzubauen ist nur die einfachere Hälfte. Man muss auch sicherstellen, dass er ordnungsgemäß funktioniert.
Hitrate ist der Anteil der Abfragen, die innerhalb des Schwellenwerts eine Übereinstimmung finden, und sie bestimmt Ihre Ersparnisse. Bei einer Hitrate von 30 % bei einem System, das täglich 10.000 LLM-Aufrufe tätigt, entfallen 3.000 Aufrufe.
Präzision bezieht sich auf den Anteil der Treffer, die die richtige Antwort liefern. Ein Cache, der zuverlässig falsche Antworten liefert, ist schlimmer als keiner.
Recall misst den Anteil der Abfragen, die tatsächlich einen Treffer erzielt haben, obwohl eine entsprechende Eintragung vorhanden war. Ein niedriger Recall bedeutet, dass keine Ersparnisse erzielt werden.
Verbesserung der Latenz vergleicht die beiden Ansätze. Nach Benchmarks liegen die Reaktionszeiten eines Caches bei etwa 2 bis 5 ms im Vergleich zu 300 bis 500 ms für einen typischen GPT-4-Aufruf – das sind etwa 160 Mal schnellere Reaktionszeiten. Was Sie tatsächlich wahrnehmen, hängt davon ab, wie oft es zu Treffern kommt, wie diese Formel zeigt:
With-Cache Latency = (avg_llm_latency × (1 - hit_rate)) + (avg_cache_latency × hit_rate)
Bei 30 % Trefferquoten lässt sich mit einem Rückgang der Gesamtlatenz um etwa 25 bis 30 % rechnen. Sobald der Cache auf 60 bis 75 % aufgeheizt ist, werden die Vorteile deutlich größer.
Den Cache als Klassifikator verwenden
Jeder Abruf endet mit einem von vier Ergebnissen:
| Der Cache liefert einen Treffer | Der Cache liefert keinen Treffer | |
|---|---|---|
| Es hätte ein Treffer sein sollen | Wahrer Positivfall | Falscher Negativfall |
| Es hätte kein Treffer sein sollen | Falscher Positivfall | Wahrer Negativfall |
Falschpositive sind die gefährlichen Fälle: Zwei Fragen, die ähnlich aussehen, aber unterschiedliche Bedeutungen haben, sodass der Cache eine falsche Antwort liefert. Eine Frage zu Rückerstattungen nach einer Änderung der Meinung und eine Frage zum allgemeinen Rückerstattungsverfahren können im Embedding-Raum nah beieinander liegen, doch jede erfordert eine eigene Antwort. Genau in solchen Fällen muss der Cache besonders vorsichtig sein, denn die von ihm gelieferte Antwort klingt völlig plausibel.
Genauigkeit erhöhen
Der Schwellenwert anpassen. Der Abstandsschwellenwert ist die einflussreichste Einstellung. Ein niedrigerer Wert erhöht die Präzision und verringert die Trefferquote, während ein höherer Wert das Gegenteil bewirkt. Probieren Sie verschiedene Werte an einem repräsentativen Testdatensatz aus und wählen Sie den, der den F1-Wert maximiert – der harmonische Mittelwert aus Präzision und Trefferquote. Dieser Ansatz steigerte die Präzision von 79 % auf 90 %, während die Trefferquote bei 25 % blieb.
Neu-ranken mit einem Cross-Encoder. Standard-Embedding-Modelle kodieren jeden Satz für sich. Ein Cross-Encoder liest die Abfrage und einen Kandidatsatz gemeinsam, wodurch Nuancen erfasst werden, die Bi-Encoder übersehen. Nehmen wir einen Satz darüber, dass eine Bank die Zinsen erhöht, und einen anderen darüber, dass nach starkem Regen ein Fluss in der Nähe der Bank über die Ufer tritt: Das gemeinsame Wort „Bank“ bringt ihre Embeddings recht nahe zusammen, doch ein Cross-Encoder erkennt, dass sie nicht miteinander verbunden sind. In Experimenten führte das Neu-ranken zu einer Präzision von 94 %. Cross-Encoder können keine suchbaren Vektoren erzeugen, daher dienen sie als zweite Ebene über den top-K-Kandidaten, die das Embedding-Modell liefert.
Bitten Sie ein kleines LLM um Beurteilung. Für maximale Genauigkeit lassen Sie ein günstiges Modell wie GPT-4o-mini mit einer einfachen Anfrage grenzwertige Treffer überprüfen: Sind diese beiden Fragen semantisch äquivalent? Es antwortet in Millisekunden für einen Bruchteil des Preises eines vollständigen Anrufs. Tests zeigten 100%ige Genauigkeit, allerdings eine geringere Trefferquote, da das Urteilsmodell manchmal gültige Übereinstimmungen ablehnt. Ob das akzeptabel ist, hängt vom Anwendungsfall ab. Im Kundenservice kann eine leicht falsche, im Cache gespeicherte Antwort schlimmer sein als ein neuer Anruf.
Erst eine vage Abgleichung durchführen. Bevor irgendwelche Embeddings berechnet werden, wird ein Abgleich der Abstandsberechnung zwischen der Anfrage und den gespeicherten Einträgen durchgeführt. Wenn der Unterschied bei zwei oder drei vertauschten Zeichen liegt, handelt es sich fast sicher um einen Tippfehler derselben Frage, sodass sie direkt abgeglichen werden kann. Dadurch wird der Embedding-Schritt völlig übersprungen, wodurch die Latenz nahe null ist. Bei hohen Ähnlichkeitsschwellenwerten erreichte dies eine Genauigkeit von 100 % und eine Trefferquote von 94 % auf Testdatensätzen voller Tippfehler.
Der größere Vorteil: Caching innerhalb von Agenten
Nur das endgültige Antwort auf eine Benutzerfrage zu cachen, ist ein unvollständiges mentales Modell. Agenten funktionieren anders. Ein typischer Ablauf für eine komplexe Supportfrage sieht so aus:
- Die Anfrage in 3 bis 5 Unterfragen aufteilen.
- Relevante Dokumente aus einer Wissensdatenbank abrufen.
- Einen LLM bitten, eine Antwort zu erstellen.
- Die Qualität der Antwort bewerten und bei Bedarf wiederholen.
Jede Unterfrage ist eine Gelegenheit zum Caching. Wenn der Agent bereits auf die Frage „Wie sehen Ihre API-Geschwindigkeitsbeschränkungen aus?“ geantwortet hat, muss er das Modell nicht erneut befragen, wenn diese Unterfrage in der komplexen Anfrage eines anderen Benutzers auftaucht.
Ein auf LangGraph basierender tiefgehender Forschungsagent wurde in drei Szenarien getestet:
| Szenario | Abrufrate | LLM-Aufrufe | Verzögerung |
|---|---|---|---|
| Neuer Benutzer, kaltes Caching | 25% | 8 | etwa 20 s |
| Zurückkehrender Benutzer, warmes Caching | 75% | 4 | etwa 13 s |
| Vorab-Bewertung vor Kauf | 75% | 6 | etwa 18 s |
Im dritten Szenario stammten drei von vier Unterfragen aus dem Cache. Die Anzahl der Modellaufrufe sank von 8 auf zwischen 4 und 6, und die End-to-End-Latenz verringerte sich um etwa ein Drittel. Die Antwortqualität blieb unverändert – es wurden in jedem Fall personalisierte und genaue Antworten gegeben.
Der Effekt verstärkt sich. Jeder erfolgreiche Aufruf fügt neue Daten zum Cache hinzu, wodurch spätere Nutzer mit ähnlichen Fragen von allen zuvor gespeicherten Informationen profitieren können. Dadurch steigen die Einsparungen, je mehr Anfragen das System bearbeiten muss.