Что такое Semantic Caching и как вы его реализуете?

Краткий тезис

Caching|Semantic Caching — это метод кэширования, при котором ответ сохраняется не для точного совпадения запроса, а для всех семантически похожих запросов. Вместо сравнения строк (exact match) используется сравнение эмбеддингов запроса через поиск|векторный поиск. Если новый запрос попадает в окрестность уже закэшированного (порог схожести превышен), возвращается сохранённый ответ. Это позволяет резко сократить количество вызовов LLM (до 30–50%) и уменьшить задержку, особенно для частых однотипных вопросов (например, «как сменить пароль?», «сброс данных»).


1. Термин и проблема

Традиционное кэширование (key-value, например, Redis) работает по точному совпадению ключа. В LLM-сервисах пользователи задают один и тот же вопрос разными словами: «поменять пароль», «сбросить пароль», «как восстановить доступ» — для системы это разные строки. Caching|Semantic Caching решает эту проблему: кэшируется смысл запроса, а не буквальный текст.

Почему это важно в контексте LLM?

  • Вызов LLM — дорогой (время, токены, деньги).
  • Повторяющиеся вопросы с разной формулировкой — частая ситуация (техподдержка, FAQ, онбординг).
  • Снижает latency (время ответа) и разгружает бэкенд.

2. Архитектура Semantic Caching

Основные компоненты:

  1. Embedding‑модель (например, text-embedding-ada-002, Sentence-BERT, all-MiniLM-L6-v2) – превращает запрос в вектор фиксированной размерности.
  2. Векторное хранилище (in‑memory или внешняя БД: FAISS, Chroma, Redis Stack, Qdrant) – хранит эмбеддинги закэшированных запросов вместе с ответами.
  3. Порог схожести (threshold) – настраиваемое значение (обычно 0.85–0.95), при превышении которого запрос считается найденным в кэше.
  4. Политика кэширования – когда сохранять ответы (по умолчанию все), как инвалидировать (TTL, версионирование).

3. Как работает: пошаговый алгоритм

  1. Запрос пользователя поступает в систему.
  2. Вычисление эмбеддинга запроса той же моделью, что использовалась для индексации.
  3. Поиск в векторном хранилище – выполняется kNN (k ближайших соседей) или ANN (приближённый) для нахождения top‑1 наиболее похожего вектора из кэша.
  4. Сравнение с порогом:
    • Если косинусная (или L2) схожесть ≥ threshold → возвращаем сохранённый ответ.
    • Иначе → передаём запрос LLM, получаем ответ, добавляем новый вектор+ответ в кэш.

Блок‑схема (упрощённо):

Запрос → embed → search in vector cache
                     ↓
               similarity >= threshold?
                   /         \
                 да           нет
                 ↓             ↓
        return cached      call LLM
        response           add to cache
                           return response

4. Инструменты для реализации

ИнструментПлюсыМинусыПримечание
GPTCacheГотовая библиотека, интеграция с LangChain/LLM, авто‑embedding.Зависимость от внешних библиотек, может быть избыточен.Рекомендован для быстрого прототипа.
Redis Stack (RediSearch)Поддержка векторного поиска, TTL, кластеризация.Требует развёртывания Redis.Надёжный вариант для продакшна.
FAISS (in‑memory)Высокая скорость, GPU‑опция, малый overhead.Нет встроенного TTL, данные теряются при перезапуске.Подходит для экспериментов и low‑latency.
ChromaПростота, встроенная эмбеддинг‑модель, настройка метаданных.Меньше гибкости, чем Redis.Хорош для прототипов на Python.

5. Выбор порога схожести (threshold)

Порог — ключевой гиперпараметр. Слишком высокий → почти нет попаданий, кэш бесполезен. Слишком низкий → много false positives (возвращается неверный ответ).

ПорогПоведениеРиск
>0.95Только очень похожие (почти точные) запросыМало попаданий
0.85 – 0.95Оптимальный компромисс (частые формулировки)Умеренный
<0.80Много попаданий, но возможна нерелевантная выдачаВысокий false positive

Как настраивать: собрать датасет пар запросов, которые должны считаться одинаковыми (например, «смени пароль» – «изменить пароль»), и подобрать порог так, чтобы 95% таких пар превышали его, а случайные разные запросы — нет.


6. Эффект (метрики)

  • Cache hit ratio (доля запросов, обслуженных из кэша): 30–50% для FAQ‑систем.
  • Latency снижение: с 2-5 секунд (вызов LLM) до 10-50 мс (поиск в векторе).
  • Экономия токенов: каждый попадание сохраняет сотни/тысячи токенов.
  • TCO (total cost of ownership): меньше инференсов → меньше затраты на API LLM.

7. Минусы и подводные камни

  1. False positives: неверное попадание → пользователь получает неправильный ответ. Критично для финансовых/медицинских систем.
  2. Stale‑cache: ответ устарел, но кэш возвращает старую версию. Решается TTL (time‑to‑live) или версионированием контента.
  3. Зависимость от эмбеддинг‑модели: если модель изменилась, все старые эмбеддинги несовместимы. Нужно переиндексировать.
  4. Память: векторное хранилище растёт с каждым новым запросом. Можно лимитировать количество записей или использовать LRU‑стратегию.

8. Пример реализации на Python (FAISS + SentenceTransformers)

from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

class SemanticCache:
    def __init__(self, embedding_model='all-MiniLM-L6-v2', threshold=0.9):
        self.model = SentenceTransformer(embedding_model)
        self.threshold = threshold
        self.index = faiss.IndexFlatL2(self.model.get_sentence_embedding_dimension())
        self.cache = []  # хранит (вектор, ответ)

    def embed(self, text):
        return self.model.encode([text], normalize_embeddings=True)

    def retrieve(self, query):
        query_vec = self.embed(query)
        if len(self.cache) == 0:
            return None
        distances, indices = self.index.search(query_vec, 1)
        # косинусная схожесть = 1 - L2 (после нормировки)
        similarity = 1 - distances[0][0] / 2  # нормированные векторы
        if similarity >= self.threshold:
            return self.cache[indices[0][0]][1]
        return None

    def store(self, query, answer):
        vec = self.embed(query)
        self.index.add(vec)
        self.cache.append((vec, answer))

Использование:

cache = SemanticCache(threshold=0.92)
answer = cache.retrieve("как поменять пароль?")
if not answer:
    answer = call_llm(query)
    cache.store("как поменять пароль?", answer)

9. Стратегии инвалидации и обновления

  • TTL: каждый кэш‑запись имеет время жизни (например, 1 час). После истечения она удаляется.
  • Версионирование: хранить метку версии документа/ответа. При обновлении базы знаний – очищать кэш с устаревшей версией.
  • Активная инвалидация: если известно, что изменился контент для конкретной темы, удалять все записи с данным семантическим тегом (можно хранить метаданные).
  • LRU (least recently used): при переполнении памяти вытеснять самые старые по доступу записи.

10. Когда стоит использовать Semantic Caching

СитуацияРекомендация
FAQ-система, техподдержкаДа – много одинаковых вопросов разными словами
Чат‑боты с узким доменомДа – высокая частота повторений
Генерация кода/творчествоНет – каждый запрос уникален, мало пользы
Приложения с требованием актуальности (новости)Осторожно – возможен stale‑cache

Пет-проект для закрепления

Задача: создать простой веб‑сервис на Flask, который отвечает на вопросы о документации. Обучить кэш на повторяющихся вопросах и измерить снижение latency.

Инструменты: Python, Flask, SentenceTransformers, FAISS. Для симуляции LLM можно использовать задержку 2 сек (time.sleep(2)).

Шаги:

  1. Написать Flask‑ручку /ask (POST), принимающую текст вопроса.
  2. Загрузить предобученную модель эмбеддингов (например, all-MiniLM-L6-v2).
  3. Реализовать класс SemanticCache как выше.
  4. При первом запросе – ждать 2 сек (имитация LLM), сохранить ответ в кэш.
  5. При втором похожем запросе – ответ из кэша (мгновенно).
  6. Сделать скрипт, который отправляет 10 запросов с разными формулировками одного вопроса и записывает время ответа.

Ожидаемый результат: первые запросы ~2.1 сек, последующие (похожие) – ~0.05 сек. Cache hit ratio около 50% для однотипных вопросов.


Связь с другими вопросами

ВопросТема
Вопрос 7Как уменьшить latency RAG-системы?
Вопрос 29Что такое векторный поиск и ANN?
Вопрос 30Как выбирать embedding-модель?
Вопрос 45Какие стратегии кэширования вы знаете?
Вопрос 60Как оценивать производительность RAG?
Вопрос 78Как обеспечить актуальность контекста в RAG?

Навигация