Протухшие цитаты в RAG: как найти устаревшие чанки
Протухшие цитаты в RAG ломают ответы и доверие к системе. Разберем проверку версий по источникам, поиск старых чанков и быстрый контроль индекса.

Почему цитата устаревает раньше, чем ответ
Документ можно поменять за минуту. Индекс обновится позже - через час, ночью по расписанию или после очередного прохода. В этот зазор RAG уже работает на старой копии, хотя источник давно живет в новой версии.
Из-за этого ответ часто выглядит почти нормальным, а цитата уже нет. Модель берет найденный чанк как факт, строит вокруг него связный текст и не понимает, что источник успел измениться. Для нее старый фрагмент так же убедителен, как новый.
Проблему усиливает сам поиск. Он любит формулировки, которые лучше совпадают с вопросом. Если старый чанк совпадает точнее, чем свежий, но переписанный фрагмент, поиск поднимет его выше. Одного такого куска хватает, чтобы испортить весь ответ: модель повторяет старую норму, добавляет уверенный текст и еще подкрепляет это цитатой.
Так появляются протухшие цитаты в RAG. Пользователь видит не просто неточный ответ, а аккуратно оформленную ошибку с опорой на документ. Это опаснее обычной галлюцинации: текст кажется проверяемым, поэтому ему верят дольше.
Сильнее всего это бьет по внутренним базам знаний, регламентам и комплаенс-документам. В банке, телекоме или госсекторе одна старая версия правила может полностью поменять смысл ответа. Если команда обновила раздел про доступ к данным или новую редакцию политики по 152-ФЗ, а индекс еще не догнал источник, система продолжит цитировать то, что уже отменили.
Модель при этом не сомневается. Она не сверяет дату документа сама по себе и не чувствует, что фраза устарела. Если в промпте нет жесткой проверки версии, а в индексе нет свежего чанка, она отвечает спокойно и уверенно.
Первые жалобы обычно звучат очень приземленно. Пользователь пишет, что в документе уже другой текст. Поддержка видит, что ссылка ведет к новой редакции, а цитата старая. Команда замечает, что сегодня и вчера бот отвечал по-разному на один и тот же вопрос. Бизнес говорит, что ассистент ссылается на отмененные правила.
Самое неприятное в том, что ошибка редко выглядит как ошибка. Ответ гладкий, термины на месте, цитата похожа на настоящую. Пока кто-то вручную не откроет источник, баг может жить долго.
Поэтому старение цитаты почти всегда заметнее, чем старение самого ответа. Пользователь еще может простить обобщение или неполный совет. Неверную цитату из документа он воспринимает как поломку всей системы.
Где брать версию документа
Версию нужно брать не из текста чанка, а из самого источника или его метаданных. Иначе проблема появляется тихо: документ уже обновили, а индекс все еще считает старый кусок актуальным.
Лучше сразу свести все источники к одному правилу. У каждого документа должны быть source_id, version_value, version_type и время, когда вы это значение получили. Формат может отличаться, смысл один: понять, изменился документ или нет.
В CMS обычно хватает трех полей: номер ревизии, дата правки и статус публикации. Номер ревизии удобнее всего, потому что он меняется при каждом сохранении. Дата правки полезна как запасной сигнал. Статус публикации помогает не тянуть в индекс черновики и снятые материалы.
В Confluence и Notion часто берут page_id, last_edited_time и автора последнего изменения. page_id отличает одну страницу от другой, а время правки показывает, пора ли переиндексировать документ. Автор не заменяет версию, но помогает быстро понять, кто внес спорное изменение, если ответ внезапно начал ссылаться на старую формулировку.
Для PDF и DOCX явной версии часто нет. Тогда надежнее хранить checksum файла, дату выгрузки и имя сборки или пакета, из которого файл приехал. Если юридический отдел раз в неделю выгружает новый PDF с тем же именем, checksum сразу покажет, что содержимое поменялось, даже если заголовок и путь остались прежними.
В базах данных и внешних API обычно хватает updated_at. Если данные загружаются пакетами, добавляйте batch_id. Если источник живет в Git или похожем хранилище, фиксируйте commit hash. Это особенно удобно для справочников, тарифов и внутренних баз знаний: можно быстро найти, какая именно поставка данных попала в индекс.
Если явной версии нет
Такое бывает чаще, чем кажется. Старый сайт может отдавать только HTML без даты правки, а внутренний сервис вообще не хранит историю изменений.
Тогда версию приходится собирать самим. Нормализуйте документ, уберите служебный шум, меню и счетчики, а потом считайте hash уже по очищенному тексту. Сохраняйте время обхода источника и помечайте способ версионирования, например content_hash.
Это не идеальный путь. Изменение одного пробела или блока навигации может дать новую версию там, где смысл не поменялся. Поэтому hash лучше считать по основному телу документа, а не по сырому HTML.
Если у вас несколько источников в одном RAG-пайплайне, не смешивайте их правила. Страница из Confluence, запись из CRM и PDF из общей папки могут иметь разные version_type, но общий контракт метаданных. Тогда поиск устаревших чанков превращается в простой фильтр по source_id и несовпадающей версии, а не в ручной разбор каждого коннектора.
Какие поля хранить рядом с каждым чанком
Если рядом с чанком лежат только текст и embedding, о его свежести вы почти ничего не знаете. Для разбора таких ошибок этого мало. Нужна короткая карточка метаданных, по которой видно, откуда взялся фрагмент, к какой версии документа он относится и не пора ли его убрать из выдачи.
Сначала храните source_id и section_id. source_id связывает чанк с документом целиком: политикой, тарифом, инструкцией или страницей базы знаний. section_id нужен для точного адреса внутри документа. Тогда вы быстро поймете, что изменилось: весь документ или только один раздел.
Рядом держите стабильный идентификатор родительского документа в исходной системе. Иначе через месяц никто не вспомнит, откуда взялся фрагмент. Поле source_type тоже полезно: wiki, PDF, Google Doc, CRM, тикет, выгрузка из ERP. Ошибки у этих источников разные, и без этого поля их сложно разбирать.
Для контроля версий нужны version_id или checksum. Если источник сам отдает номер ревизии, храните его. Если нет, считайте checksum от исходного текста или от нормализованного блока. Это простой способ увидеть, что документ уже другой, даже если название и URL не поменялись.
Нужны и два времени, которые часто путают. document_updated_at показывает, когда документ изменили в источнике. indexed_at показывает, когда ваш пайплайн в последний раз обработал этот чанк. Разница между ними сразу выдает проблему: документ обновили вчера, а индекс все еще живет состоянием прошлой недели.
Еще одно обязательное поле - статус жизни документа. Храните is_deleted, is_archived или единое lifecycle_status. Без этого старые фрагменты продолжают всплывать в ответах, хотя документ уже удалили, отправили в архив или заменили новой редакцией.
На практике этого хватает для быстрых проверок. Например, в базе знаний банка обновили тарифы только в одном разделе. Вы находите все чанки с тем же source_id, смотрите, у каких не совпадает version_id или checksum, и сразу видите точный набор устаревших кусков. Не весь индекс, а конкретные чанки, которые портят ответ.
Как проверять версию на каждом проходе индексации
Каждый проход индексации лучше начинать не с пересборки чанков, а с простого вопроса: что изменилось в источниках с прошлого раза. Для этого индексатор сначала собирает свежий список документов и их версий. Маркер версии может быть разным: revision_id, ETag, hash содержимого, дата последнего изменения. Важен не формат, а то, что маркер меняется вместе с документом.
Дальше система сверяет этот список с тем, что уже лежит рядом с индексом. Обычно хватает служебной таблицы с document_id, последней известной версией и списком chunk_id. Если у источника версия 18, а в индексе все еще 17, документ нельзя оставлять как есть.
Рабочий цикл
После сверки удобно разложить документы на четыре группы. Новые документы добавляете в индекс. Измененные пересобираете и заменяете старые чанки. Удаленные сразу скрываете из поиска или удаляете. Неизмененные пропускаете.
Такой проход экономит много времени. Вы не трогаете весь корпус, а переиндексируете только затронутые документы. Если база большая, разница заметна сразу: вместо полной пересборки на часы вы обновляете несколько документов и их чанки.
Есть простое правило: не пытайтесь вручную подлатать старые чанки, если документ изменился целиком. Проще снять старую версию из выдачи и пересобрать документ заново. Частичное обновление имеет смысл только там, где структура документа стабильна и вы точно знаете, какой раздел поменялся.
Что писать в лог
Лог нужен не для красоты, а для разбора ошибок. Если ответ внезапно сослался на старый текст, команда должна быстро понять, почему индекс не обновился.
Минимальный набор такой: document_id, старая версия, новая версия, действие и причина. Причина должна быть понятной человеку, например source_version_changed, document_deleted, first_index, parser_failed. Если вы работаете в среде с требованиями к аудиту, такой лог сильно упрощает разбор инцидента и показывает, какой документ, когда и почему попал в переиндексацию.
Как быстро находить устаревшие чанки
Сломанный ответ часто начинается не с плохого поиска, а с одного старого чанка, который тихо пережил обновление исходного документа. Если смотреть только на дату последней индексации, такие случаи легко пропустить. Нужен короткий набор признаков, по которым чанк сразу попадает в зону риска.
Сначала сравните version_id у чанка с текущей версией источника. Если документ уже живет в версии v18, а чанк все еще ссылается на v16, его лучше не отдавать в поиск до переиндексации. Это самый быстрый фильтр: он не требует сложной логики и сразу отсекает явное расхождение.
После этого проверьте checksum в двух точках: у сырого текста и у текста после нормализации. Это помогает поймать неприятный случай, когда страница изменилась формально, но смысл не поменялся, или наоборот. CMS могла убрать таблицу, изменить подписи и переставить блоки. Сырой checksum почти наверняка изменится, а checksum после нормализации покажет, затронуло ли это сам текст, который пошел в индекс.
Что считать подозрительным
Подозрительным стоит считать любой чанк, у которого version_id не совпадает с версией документа в источнике. Туда же попадают случаи, когда checksum после нормализации изменился, а чанк в индексе остался прежним; источник уже удален, но его чанки все еще участвуют в выдаче; страница или файл не проверялись дольше заданного TTL; у документа резко поменялся размер или число чанков.
Отдельно ищите сироты - чанки от страниц и файлов, которых уже нет. Они часто остаются после ручной чистки папок, переноса базы знаний или замены URL. Если источник вернул 404, пустой файл или статус deleted, не ждите ночного запуска. Сразу помечайте связанные чанки как неактуальные и убирайте их из кандидатов на выдачу.
TTL полезен для источников, которые меняются часто: тарифов, оферт, SLA и внутренних регламентов. Для таких документов лучше жить не по общему расписанию индексации, а по короткому сроку годности. Если TTL истек, чанк уже подозрителен, даже если явного обновления вы не увидели.
Простой отчет, который действительно помогает
Хватает ежедневной таблицы с пятью полями: source_id, текущий version_id, chunk_version_id, статус источника и причина флага. Добавьте число затронутых чанков и дату последней проверки. По такому отчету команда за несколько минут видит, что надо переиндексировать, что удалить, а где проблема сидит в коннекторе, а не в RAG-пайплайне.
Пример: обновили тарифы, а ответ остался старым
У команды был PDF с тарифами и лимитами для клиентов. В новой версии подняли месячный лимит и поменяли комиссию. Файл загрузили в то же хранилище под тем же именем, и ночной запуск честно забрал обновление.
Проблема началась дальше. Индексация добавила новые чанки, но старые не удалила и даже не пометила как неактуальные. В итоге в индексе жили сразу две версии одного и того же документа: вчерашняя и сегодняшняя.
Утром сотрудник спросил у внутреннего ассистента про лимит. Система выдала старую цифру, хотя ссылка в ответе указывала уже на новый PDF. Со стороны это выглядело особенно неприятно: цитата как будто подтверждала ответ, но сам текст чанка пришел из прошлой версии.
Причина обычно очень простая. Команда считает документ по пути к файлу или по doc_id, а версию не проверяет. Если файл перезаписали по тому же пути, система видит тот же документ и складывает свежие чанки рядом со старыми.
В логах конфликт замечают быстро. Для одного и того же doc_id совпадают название документа и путь, но version_id расходится. Чанк, который поиск поднял в ответ, несет старый version_id, а карточка источника уже показывает новый.
На практике полезно сверить хотя бы четыре поля: doc_id, version_id, chunk_id и indexed_at. Если doc_id один и тот же, а version_id разный, индекс уже смешал версии. Если при этом indexed_at свежий, путаница почти очевидна: новая версия приехала, но старая никуда не делась.
После этого команда чистит индекс по старому version_id, пересобирает embeddings только для актуального файла и заново прогоняет проверочный набор вопросов. Ответ сразу меняется на свежую цифру, а цитата начинает совпадать с текстом документа.
Такие сбои редко живут только в одном PDF. Если процесс один раз пропускает удаление старых чанков, он повторит это и для регламентов, оферт, лимитов и SLA. Один фильтр по version_id на этапе выдачи ловит эту ошибку за минуты, а не после жалобы от пользователя.
Где команды чаще всего ошибаются
Чаще всего проблема не в модели, а в дисциплине вокруг источников. Команда уверена, что индекс обновляется каждый день, но ответ все равно тянет старую цитату из чанка, который давно пора было убрать или пересобрать.
Первая частая ошибка - смотреть только на updated_at. Поле удобное, но оно ловит не все. Редактор может тихо поправить абзац, заголовок или таблицу без смены даты на странице. В итоге RAG-пайплайн считает документ прежним, хотя смысл уже изменился.
Вторая ошибка - хранить версию документа, но не версию чанка. На практике это ломает поиск причины. Вы видите, что документ уже на версии 12, а в выдаче остается кусок текста, собранный из версии 9. Если у чанка нет своей метки версии, времени индексации и hash текста, вы долго ищете, где именно застрял старый фрагмент.
Третья ошибка встречается постоянно: документ удалили или перенесли, но индекс не получил явную метку удаления. Чанк продолжает жить сам по себе и выглядит как нормальный источник. Похожая путаница возникает, когда несколько источников складывают под одним source_id. Например, страница из базы знаний, PDF и выгрузка из CRM получают один и тот же идентификатор, и потом уже неясно, что именно процитировал поиск.
Есть и более тихая ошибка: команда думает, что раз документ свежий, то и цитата свежая. Это неверно, если чанк не пересобрали после правки структуры, таблицы или списка. Еще хуже, когда перед ответом не делают быструю проверку цитаты по текущей версии источника. Тогда в ответ попадает текст, которого уже нет в оригинале. Если к этому добавить логи, где хранится только запрос и ответ, а не версия источника, разбор после жалобы идет почти вслепую.
Отдельно стоит следить за тихими правками в регламентах, тарифах и офертах. Там часто меняют одну цифру или одно условие, а updated_at остается прежним. Для бизнеса это самый неприятный случай: ответ выглядит правдоподобно, цитата есть, но она уже устарела.
Если у вас несколько контуров данных, разделяйте идентичность источника жестко: документ, его ревизия, чанк и статус удаления. И перед выдачей ответа проверяйте не только наличие цитаты, но и то, что она существует в текущей версии документа. Это добавляет миллисекунды, но экономит часы разбора и неприятные ошибки в проде.
Быстрый чек перед релизом
Перед выкладкой проверьте не только точность ответа, но и путь каждой цитаты до исходного документа. Такие ошибки чаще всего попадают в прод не из-за модели, а потому что индекс хранит старый чанк, поиск смешивает версии, а логи не дают быстро понять, где сбой.
Один короткий прогон уже многое показывает. Возьмите 10-20 запросов по документам, которые недавно менялись: тарифы, регламенты, оферты, лимиты. Если система хотя бы раз подтянула старую формулировку рядом с новой, релиз лучше остановить.
Проверьте пять вещей:
- У каждого чанка в индексе есть
source_idиversion_id. - Индексация умеет удалять чанки документа, который исчез из источника.
- Поиск не смешивает старую и новую версии одного источника в одном ответе.
- Ответ показывает дату, номер версии или другой понятный маркер источника.
- Логи связывают один запрос с retrieved chunks,
source_id,version_idи идентификатором прохода индексации.
Я бы добавил еще один простой тест. Измените один документ в тестовой базе, переиндексируйте его и сразу задайте вопрос, который точно заденет обновленный фрагмент. Если ответ все еще опирается на старую цитату, ищите проблему в очереди индексации, дедупликации или фильтре активной версии.
Для команд с жесткими требованиями к аудиту это особенно важно. Если вы храните аудит-трейл и метки запроса рядом с версией источника, разбор инцидента идет заметно быстрее. Иначе спор о том, какой документ видел RAG, быстро превращается в догадки.
Что сделать дальше
Не пытайтесь сразу привести в порядок все источники. Возьмите один документ, который меняется часто: тарифы, оферту, справку по лимитам или внутренний регламент. На таком источнике ошибки видны быстро, и команда сразу поймет, где система держит старую версию дольше, чем нужно.
Сначала настройте для этого источника простой журнал изменений. В нем хватит нескольких полей: идентификатор документа, версия в источнике, версия в индексе, время последней проверки и статус. Если версия в источнике уже новая, а в индексе еще старая, система должна подать сигнал, а не молча ждать следующего большого прохода.
Минимальный план
- Выберите один источник с частыми обновлениями.
- Зафиксируйте, откуда берется версия и как часто вы ее проверяете.
- Включите алерт на рассинхрон между источником и индексом.
- Прогоните тесты на удаление, откат и повторную индексацию.
Тесты лучше делать на коротком сценарии. Например, вы меняете цену в документе, потом удаляете старую редакцию, потом возвращаете прошлую версию и снова обновляете индекс. Если после этого поиск все еще находит старый чанк, проблема обычно не в модели, а в том, как вы храните связи между чанком, документом и его версией.
Отдельно проверьте удаление. Многие команды хорошо обрабатывают только появление новой версии, но забывают убрать чанки документа, который исчез из источника. В результате поиск достает цитату, которой уже нет ни в базе знаний, ни в действующем документе.
Откат версии тоже часто ломает логику. Система может увидеть, что номер версии стал меньше, и решить, что это ошибка, а не нормальный сценарий. Если у вас бывают ручные откаты после неудачной публикации, заложите это в правила индексации сразу.
Повторная индексация нужна не только после обновления текста, но и после смены правил чанкинга, нормализации и извлечения метаданных. Иначе у вас появятся два набора чанков: старый по одной схеме и новый по другой. Поиск в такой смеси работает плохо даже при правильных версиях.
Если вы строите RAG в российском контуре, удобно, когда следы запроса, логи и аудит-трейлы не размазаны по разным системам. Например, RU LLM на rullm.com дает OpenAI-совместимый API-шлюз с логами и аудит-трейлами внутри РФ. Это не заменяет версионирование источников, но упрощает разбор: какой запрос ушел в модель, какие метки были у вызова и где начался рассинхрон.
Хороший следующий шаг простой: доведите один живой источник до предсказуемого состояния и только потом переносите схему на остальные. Так ошибок будет меньше, а чинить их станет намного проще.
Часто задаваемые вопросы
Почему в RAG чаще устаревает цитата, а не сам ответ?
Потому что индекс часто хранит старый чанк дольше, чем живет старая версия документа. Поиск находит этот кусок, модель строит вокруг него связный текст и уверенно цитирует то, что уже отменили.
Откуда брать `version_id` для документа?
Берите версию из самого источника или его метаданных, а не из текста чанка. Для CMS обычно хватает ревизии и статуса публикации, для wiki — page_id и last_edited_time, для Git — commit hash, для файлов — checksum.
Что делать, если у источника нет явной версии?
Если источник не отдает версию, очистите документ от меню, счетчиков и другого шума, а потом посчитайте content_hash по основному тексту. Так вы заметите смену содержимого и не будете ловить ложные обновления из-за служебной разметки.
Какие метаданные нужны у каждого чанка?
Рядом с чанком храните source_id, section_id, source_type, version_id или checksum, document_updated_at, indexed_at и lifecycle_status. Тогда вы сразу видите, откуда взялся фрагмент, к какой версии он относится и нужно ли убрать его из выдачи.
Как проверять версии на каждом проходе индексации?
Каждый проход начинайте со сверки текущих версий в источниках с тем, что уже лежит рядом с индексом. Новые документы добавляйте, измененные пересобирайте с заменой старых чанков, удаленные сразу выключайте из поиска, а неизмененные пропускайте.
По каким признакам быстро найти устаревший чанк?
Самый быстрый фильтр — сравнить chunk_version_id с текущей версией источника. Еще смотрите на checksum после нормализации, статус deleted или archived, истекший TTL и резкий скачок размера документа или числа чанков.
Что делать с чанками удаленного или архивного документа?
Как только источник удалили или отправили в архив, пометьте связанные чанки как неактуальные и исключите их из retrieval. Если ждать ночной проход, поиск успеет достать сиротские фрагменты и вставить их в ответ.
Почему одного `updated_at` мало?
updated_at легко пропускает тихие правки. Редактор может поменять абзац, таблицу или одно условие, а дата на странице не сдвинется, и тогда пайплайн оставит старую цитату как будто все в порядке.
Какие логи помогут быстро разобрать такой сбой?
В логах оставляйте document_id, старую и новую версию, действие и понятную причину вроде source_version_changed или parser_failed. Привязывайте к запросу retrieved chunks, source_id, version_id и идентификатор прохода индексации, чтобы команда быстро нашла место сбоя.
Какой минимальный тест сделать перед релизом?
Перед релизом возьмите 10–20 вопросов по документам, которые недавно менялись, и проверьте, что ответ не смешивает две версии одного источника. Потом измените один тестовый документ, переиндексируйте его и сразу спросите про обновленный фрагмент — этот прогон быстро ловит старые чанки в выдаче.