Практика эксплуатации
Проверяемое удаление документа из всех слоёв RAG
Удалить исходный файл недостаточно: его фрагменты могут остаться в индексе, кэше, очередях повторной обработки и уже подготовленных ответах. Ниже — процедура, которая превращает удаление в трассируемую операцию с проверяемым результатом.
Почему удаление файла ничего не доказывает
RAG обычно хранит не один объект, а цепочку производных представлений: исходник, извлечённый текст, фрагменты, эмбеддинги, записи полнотекстового индекса, кэш поиска и кэш финальных ответов. Кроме того, сообщение в очереди может повторно создать удалённые фрагменты уже после успешной очистки индекса.
Поэтому корректное утверждение звучит не как «файл удалён», а как «все активные проекции документа с известным идентификатором удалены, повторная индексация заблокирована, а чтение через пользовательский путь не возвращает его данные».
Модель удаления: запрет сначала, очистка потом
Надёжная последовательность состоит из двух фаз:
- Логический запрет. Документ помечается как удаляемый, и слой чтения немедленно перестаёт считать его допустимым.
- Физическая очистка. В фоне удаляются производные данные, после чего независимая проверка фиксирует результат.
Такой порядок закрывает опасное окно между запросом на удаление и завершением фоновых задач. Даже если физическая очистка задержалась, фильтр авторизации выдачи уже не позволяет документу участвовать в поиске или ответе.
Минимальный контракт данных
Пример схемы метаданных; названия полей следует адаптировать к вашей системе:
{
"tenant_id": "tenant-example",
"document_id": "doc-example-001",
"document_version": 7,
"chunk_id": "doc-example-001:7:000042",
"ingestion_id": "ing-example-019",
"content_hash": "sha256:example-not-a-real-hash",
"lifecycle_state": "active"
}
document_id связывает все проекции, document_version защищает от восстановления старой версии, а ingestion_id помогает найти сообщения конкретного запуска загрузки. Хеш полезен как дополнительный индикатор, но не должен быть единственным ключом: одинаковый контент может законно принадлежать нескольким документам.
Воспроизводимая процедура
Шаг 1. Зафиксируйте область удаления
Создайте неизменяемую запись операции до первой мутации:
{
"deletion_id": "del-example-0042",
"tenant_id": "tenant-example",
"document_id": "doc-example-001",
"requested_at": "YYYY-MM-DDThh:mm:ssZ",
"requested_by": "service-or-actor-id",
"reason_code": "user_request",
"state": "requested",
"scope": [
"source",
"extracted_text",
"chunk_store",
"vector_index",
"lexical_index",
"retrieval_cache",
"answer_cache",
"queues"
]
}
Это именно пример, а не готовая запись для производственной среды. Не помещайте в журнал исходный текст, секреты, персональные данные или полный пользовательский запрос. Для аудита достаточно идентификаторов, времени, результата и счётчиков.
Шаг 2. Поставьте tombstone транзакционно
Сначала запретите чтение и повторную обработку. Пример безопасного параметризованного SQL:
BEGIN;
UPDATE documents
SET lifecycle_state = 'deleting',
deletion_id = :deletion_id,
deleted_at = CURRENT_TIMESTAMP,
version = version + 1
WHERE tenant_id = :tenant_id
AND document_id = :document_id
AND lifecycle_state = 'active';
INSERT INTO deletion_outbox (
deletion_id,
tenant_id,
document_id,
target_version,
event_type
)
VALUES (
:deletion_id,
:tenant_id,
:document_id,
:target_version,
'document.deletion.requested'
);
COMMIT;
Параметры здесь обозначены плейсхолдерами. Не подставляйте значения конкатенацией строк. Если обновлено ноль строк, обработчик должен различить повторный запрос, неизвестный документ и конфликт арендатора — молчаливый успех скроет ошибку области.
Каждый запрос к поиску после этого обязан применять серверный фильтр:
tenant_id = :tenant_id
AND lifecycle_state = 'active'
AND document_version = :current_version
Лучше добавлять его внутри общего слоя доступа, а не доверять каждому вызывающему сервису.
Шаг 3. Остановите повторное появление данных
Каждый обработчик загрузки перед записью очередной порции должен сверять состояние и версию документа с реестром. Проверки только в начале длительной задачи недостаточно.
if registry.state(document_id) != "active":
acknowledge_without_indexing(message)
record("ingestion_suppressed", document_id, ingestion_id)
return
if message.document_version != registry.current_version(document_id):
acknowledge_without_indexing(message)
record("stale_version_suppressed", document_id, ingestion_id)
return
Сообщения из основной очереди, очереди повторов и dead-letter-очереди проверяются одинаково. Если брокер не поддерживает адресное удаление сообщений, безопаснее оставить сообщение, но сделать потребителя идемпотентным и осведомлённым о tombstone.
Шаг 4. Удалите проекции по явным ключам
Оркестратор проходит слои отдельно и записывает для каждого попытку, число найденных объектов, число удалённых объектов и ошибку. Операции должны быть идемпотентными: повторный запуск после частичного сбоя не должен восстанавливать данные или завершаться ложной ошибкой.
| Слой | Ключ удаления | Что проверить |
|---|---|---|
| Исходное хранилище | tenant_id + document_id |
Объект и его активные версии недоступны обычному чтению |
| Извлечённый текст | document_id |
Нет строк, blob-объектов и временных файлов |
| Хранилище фрагментов | document_id |
Счётчик строк равен нулю |
| Векторный индекс | Фильтр по tenant_id и document_id |
Фильтрованный поиск не возвращает записей |
| Полнотекстовый индекс | document_id |
После refresh/commit нет совпадений |
| Кэш поиска | Обратный индекс зависимостей | Удалены ключи, зависящие от документа |
| Кэш ответов | document_id в provenance |
Ответы с этой зависимостью инвалидированы |
| Очереди | document_id, ingestion_id |
Повторная обработка подавляется tombstone |
Пример псевдокоманд административного интерфейса:
rag-admin deletion run \
--tenant-id "tenant-example" \
--document-id "doc-example-001" \
--deletion-id "del-example-0042" \
--require-state "deleting" \
--dry-run
rag-admin deletion run \
--tenant-id "tenant-example" \
--document-id "doc-example-001" \
--deletion-id "del-example-0042" \
--require-state "deleting"
rag-admin — условное имя интерфейса. Команды показывают желаемые свойства: обязательную область арендатора, предварительный просмотр, проверку состояния и отдельный идентификатор операции. Не запускайте непроверенный аналог с широким шаблоном или пустым идентификатором.
Шаг 5. Инвалидируйте кэш по происхождению
Кэш нельзя надёжно очистить поиском текста документа: нормализация, сжатие или перефразирование меняют содержимое. При создании кэшированной записи храните техническое происхождение:
{
"cache_key": "answer:example-key",
"depends_on": [
{
"tenant_id": "tenant-example",
"document_id": "doc-example-001",
"document_version": 7
}
],
"policy_epoch": 31
}
Удаление документа увеличивает policy_epoch либо адресно удаляет зависимые ключи. Epoch проще и надёжнее, но может снизить долю попаданий в кэш. Обратный индекс зависимостей точнее, однако сам становится критическим слоем данных и требует проверки полноты.
Шаг 6. Дождитесь согласованности, не подменяя её паузой
Фраза «подождать несколько секунд» не является гарантией. Для каждого асинхронного слоя нужен наблюдаемый барьер: номер commit, поколение индекса, подтверждённый offset события или иной маркер, документированный выбранной системой.
{
"deletion_id": "del-example-0042",
"barriers": {
"vector_index_generation": "generation-example",
"lexical_index_commit": "commit-example",
"deletion_event_offset": "offset-example",
"cache_policy_epoch": 31
}
}
Проверку выполняют только после достижения этих барьеров всеми читателями, обслуживающими активный трафик.
Как проверить результат
Нужны два независимых уровня доказательства: проверка внутренних слоёв и проверка реального пользовательского пути.
1. Структурная проверка
Проверяющий процесс с доступом только на чтение запрашивает каждый слой по точным идентификаторам. Пример ожидаемого отчёта:
{
"deletion_id": "del-example-0042",
"document_id": "doc-example-001",
"checks": {
"source_active_objects": 0,
"extracted_text_rows": 0,
"chunk_rows": 0,
"vector_records": 0,
"lexical_records": 0,
"retrieval_cache_dependencies": 0,
"answer_cache_dependencies": 0,
"pending_reindex_jobs": 0
},
"active_read_allowed": false,
"verdict": "verified"
}
Такой отчёт является форматом примера. Значение verified допустимо только после успешного завершения всех обязательных проверок. Тайм-аут, недоступный индекс или неизвестный счётчик должны давать inconclusive, а не успех.
2. Проверка активной выдачи
Отправьте запрос через тот же API, маршрутизацию, права доступа, кэш и поисковые адаптеры, что используются в производственной выдаче. Проверьте:
- точечное получение по
document_idзапрещено или возвращает отсутствие; - фильтрованный поиск по
document_idвозвращает ноль результатов; - поиск по заранее сохранённым диагностическим признакам документа не содержит его
document_idв provenance; - ответный кэш не отдаёт запись, зависевшую от удалённой версии;
- повторная доставка старого сообщения не создаёт новых фрагментов.
Диагностические признаки выбирают до удаления и хранят в защищённой тестовой записи. Не переносите чувствительные фразы документа в общие логи. Проверка по тексту дополняет проверку по идентификатору, но не заменяет её: модель может независимо сформулировать похожий ответ из других источников.
3. Проверка после повторной доставки
Если инфраструктура позволяет безопасно воспроизвести событие в изолированном контуре, повторно передайте старое сообщение загрузки и убедитесь, что обработчик зафиксировал подавление. В рабочем контуре не публикуйте искусственное событие без штатного механизма тестирования. Альтернатива — дождаться естественного retry и проверить журнал решения потребителя.
4. Закрытие операции
После успешной независимой проверки переведите запись удаления в состояние verified. Сохраните только минимальное доказательство:
- идентификатор операции и документа;
- времена запроса, логического запрета, очистки и проверки;
- версии или поколения проверенных слоёв;
- нулевые счётчики активных проекций;
- версию процедуры и идентификатор проверяющего компонента.
Не храните удалённый контент в «доказательстве удаления»: иначе журнал аудита станет ещё одной неучтённой копией.
Рекомендуемая машина состояний
active
→ deleting
→ purging
→ verifying
→ verified
Любой этап:
→ failed_retryable
→ manual_review
Переходы должны быть монотонными и условными. Завершившийся с опозданием обработчик не может вернуть документ из deleting в active. Повторная операция с тем же deletion_id продолжает незавершённую работу; новая операция для уже проверенного документа отвечает предсказуемым идемпотентным результатом.
Типовые ошибки
Удаление только из векторного индекса
Полнотекстовый поиск, кэш или сохранённые фрагменты продолжают участвовать в выдаче. Ведите явный реестр всех проекций документа и владельцев каждого слоя.
Поиск и удаление по имени файла
Имена меняются, повторяются и нормализуются по-разному. Используйте внутренний устойчивый идентификатор с обязательной областью tenant_id.
Широкий delete без tenant-фильтра
Одинаковый document_id в разных областях либо ошибка генерации ключей может привести к удалению чужих данных. Сделайте tenant-фильтр обязательной частью интерфейса и ограничения базы.
Очистка очереди вместо защиты потребителя
Сообщение может находиться в retry-хранилище, у активного consumer или в журнале событий. Tombstone и проверка версии должны работать независимо от возможности удалить конкретное сообщение.
«Ноль результатов» после одного запроса
Запрос мог попасть в одну реплику, обойти кэш или завершиться до refresh индекса. Проверяйте все активные маршруты после наблюдаемого барьера согласованности.
Сохранение текста в журнале удаления
Это создаёт новую копию данных с другим сроком хранения. Журналируйте идентификаторы, хеши при необходимости, счётчики и решения, но не содержимое.
Ложный успех при недоступной проверке
Если один слой не ответил, отсутствие документа не доказано. Используйте отдельный результат inconclusive и повторяйте проверку.
Ограничения и границы обещания
Описанная процедура доказывает отсутствие документа в определённой области: перечисленных активных слоях, на зафиксированных поколениях и через проверенные пути чтения. Она не доказывает физическое исчезновение каждого байта из резервных копий, снапшотов, журналов транзакций или носителей.
Для резервных копий обычно применяется отдельная политика: запрет восстановления удалённого объекта через реестр tombstone, ограниченный срок хранения backup и повторное применение журнала удалений после восстановления. Если нормативные требования требуют криптографического стирания или уничтожения ключа, это должно быть самостоятельным контролем, а не подразумеваемым свойством RAG-процедуры.
Также нельзя доказать, что языковая модель «забыла» информацию, если документ использовался для обучения или дообучения модели. Удаление из RAG закрывает контекст активной выдачи, но не меняет параметры модели. Эти контуры следует учитывать раздельно.
Наконец, совпадающий факт может законно присутствовать в другом документе. Проверяемое удаление гарантирует отсутствие конкретной проекции и её provenance, а не исчезновение знания как такового.
Контрольный список перед вводом в эксплуатацию
- У каждого производного объекта есть
tenant_id,document_idи версия. - Логический запрет вступает в силу раньше физической очистки.
- Все читатели фильтруют неактивные документы централизованно.
- Потребители очередей сверяют tombstone перед каждой записью.
- Кэш хранит зависимости от документов или общий policy epoch.
- Удаление каждого слоя идемпотентно и имеет отдельный результат.
- Согласованность подтверждается барьером, а не фиксированной паузой.
- Проверка выполняется отдельным read-only процессом.
- Недоступность слоя даёт неопределённый результат, а не успех.
- Журнал доказательств не содержит удаляемого текста.
- После восстановления из backup журнал удалений применяется повторно.
Итог
Проверяемое удаление — это не команда DELETE, а протокол: устойчивый идентификатор, ранний tombstone, идемпотентная очистка всех проекций, защита от повторной индексации, наблюдаемые барьеры согласованности и независимая проверка активного пути.
Главный артефакт процедуры — не сообщение «успешно», а компактный отчёт, связывающий одну операцию удаления с нулевыми счётчиками во всех заявленных слоях и с отрицательным результатом пользовательского чтения. Именно он позволяет воспроизвести проверку и обосновать границы сделанного утверждения.