RAG с кэшированием ответов
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: RAG с кэшированием ответов
1. Цель задачи
Спроектировать и реализовать слой кэширования для RAG-системы с использованием Redis. Кэш должен хранить ответы на повторяющиеся запросы пользователей с TTL 1 час, чтобы снизить latency для повторных запросов на 80% по сравнению с полным RAG-циклом (retrieval + генерация). В процессе вы отработаете стратегию инвалидации кэша, выбор ключа и измерение производительности.
Ключевой результат Рабочий прототип RAG-системы с Redis-кэшем, в котором для повторных запросов время ответа снижено не менее чем на 80%.
2. Исходные данные
Перед началом необходимо иметь:
| Что нужно | Откуда взять |
|---|---|
| Базовая RAG-система (retrieval + LLM) | Собранный пет-проект (например, Pet 221) или минимальная реализация (LangChain + FAISS) |
| Redis (локально или в Docker) | Установить docker run -p 6379:6379 redis:7 или через brew/apt |
| Тестовый набор запросов (50–100 штук) | Сформировать вручную из предметной области (FAQ, техподдержка) |
| Инструмент для нагрузочного тестирования | Locust / k6 / простой Python скрипт с timeit |
| Python 3.10+ | Локальная среда |
Если нет реального инструмента — симулируем:
- Установите Redis через Docker:
docker run --name redis-rag -p 6379:6379 -d redis:7 - Напишите простой RAG-пайплайн на LangChain: FAISS-индекс + OpenAI API (или локальная модель через Ollama)
- Создайте 20–30 уникальных запросов и 50 их копий (повторов) для измерения cache-hit.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| RAG-пайплайн | LangChain / LlamaIndex | Оркестрация retrieval + генерации |
| Векторная база | FAISS (in-memory) или Qdrant в Docker | Хранение и поиск эмбеддингов |
| LLM | OpenAI API / Ollama (local) | Генерация ответов |
| Кэш | Redis (python redis или redis-py) | Хранение пар <вопрос, ответ> |
| Язык | Python 3.10+ | Реализация логики |
| Мониторинг | Python timeit + простой логгер | Замер latency |
| Нагрузочное тестирование | Locust или кастомный скрипт | Симуляция повторных запросов |
4. Этапы выполнения
Этап 1: Подготовка и настройка Redis (30 минут)
Действия
- Установите Redis (Docker или локально). Проверьте подключение через
redis-cli ping→ PONG. - Установите Python-клиент:
pip install redis. - Создайте файл
cache.pyс классомRedisCache:import redis import json class RedisCache: def __init__(self, host='localhost', port=6379, db=0, ttl=3600): self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True) self.ttl = ttl def get(self, key: str) -> dict | None: data = self.client.get(key) if data: return json.loads(data) return None def set(self, key: str, value: dict) -> None: self.client.setex(key, self.ttl, json.dumps(value)) def flush(self): self.client.flushdb() - Напишите тест подключения:
cache = RedisCache() cache.set("hello", {"answer": "world"}) print(cache.get("hello")) # {'answer': 'world'}
Ожидаемый результат этапа Рабочий класс для чтения/записи в Redis с TTL 1 час.
Этап 2: Интеграция кэша в RAG-пайплайн (1.5 часа)
Действия
- Имея базовый RAG-пайплайн (создайте его, если нет), оберните вызов генерации в функцию
get_rag_answer(question: str) -> dict. - Модифицируйте функцию: перед retrieval проверьте наличие ключа в Redis.
Определите стратегию ключа используйте нормализованный (lowercase, strip) и хэшированный (MD5) текст вопроса для компактности.import hashlib def make_cache_key(question: str) -> str: normalized = question.strip().lower() return hashlib.md5(normalized.encode()).hexdigest() - Реализуйте двухуровневую логику:
def get_answer(question: str) -> dict: key = make_cache_key(question) cached = cache.get(key) if cached: return {"source": "cache", "answer": cached["answer"], "latency": 0} # Полный RAG-процесс start = time.time() docs = retriever.get_relevant_documents(question) context = "\n".join([d.page_content for d in docs]) prompt = f"Context: {context}\nQuestion: {question}\nAnswer:" answer = llm.invoke(prompt) elapsed = time.time() - start # Сохранить в кэш cache.set(key, {"answer": answer, "context": context}) return {"source": "rag", "answer": answer, "latency": elapsed} - Протестируйте на одном и том же вопросе дважды — первый ответ должен приходить с source=rag, второй с source=cache.
- Добавьте обработку ошибок Redis (если Redis недоступен, система должна работать в режиме fallback без кэша).
Ожидаемый результат этапа Функция get_answer корректно возвращает кэшированные ответы и измеряет latency.
Этап 3: Нагрузочное тестирование и замеры (1 час)
Действия
- Подготовьте тестовый набор: 10 уникальных вопросов, каждый повторить 5 раз (всего 50 запросов). Сохраните в
test_queries.py. - Напишите скрипт для прогона всех запросов последовательно и сбора статистики по latency. Разделите на две фазы:
- Фаза 1 (cold): первый прогон всех уникальных вопросов (все попадают в RAG).
- Фаза 2 (hot): повторный прогон тех же вопросов (все должны быть из кэша).
- Посчитайте среднюю latency для каждой фазы.
- Вычислите процент снижения:
(cold_latency - hot_latency) / cold_latency * 100. - Подтвердите, что снижение >= 80%.
- Проверьте TTL: подождите 1 час (или для теста установите TTL=10 секунд) и убедитесь, что запросы снова выполняются через RAG.
Ожидаемый результат этапа Таблица с latency для cold/hot и итоговый процент ускорения.
Этап 4: Оптимизация и документирование (30 минут)
Действия
- Если ускорение меньше 80%, проанализируйте причины: возможно, кэш-ключ не захватывает синонимы, или Redis overhead высок. Попробуйте использовать
pipelineдля массового чтения. - Добавьте инвалидацию кэша по событию (например, при обновлении базы знаний) — симулируйте очистку кэша через
flushпри загрузке новых документов. - Напишите краткую документацию в README: архитектура, как запустить, результаты тестов.
- Подготовьте скрипт для воспроизведения метрик (
run_benchmark.py).
Ожидаемый результат этапа Оптимизированная система с документированными метриками.
Этап 5: Интеграция мониторинга (опционально, 30 минут)
Действия
- Добавьте логирование каждого запроса: время, source (cache/rag), latency.
- Экспортируйте метрики в Prometheus-формат (через
prometheus_client) или просто в CSV. - Нарисуйте график latency по времени (например, в Excel или Python matplotlib).
Ожидаемый результат этапа Дашборд или график, наглядно показывающий снижение latency для повторных запросов.
5. Критерии приемки (Definition of Done)
- Redis-сервер запущен, Python-клиент успешно подключается.
- Каждый запрос сначала проверяется в кэше; при наличии ответ возвращается из кэша.
- TTL для каждой записи установлен ровно 1 час (3600 секунд).
- После истечения TTL запрос повторно попадает в RAG.
- Средняя latency для повторных запросов снижена на 80% относительно первого запроса.
- При падении Redis система падает корректно (fallback на полный RAG без кэша).
- Кэш-ключ строится на основе нормализованного текста вопроса (без учёта регистра и лишних пробелов).
- Нагрузочный тест выполнен, результаты зафиксированы в таблице.
- Код размещён в репозитории с README и комментариями.
- В README описаны метрики (cold latency vs hot latency) и инструкция по запуску.
6. Ожидаемый результат
Основной артефакт
- Репозиторий с кодом (Python-модули
rag_cache.py,cache.py,test_queries.py,run_benchmark.py).
Содержание
- Реализация кэширующего RAG с использованием Redis.
- Скрипт нагрузочного тестирования, выводящий
cold_latency,hot_latency,speedup_percent. - README с инструкцией и результатами теста.
Дополнительные результаты
- График/таблица сравнения latency (опционально).
- Описание стратегии инвалидации кэша.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Redis недоступен | Добавить try/except и fallback на RAG без кэша. |
| Хэш-ключи коллизируют | Использовать более длинный хэш (SHA256) или комбинировать с контекстом. |
| TTL не точно соблюдается | Проверить, что setex устанавливает миллисекунды — использовать ex=3600. |
| Latency падает меньше 80% | Проверить, что overhead Redis (сеть/сериализация) невелик; возможно, нужно уменьшить TTL или использовать pipeline для массовых операций. |
| Разные формулировки одного вопроса не дают cache-hit | Использовать эмбеддинг-похожесть (например, cosine distance) для fuzzy-кэша — усложнение для продвинутого этапа. |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Настройка Redis | 30 мин |
| Этап 2: Интеграция кэша | 1.5 ч |
| Этап 3: Нагрузочное тестирование | 1 ч |
| Этап 4: Оптимизация и документирование | 30 мин |
| Этап 5 (опционально): Мониторинг | 30 мин |
| Итого | ~4–5 часов |
Примечание Для первого раза закладывайте +1 час на отладку интеграции с RAG и Redis.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 127 | Как организовать кэширование в RAG-системе? |
| 201 | Redis vs Memcached для кэша LLM |
| 310 | Метрики latency в AI-сервисах |
| 412 | TTL и стратегии инвалидации кэша |
| 506 | Нагрузочное тестирование RAG-систем |
| 608 | Интеграция Redis с Python (redis-py) |
| 719 | Fallback-механизмы при отказе кэша |
| 804 | Оптимизация ключей кэша (нормализация, хэши) |
| 888 | Построение графика latency в Python |
10. Чек-лист самопроверки
- Я проверил, что Redis запущен и отвечает на
ping. - Я убедился, что кэш-ключи детерминированы (один и тот же вопрос → один ключ).
- Я протестировал TTL: через 1 час (или тестовый интервал) запрос снова прошёл RAG.
- Я замерил latency для 10+ запросов в cold и hot режиме и получил ускорение >= 80%.
- Я обработал случай, когда Redis падает, и система не ломается.
- Я написал README, в котором описаны шаги запуска и результаты.