Реализовать семантический кэш
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать семантический кэш
1. Цель задачи
Разработать семантический кэш для LLM-приложения, который на основе эмбеддингов запросов и порога косинусного сходства позволяет повторно использовать ранее сгенерированные ответы. Кэш хранит векторные представления запросов в Qdrant, а для быстрого доступа к метаданным использует Redis. В результате ожидается cache hit rate 30–50% на репрезентативном наборе запросов (не менее 1 000 уникальных). Ключевой результат рабочий семантический кэш с hit rate ≥ 30% при latency < 200 мс на поиск.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| Датасет вопросов (query) и ответов (response) | Открытые датасеты: MS MARCO (passage ranking) или SQuAD 2.0 (QA). Выбрать не менее 2 000 пар. |
| Модель эмбеддингов | sentence-transformers/all-MiniLM-L6-v2 из Hugging Face. |
| Qdrant (векторная БД) | Установка через Docker (qdrant/qdrant:latest) или in-memory-режим (Python-клиент). |
| Redis (кэш быстрого доступа) | Установка через Docker (redis:7-alpine) или redis-py / fakeredis. |
| Инструмент генерации запросов-вариаций | Для симуляции семантической близости — nlpaug или ручное перефразирование. |
Если нет реального инструмента — симулируем:
- Скачать датасет вопросов (например, 2 000 строк из SQuAD) в CSV.
- Создать скрипт
generate_variations.py, который для каждого вопроса генерирует 3 синонимичных варианта с помощьюnlpaug.ContextualWordEmbsAug(model='bert-base-uncased'). - Установить Qdrant локально (docker-compose) или использовать in-memory Qdrant из
qdrant-client(режим:memory:). - Redis заменить на
fakeredisдля тестов в памяти — это позволит не разворачивать сервисы.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык | Python 3.10+ | Основной язык реализации |
| Эмбеддинги | sentence-transformers (all‑MiniLM‑L6‑v2) | Получение векторных представлений запросов |
| Векторная БД | qdrant-client (режим :memory: или Docker) | Хранение эмбеддингов и поиск по сходству |
| Быстрый кэш | redis-py / fakeredis | Хранение хэшей запросов, метаданных (TTL, время создания) |
| Обработка текста | nlpaug (опционально), pandas | Генерация вариаций запросов, загрузка датасета |
| Тестирование | pytest, timeit | Unit-тесты, замер latency, hit rate |
| Оценка | scikit-learn (cosine_similarity) | Контроль порога сходства, эмпирический подбор |
| Оркестрация | Docker / docker-compose | Подъём Qdrant и Redis (если не in-memory) |
4. Этапы выполнения
Этап 1: Подготовка окружения и данных (1 ч)
Действия
- Установить зависимости:
pip install sentence-transformers qdrant-client fakeredis pandas nlpaug scikit-learn. - Скачать датасет (SQuAD) или сгенерировать синтетический набор пар (вопрос‑ответ).
- Написать скрипт
prepare_data.py, который:- загружает датасет в DataFrame;
- генерирует для каждого вопроса 3 вариации (если нужна симуляция попаданий);
- сохраняет train/test split (80/20) в
data/queries.parquet.
- Инициализировать in-memory Qdrant и fakeredis:
from qdrant_client import QdrantClient from fakeredis import FakeStrictRedis qdrant = QdrantClient(":memory:") redis = FakeStrictRedis()
Ожидаемый результат этапа Настроенное окружение, файл data/queries.parquet с колонками query, response, variations, готовые клиенты Qdrant и Redis без развёртывания контейнеров.
Этап 2: Разработка модуля эмбеддингов (1 ч)
Действия
- Создать
embedder.pyс классомEmbedder:from sentence_transformers import SentenceTransformer class Embedder: def __init__(self, model_name: str = "all-MiniLM-L6-v2"): self.model = SentenceTransformer(model_name) def encode(self, text: str) -> list[float]: return self.model.encode(text, normalize_embeddings=True).tolist() - Добавить кэширование запросов в Redis: перед вызовом модели проверять, есть ли хэш запроса (SHA256) в Redis. Если есть — возвращать закэшированный эмбеддинг, иначе вычислять и сохранять с TTL (1 час).
- Протестировать скорость: один вызов
encodeне должен превышать 50 мс на CPU (опционально GPU).
Ожидаемый результат этапа Модуль embedder.py с методом encode, использующий Redis для кэширования эмбеддингов. Юнит-тест проверяет, что повторный вызов того же запроса не пересчитывает эмбеддинг.
Этап 3: Реализация семантического кэша (2 ч)
Действия
- Создать
semantic_cache.pyс классомSemanticCache:class SemanticCache: def __init__(self, embedder, qdrant_client, redis, collection="cache", threshold=0.85): self.embedder = embedder self.qdrant = qdrant_client self.redis = redis self.collection = collection self.threshold = threshold def get(self, query: str) -> str | None: # 1. Получить эмбеддинг запроса # 2. Поиск в Qdrant по cosine distance (limit=1) # 3. Если distance >= threshold, вернуть ответ из payload точки # 4. Иначе None def put(self, query: str, response: str): # 1. Вычислить эмбеддинг # 2. Вставить точку в Qdrant: id = hash(query), vector, payload={query, response} # 3. (Опционально) сохранить метаданные в Redis (time, frequency) - Настроить Qdrant коллекцию:
from qdrant_client import models qdrant.recreate_collection( collection_name="cache", vectors_config=models.VectorParams( size=384, # all-MiniLM-L6-v2 distance=models.Distance.COSINE ) ) - Реализовать
getс учётом порога: если расстояние (косинусная дистанция = 1 - similarity) меньше1 - threshold, считаем попаданием. - Добавить обработку ошибок: пустой запрос, сбой Qdrant, отсутствие коллекции.
Ожидаемый результат этапа Рабочий класс SemanticCache, который при put сохраняет запрос-ответ, а при get возвращает ответ на семантически похожий запрос. Unit-тест: после put запроса возвращается ответ на его вариацию с порогом 0.85.
Этап 4: Интеграция и тестирование (2 ч)
Действия
- Написать скрипт
test_cache.py, который:- загружает 1 000 запросов из датасета (только оригиналы) и сохраняет их в кэш;
- затем прогоняет 1 000 вариаций (похожих, но не идентичных);
- замеряет hit rate = (число попаданий) / 1 000.
- Экспериментально подобрать порог
thresholdдля достижения 30–50% hit rate (перебор от 0.7 до 0.95 с шагом 0.05). - Замерить среднюю и p99 latency для
getиput. - Проверить, что кэш корректно работает при повторных вставках (обновление ответа, если запрос уже есть).
Ожидаемый результат этапа Таблица с hit rate для разных порогов, итоговый выбранный порог, метрики latency (get < 200 мс, put < 300 мс). График зависимости hit rate от порога (опционально).
Этап 5: Оптимизация и финальная упаковка (2 ч)
Действия
- Оптимизировать Qdrant: добавить индексирование по payload-полям (например,
query_textдля fallback точного совпадения), использоватьupsertдля пакетной загрузки. - Реализовать fallback: если Qdrant возвращает точное текстовое совпадение — отдавать ответ без проверки порога.
- Написать CLI-интерфейс
cli.pyс командамиput,get,stats. - Оформить код в виде пакета
semantic_cache/с__init__.py. - Подготовить
README.md: описание, пример использования, результаты экспериментов.
Ожидаемый результат этапа Готовый пакет, CLI, документация. Код проходит flake8 и pytest с покрытием > 70%.
5. Критерии приемки (Definition of Done)
- Реализован класс
SemanticCacheс методамиgetиput. - Кэш использует Qdrant (in-memory или Docker) и Redis (или fakeredis).
- При одинаковом/похожем запросе с порогом 0.85 возвращается ответ из кэша.
- На тестовом наборе из 1 000 пар (оригинал + вариация) hit rate ≥ 30%.
- Среднее время
getне превышает 200 мс без GPU. - Код покрыт юнит-тестами (pytest) — минимум 5 тестов.
- Весь код опубликован в репозитории (GitHub) с файлами зависимостей (
requirements.txt). - Есть CLI или скрипт для воспроизведения теста hit rate.
- В README приведена таблица hit rate при разных порогах.
- Обработка крайних случаев: пустой запрос, отсутствие коллекции, ошибки Redis.
6. Ожидаемый результат
Основной файл semantic_cache/semantic_cache.py — реализация кэша.
Скрипт тестирования evaluate.py — замер hit rate, генерация отчёта.
Артефакты
- Таблица hit rate для порогов 0.70–0.95 (CSV или Markdown в
results/). - График зависимости (опционально, PNG).
- Юнит-тесты в
tests/. requirements.txt.
Дополнительно Докер-композ для production-развёртывания (если использовался не in-memory).
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Низкий hit rate (ниже 30%) | Уменьшить порог сходства (попробовать 0.75–0.8). Убедиться, что модель эмбеддингов адекватна для предметной области (попробовать all-mpnet-base-v2). |
| Высокая latency из-за Qdrant | Использовать in-memory Qdrant; батчить upsert; применить payload_index для быстрого exact‑match. |
| Неправильное определение distance | В Qdrant COSINE distance = 1 - cosine similarity. Порог сходства 0.85 → distance ≤ 0.15. |
| Redis становится узким местом | Заменить fakeredis на реальный Redis только при нагрузочном тестировании; для начала достаточно in-memory. |
| Плохие вариации запросов (слишком далёкие от оригинала) | Использовать более сильный аугментатор (например, через LLM: Llama 3.1 8B) или вручную подготовить 200–300 близких пар для калибровки порога. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Подготовка окружения и данных | 1 ч |
| Этап 2: Модуль эмбеддингов | 1 ч |
| Этап 3: Реализация семантического кэша | 2 ч |
| Этап 4: Интеграция и тестирование | 2 ч |
| Этап 5: Оптимизация и упаковка | 2 ч |
| Итого | 8 ч |
Примечание При первом выполнении время может увеличиться до 12 ч из-за отладки Qdrant и подбора порога.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 42 | Основы векторных баз данных (Qdrant) |
| 89 | Выбор модели эмбеддингов для семантического поиска |
| 135 | Тюнинг порога косинусного сходства |
| 210 | Архитектура кэша для LLM-приложений |
| 304 | Интеграция Redis с Python (кэширование эмбеддингов) |
| 405 | Аугментация текста для тестирования кэша |
| 512 | Замер latency и пропускной способности API |
| 678 | Пакетная вставка векторов в Qdrant |
| 721 | Fallback механизмы при недоступности кэша |
| 889 | Оценка hit rate и метрики эффективности кэша |
10. Чек-лист самопроверки
- Я развернул Qdrant и Redis (или использовал in-memory) и проверил подключение.
- Мой
Embedderзагружает модель и выдаёт нормализованные векторы. - Я реализовал
getиputвSemanticCacheс учётом порога сходства. - Я прогнал тест на 1 000 вариаций и получил hit rate ≥ 30%.
- Я замерил среднее время ответа
get— оно не превышает 200 мс. - Я написал минимум 5 юнит-тестов (точное совпадение, семантическое, пустой запрос, ошибка Qdrant, ошибка Redis).
- Я оформил код согласно PEP8 и добавил type hints.
- В README я привёл пример использования и таблицу hit rate для разных порогов.
- Я убедился, что кэш корректно обрабатывает дубликаты (обновление ответа).
- Я проверил, что падение Redis не ломает
get(использую try‑except и fallback на Qdrant только).