PunchMining Marshals urge miners to obtain valid licencesThe Jerusalem PostTrump begins final push to rally Republican voters for midterms as polls set to open across USDaily MaverickWHAT’S COOKING: Chow Mein — the history and how to make itZDF heuteEntdecken Sie das ZDF-Nachrichtenstudio20 Minuten«Bist du Feminist?»: Sidos Besuch bei «Take Me Späti» in KritikVanguardNorth Korea test fires strategic cruise missilesIl Fatto QuotidianoTrump ringrazia la Danimarca e l’Ue: “Ora abbiamo la Groenlandia” – VideoDigital SpyCoronation Street star Helen Flanagan unleashes "wrath" in parking disputeThe Sydney Morning HeraldTubby rips Aussies after Cape Town disasterSRF NewsMittlerer Westen vor Midterms – Wie Trumps Iran-Krieg seine Partei bei den Wahlen belastet7sur7Deux hélicoptères se percutent à l’atterrissage près de l’EverestHet Laatste NieuwsKoning Filip begint aan gespreksronde met Les Engagés-voorzitter Yvan Verougstraete: “Match is nog niet gespeeld”
The Daily Newsstand · Free, Always
Tuesday, November 3, 2026

Как проверить, что ИИ-приложение действительно восстановилось после сбоя

Translate

Итак, резервные копии готовы, инфраструктура восстановлена, приложение снова отвечает на запросы. Но можем ли мы доверять его ответам? После сбоя ИИ-помощник рискует использовать устаревшие документы, возвращать отозванные права доступа и повторно выполнять действия, которые уже совершил. Разберемся, как избежать таких ошибок при восстановлении RAG-приложения.

Привет, Хабр! На связи Михаил Витко, менеджер PaaS-продуктов Рег.облака. В первой части статьи мы разобрали, какие данные ИИ-помощника нужно включать в резервные копии, что можно восстановить заново и как организовать бэкапы в S3, PostgreSQL и Kubernetes.

Теперь обсудим, как собрать из этих копий работоспособное приложение. Даже если хранилища успешно восстановлены, их данные могут не соответствовать друг другу: поисковый индекс будет содержать старую редакцию документа, а база — уже новую. При откате могут вернуться отозванные права доступа или повторно выполниться действия ИИ-агента.

В этой части соберем согласованный набор для восстановления RAG, проверим документы и индексы, разберем защиту от возврата отозванных прав и покажем, как тестировать восстановление на практике.

Навигация по тексту:

Как не получить сломанный RAG после восстановления из разных копий

Как возникает рассогласование

В 12:00 документ doc-42 имел редакцию 1 со сроком возврата 14 дней. В 12:03 загрузили редакцию 2 со сроком 30 дней. В 12:04 база переключила активную редакцию, а в 12:05 обработчик завершил обновление индекса.

Если взять базу за 12:04 и индекс за 12:00, поиск вернет старый текст. Если взять ещё и файл за 12:05, ссылка на источник откроет новый регламент, который противоречит ответу. Все три хранилища при этом могут успешно проходить собственные проверки целостности.

Другой вариант опаснее: после резервирования у сотрудника отозвали доступ. Откат таблицы разрешений вернул ему прежние права. Восстановление прошло без ошибок, но конфиденциальные документы снова доступны.

Фиксируем идентичность документов и фрагментов

Нужны идентификаторы, которые переживают отказ. Для каждого исходника сохраняем организацию, документ, редакцию, точный ключ или версию S3 и SHA-256. Для фрагмента дополнительно сохраняем chunk_id, хеш текста и pipeline_id — идентификатор конвейера обработки.

В pipeline_id должны однозначно разрешаться версии парсера, правила разбиения, токенизатор, модель эмбеддингов и ее revision, размерность, нормализация и метрика расстояния. Название вроде «модель v1» годится только в том случае, если рядом хранится ее неизменяемая спецификация и доступны все нужные артефакты.

Эти поля записываем в PostgreSQL как каталог ожидаемых фрагментов, а в Qdrant — в payload каждой точки. Хеш текста связывает вектор с конкретным фрагментом. Сами байты векторов защищает контрольная сумма снимка или отдельного экспорта.

В примере вводим поколение корпуса g42: опубликованный набор редакций документов и соответствующая ему коллекция kb_g42. После публикации ее содержимое не меняем. Новые редакции собираем в следующее поколение. Это упрощает восстановление, но требует места под несколько поколений и процедуры их удаления.

Создаем один набор восстановления

Для начала выберем понятную схему с паузой изменений. Это ограничение приложения, а не настройка S3 или PostgreSQL. Без него независимые снимки не становятся согласованными просто потому, что сделаны рядом по времени.

  1. Приостанавливаем загрузку и редактирование документов, публикацию поколений и фоновые записи в корпус, включая интеграции в обход API. Отзывы доступа и удаления продолжаем фиксировать в независимом сервисе политик: запрет начинает действовать сразу, а физическое изменение замороженного корпуса можно завершить после копирования. Остальные новые изменения явно отклоняем либо надежно журналируем до подтверждения приема.

  2. Дожидаемся завершения уже принятых операций во всех хранилищах. Нельзя считать систему остановленной, пока очередь пуста, но обработчик ещё выполняет запись. Неопубликованные поколения исключаем из активного корпуса и отмечаем для повторной сборки.

  3. Фиксируем активное поколение g42, точные редакции документов и каталог фрагментов. Проверяем, что коллекция содержит именно их и все записи завершены.

  4. Создаем дамп PostgreSQL и штатный снимок kb_g42, сохраняем файлы и артефакты в резервном контуре. Снимки могут создаваться последовательно: пауза сохраняет неизменность связанного состояния. История диалогов, если запись в нее разрешена, восстанавливается на момент снимка PostgreSQL.

  5. Проверяем наличие всех частей и их SHA-256. Последним публикуем манифест со статусом complete. Незавершенный набор не допускаем к восстановлению. После этого снимаем паузу.

Для какой нагрузки подходит пауза и что делать с RPO 15 минут

Схема с паузой подходит для пакетно обновляемой базы знаний, если бизнес допускает окно без новых загрузок и публикаций. Ее предел задается измеренным временем осушения очередей, дампа, снимка, передачи копий и проверки. Универсального порога в гигабайтах нет: одинаковый объем из PDF и коротких текстовых файлов создает разную нагрузку.

Для оценки возьмем только передачу данных: 5 ГиБ при устойчивых 50 МиБ/с — около 1 минуты 43 секунд; 100 ГиБ при той же скорости — примерно 34 минуты. Создание снимка и валидация добавятся сверху. Это расчет, а не замер Рег.облака. Первую схему разумно проверять на корпусе, который помещается в разрешенное окно, и при обновлениях пакетами, например один-два раза в день. При постоянной загрузке документов окно паузы становится отдельным ограничением доступности.

Для RPO важна дата последней завершенной копии в независимом контуре, а не время старта текущей задачи. Если при продолжающихся изменениях запускать копии каждые 10 минут, а их создание и доставка занимают 40 минут, возраст последней защищенной точки перед отказом может приблизиться к 50 минутам даже при параллельных заданиях. Такая схема не обеспечивает RPO 15 минут.

Полная пауза уменьшает число новых принятых изменений, но ценой недоступности загрузки. Ее нельзя выдавать за непрерывную работу с гарантированным RPO. Нельзя и объявлять набор complete, пока его части остаются только на исходной площадке.

Если длинный снимок нужно совместить с RPO до 15 минут, разделяем базовую копию и поток изменений. Опубликованное поколение и его снимок остаются неизменными, а новые документы и диалоги принимаются в журнал с сохранением исходных байтов вне рабочего контура до подтверждения клиенту. Журнал содержит устойчивые ID, порядок изменений, ссылки на редакции и версии конвейера; его операции должны безопасно повторяться. PostgreSQL восстанавливаем подходящим механизмом PITR или воспроизведением прикладных событий, а индекс доводим из сохраненных исходников и эмбеддингов. Один WAL не заменяет сохранение файлов S3.

Тогда отдельно контролируем задержку защищенного журнала и цепочки WAL, последнюю точку, до которой можно воспроизвести все зависимые данные, и время догонки индекса. Если журнал отстает от допустимых 15 минут, система перестает подтверждать новые изменения либо явно теряет заявленную гарантию. Длительность фонового снимка влияет прежде всего на объем последующего воспроизведения и RTO. В этой статье базовая схема проще; журналирование всех изменений — отдельное усложнение для непрерывно обновляемого сервиса.

Манифест — обычный машинно-читаемый каталог набора. Сокращенный пример с условными именами:

{
  "backup_id": "b42",
  "state": "complete",
  "generation": "g42",
  "postgres_dump": "postgres.dump",
  "index_snapshot": "kb_g42.snapshot",
  "objects_catalog": "objects.jsonl",
  "chunks_catalog": "chunks.jsonl",
  "pipeline_id": "embed-v1-chunk500-overlap50",
  "embedding_dimension": 768,
  "distance": "cosine",
  "policy_checkpoint": "policy-98.json",
  "checksums": "SHA256SUMS"
}

В полном наборе также фиксируем версии серверов, схему БД, digest образов, хеш спецификации конвейера и время снимка базы. Каталоги содержат точные ссылки на версии исходников и полный состав фрагментов. Манифест защищаем от подмены теми же независимыми правами, что и копии. Контрольная сумма рядом с файлом обнаруживает повреждение, но не защищает от атакующего, который может заменить оба.

Статус complete означает, что набор собран и проверен по каталогу. Возможность запустить из него сервис подтверждает отдельное тестовое восстановление.

Проверяем восстановленное состояние до открытия доступа

Сначала разворачиваем приложение без пользовательского трафика, фоновых обработчиков и внешних действий. Восстанавливаем базу и исходники из одного набора, затем соответствующую коллекцию. Последовательность загрузки не создает согласованность — ее задает состав выбранного набора.

Проверяем, что каждый опубликованный документ указывает на существующие байты нужной версии, а SHA-256 совпадает с каталогом. Проверка одного бакета через список текущих ключей недостаточна: может требоваться старая версия объекта. Затем сопоставляем ожидаемые фрагменты в базе с полным экспортом payload восстановленной коллекции.

Например, экспортируем payload всех точек коллекции с пагинацией во временную таблицу restored_index_export. В обеих таблицах колонки ниже имеют одинаковые типы; rag_chunks — каталог фрагментов приложения. Поколение g42 берем из манифеста выбранного набора. Запрос ищет пропуски и лишние записи:

WITH expected AS (
  SELECT generation, tenant_id, doc_id, revision,
        chunk_id, text_sha256, pipeline_id
  FROM rag_chunks WHERE generation = 'g42'
), actual AS (
  SELECT generation, tenant_id, doc_id, revision,
        chunk_id, text_sha256, pipeline_id
  FROM restored_index_export
)
SELECT 'missing' AS problem, FROM (
  SELECT
FROM expected EXCEPT SELECT FROM actual
) AS missing
UNION ALL
SELECT 'extra' AS problem,
FROM (
  SELECT FROM actual EXCEPT SELECT FROM expected
) AS extra;

Ожидаемый результат — ноль строк. Проверяем и повторы логического ключа фрагмента: EXCEPT работает с множествами и сам по себе дубликаты не выявляет. Сравнение одного количества точек тоже недостаточно. Старые и новые редакции могут содержать одинаковое число фрагментов.

Дополнительно сверяем размерность и метрику коллекции, версию конвейера и модель, которой API векторизует запрос. Даже полностью восстановленный индекс будет искать неправильно, если запросы кодирует другая модель. После структурных проверок запускаем контрольные вопросы с ожидаемыми источниками. Проверять только текст ответа LLM ненадежно: формулировка может измениться без поломки поиска.

Не возвращаем удаленные документы и отозванные права

После отката используем независимый сервис политик. В простом варианте это отдельный небольшой сервис и транзакционная БД вне исходной площадки, с собственными доступами и резервированием. Он хранит текущие запреты и упорядоченный журнал для каждой организации. Событие удаления или отзыва сначала надежно фиксируется здесь; только затем API подтверждает изменение. Асинхронный экспорт раз в час не обеспечивает такой гарантии.

Каждое событие содержит event_id, tenant_id, возрастающий seq, тип запрета, субъект и ресурс. Дополнительно сохраняем хеш события и хеш предыдущего. Сервис выдает доверенную контрольную точку head с последним зафиксированным seq и хешем цепочки. Доступ к журналу и его контрольным точкам отделяем от доступа приложения. Хеши проверяют целостность выгрузки; компрометацию самого сервиса они не устраняют.

Сокращенный пример отзыва доступа:

{
  "event_id": "evt-100",
  "tenant_id": "tenant-a",
  "seq": 100,
  "type": "access_revoked",
  "subject_id": "user-2",
  "resource_id": "doc-11"
}

В копии b42 применены события по seq=98. При восстановлении запрашиваем доверенный head=101, получаем события 99–101 и проверяем непрерывность последовательности, цепочку хешей и совпадение конечного хеша с head. Поэтому ловим и пропуск события 100 внутри выгрузки, и обрезанный хвост, который заканчивается на 100 вместо 101.

Событие evt-100 добавляет запрет для user-2 на doc-11 в таблицу ограничений поверх восстановленных прав. Применение события и обновление позиции журнала выполняем одной транзакцией, повторный event_id не создает второй эффект. Событие document_deleted аналогично блокирует документ для всей организации. На время открытия доступа ещё раз сверяем актуальный head; последующие запросы используют действующую политику. Удаленные исходники и фрагменты затем очищаем по принятой политике хранения.

Если журнал, доверенный head или его резервная копия недоступны, открывать доступ по старым разрешениям нельзя. Закрываем затронутую организацию до сверки; если область изменений неизвестна, закрываем весь ее корпус. Последовательные номера без независимой конечной контрольной точки не доказывают полноту: последняя часть выгрузки тоже могла потеряться. Срок хранения журнала должен покрывать возраст самой старой допустимой копии и время восстановления.

Проверка прав должна происходить до передачи фрагментов в LLM. Если права продублированы в payload индекса, их также актуализируем; одной старой фильтрации в Qdrant недостаточно. Кэши поиска и ответов очищаем либо переводим в новое пространство ключей, учитывающее поколение и версию прав.

Только после этих проверок переключаем приложение на проверенное поколение. Каждый запрос использует один и тот же идентификатор поколения для метаданных и поиска. Фоновые задания восстанавливаем из авторитетного состояния базы: старую очередь нельзя слепо запускать на новом корпусе. Операции обработки должны выдерживать повторное выполнение, а внешние действия агентов требуют отдельной сверки.

Проверяем восстановление на практике

Проверку разделим на два уровня. Для статьи выполнены 14 локальных проверок логики на Python 3.12.14, SQLite 3.53.1 и файловых фикстурах. Каталог индекса представлен payload-записями без векторов, а внешний исполнитель — отдельной SQLite-базой с уникальным ключом операции. PostgreSQL, Qdrant, облачный S3 и LLM в этом прогоне не запускались. Ниже приведены фактические результаты этих проверок; полноценное восстановление в Рег.облаке требует отдельного прогона.

Что уже проверено локально

Все 14 проверок завершились успешно, включая отрицательные случаи: для них успех означает обнаружение ошибки и запрет публикации состояния. Код и results.json приложены к материалу; повторный запуск — python3 test_recovery.py.

Из этого следует конкретный вывод: равенство количества записей не подтверждает согласованность RAG, а откат прикладного статуса не требует повторять внешний эффект, если сохранен ключ и исполнитель поддерживает идемпотентность. Тест переписки также показывает границу копии: сообщение, подтвержденное после нее, логический дамп не вернет. Эти выводы относятся к проверенной модели; задержки сети, WAL и загрузку настоящего индекса нужно измерять отдельно.

Далее — сценарий интеграционного отказа для Рег.облака, с ожидаемыми результатами. Исходный стенд должен быть недоступен восстановленному приложению, а новое окружение получает только независимые копии и доступ к действующему сервису политик.

Подготовка

Создаем 100 тестовых текстовых документов двух организаций, по 50 каждой. Для воспроизводимой проверки каталога заранее задаем по 20 фрагментов на документ: всего 2000. В нескольких документах размещаем уникальные контрольные факты. Для вопросов фиксируем ожидаемые doc_id и редакции в top-10 результатов поиска; сам набор сначала должен пройти эти проверки до отказа.

Публикуем g42 и создаем набор b42. Затем в первой организации меняем срок возврата в doc-42 с 14 на 30 дней, сохраняя 20 фрагментов. Удаляем doc-7, добавляем doc-101 с 20 фрагментами и отзываем у пользователя доступ к doc-11. Удаление и отзыв записываем также в независимый журнал.

Публикуем g43 и создаем полный набор b43. Число активных документов и фрагментов остается прежним: 100 и 2000. Поэтому проверка только количества записей не отличит эти поколения.

Отказ и два варианта восстановления

Восстанавливаем стенд с нуля, используя только сохраненные артефакты и инструкции. Исходные сервисы должны быть недоступны: иначе тест может незаметно прочитать недостающие данные из старого окружения. Обработчики и исходящие действия выключены.

В первом прогоне намеренно смешиваем дамп PostgreSQL из b43 и снимок коллекции из b42. Задача проверки — заблокировать публикацию этого состояния. Во втором прогоне восстанавливаем все компоненты из b43 и допускаем сервис к функциональным тестам только после проверки согласованности.

Добавляем отрицательные прогоны: удаляем один объект из тестовой восстановленной копии, подменяем редакцию файла при том же ключе, включаем другую модель эмбеддингов той же размерности. Каждый прогон должен завершаться ошибкой соответствующей проверки, а не открытием частично исправного сервиса.

Отдельно восстанавливаем старый согласованный набор b42. Его каталог и контрольные суммы могут пройти проверку, но независимый журнал всё равно должен заблокировать doc-7 и отозванный доступ к doc-11. Отсутствие doc-101 и старую редакцию doc-42 учитываем как откат содержимого; допустимость такого состояния определяется RPO и требованиями продукта.

Проверка переписки

До резервирования создаем три диалога по восемь сообщений, одно — со ссылкой на вложение. Сохраняем ID, организацию, роли, порядок, текст и SHA-256 вложения. После точки копии добавляем ещё одно подтвержденное сообщение и восстанавливаем базу из этой копии. Проверяем точное совпадение 24 исходных сообщений, порядок и доступность вложения; запрос другой организации не должен вернуть переписку. Последнее, 25-е сообщение, отсутствует: для его сохранности нужны более свежая точка PITR или независимый журнал сообщений. В локальном прогоне получен именно этот результат.

Проверка завершенного действия агента

До копирования записываем намерение создать тикет с ключом tenant-a:act-007 и статусом pending. После копирования исполнитель создает тикет и возвращает квитанцию ticket-1; затем имитируем отказ. Восстановленная база снова видит pending, но обращается к исполнителю с тем же ключом. Он возвращает ticket-1, не создавая второй тикет. В локальном тесте при двух обращениях эффект был один.

Этот результат зависит от контракта внешнего API. Срок хранения ключей у исполнителя должен покрывать возраст восстановленной копии и возможные повторы. Если ключ потерян, его срок истек или исполнитель не поддерживает идемпотентность, автоматически повторять действие нельзя: сначала сверяем квитанцию или внешний статус, иначе переводим операцию в ручную проверку. В локальном отрицательном тесте такой случай дал manual_review и ноль повторных обращений.

Что измеряем

Записываем момент отказа и момент, когда восстановленный сервис прошел проверки и готов принять трафик. Между ними учитываем обнаружение, получение доступа к копиям, развертывание, передачу данных, загрузку индекса, сверку прав и тесты. Отдельно фиксируем время каждого этапа, чтобы понимать причину задержки.

Для RPO сверяем журнал принятых изменений с восстановленным состоянием: какие документы и сообщения потеряны и какой период они покрывают. Интервал между запуском резервных заданий сам по себе этого не доказывает. Цель вроде «RPO до 15 минут, RTO до часа» становится подтвержденной только после успешного прогона с подходящим объемом данных.

Стенд из 100 документов проверяет корректность процесса. По его времени нельзя обещать RTO для миллиона фрагментов. Для такой оценки нужен отдельный прогон на сопоставимом объеме, с теми же ограничениями API, сети и дисков.

Чек-лист: готов ли ИИ-помощник к восстановлению

  • Определены RPO и RTO для документов, диалогов и состояния агентов.

  • У каждого документа есть точная редакция исходника, у каждого фрагмента — устойчивый идентификатор и связь с ней.

  • Сохранены версии парсера, разбиения, моделей, промптов, схемы базы и образов приложения.

  • Есть полный набор восстановления с защищенным манифестом, каталогами и контрольными суммами.

  • Жизненный цикл объектов не удаляет данные раньше, чем истечет срок хранения связанных копий.

  • Копии, ключи расшифрования и доступ к ним переживают выбранный сценарий отказа; права приложения не позволяют удалить копии.

  • Восстановление блокируется при несовпадении поколений, версий, файлов или фрагментов.

  • Актуальные удаления и отзывы прав применяются до передачи документов в LLM.

  • Кэши и фоновые задания не возвращают старые ответы, доступы или уже выполненные действия.

  • Сервис восстановлен на отдельном стенде; проверены источники ответов, границы организаций и фактическая потеря данных.

  • Возраст последней завершенной копии и защищенного журнала укладывается в RPO; длительность снимка не подменяет эту проверку.

  • Журнал политик проверен до доверенного head; при пропуске или недоступности доступ закрывается.

  • Переписка сверяется по содержимому и порядку; действия агента сверяются по устойчивому ключу и квитанции.

Начать можно с инвентаризации данных, измеренной пересборки и одного согласованного набора. Затем провести отказ на отдельном стенде и проверить, что приложение отказывается работать со смешанными версиями. Именно этот тест показывает, достаточно ли имеющихся копий для восстановления помощника.

На этом завершаем разбор. Надеемся, материал поможет подготовить ИИ-приложение к возможным сбоям и сделать восстановление предсказуемым. Спасибо, что дочитали до конца! Буду рад вашим вопросам и комментариям.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.

Как проверить, что ИИ-приложение действительно восстановилось после сбоя — KioskNews