Главная / Статьи

Качество RAG во время индексации: типы фрагментов, очистка данных и метки для поиска

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

Издатель: REACTAPP.TOP12 мин чтения2701 слов

Источники: The Part of RAG Where Quality Is Actually Won

Как демо-версия превращается в производственную систему

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

Это также самый негламурный аспект. Почти никто не пишет о удалении шума, и именно по этой причине существует столько примеров успешного применения подобных методов. Приведённые ниже цифры основаны на небольшой внутренней оценке, а не на строгом тестировании, поэтому к ним следует относиться соответственно.

Три вида фрагментов, три инструмента создания

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

Фрагменты текста соответствуют структуре

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

Таблицы сохраняют целостность

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

В предыдущей версии этой системы здесь была ошибка. Оглавления обнаруживались с помощью примитивного теста, при котором многие строки заканчивались небольшими числами. Таблицы с тарифами также соответствовали этому критерию, поэтому реальная таблица с тарифами могла исчезнуть из индекса без какого-либо предупреждения. Новый тест более точен: на настоящей странице оглавления номера страниц постоянно растут, в то время как суммы тарифов, такие как 35 долларов, 30 долларов и 7 долларов, могут снижаться и повышаться. Таблица считается частью индекса только в том случае, если числа в конце никогда не уменьшаются. Теперь таблицы с тарифами остаются, страницы оглавления продолжают появляться, и одно условие устранило эту ошибку.

Изображения определяются по одному

Каждое изображение может стоить денег, поэтому для каждого из них пробуются три варианта по порядку, и процесс останавливается при первом успешном варианте.

  1. В режиме без обработки используется текст, уже извлеченный парсером. Если в документе присутствует пригодный для использования подпись или аннотация, которая не является местоимением вроде «image», то используется именно этот текст, и никакая модель распознавания изображений не запускается. Во многих документах от трети до половины изображений проходят по этому пути, что является полной экономией ресурсов.
  2. В режиме с оплатой запускается модель визуального анализа. Это применяется, когда нет доступного текста, а изображение больше, чем маленький разделитель. Четко сформулированный запрос заставляет модель сначала классифицировать изображение. Если это логотип или украшение, модель отвечает одним словом «DECORATIVE», и этот фрагмент игнорируется. В остальных случаях заполняются два поля. В поле SCREEN указывается одна строка с описанием назначения экрана — например, экран ввода данных для бизнес-аккаунтов — формулировка подбирается так, чтобы она находилась рядом с вопросами пользователя в векторном пространстве. В поле CONTENT перечисляются все метки полей, кнопки и видимый код в порядке чтения, что позволяет поисковикам использовать точные строки, которые они могут ввести. Таким образом, скриншот можно найти как по назначению, так и по написанному на нем тексту.
  3. Путь резервного варианта использует заголовок. Если оба варианта не сработают, но секция известна, сохраняется маркер, указывающий, что это за изображение и где оно находится, чтобы его можно было обратиться через соответствующую секцию во время запроса.

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

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

Улучшение того, что парсер сделал правильно

Именно надежность делает парсеры проблемой в данном случае. Они извлекают всё с страницы: штамп «КОНФИДЕНЦИАЛЬНО», повторяющийся на 200 страницах, номера страниц, информацию об авторских правах, предложения, прерванные разделителем страниц. Каждый из этих элементов становится отдельным фрагментом, и все они создают избыточность. Избыточность также влияет на ранжирование, поскольку запрос пользователя может так же легко наткнуться на рекламный баннер, как и на нужную процедуру.

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

  1. Фильтрация стандартных элементов позволяет убрать простые номера страниц, штампы версий и строки с ссылками вроде «Свяжитесь с нами» или «Прочитать больше» в начале документа. Набор правил варьируется в зависимости от типа файла, поскольку у PDF, файлов Word и презентаций есть свои особенности избыточности.
  2. Удаляются повторяющиеся элементы. Текст, идентичный на многих страницах, считается избыточным. Фраза «КОНФИДЕНЦИАЛЬНО, ТОЛЬКО ДЛЯ ВНУТРЕННЕГО ИСПОЛЬЗОВАНИЯ», встречающаяся на пятнадцати страницах, удаляется, тогда как фраза «См. страницу 3», встречающаяся дважды на одной странице, остается, поскольку она не распространяется на несколько страниц.
  3. Исправляются некорректные разрывы. Фрагмент, начинающийся посередине предложения или с отсутствующим пунктом, был разделен в неправильном месте. Если его можно объединить с предыдущим фрагментом в том же разделе, они сливаются. В противном случае он помечается как продолжение, чтобы его метка отражала соответствующий контекст.
  4. Сливаются одинокие фрагменты. Фрагмент длиной 40 символов, оказавшийся на границе раздела, поглощается соседним фрагментом, но только если у обоих точно одинаковый раздел и заголовок. Слияние между разделами невозможно, а краткий отдельный элемент остается без изменений.
  5. Функция удаления дубликатов объединяет предложение, повторяющееся под одним заголовком, в один фрагмент. Если оно встречается под двумя заголовками, сохраняются обе копии, поскольку с учётом пути раздела они содержат разную информацию. Изображения полностью исключаются из этого правила, так как два похожих экрана по-прежнему считаются двумя отдельными скриншотами.

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

Метка поиска

Следующий шаг приносит наибольшую пользу, но большинство систем его игнорируют. Возьмём простой фрагмент вроде этого:

Введите сумму и нажмите F4.

В нём указан путь к разделу — Wire Transfer > Initiation > Amount Entry, что уже помогает. Однако перед встраиванием языковая модель также пишет краткую метку для поиска длиной от 18 до 22 слов, которая хранится вместе с фрагментом. Для этого фрагмента метка выглядит примерно так:

Шаг процедуры: введите сумму перевода, затем нажмите F4, чтобы подтвердить начало банковского перевода.

Рассмотрим эффект. Без метки инструкция остаётся в пространстве встраивания рядом со всеми остальными шагами, требующими ввести значение и нажать клавишу. С меткой она оказывается рядом с указаниями на банковские переводы и подтверждения, что является словарём, который использовал бы настоящий пользователь. Перефразированный вопрос вроде «Как подтвердить сумму при банковском переводе?» тогда находит именно этот фрагмент. Во внутренней оценке метки сократили примерно вдвое количество случаев неверного поиска. Это был небольшой тест на одном корпусе данных, поэтому рассматривайте его как сильный намёк, а не как универсальную константу.

Эта техника происходит от метода Contextual Retrieval компании Anthropic, который показал, что размещение предложения с контекстом, специфичного для каждого фрагмента, перед ним перед индексацией значительно снижает количество ошибок при поиске. Здесь метод был адаптирован для контента, где важны точные идентификаторы. Коды транзакций и функциональные клавиши должны копироваться без изменений и ни в коем случае не перефразироваться, причем каждая метка начинается с слова, обозначающего роль, такого как «Шаг процедуры» или «Таблица ссылок», чтобы фрагменты группировались с соответствующими типами вопросов.

Что видит автор меток

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

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

Резюме документа формируется в результате отдельного запроса, сделанного до начала маркировки. В нем берутся примеры блоков из начала, середины и конца, а также одна таблица и один изображение; затем возвращаются два-три предложения с указанием типа документа, его аудитории и связанных продуктов и кодов. Каждый запрос на маркировку затем включает это резюме. Фраза «указать сумму» должна маркироваться по-разному в руководстве по банковским переводам и в документе с информацией о кредите, и именно благодаря резюме модель определяет, какой документ она читает.

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

  1. самый близкий элемент в том же пути раздела
  2. в противном случае — самый близкий элемент в том же родительском разделе
  3. в противном случае — самый близкий элемент в рамках того же заголовка

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

В примере F4 модель понимает, что предыдущий фрагмент в её разделе касался выбора получателя, а следующий — подтверждения перевода. Это позволяет ей сформировать чёткую метку относительно ввода суммы банковского перевода, а не расплывчатое резюме. Хорошо подобранные соседние фрагменты обеспечивают точность меток.

На рисунке показано, как модель меток читает соседние фрагменты из того же раздела, затем формирует одно конкретное предложение, которое сохраняется рядом с фрагментом и используется в обоих каналах поиска.

Почему это доступно по цене

Три фактора делают этот подход практичным и не слишком дорогим.

  • Однократная стоимость. Метка генерируется во время индексации и сохраняется, поэтому запросы никогда не оплачивают её.
  • Низкая стоимость за единицу. Небольшая, быстрая модель маркирует фрагменты пачками по 25 штук; несколько таких групп работают одновременно, и расходы составляют менее десятой доли цента за фрагмент.
  • Грациозная ошибка. Блок, в котором произошла ошибка маркировки, просто остается без меток в индексе, пока продолжается обработка. Такие блоки сохраняют свой текст и путь к разделу, но лишаются дополнительных преимуществ; поэтому сбои влияют на качество, но никогда не прерывают процесс индексации.

Метка хранится в отдельном поле, отдельно от основного содержимого блока. Это разделение важно позже, при выполнении запроса, когда метке присваивается дополнительный вес при поиске по ключевым словам. Сейчас важно то, что самые ресурсоемкие операции выполняются до того, как кто-либо начнёт поиск, и именно это позволяет работать с перефразированными запросами.

Детерминизм и версионированный запрос

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

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

Встраивание и хранение

После очистки, размещения и маркировки последней операцией во время индексации является генерация вектора. Один модель встраивания охватывает все типы фрагментов: text-embedding-3-large, сокращённый до 1536 измерений, так что текст, таблицы и скриншоты сортируются по отношению к запросу в одном и том же пространстве.

В элемент встраивания входит составленная строка в фиксированном порядке: структурный префикс, метка, затем основной текст. Префикс основан на уже известных фактах о данном фрагменте. К ним относятся тип документа, определённый по пути раздела, например, график тарифов или процедура банковского перевода, а также заголовок и путь раздела; для изображений — страница и местоположение. Маркировка фрагмента, похожего на процедуру, как процедуры позволяет связать его с вопросами, начинающимися с фразы «как что-то сделать».

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

Готовый вектор записывается в хранилище вместе с содержимым блока, меткой, структурными метаданными и столбцом полного текста, из которого осуществляется поиск по ключевым словам. После этого документ становится активным. Если какой-либо этап обработки даст сбой, документ отмечается как отклонённый, а причина сохраняется для отладки; при этом предыдущая активная версия остаётся доступной для поиска на протяжении всего времени. Документ с частичным индексированием никогда не становится видимым.

Как выглядит готовая строка

Чтобы проиллюстрировать это, приведён фрагмент «Введите сумму и нажмите F4» в его окончательном виде в магазине. Каждое поле в нём было взято из предыдущего шага.

text              : "Enter amount and press F4."
                     (built + cleaned + section-prefixed)
retrieval_context : "Procedure step entering the amount
                     and pressing F4 during wire transfer
                     initiation."          (the label)
chunk_type        : TEXT
section_path      : "Wire Transfer > Initiation >
                     Amount Entry"         (Part 2)
preceding_heading : "Amount Entry"
table_headers     : null                  (TABLE only)
page_start / end  : 12 / 12
image_object_key  : null                  (IMAGE only)
embedding_model   : text-embedding-3-large
embedding_dim     : 1536
embedding         : [ ...1536 floats... ] (body+label)
text_search_vector: label weighted high, body lower

В блоке таблицы заголовки столбцов заполняют место, где в текстовом блоке имеются значения null. Блок изображения хранит ключ blob, тип MIME и позицию на странице; данные CONTENT сохраняются в поле text, а информация о SCREEN — в поле retrieval_context. Все три вида блоков имеют одинаковую структуру строк. Для поиска используются два столбца: embedding для векторного поиска и text_search_vector для поиска по ключевым словам; оба формируются на основе содержимого и метки. Остальные столбцы предназначены для определения местоположения блока, его отображения или ограничения доступа конкретному пользователю.

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

Связанная литература

  • Размер блоков и их перекрытие в RAG: шаг, лимиты токенов и границы — Как разбивать длинную книгу на блоки для RAG: выбор размера и степени перекрытия, понимание шага, соблюдение лимита токенов для генерации эмбеддингов, хранение метаданных и просмотр перекрытия при реальной разбивке.