395 материалов в базеАвтор: Андрей Лукин
Практический разборВремя чтения: 7 мин· 6 января 2026 г.0

Гибридное кэширование векторов Redis + Qdrant для LLM-пайплайнов: архитектура и оптимизация

Гибридное кэширование векторов Redis + Qdrant для LLM-пайплайнов: архитектура и оптимизация

Гибридное кэширование семантических векторов Redis + Qdrant: архитектура, реализация и оптимизация LLM-пайплайнов

Проблема дублирования вычислений эмбеддингов в продакшн-системах на базе LLM приводит к переполнению GPU и росту задержек (latency). Решение заключается в разделении ответственности: Redis выступает как L1-кэш для мгновенного O(1) доступа к хешам запросов и ссылкам на векторы, а Qdrant — как L2-хранилище для семантического поиска по миллионам записей. Этот подход позволяет снизить нагрузку на инференс-модели до 90% при наличии повторяющихся паттернов в трафике.

Технические параметры и сводная таблица

Параметр / Инструмент Значение / Версия Назначение / Команда Примечание
Redis Stack 8.2.0 L1-кэш для хешей и метаданных Режим volatile-lru, TTL 5-10 мин
Qdrant 1.11.0 L2-хранилище векторов и метаданных on_disk_payload, Cosine distance
LangChain 0.3.15 Оркестрация пайплайнов Интеграция кастомного SemanticCache
ONNX Runtime 1.20.0 Инференс эмбеддингов Cold-start 0.15с вместо 1.2с
HuggingFace Transformers 4.48.0 Модель эмбеддингов Поддержка ONNX Runtime
Docker Compose v2.30.0 Оркестрация сервисов Ограничение CPU, сеть bridge

Архитектура гибридного кэша: Redis + Qdrant

Система строится на паттерне Write-Through с асинхронным дублированием. При поступлении запроса система сначала проверяет Redis по MD5-хешу нормализованного текста. Если хеш найден, вектор извлекается из кэша и сразу отправляется в Qdrant для поиска, минуя генерацию эмбеддингов. Если хеш отсутствует, модель (через ONNX Runtime) генерирует вектор, сохраняет его в Redis с TTL и асинхронно дублирует в Qdrant через upsert.

Важно

Кэшировать необходимо только "чистые" текстовые запросы. Промпты с переменными (например, {{user_name}}) не должны попадать в кэш, так как это приведет к промаху (miss rate) около 40% при изменении параметров пользователя.

Внимание

Кэширование запросов без нормализации (lowercase, strip, remove_punct) вызывает утечку памяти из-за дублирования хешей для идентичных по смыслу, но различающихся по регистру или знакам препинания запросов.

Настройка инфраструктуры (Docker & Config)

Запуск сервисов

Используйте Docker Compose для изоляции и управления ресурсами. Ограничьте количество CPU-ядер для предотвращения перегрузки хоста.

version: '3.9'
services:
  redis-cache:
 image: redis:8.2-alpine
 ports:
 - "6379:6379"
 command: redis-server --maxmemory 2gb --maxmemory-policy volatile-lru
 deploy:
 resources:
 limits:
 cpus: '1'
 networks:
 - ai_infra

  qdrant:
 image: qdrant/qdrant:1.11.0
 ports:
 - "6333:6333"
 volumes:
 - qdrant_storage:/qdrant/storage
 deploy:
 resources:
 limits:
 cpus: '2'
 networks:
 - ai_infra

  app:
 build: .
 depends_on:
 - redis-cache
 - qdrant
 networks:
 - ai_infra

volumes:
  qdrant_storage:
 driver: local

networks:
  ai_infra:
 driver: bridge

Конфигурация Qdrant

Настройте коллекцию с включением on_disk_payload для хранения тяжелых метаданных вне оперативной памяти, что критично при работе с большими объемами данных.

# config/qdrant_config.yaml
collection_name: semantic_vectors
vectors:
  size: 768
  distance: Cosine
shard_number: 4
replication_factor: 1
on_disk_payload: true
Совет

Включите on_disk_payload в Qdrant. Это позволяет хранить метаданные (например, ID документа, дату создания) на диске, освобождая RAM для самих векторов и ускоряя загрузку коллекции при старте.

Реализация логики кэширования на Python (LangChain)

Подготовка зависимостей

Установите необходимые библиотеки, включая оптимизированную версию ONNX Runtime для трансформеров.

pip install qdrant-client redis onnxruntime-transformers==1.20.0

Логика менеджера кэша

Реализуйте кастомный класс SemanticCache, который интегрируется в цепочку LangChain. Ключевым этапом является нормализация запроса перед вычислением хеша.

import hashlib
import pickle
import base64
import time
from typing import Optional, List, Dict, Any
import redis
import qdrant_client
from qdrant_client.http import models

class SemanticCache:
 def __init__(self, redis_host: str = "localhost", qdrant_host: str = "localhost"):
 self.redis_client = redis.Redis(host=redis_host, port=6379, decode_responses=True)
 self.qdrant_client = qdrant_client.QdrantClient(host=qdrant_host)
 self.model = None # Инициализация модели эмбеддингов (например, BERT или SentenceTransformer)
 self.version = "v1.0" # Версия модели для валидации хешей

 def normalize_query(self, query: str) -> str:
 """Строгая нормализация: lowercase, strip, удаление пунктуации."""
 import re
 text = query.lower().strip()
 text = re.sub(r'[^\w\s]', '', text)
 return text

 def get_cached_vector(self, query: str) -> Optional[List[float]]:
 """Получение вектора из кэша Redis."""
 normalized = self.normalize_query(query)
 hash_val = hashlib.md5(normalized.encode()).hexdigest()
 key = f"vec:{hash_val}"
 
 cached_data = self.redis_client.get(key)
 if cached_data:
 # Десериализация вектора
 vector = pickle.loads(base64.b64decode(cached_data))
 return vector
 
 return None

 def save_vector(self, query: str, vector: List[float]):
 """Сохранение вектора в Redis и асинхронная запись в Qdrant."""
 normalized = self.normalize_query(query)
 hash_val = hashlib.md5(normalized.encode()).hexdigest()
 key = f"vec:{hash_val}"
 
 # 1. Запись в Redis с TTL (5-10 минут)
 ttl_seconds = 300 
 self.redis_client.setex(key, ttl_seconds, base64.b64encode(pickle.dumps(vector)).decode())
 
 # 2. Асинхронная запись в Qdrant
 import asyncio
 asyncio.run(self._async_upsert_to_qdrant(hash_val, vector))

 async def _async_upsert_to_qdrant(self, point_id: str, vector: List[float]):
 """Асинхронное добавление точки в Qdrant."""
 self.qdrant_client.upsert(
 collection_name="semantic_vectors",
 points=[
 models.PointStruct(
 id=point_id,
 vector=vector,
 payload={"original_query": None, "timestamp": time.time()}
 )
 ]
 )

 def search(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
 """Поиск по вектору с проверкой кэша."""
 vector = self.get_cached_vector(query)
 
 if vector:
 # Вектор найден в кэше, ищем в Qdrant
 results = self.qdrant_client.search(
 collection_name="semantic_vectors",
 query_vector=vector,
 limit=top_k
 )
 return [{"id": r.id, "score": r.score} for r in results]
 else:
 # Вектор не найден, генерируем его заново
 # Здесь должна быть логика вызова модели (ONNX Runtime)
 # vector = self.model.encode(query) 
 # self.save_vector(query, vector)
 # return self.search(query, top_k) # Рекурсивный вызов после сохранения
 raise Exception("Vector generation required (cache miss)")
Совет

Используйте ONNX Runtime для генерации эмбеддингов перед записью в Redis. Это снижает время cold-start с 1.2 секунды до 0.15 секунды, что критично для пользовательского опыта.

Диагностика и устранение возможных сбоев

Сбой: "Кэш не находит вектор при идентичном запросе"

Причина: Отсутствие нормализации текста. Запросы "Привет" и "привет" или "Привет!" и "Привет" имеют разные хеши. Решение: Внедрите метод normalize_query в класс кэша, который унифицирует регистр и убирает лишнюю пунктуацию перед вычислением MD5.

Сбой: "Redis переполнен памятью"

Причина: Отсутствие политики очистки памяти или слишком большой TTL. Решение: Установите maxmemory-policy volatile-lru в конфигурации Redis. Это гарантирует, что при достижении лимита памяти (например, 2GB) старые векторы будут автоматически удалены.

Сбой: "Неверные результаты поиска после обновления модели"

Причина: Модель эмбеддингов обновилась, но хеши в Redis остались от старой версии. Решение: Добавьте временную метку или версию модели (self.version) в хеш ключа. При смене версии хеш изменится, и система сгенерирует новые векторы.

Часто задаваемые вопросы (FAQ)

Как часто обновлять кэш векторов в Redis?

Рекомендуемый TTL — 5-10 минут. Более длительное хранение увеличивает риск устаревания контекста в чат-ботах, а слишком короткий срок повышает нагрузку на GPU при повторных запросах.

Стоит ли кэшировать результаты поиска в Qdrant или только векторы?

Кэшировать нужно только векторы и хеш запроса. Результаты поиска (ранжированные точки) не кэшируются глобально, так как они зависят от фильтров и весов, которые могут меняться динамически.

Как обрабатывать ситуации, когда вектор в Redis устарел, но запрос идентичен?

При вычислении хеша добавляем в него временную метку или версию модели эмбеддингов. Если версия изменилась, хеш изменится, и система сгенерирует новый вектор.

Что делать при падении Redis в продакшене?

Используйте паттерн Cache-Aside. При недоступности Redis система автоматически переключается на прямой расчет векторов с флагом skip_cache=true, обеспечивая отказоустойчивость за счет небольшого роста задержки.

Как масштабировать решение при росте трафика?

Настройте Redis Cluster для горизонтального масштабирования кэша при росте трафика. Используйте Qdrant Distributed (sharding) для распределения нагрузки по узлам при хранении >10 млн векторов.

Андрей Лукин
Андрей Лукин Автор

Full-stack разработчик и Digital-спасатель. Пишу о технологиях, ИИ и реальной инженерной практике без воды.

Для читателей этой статьи

Понравился материал?

Подпишитесь на Telegram-канал: свежие инсайты, промпты и разборы первыми. Без спама, только польза.

Подписаться в Telegram
Ещё в рубрике «Практика и best practices»

Материалы по теме

Читаете:Гибридное кэширование векторов Redis + Qdrant для LLM-пайплайнов: архитектура и оптимизацияОбсудить в TG