Семантичний кеш для LLM-застосунків на Ubuntu 24.04 з Qdrant
Скоротіть витрати на LLM API, кешуючи відповіді за змістом, а не за текстом: Qdrant на Ubuntu 24.04, ізоляція орендарів, пороги схожості за маршрутами і ціна IOPS.
Щойно генеративний ШІ-застосунок виходить у продакшн, рахунок швидко привертає увагу, адже оплачується кожен токен. Перегляньте логи промптів, і, найімовірніше, з'ясується, що до 40% запитів користувачів несуть той самий намір різними словами. «Як скинути пароль?» і «Не можу увійти в акаунт, потрібен доступ» означають одне й те саме. Класичне сховище «ключ–значення» бачить два непов'язані запити, двічі викликає OpenAI API і обидва рази виставляє повний рахунок.
Семантичний кеш це виправляє. Нижче показано, як зібрати його на Ubuntu 24.04 LTS з погляду експлуатації, а також три пастки, які туторіали зазвичай оминають.
Точний збіг проти змісту
Два способи пошуку поводяться дуже по-різному:
- Лексичне кешування, наприклад звичайне сховище «ключ–значення» в Redis, потребує ідентичних рядків. «Де моє замовлення?» закешовано, але «Відстежити посилку» промахується, бо символи інші.
- Семантичне кешування перетворює промпти на багатовимірні вектори за допомогою моделі ембедингів. «Де моє замовлення?» і «Відстежити посилку» опиняються поруч у векторному просторі, тож збережена відповідь повертається одразу. Затримка пошуку падає приблизно з 3000 мс до 15 мс.
Розгортаємо векторний кеш на Qdrant
Як сховище запустіть Qdrant у Docker із постійним томом, потім підготуйте середовище Python:
# 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
Тримайте орендарів окремо
Єдиний глобальний семантичний кеш у багатоорендному SaaS-продукті загрожує серйозними витоками між клієнтами. Уявіть, що користувач A просить зведення останніх транзакцій на своєму рахунку. Модель пише відповідь із його приватними фінансовими даними, і відповідь кешується. Пізніше користувач B просить зведення своїх транзакцій. Кеш бачить збіг наміру на 98% і віддає B відповідь, написану для A.
Рішення в тому, щоб додати в payload Qdrant суворий простір імен, tenant_id, і виконувати кожен пошук за схожістю під жорстким фільтром метаданих. Без цього фільтра пошук узагалі не повинен запускатися. Код нижче створює колекцію з потрібним розміром вектора (384 для локального енкодера MiniLM) і виконує пошук із фільтром за орендарем:
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
Пастка порогу
Вибір порогу схожості — це баланс між точністю і повнотою. Багато посібників радять одне загальне значення 0,80, і це дає неприємні хибні відповіді. Близькість у просторі ембедингів не означає однаковий зміст. «Назвіть столицю Франції» і «Назвіть столицю Німеччини» мають однакову структуру речення, тому лежать дуже близько одне до одного, з косинусною схожістю понад 0,85. За порогу 0,80 людина, яка спитала про Німеччину, отримає закешовану відповідь про Францію.
Натомість налаштовуйте поріг для кожного маршруту:
- 0,95 і вище для суворих фактичних, технічних чи фінансових питань
- 0,88 для типових розмовних FAQ
Коли вузьким місцем стає швидкість диска
Векторні бази на індексах HNSW сильно навантажують пропускну здатність пам'яті й диска. Важкий пошук за схожістю в публічній хмарі зазвичай означає блочне сховище з виділеними IOPS, а такі рахунки за io2 можуть бути дуже високими.
Один з обхідних шляхів — розмістити весь стек кешу на виділених bare-metal серверах. Під'єднані напряму корпоративні NVMe-диски на PCIe дають мільйони IOPS без доплати, а великий обсяг DDR5 RAM на виділеному залізі дозволяє тримати пошук за ембедингами швидшим за 15 мс навіть за великого паралельного навантаження. Так зникає податок на сховище, який хмарні провайдери додають згори до обчислень.