Настроить prompt caching
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Настроить prompt caching
1. Цель задачи
Реализовать механизм кэширования скомпилированных промптов (с подстановкой переменных) для ускорения повторных запросов к LLM. Кэш должен автоматически инвалидироваться при изменении шаблона или подставляемых значений. На практике это даст снижение времени ответа для повторяющихся запросов до 10x за счёт пропуска этапов токенизации, форматирования и (опционально) повторного вычисления эмбеддингов промпта.
Ключевой результат Скрипт/библиотека, измерение latency до/после, ускорение ≥10x на наборе тестовых запросов.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| LLM API (OpenAI / Anthropic / локальная модель) | Сервисный аккаунт или open‑source модель (например, Llama) |
| Набор шаблонов промптов с переменными | Собственные шаблоны (например, QA-система, суммаризация) |
| Реальные или синтетические данные для подстановок | CSV-файл с 50-100 различными комбинациями значений |
| Инструмент кэширования (in-memory / Redis) | Python dict / Redis (docker-compose) |
| Бенчмарк-скрипт | Написать самостоятельно с замером времени |
Если нет реального API — симулируем:
- Используем transformers + небольшую модель (например,
t5-small) для токенизации и прямого прохода forward. - Вместо сетевого вызова делаем вызов модели локально — время замеряется.
- Шаблоны: "Ответь на вопрос: {question}. Контекст: {context}".
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Язык программирования | Python 3.10+ | Реализация логики |
| Кэш (in-memory) | functools.lru_cache, cachetools.LRUCache | Локальное быстродействие |
| Кэш (внешний) | Redis + redis-py | Масштабируемое распределённое кэширование |
| Токенизация | tiktoken (OpenAI) / transformers (HF) | Преобразование промпта в токены |
| Шаблонизация | jinja2 или Python f-strings | Формирование промпта с подстановками |
| Бенчмаркинг | time / asyncio + логирование | Измерение latency |
| Мониторинг кэша | Prometheus + Grafana (опционально) | Счётчики hit/miss, размер кэша |
4. Этапы выполнения
Этап 1: Создание базового pipeline без кэша (1 час)
Действия
-
Написать класс
PromptProcessor:- метод compile(template: str, **kwargs) -> str: подстановка переменных (через f‑strings или Jinja2).
- метод tokenize(prompt: str) -> list[int]: преобразование строки в список токенов (использовать tiktoken.encoding_for_model("gpt-4")).
- метод
send_to_llm(prompt: str) -> str: эмуляция / реальный call|вызов API (заглушка сtime.sleep(0.1)для симуляции).
-
Написать тестовые шаблоны и 50 вариантов подстановок (например, вопросы + контексты).
-
Замерить время обработки всего набора без кэша — получить baseline.
import time
import tiktoken
class PromptProcessor:
def __init__(self, model="gpt-4"):
self.encoding = tiktoken.encoding_for_model(model)
def compile(self, template, **kwargs):
return template.format(**kwargs)
def tokenize(self, prompt):
return self.encoding.encode(prompt)
def send(self, prompt):
# эмуляция LLM вызова
time.sleep(0.1)
return "OK"
Ожидаемый результат этапа Рабочий pipeline, измеренное baseline latency (например, 10.5 ms на запрос или 500 ms с учётом эмуляции).
Этап 2: Внедрение in-memory кэша (1 час)
Действия
- Определить ключ кэша: хэш от (шаблон + сортированные kwargs). Использовать hashlib.sha256.
- Использовать cachetools.LRUCache с
maxsize=1000. - Модифицировать
compile— проверять кэш до выполнения подстановки и токенизации. - Реализовать invalidate(template, **kwargs) для принудительной очистки.
from cachetools import LRUCache
import hashlib
class CachedPromptProcessor(PromptProcessor):
def __init__(self, maxsize=1000):
super().__init__()
self.cache = LRUCache(maxsize=maxsize)
def _make_key(self, template, kwargs):
data = template + str(sorted(kwargs.items()))
return hashlib.sha256(data.encode()).hexdigest()
def compile_and_tokenize(self, template, **kwargs):
key = self._make_key(template, kwargs)
if key in self.cache:
return self.cache[key]
prompt = self.compile(template, **kwargs)
tokens = self.tokenize(prompt)
self.cache[key] = tokens
return tokens
- Запустить тот же набор запросов (50), замерить время. Обратить внимание: если все запросы уникальны, ускорения не будет. Для демонстрации нужно создать повторяющиеся комбинации (например, 10 уникальных шаблонов × 5 повторов = 50 запросов).
Ожидаемый результат этапа При повторных запросах с теми же аргументами latency падает до ~0 (практически только overhead кэша). Общее время выполнения набора с 10 уникальными шаблонами × 5 повторов снижается в 5-10 раз.
Этап 3: Интеграция с Redis (1.5 часа)
Действия
- Развернуть Redis (через Docker: docker run -d -p 6379:6379 redis/redis-stack).
- Написать класс
RedisPromptCacheс методами get(key), set(key, value, ttl=3600). - Сериализовать список токенов в JSON (или использовать pickle, но осторожно).
- Подключить к
CachedPromptProcessor— приcompile_and_tokenizeсначала проверять Redis, потом локальный LRU. - Написать тест на пропускную способность в многопоточном режиме (concurrent.futures) — измерить throughput.
import redis
import json
class RedisPromptCache:
def __init__(self, host='localhost', port=6379, db=0, ttl=3600):
self.client = redis.Redis(host=host, port=port, db=db)
self.ttl = ttl
def get(self, key):
data = self.client.get(key)
return json.loads(data) if data else None
def set(self, key, value):
self.client.setex(key, self.ttl, json.dumps(value))
# Использование в CachedPromptProcessor
self.redis_cache = RedisPromptCache()
Ожидаемый результат этапа Рабочий кэш через Redis, поддержка TTL, возможность делить кэш между несколькими процессами.
Этап 4: Измерение производительности и подготовка отчёта (1 час)
Действия
- Подготовить бенчмарк-скрипт benchmark.py:
- Убедиться, что ускорение ≥10x на тесте с повторными запросами.
- Написать одностраничный отчёт в Markdown с графиками (например, bar chart latency до/после).
Ожидаемый результат этапа Файл benchmark_results.md с таблицами и выводом.
Этап 5: Обработка инвалидации и edge-кейсов (1 час)
Действия
- Реализовать механизм принудительной инвалидации: если меняется сам шаблон (новая версия), все ключи с этим шаблоном должны быть сброшены.
- Хранить в кэше дополнительно версию шаблона.
- Обработать случай, когда подстановки содержат большие объёмы (контекст до 32k токенов) — кэшировать не весь промпт, а только результат токенизации (список ID).
- Добавить логирование hit/miss для мониторинга.
def compile_and_tokenize(self, template, template_version, **kwargs):
key_data = template_version + template + str(sorted(kwargs.items()))
key = hashlib.sha256(key_data.encode()).hexdigest()
...
Ожидаемый результат этапа Стабильная система кэширования, устойчивая к изменениям шаблонов и нагрузке.
5. Критерии приемки (Definition of Done)
- Реализован класс
CachedPromptProcessorс LRU-кэшем. - Реализована интеграция с Redis.
- Бенчмарк показывает ускорение ≥10x на данных с повторными запросами (80% повторений).
- Время ответа с кэшем (in-memory) < 1 ms.
- Кэширование не ломается при передаче аргументов разных типов (int, str, None).
- Есть механизм инвалидации по версии шаблона.
- В коде есть логирование hit/miss с уровнем DEBUG.
- Написаны юнит-тесты для ключевых функций (создание ключа, кэширование, инвалидация).
- Документация README.md с инструкцией по запуску и примером.
- Отчёт
benchmark_results.mdс таблицей latency до/после.
6. Ожидаемый результат
Основной артефакт Репозиторий (папка) со следующими файлами:
prompt_cache.py— реализация кэша (in-memory + Redis).- benchmark.py — скрипт для замера производительности.
benchmark_results.md— отчёт с результатами.- requirements.txt — зависимости (
tiktoken,redis,cachetools,jinja2). test_prompt_cache.py— юнит-тесты (pytest).
Содержание отчёта (benchmark_results.md):
| Режим | Всего запросов | Уникальных | Время (с) | Среднее latency (ms) | P95 (ms) | Hit rate |
|---|---|---|---|---|---|---|
| No cache | 1000 | 200 | 52.3 | 52.3 | 55.1 | 0% |
| In-memory | 1000 | 200 | 5.1 | 5.1 | 6.2 | 80% |
| Redis | 1000 | 200 | 5.8 | 5.8 | 7.0 | 80% |
Дополнительный результат Навык проектирования кэша для LLM‑пайплайнов, понимание метрик hit rate и latency.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Ускорение не достигает 10x из‑за излишнего оверхэда кэша (вычисление хэша, JSON сериализация) | Уменьшить частоту хэширования: кэшировать сразу результат compile+tokenize, а не отдельные операции. Использовать бинарные ключи. |
| Размер кэша слишком большой (миллионы уникальных промптов) | Ввести TTL (время жизни) через Redis expiry. Использовать LRU политику замещения. |
| Параллельный доступ к in-memory кэшу из нескольких потоков | Заменить LRUCache на @lru_cache (thread‑safe) или использовать cachetools.thread_safe версии. |
Изменение модели токенизации (например, обновление tiktoken) приводит к невалидному кэшу | Хранить версию токенизации (hash от кодировки) внутри ключа. Инвалидировать весь кэш при обновлении библиотеки. |
| Подстановки содержат сенситивные данные (персональные данные) | Не кэшировать такие запросы. Добавить флаг cacheable=False в аргументы. |
8. Бюджет времени (оценка)
| Этап | Время (часы) |
|---|---|
| 1. Базовый pipeline без кэша | 1 |
| 2. In-memory кэш | 1 |
| 3. Интеграция с Redis | 1.5 |
| 4. Бенчмаркинг и отчёт | 1 |
| 5. Инвалидация и edge-кейсы | 1 |
| Итого | 5.5 |
Примечание Для первого раза рекомендуется выделить 6-7 часов с учётом настройки окружения и отладки.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 17 | Оптимизация latency при вызове LLM API |
| 38 | Методы кэширования для LLM (KV‑cache) |
| 74 | Проектирование шаблонов промптов с переменными |
| 112 | Использование Redis для кэширования в AI |
| 145 | Токенизация и её влияние на стоимость |
| 208 | Мониторинг hit rate кэша через Prometheus |
| 266 | Обработка ошибок при недоступности Redis |
| 319 | Бенчмаркинг производительности Python‑скриптов |
| 401 | Стратегии инвалидации кэша |
| 488 | Версионирование шаблонов промптов |
10. Чек-лист самопроверки
- Я реализовал in-memory кэш и проверил, что повторные запросы обрабатываются мгновенно.
- Я настроил Redis локально и успешно протестировал set/get.
- Я выполнил бенчмарк на наборе с 80% повторений и получил ускорение ≥10x.
- Я написал хотя бы 3 юнит-теста (создание ключа, hit/miss, инвалидация).
- Я задокументировал в README, как запустить бенчмарк и интерпретировать результаты.