Strona główna / Artykuły

Cache semantyczny dla agentów LLM z Redis: zmniejszenie kosztów i opóźnień

Jak cache semantyczne zmniejsza koszty i opóźnienia w LLM: embeddingi, progi, Redis i RedisVL, dostosowywanie dokładności oraz przechowywanie w cache podpytań w ramach procesów agenta.

Wydawca: REACTAPP.TOP8 min czytania1736 słów

Źródła: Semantic Caching for AI Agents: How to Make Your LLM Apps Faster and Cheaper

Wywołania silnego modelu LLM kosztują prawdziwe pieniądze i są powolne. Każdy, kto obserwował wzrost rachunków API po wdrożeniu agenta przed prawdziwymi użytkownikami, zna ten dylemat: modele, które dają najlepsze odpowiedzi, są jednocześnie najdroższe i najpowolniejsze. Modele klasy GPT-4 mogą kosztować kilka dolarów za tysiąc zapytań, a procesy oparte na agentach pogarszają sytuację, ponieważ jedno zapytanie użytkownika może wywołać od 8 do 20 wywołań modelu.

Kasowanie semantyczne bezpośrednio adresuje ten problem kosztowy, ale większość zespołów go niedostatecznie wykorzystuje. Jego implementację można łatwo stworzyć za pomocą Redis.

Dlaczego zwykłe cache zawodzą

Zwykły cache traktuje dwa zapytania jako takie same tylko wtedy, gdy ich rozmiar w bajtach jest identyczny. To działa w przypadku zapytań do bazy danych i żądań HTTP, ale nie sprawdza się przy języku naturalnym. Wyobraźmy sobie agenta obsługi, który otrzymuje następujące zapytania od trzech różnych użytkowników:

  • Klient pyta, jak złożyć prośbę o zwrot pieniędzy
  • Kolejny żąda zwrotu pieniędzy
  • Trzeci chce wiedzieć, co mówi polityka zwrotów

Cache na poziomie bajtów widzi trzy niepowiązane dane wejściowe i opłaca trzy wywołania LLM. Człowiek powiedziałby, że wszyscy trzej chcą tej samej odpowiedzi. Cache semantyczny działa na podstawie znaczenia, a nie znaków.

Mechanizm w czterech krokach

  1. Wbuduj nadchodzące zapytanie, przekształcając je w wektor wysokowymiarowy za pomocą modelu embedding.
  2. Porównaj ten wektor z każdym zapisanym wpisem, używając odległości kosinowej lub podobnej miary.
  3. Jeśli najbliższy wpis mieści się w ustawionej przez ciebie granicy odległości, zalicz go jako trafienie i natychmiast zwróć zapisaną odpowiedź.
  4. Jeśli nic nie jest wystarczająco bliskie, prześlij zapytanie przez swój pipeline RAG lub LLM, a następnie zapisz pytanie i odpowiedź na później.

Zadania sformułowane w inny sposób ostatecznie dzielą te same odpowiedzi. W miarę wypełniania się pamięci cache, coraz mniejszy udział ruchu dociera do modelu.

Minimalna wersja od zera

Zanim użyjemy biblioteki produkcyjnej, przydatne jest zrozumienie jej elementów składowych. Zacznijmy od importów:

from sentence_transformers import SentenceTransformer
import numpy as np

Następnie wstaw mały zestaw FAQ i napisz funkcję wyszukiwania według najbliższego sąsiada:

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

Próbowano zadania testowego dotyczącego tego, ile dni zajmuje nadejście zwrotu pieniędzy, a wyszukiwanie zwróciło jako najbliższy wynik zapisane FAQ na temat otrzymania zwrotu, przy odległości kosinowej około 0,33. Nie doszło do żadnego dopasowania na podstawie samych wspólnych słów; wektory po prostu znajdują się blisko siebie.

Uruchamianie w Redis

Wersja demonstracyjna nie zapewnia trwałości danych, nie ma terminu ważności, nie rozdziela użytkowników oraz nie oferuje optymalnego wyszukiwania wektorowego. Dla wszystkiego, co jest skierowane do klientów, potrzebna jest prawdziwa baza danych. Redis w połączeniu z otwartym SDK RedisVL dostarcza te elementy od razu.

from redisvl.extensions.cache.llm import SemanticCache
from redisvl.utils.vectorize import HFTextVectorizer

Wybierz model embeddingowy stworzony specjalnie do tego zadania, a następnie utwórz 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

Model langcache-embed-v1 to model embedding o otwartych wagach, dostosowany specjalnie do semantycznego cacheowania, dzięki czemu zazwyczaj przewyższa ogólne narzędzia embeddingowe w tym zadaniu.

Trzy funkcje produkcyjne są dostępne w wersji Redis:

  • TTL. Wpisy wygasają same z siebie, więc przestarzałe odpowiedzi nie pozostają w pamięci w miarę zmian produktu.
  • Namespacing. Zespoły, użytkownicy lub najemcy mogą mieć oddzielne cache’y w ramach jednej instancji Redis.
  • Optymalizowana wyszukiwarka wektorowa. Indeksowanie HNSW zapewnia szybkie wyszukiwanie najbliższych sąsiadów nawet przy dużych skaliach.

Cztery wskaźniki, na które należy zwrócić uwagę

Budowa cache’a to łatwa część. Musisz również upewnić się, że działa poprawnie.

Wskaźnik trafień to udział zapytań, które znajdują odpowiedź w określonym progu, i on określa twoje oszczędności. Przy 30% wskaźniku trafień w systemie wykonującym 10 000 wywołań LLM dziennie, 3 000 wywołań zostaje pominiętych.

Dokładność pokazuje, jaka część trafień przyniosła właściwą odpowiedź. Pamięć cache, która z pewnością podaje błędne odpowiedzi, jest gorsza od takiej, która w ogóle nie odpowiada.

Pamięć określa, jaka część zapytań, które powinny zostać odnalezione, rzeczywiście została odnaleziona, przy założeniu istnienia odpowiedniej informacji. Niska pamięć oznacza, że oszczędności nie są realizowane.

Poprawa opóźnienia porównuje te dwa sposoby działania. Badania pokazują, że odpowiedzi z pamięci cache trwają około 2 do 5 ms, podczas gdy typowe wywołanie GPT-4 zajmuje 300 do 500 ms, co oznacza prędkość około 160 razy większą. To, co faktycznie odczujesz, zależy od częstotliwości trafień, jak pokazuje ta formuła:

With-Cache Latency = (avg_llm_latency × (1 - hit_rate)) + (avg_cache_latency × hit_rate)

Przy 30% trafień można oczekiwać spadku ogólnej latencji o około 25 do 30%. Gdy poziom wykorzystania pamięci cache osiągnie 60 do 75%, korzyści stają się znaczne.

Traktowanie pamięci cache jako klasyfikatora

Każde wyszukiwanie kończy się jednym z czterech wyników:

Pamięć cache zwraca trafienie Pamięć cache zwraca brak trafienia
Miałoby dojść do trafienia Prawdziwy pozytyw Fałszywy negatyw
Miałoby dojść do braku trafienia Fałszywy pozytyw Prawdziwy negatyw

Błędne pozytywy są najniebezpieczniejsze: dwa pytania, które wyglądają podobnie, ale oznaczają coś innego, przez co cache zwraca błędną odpowiedź. Pytanie dotyczące zwrotu pieniędzy po zmianie zdania oraz pytanie o ogólny proces zwrotu mogą znajdować się blisko siebie w przestrzeni embeddingów, jednak każde z nich wymaga odrębnej odpowiedzi. W takich przypadkach cache musi być szczególnie ostrożny, ponieważ zwracana przez niego odpowiedź brzmi zupełnie wiarygodnie.

Zwiększanie dokładności

Ustalanie progu. Próg odległości jest najważniejszym ustawieniem. Niższa wartość zwiększa precyzję, ale obniża trafność, natomiast wyższa robi odwrotnie. Spróbuj różnych wartości na reprezentatywnym zestawie testowym i wybierz tę, która maksymalizuje wskaźnik F1, będący harmoniczną średnią precyzji i trafności. Taki podejście podniosło precyzję z 79% do 90%, przy jednoczesnym utrzymaniu wskaźnika trafności na poziomie 25%.

Przeprowadź ponowną klasyfikację za pomocą cross-encodera. Standardowe modele embeddingów kodują każde zdanie osobno. Cross-encoder czyta zapytanie i kandydatów razem, dzięki czemu wykrywa subtelności, których nie dostrzegają bi-encodery. Weźmy jedno zdanie o banku podnoszącym stopy procentowe oraz drugie o zalaniu rzeki w pobliżu banku po ulewnych deszczach: wspólne słowo „bank” sprawia, że ich embeddingi są dość bliskie, ale cross-encoder dostrzega, że nie są ze sobą powiązane. W eksperymentach ponowna klasyfikacja doprowadziła do precyzji na poziomie 94%. Cross-encodery nie mogą tworzyć wektorów nadających się do wyszukiwania, dlatego działają jako drugi etap obok kandydatów typu top-K, które wybiera model embeddingów.

Zaproś mały model LLM do oceny. Aby osiągnąć maksymalną dokładność, niech tani model, np. GPT-4o-mini, zweryfikuje przypadki na granicy poprawności za pomocą prostego pytania: czy te dwa pytania są semantycznie równoważne? Odpowiada w ułamku sekundy za ułamek ceny pełnego połączenia. Testy wykazały 100% dokładności, ale niższy wskaźnik odnalezienia, ponieważ model czasami odrzuca poprawne dopasowania. To, czy jest to do przyjęcia, zależy od przypadku użycia. W obsłudze klienta nieco błędna odpowiedź z pamięci podręcznej może być gorsza niż nowe połączenie.

Najpierw sprawdź dopasowanie nieprecyzyjne. Zanim obliczysz jakiekolwiek embeddingi, przeprowadź porównanie odległości edycyjnej zapytania z zapisanymi wpisami. Jeśli różni się od jednego z nich o dwa lub trzy przestawione znaki, to prawie na pewno jest to błąd pisarski tego samego pytania, więc dopasuj je bezpośrednio. Dzięki temu całkowicie pomijasz krok z embeddingami, co sprawia, że opóźnienie jest niemal zerowe. Przy wysokich prógach podobieństwa osiągnięto 100% dokładności i 94% powtarzalności na zestawach testowych pełnych błędów pisarskich.

Większy sukces: buforowanie wewnątrz agentów

Buforowanie tylko ostatecznej odpowiedzi na pytanie użytkownika to niepełny model mentalny. Agenci działają inaczej. Typowy proces rozpatrywania skomplikowanego pytania obsługi klienta wygląda następująco:

  1. Rozbierz zapytanie na 3 do 5 podpytań.
  2. Pobierz odpowiednie dokumenty z bazy wiedzy.
  3. Zapytaj model językowy o stworzenie odpowiedzi.
  4. Oceń jakość odpowiedzi i w razie potrzeby dokonaj korekty.

Każde podpytanie to szansa na wykorzystanie pamięci podręcznej. Jeśli agent już odpowiedział na pytanie „Jakie są limity szybkości API?“, nie musi ponownie pytać modelu, gdy to podpytanie pojawi się w złożonym zapytaniu innego użytkownika.

Agent do głębokiego badania oparty na LangGraph został przetestowany w trzech scenariuszach:

Scenariusz Wskaźnik trafień Liczba wywołań LLM Czas reakcji
Nowy użytkownik, zimna pamięć podręczna 25% 8 około 20 s
Użytkownik powracający, ciepła pamięć podręczna 75% 4 około 13 s
Przegląd przed zakupem 75% 6 około 18 s

W trzecim scenariuszu trzy na każde cztery podpytania pochodziły z pamięci podręcznej. Liczba wywołań modelu spadła z 8 do wartości między 4 a 6, a opóźnienie od początku do końca zmniejszyło się o około jedną trzecią. Jakość odpowiedzi pozostała bez zmian – w obu przypadkach uzyskiwano spersonalizowane i dokładne odpowiedzi.

Efekt ten się kumuluje. Każdy udany proces dodaje dane do pamięci podręcznej, a późniejsi użytkownicy z podobnymi pytaniami korzystają z wszystkich wcześniejszych informacji, dzięki czemu oszczędności rosną w miarę, jak system obsługuje setki lub tysiące zapytań.