Как вы делаете cache invalidation для semantic cache при обновлении знаний?

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

Инвалидация semantic cache (семантического кэша) — одна из самых сложных задач в RAG-системах, особенно при частом обновлении базы знаний. Идеального решения нет: простой TTL (Time‑to‑Live) может выдавать устаревшие ответы, а точное версионирование документов требует отслеживания зависимостей между кэшированными ответами и исходными чанками. На практике часто используют гибридные стратегии: TTL для быстро меняющихся данных и версионирование для стабильных документов, дополняя их event‑driven invalidation (инвалидацией по событиям) при обновлении.


1. Термин: Semantic Cache (семантический кэш)

cache|Semantic cache — это кэш, который хранит не точные совпадения запросов, а их семантические эмбеддинги. При новом запросе система ищет в кэше наиболее похожий (по косинусной близости) запрос и, если сходство выше порога, возвращает сохранённый ответ без обращения к LLM и retrieval.

Зачем нужен

  • Снижение latency (задержки) — ответ из кэша приходит за миллисекунды вместо секунд.
  • Экономия токенов и стоимости LLM.
  • Разгрузка векторной БД и пайплайна retrieval.

Где хранится обычно в in‑memory store (Redis, Memcached) или в быстрой векторной БД (FAISS, Milvus).


2. Проблема: stale cache при обновлении знаний

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

Пример:
В базе знаний был документ «Столица Франции — Париж». Кэш сохранил ответ на вопрос «Какая столица Франции?». Затем документ исправляют на «Столица Франции — Лион» (гипотетически). Кэш продолжает выдавать «Париж» до инвалидации.

Ключевой вызов semantic cache хранит ответы, а не исходные документы. Нужно понять, какие кэшированные ответы зависят от обновлённого документа, и удалить или пересчитать их.


3. Подход 1: TTL (Time‑to‑Live)

TTL — время жизни кэш-записи. По истечении TTL запись автоматически удаляется или помечается как устаревшая.

Как работает

  • При сохранении ответа в кэш устанавливается TTL (например, 1 час, 1 день).
  • По истечении срока запись исчезает, следующий запрос пойдёт в полный пайплайн.

Плюсы

  • Простота реализации.
  • Не требует отслеживания зависимостей.
  • Подходит для данных, которые меняются предсказуемо (например, новости обновляются раз в час).

Минусы

  • Не гарантирует актуальность: если документ изменился через 5 минут после кэширования, ответ будет неверным ещё 55 минут.
  • Избыточная инвалидация: если документ не менялся, кэш всё равно сбрасывается.

Когда использовать

  • Данные обновляются редко и по расписанию (например, ежедневные отчёты).
  • Допустима небольшая задержка в актуализации.

4. Подход 2: Versioned Cache (версионирование документов)

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

Как работает

  1. При генерации ответа система фиксирует версии всех чанков, попавших в контекст.
  2. В кэш сохраняется кортеж (эмбеддинг запроса, ответ, список версий).
  3. При попадании в кэш проверяется, совпадают ли текущие версии документов с сохранёнными.
  4. Если хотя бы одна версия изменилась → кэш считается невалидным, ответ перегенерируется.

Плюсы

  • Высокая точность: кэш инвалидируется только при реальном изменении зависимых документов.
  • Минимум ложных срабатываний.

Минусы

  • Сложность: нужно хранить версии для каждого документа, обновлять их при изменениях.
  • Дополнительное хранилище для маппинга (документ → версия).
  • При каждом запросе из кэша нужно проверять версии (несколько чтений).

Реализация версий

  • Использовать hash содержимого документа (SHA256) — автоматически меняется при любом изменении.
  • Или sequence number (инкремент) — проще, но требует ручного управления.

5. Подход 3: Invalidation by Dependency (отслеживание зависимостей)

Это развитие versioned cache: система хранит граф зависимостей между кэшированными ответами и документами (чанками). При обновлении документа инвалидируются все ответы, которые ссылаются на этот документ.

Как работает

  • В отдельной таблице (или графовой БД) хранятся связи: (id_ответа, id_документа).
  • При обновлении документа система находит все ответы, зависящие от него, и удаляет их из кэша.
  • Можно делать lazy invalidation (помечать ответ как «возможно устаревший» и проверять при следующем запросе) или eager (немедленно удалять).

Плюсы

  • Минимальное время неактуальности: ответы инвалидируются сразу после обновления документа.
  • Не нужно хранить версии в самом кэше.

Минусы

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

Когда использовать

  • Системы с высокой частотой обновлений (например, вики-подобные базы знаний).
  • Когда критична актуальность ответов (финансовые, медицинские данные).

6. Подход 4: Event‑driven invalidation (инвалидация по событиям)

Система подписывается на события обновления документов (например, через message queueKafka, RabbitMQ). При получении события о изменении документа запускается процедура инвалидации.

Как работает

  1. При обновлении документа публикуется событие document_updated(document_id, new_version).
  2. Consumer (слушатель) получает событие и инвалидирует все кэшированные ответы, связанные с этим документом.
  3. Инвалидация может быть асинхронной — кэш очищается в фоне.

Плюсы

  • Слабая связанность: обновление документа и инвалидация кэша разделены.
  • Масштабируемость: можно добавлять новые обработчики событий.

Минусы

  • Задержка между событием и фактической инвалидацией (eventual consistency).
  • Необходимость в инфраструктуре очередей.

Когда использовать

  • Микросервисная архитектура, где обновление документа и кэш — разные сервисы.
  • Высоконагруженные системы, где синхронная инвалидация заблокирует запись.

7. Сравнительная таблица подходов

ПодходТочностьСложностьЗадержка инвалидацииНагрузка на хранилищеПример использования
TTLНизкаяОчень низкаяДо TTLНизкаяНовостная лента, прогноз погоды
Versioned cacheВысокаяСредняяМгновенно (при проверке)Средняя (версии)Корпоративная база знаний, FAQ
Dependency graphОчень высокаяВысокаяМгновенно (eager) или ленивоВысокая (граф)Медицинские справочники, юридические документы
Event‑drivenСредняяСредняяНесколько секундНизкая (очередь)Микросервисные RAG-системы

8. Практические рекомендации (гибридные стратегии)

В реальных проектах редко используют один подход. Часто комбинируют:

  • TTL + versioned cache для «горячих» данных (часто запрашиваемых) — TTL короткий (5–10 минут), для остальных — версионирование.
  • Lazy invalidation при попадании в кэш проверять не только TTL, но и версию документа (если версия устарела — перегенерировать). Это снижает нагрузку на фоновую инвалидацию.
  • Probabilistic invalidation использовать Bloom filter для быстрой проверки, изменился ли документ, без хранения полного списка версий.

Пример гибрида в коде (псевдокод):

class SemanticCache:
    def __init__(self, ttl_seconds=3600):
        self.cache = {}  # key: embedding_hash, value: (response, doc_versions, timestamp)
        self.doc_versions = {}  # doc_id -> version

    def get(self, query_embedding):
        key = self._hash(query_embedding)
        if key not in self.cache:
            return None
        response, versions, ts = self.cache[key]
        # 1. Check TTL
        if time.time() - ts > self.ttl_seconds:
            del self.cache[key]
            return None
        # 2. Check versions
        for doc_id, cached_version in versions.items():
            current_version = self.doc_versions.get(doc_id)
            if current_version != cached_version:
                del self.cache[key]
                return None
        return response

    def set(self, query_embedding, response, doc_ids):
        versions = {doc_id: self.doc_versions[doc_id] for doc_id in doc_ids}
        key = self._hash(query_embedding)
        self.cache[key] = (response, versions, time.time())

9. Связь с Agentic RAG

В Agentic RAG агенты могут кэшировать не только ответы LLM, но и результаты выполнения инструментов (например, вызов API, результаты поиска). Инвалидация становится ещё сложнее:

  • Агент может выполнить цепочку действий, и кэшированный результат одного шага может зависеть от нескольких документов.
  • Нужно хранить трассировку зависимостей (trace) для каждого кэшированного вывода.
  • Рекомендуется использовать versioned cache с графом зависимостей, где узлы — не только документы, но и результаты вызовов инструментов.

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

Задача Реализовать semantic cache с versioned invalidation для простой RAG-системы на основе Wikipedia.

Инструменты

  • Python, Flask/FastAPI для API.
  • Sentence‑Transformers для эмбеддингов.
  • Redis (in‑memory store) для кэша.
  • SQLite для хранения версий документов.

Шаги:

  1. Загрузите несколько статей Wikipedia (например, 10) в локальную векторную БД (FAISS).
  2. Реализуйте простой RAG: retrieval + LLM (можно использовать OpenAI API или локальную модель).
  3. Добавьте semantic cache: при запросе ищите похожий запрос в Redis (по косинусной близости эмбеддингов).
  4. Внедрите версионирование: при добавлении/обновлении статьи увеличивайте её версию в SQLite.
  5. При попадании в кэш проверяйте версии документов, на которые ссылается ответ.
  6. Напишите скрипт, который обновляет одну из статей и проверяет, что кэш для связанных запросов стал невалидным.

Ожидаемый результат
Работающий сервис, который возвращает актуальные ответы после обновления базы знаний, при этом для неизменённых документов продолжает использовать кэш.


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

ВопросТема
244Проектирование semantic cache
246Версионирование документов
247Согласованность данных
248Оценка актуальности
249Тестирование инвалидации
250Интеграция с внешними источниками

12. Навигация


Навигация