Как вы делаете cache invalidation для semantic cache при обновлении знаний?
Краткий тезис
cache|Semantic cache (Caching|семантический кэш) ускоряет ответы RAG-системы, сохраняя результаты для семантически похожих запросов. При обновлении знаний (документов, чанков) кэшированные ответы могут стать невалидными. Основные стратегии инвалидации: cache|versioned cache с отслеживанием зависимостей (каждый ответ хранит версию документа), TTL-based eventual consistency (кэш живёт фиксированное время) и полная инвалидация при глобальном version bump. Выбор стратегии — компромисс между консистентностью, сложностью и стоимостью.
1. Термин: Semantic Cache (семантический кэш)
Semantic cache — это кэш, который хранит ответы LLM на запросы, сгруппированные по семантической близости. В отличие от exact-match cache (кэш точных совпадений), semantic cache использует эмбеддинги запросов: если новый запрос семантически похож на один из ранее закэшированных (косинусная близость выше порога), возвращается сохранённый ответ. Это снижает latency (задержку) и нагрузку на LLM.
Зачем нужен:
- Ускорение ответов для повторяющихся или похожих вопросов.
- Экономия токенов и стоимости API.
- Улучшение пользовательского опыта.
Проблема: при обновлении базы знаний (добавлении, изменении, удалении документов) старые кэшированные ответы могут ссылаться на устаревшую информацию. Если не инвалидировать кэш, пользователь получит неактуальный ответ.
2. Почему cache invalidation сложна в semantic cache
В отличие от exact-match cache, где ключ — точный текст запроса, в semantic cache ключ — эмбеддинг запроса, а ответ зависит от множества документов, найденных retrieval'ом. При обновлении одного документа нужно понять, какие кэшированные ответы его использовали. Это требует отслеживания зависимостей между запросами и документами.
Факторы сложности:
- Семантическое перекрытие: два разных запроса могут использовать один и тот же документ.
- Динамический retrieval: при каждом запросе retrieval может вернуть разный набор документов (из-за изменения эмбеддингов или индекса).
- Агентные сценарии: в Agentic RAG агент может вызывать инструменты, и кэш может хранить результаты вызовов, которые тоже нужно инвалидировать.
3. Стратегия 1: Versioned Cache с отслеживанием зависимостей
Каждый кэшированный ответ хранит метаданные:
- knowledge_version — версия документа или набора документов, на основе которых сгенерирован ответ.
- doc_ids — список идентификаторов документов, которые были использованы при генерации.
При обновлении документа:
- Увеличиваем его версию (timestamp или монотонный счётчик).
- Находим все кэши, в doc_ids которых есть этот документ.
- Удаляем или помечаем их как невалидные.
Реализация:
- Храним mapping document_id -> set of query_hashes (или ключей кэша).
- При обновлении документа проходим по этому mapping и удаляем соответствующие записи из кэша.
Плюсы: точная инвалидация, минимальная потеря кэша. Минусы: нужно хранить и поддерживать mapping; сложность возрастает с числом документов и запросов.
4. Стратегия 2: TTL + Eventual Consistency
Устанавливаем Time-To-Live (TTL) для каждой записи кэша (например, 1 час). По истечении TTL запись считается недействительной и удаляется при следующем обращении. При обновлении знаний ничего не делаем — кэш постепенно вытесняется.
Плюсы: простота реализации, не нужно отслеживать зависимости. Минусы: в течение TTL пользователь может получать устаревшие ответы (eventual consistency). Не подходит для сценариев, где критична актуальность (например, финансовая отчётность).
Вариант: динамический TTL в зависимости от типа документа (критичные документы — короткий TTL, редко меняющиеся — длинный).
5. Стратегия 3: Version Bump (полная инвалидация)
При любом обновлении знаний увеличиваем глобальную версию кэша (например, cache_version += 1). Все старые записи считаются недействительными. Можно либо сразу очистить весь кэш, либо проверять версию при каждом get.
Плюсы: максимальная консистентность, простота логики. Минусы: дорого — после каждого обновления кэш пуст, latency возрастает до перезаполнения. Подходит для систем с редкими обновлениями (раз в день) или малым объёмом кэша.
Гибрид: version bump только для затронутых документов (групповая инвалидация). Например, если обновляется документ из категории "новости", инвалидируются все кэши этой категории.
6. Сравнение стратегий
| Стратегия | Консистентность | Сложность реализации | Стоимость (потеря кэша) | Latency после обновления |
|---|---|---|---|---|
| Versioned + зависимости | Сильная (точная) | Высокая (нужен mapping) | Низкая (инвалидируется только связанное) | Низкая (большая часть кэша жива) |
| TTL | Слабая (eventual) | Низкая | Средняя (кэш умирает постепенно) | Средняя (часть запросов без кэша) |
| Version Bump | Сильная (мгновенная) | Низкая | Высокая (весь кэш сбрасывается) | Высокая (все запросы без кэша) |
| Гибрид (TTL + версионирование) | Средняя | Средняя | Средняя | Средняя |
7. Как определить, какие документы изменились
Для инвалидации нужно знать факт изменения документа. Способы:
- Хэширование: хранить хэш содержимого документа; при изменении хэш меняется.
- File system watchers: inotify, Watchdog — отслеживание изменений файлов.
- Event-driven: при обновлении через API публикуется событие
document_updated. - Polling: периодически проверять дату модификации.
В Agentic RAG документы могут обновляться асинхронно (например, через веб-скрапинг). Рекомендуется использовать message queue (Kafka, RabbitMQ) для уведомлений об изменениях.
8. Пример реализации на Python (упрощённый)
import hashlib
import time
from collections import defaultdict
class SemanticCache:
def __init__(self):
self.cache = {} # query_hash -> (response, doc_ids, version)
self.doc_to_queries = defaultdict(set) # doc_id -> set of query_hashes
self.doc_versions = {} # doc_id -> version
def _hash_query(self, query_embedding):
# упрощённо: используем first 8 bytes as hex
return hashlib.md5(str(query_embedding).encode()).hexdigest()
def get(self, query_embedding):
qhash = self._hash_query(query_embedding)
if qhash not in self.cache:
return None
response, doc_ids, version = self.cache[qhash]
# проверяем, что все документы актуальны
for doc_id in doc_ids:
if self.doc_versions.get(doc_id, 0) != version:
self.invalidate(qhash)
return None
return response
def set(self, query_embedding, response, doc_ids):
qhash = self._hash_query(query_embedding)
# версия = максимальная версия среди использованных документов
version = max(self.doc_versions.get(doc_id, 0) for doc_id in doc_ids)
self.cache[qhash] = (response, doc_ids, version)
for doc_id in doc_ids:
self.doc_to_queries[doc_id].add(qhash)
def invalidate(self, qhash):
if qhash in self.cache:
_, doc_ids, _ = self.cache.pop(qhash)
for doc_id in doc_ids:
self.doc_to_queries[doc_id].discard(qhash)
def on_document_updated(self, doc_id):
self.doc_versions[doc_id] = self.doc_versions.get(doc_id, 0) + 1
# инвалидируем все кэши, использующие этот документ
for qhash in list(self.doc_to_queries.get(doc_id, [])):
self.invalidate(qhash)
Примечание: в реальной системе query_hash должен быть на основе эмбеддинга, а не md5 от эмбеддинга (потеря точности). Лучше использовать FAISS или Annoy для поиска ближайших соседей.
9. Компромиссы и рекомендации
- Когда использовать versioned cache: если обновления частые, но затрагивают малую часть документов, и важна высокая консистентность.
- Когда использовать TTL: если обновления редкие или не критична актуальность (например, справочная информация).
- Когда использовать version bump: если обновления очень редкие (раз в день) или кэш небольшой.
- Гибридный подход: TTL для большинства запросов + versioned инвалидация для критичных документов (например, юридические тексты).
В Agentic RAG дополнительно нужно учитывать, что агент может кэшировать результаты вызовов инструментов. Инвалидация таких кэшей должна происходить при изменении состояния инструмента (например, обновлении API).
10. Связь с другими аспектами RAG
- Chunking: размер чанков влияет на количество документов, от которых зависит ответ. Мелкие чанки -> больше зависимостей -> сложнее инвалидация.
- Retrieval quality: если retrieval нестабилен (разные документы на один запрос), versioned cache может часто инвалидироваться.
- Self-RAG: в Self-RAG модель сама решает, нужен ли retrieval; кэш может хранить как ответы без retrieval, так и с retrieval — инвалидация только для вторых.
Пет-проект для закрепления
Задача: Реализовать semantic cache с versioned инвалидацией для RAG-системы на синтетических данных.
Инструменты: Python, FAISS (для семантического поиска), SQLite (для хранения mapping doc->queries), Flask (простой API).
Шаги:
- Создать 100 синтетических документов (например, факты о городах).
- Реализовать эмбеддинги через sentence-transformers.
- Написать класс SemanticCache с методами
get,set,invalidate_by_doc_id. - Смоделировать запросы (50 уникальных, 50 повторяющихся).
- Измерить latency до и после кэширования.
- Обновить 10 документов и проверить, что кэш для связанных запросов инвалидирован.
Ожидаемый результат:
- Снижение среднего времени ответа на 60-80% для повторяющихся запросов.
- После обновления документов кэш для затронутых запросов пуст, для остальных — жив.
- Понимание trade-off'ов между точностью инвалидации и накладными расходами на хранение mapping.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 9 | Как вы обновляете документы в существующей RAG-системе? |
| 7 | Как вы уменьшаете latency RAG-системы (время ответа)? |
| 10 | Что такое Self-RAG и когда его использовать? |
| 5 | Как вы оцениваете качество retrieval'а в RAG-системе? |
| 3 | Какие стратегии chunking'а вы знаете и когда какую применяете? |
| 1 | Как бы вы спроектировали RAG-систему для 10 000 документов с разной структурой? |
Навигация
- Предыдущий: 411
- Следующий: 413
- Индекс: 00. Индекс разборов