Перейти к содержанию
Innovialabs

Семантическое кэширование в LLM-приложениях: быстрее и дешевле

Продвинутое4 мин

Коротко

Семантический кэш хранит ответы LLM рядом с эмбеддингом вопроса и отдаёт готовый ответ, если новый вопрос достаточно близок по смыслу, — это экономит время и токены, но безопасно только для ответов, не зависящих от пользователя и быстро меняющихся данных.

Что такое семантический кэш и когда он окупается

Обычный кэш работает по точному совпадению: одна и та же строка — один и тот же ответ. Но пользователи редко пишут одинаково. «Как сбросить пароль?» и «забыл пароль, что делать» — разные строки с одним намерением.

Семантический кэш сравнивает вопросы по смыслу. Каждый вопрос превращается в эмбеддинг (вектор), и если вектор нового вопроса достаточно близок к сохранённому, вы возвращаете готовый ответ без вызова модели.

Он окупается, когда:

  • многие пользователи задают похожие вопросы (бот поддержки, FAQ-ассистент, внутренняя база знаний);
  • ответы одинаковы для всех и меняются редко;
  • вызов модели медленный или дорогой по сравнению с вызовом embedding-модели и поиском по векторам.

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

Как это работает по шагам

  1. Нормализуйте вопрос: уберите лишние пробелы, приведите регистр, отбросьте приветствия, если они не меняют смысл.
  2. Получите эмбеддинг той же моделью, что и для сохранённых записей.
  3. Найдите ближайшего соседа в векторном хранилище с фильтром по области (см. ключ кэша ниже).
  4. Если сходство выше порога — верните ответ из кэша.
  5. Иначе вызовите 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-моделей.
  • Ставить порог «на глаз» и не измерять ложные попадания.
  • Не иметь способа очистить записи после обновления контента.
  • Не логировать попадания в кэш — тогда невозможно проверить, что реально получили пользователи.

Частые вопросы

Prompt caching у провайдера переиспользует обработку одинакового префикса промпта, но ответ всё равно генерируется заново. Семантический кэш полностью пропускает вызов модели и отдаёт сохранённый ответ на близкий по смыслу вопрос. Это разные задачи, и их можно сочетать.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.