Семантическое кэширование в LLM-приложениях: быстрее и дешевле
Продвинутое4 минРедакция Innovialabs
Коротко
Семантический кэш хранит ответы LLM рядом с эмбеддингом вопроса и отдаёт готовый ответ, если новый вопрос достаточно близок по смыслу, — это экономит время и токены, но безопасно только для ответов, не зависящих от пользователя и быстро меняющихся данных.
Что такое семантический кэш и когда он окупается
Обычный кэш работает по точному совпадению: одна и та же строка — один и тот же ответ. Но пользователи редко пишут одинаково. «Как сбросить пароль?» и «забыл пароль, что делать» — разные строки с одним намерением.
Семантический кэш сравнивает вопросы по смыслу. Каждый вопрос превращается в эмбеддинг (вектор), и если вектор нового вопроса достаточно близок к сохранённому, вы возвращаете готовый ответ без вызова модели.
Он окупается, когда:
- многие пользователи задают похожие вопросы (бот поддержки, FAQ-ассистент, внутренняя база знаний);
- ответы одинаковы для всех и меняются редко;
- вызов модели медленный или дорогой по сравнению с вызовом embedding-модели и поиском по векторам.
Если каждый запрос уникален (генерация кода под конкретный файл, длинные личные диалоги), попаданий будет мало, а кэш только добавит задержку.
Как это работает по шагам
- Нормализуйте вопрос: уберите лишние пробелы, приведите регистр, отбросьте приветствия, если они не меняют смысл.
- Получите эмбеддинг той же моделью, что и для сохранённых записей.
- Найдите ближайшего соседа в векторном хранилище с фильтром по области (см. ключ кэша ниже).
- Если сходство выше порога — верните ответ из кэша.
- Иначе вызовите LLM, верните ответ и сохраните запись: вектор, вопрос, ответ, метаданные, время.
Минимальный поиск по косинусному сходству на нормализованных векторах:
import numpy as np
def lookup(query_vec, entries, threshold):
# query_vec и entry["vec"] нормализованы по L2
best, best_score = None, -1.0
for entry in entries:
score = float(np.dot(query_vec, entry["vec"]))
if score > best_score:
best, best_score = entry, score
return best["answer"] if best and best_score >= threshold else None
В продакшене цикл заменяет индекс приближённого поиска соседей в векторной базе, но логика та же.
Как выбрать порог сходства
Универсального числа нет: он зависит от embedding-модели, языка и предметной области. Калибруйте на своих данных.
- Соберите реальные пары вопросов из логов и разметьте: «подходит тот же ответ» или «нужен другой».
- Посчитайте сходство для каждой пары и посмотрите, где две группы пересекаются.
- Выберите порог, при котором ложных попаданий (выдан неверный ответ) почти нет, даже ценой меньшего числа попаданий. Промах стоит одного вызова модели, ложное попадание — доверия пользователя.
- Особое внимание — почти одинаковым вопросам с разными деталями: «возврат по заказу 1452» и «по заказу 1453», «тариф на 2 пользователей» и «на 20». Эмбеддинги часто ставят их очень близко.
Рабочий приём — два порога: выше верхнего отдаём кэш, между порогами просим дешёвую модель или reranker подтвердить совпадение, ниже нижнего вызываем основную модель.
Ключ кэша — это не только вектор
Похожего смысла мало. Запись корректна только в тех же условиях, в которых она создана. Храните и фильтруйте по:
- модели и версии системного промпта — новый промпт может изменить правильный ответ;
- языку ответа;
- тенанту, роли или уровню доступа — менеджер и клиент могут получать разные факты;
- версии исходных данных для RAG-ответов (версия индекса или документа).
Инвалидация и устаревание
Ответы в кэше стареют. Заложите это с первого дня:
- TTL по категориям: долгий для стабильной справочной информации, короткий для цен, расписаний, остатков.
- Инвалидация по событию: изменился документ в базе знаний — удалите записи, построенные на нём. Для этого храните с каждой записью ID документов-источников.
- Смена версии: новый промпт или модель открывают новое пространство имён вместо ручной чистки.
- Обратная связь: дизлайк или жалоба «ответ неверный» удаляет запись и отправляет её на проверку.
Когда кэшировать опасно
Не отдавайте ответы из кэша, если:
- в ответе есть персональные данные или он зависит от аккаунта, истории или прав пользователя;
- ответ зависит от состояния в реальном времени: баланс, статус заказа, наличие;
- запрос запускает действия через инструменты (бронирование, оплата, отправка сообщений) — кэшируйте знания, но не побочные эффекты;
- юридические, медицинские или финансовые формулировки нужно проверять в каждом случае.
Худший сценарий — утечка между пользователями: пользователь Б получает ответ, сгенерированный для пользователя А. Защита — разделять кэш по тенанту и пользователю или вовсе исключать персонализированные сценарии.
Частые ошибки
- Кэшировать целую реплику диалога вместо самостоятельного вопроса: уточнение «а сколько это стоит?» без контекста бессмысленно.
- Смешивать в одном индексе векторы разных embedding-моделей.
- Ставить порог «на глаз» и не измерять ложные попадания.
- Не иметь способа очистить записи после обновления контента.
- Не логировать попадания в кэш — тогда невозможно проверить, что реально получили пользователи.