Семантический кэш для 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 мс даже при большой параллельной нагрузке. Так исчезает налог на хранилище, который облачные провайдеры добавляют сверху к вычислениям.