Semantyczny cache dla aplikacji LLM na Ubuntu 24.04 z Qdrant
Obniż wydatki na API LLM, cache'ując odpowiedzi według znaczenia, a nie tekstu: Qdrant na Ubuntu 24.04, izolacja najemców, progi podobieństwa dla tras i koszt IOPS.
Gdy aplikacja generatywnej AI trafia na produkcję, rachunek szybko zwraca na siebie uwagę, bo płaci się za każdy token. Przejrzyj logi promptów, a prawdopodobnie okaże się, że nawet 40% zapytań użytkowników wyraża tę samą intencję innymi słowami. "Jak zresetować hasło?" i "Nie mogę wejść na konto, potrzebuję dostępu" znaczą to samo. Klasyczny magazyn klucz–wartość widzi dwa niezwiązane żądania, dwa razy wywołuje API OpenAI i za każdym razem nalicza pełną opłatę.
Semantyczny cache to naprawia. Oto jak zbudować go na Ubuntu 24.04 LTS z perspektywy utrzymania systemu, razem z trzema pułapkami, które tutoriale zwykle pomijają.
Dokładne dopasowanie kontra znaczenie
Dwa sposoby wyszukiwania zachowują się zupełnie inaczej:
- Cache leksykalny, na przykład zwykły magazyn klucz–wartość w Redis, wymaga identycznych ciągów znaków. "Gdzie jest zamówienie?" jest w cache, ale "Śledź paczkę" nie trafia, bo znaki są inne.
- Cache semantyczny zamienia prompty na wielowymiarowe wektory za pomocą modelu embeddingów. "Gdzie jest zamówienie?" i "Śledź paczkę" lądują blisko siebie w przestrzeni wektorowej, więc zapisana odpowiedź wraca od razu. Opóźnienie wyszukiwania spada z około 3000 ms do mniej więcej 15 ms.
Konfiguracja cache wektorowego z Qdrant
Jako magazyn uruchom Qdrant w Dockerze z trwałym wolumenem, a potem przygotuj środowisko Pythona:
# 1. Install Qdrant via Docker on Ubuntu 24.04 (Persistent Storage)
sudo apt update && sudo apt install docker.io -y
sudo docker run -d --restart unless-stopped -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage:z \
qdrant/qdrant
# 2. Setup Python environment
python3 -m venv llm_cache_env
source llm_cache_env/bin/activate
pip install qdrant-client sentence-transformers openai
Trzymaj najemców osobno
Jeden globalny cache semantyczny w wielonajemczym produkcie SaaS to proszenie się o poważne wycieki między klientami. Wyobraź sobie, że użytkownik A prosi o przegląd ostatnich transakcji na swoim koncie. Model pisze odpowiedź z prywatnymi danymi finansowymi A i trafia ona do cache. Później użytkownik B prosi o podsumowanie własnych transakcji. Cache widzi 98% zgodności intencji i podaje B odpowiedź napisaną dla A.
Rozwiązaniem jest ścisła przestrzeń nazw, tenant_id, w payloadzie Qdrant i wykonywanie każdego wyszukiwania podobieństwa pod twardym filtrem metadanych. Bez tego filtra wyszukiwanie w ogóle nie powinno się uruchomić. Poniższy kod tworzy kolekcję z właściwym rozmiarem wektora (384 dla lokalnego enkodera MiniLM) i wykonuje wyszukiwanie filtrowane po najemcy:
from qdrant_client import QdrantClient
from qdrant_client.http import models
from sentence_transformers import SentenceTransformer
# Initialize Local Embedding Model (Free & Fast)
encoder = SentenceTransformer("all-MiniLM-L6-v2")
client = QdrantClient(host="localhost", port=6333)
COLLECTION_NAME = "semantic_cache_prod"
# Explicitly create collection with correct Vector Dimensions (384)
if not client.collection_exists(collection_name=COLLECTION_NAME):
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=models.VectorParams(
size=384, # Matches 'all-MiniLM-L6-v2' output dimension
distance=models.Distance.COSINE
),
)
def check_secure_cache(user_query: str, tenant_id: str, threshold: float = 0.90):
query_vector = encoder.encode(user_query).tolist()
# SRE Fix: Hard Metadata Filtering by tenant_id
hits = client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
query_filter=models.Filter(
must=[models.FieldCondition(
key="tenant_id",
match=models.MatchValue(value=tenant_id)
)]
),
limit=1,
score_threshold=threshold
)
if hits:
print(f" Secure Cache HIT! (Score: {hits[0].score:.3f})")
return hits[0].payload["llm_response"]
return None
Pułapka progu
Wybór progu podobieństwa to kompromis między precyzją a kompletnością. Wiele poradników zaleca jedną, ogólną wartość 0,80, a to prowadzi do przykrych błędnych odpowiedzi. Bliskość w przestrzeni embeddingów nie oznacza takiego samego znaczenia. "Podaj stolicę Francji" i "Podaj stolicę Niemiec" mają tę samą budowę zdania, więc leżą bardzo blisko siebie, z podobieństwem kosinusowym powyżej 0,85. Przy progu 0,80 ktoś, kto pyta o Niemcy, dostanie z cache odpowiedź o Francji.
Zamiast tego dobieraj próg dla każdej trasy:
- 0,95 i więcej dla ścisłych pytań faktograficznych, technicznych lub finansowych
- 0,88 dla ogólnych, konwersacyjnych FAQ
Gdy wąskim gardłem staje się szybkość dysku
Bazy wektorowe oparte na indeksach HNSW mocno obciążają przepustowość pamięci i dysku. Ciężkie wyszukiwanie podobieństwa w chmurze publicznej zwykle oznacza magazyn blokowy z zarezerwowanymi IOPS, a opłaty za io2 potrafią być wysokie.
Jednym z wyjść jest uruchomienie całego stosu cache na dedykowanych serwerach bare-metal. Podłączone bezpośrednio przez PCIe dyski NVMe klasy enterprise dają miliony IOPS bez dopłat, a duża pula pamięci DDR5 na dedykowanym sprzęcie pozwala utrzymać wyszukiwanie embeddingów poniżej 15 ms nawet przy dużym ruchu równoległym. Znika wtedy podatek od magazynu, który dostawcy chmury doliczają do mocy obliczeniowej.