Основная навигация

Практические примеры специализированных RAG-решений

Практические примеры показывают, как специализированные RAG-системы решают сложные задачи управления корпоративными знаниями.

В последнее время о RAG порой отзываются пренебрежительно: одни считают эту технологию совершенно элементарной (начать и правда легко, а вот масштабировать — уже нет), а другие полагают, что её вытеснили «агентные системы» (хотя во многих случаях стоит присмотреться — и они быстро начинают напоминать RAG…).

В этой статье мы разберём несколько практических примеров и покажем, как решаем распространённые задачи, а именно:

  • Работа со смешанными текстовыми и числовыми данными и причины, по которым они ставят простейшие RAG-системы в тупик: ключевые слова совпадают, а числа сами по себе не несут семантического смысла.

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

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

  • Когда полагаться на код и модели Pydantic: если важно дословно сохранить содержимое, для надёжности сочетайте вызовы LLM с пользовательским кодом и/или моделью Pydantic.

Создание специализированных RAG-решений

Основы

RAG-системы лежат в основе самых разных решений — от ботов поддержки до внутренних помощников по работе с базами знаний.

Обычно внутри происходит следующее:

  1. Разбиение исходных документов на фрагменты

  2. Преобразование каждого фрагмента в векторное представление

  3. Извлечение K наиболее подходящих фрагментов при обработке запроса

  4. Генерация ответа с учётом этих фрагментов

Популярные инструменты — LangChain, LlamaIndex и Filestore от OpenAI — делают эти этапы почти элементарными. Но в реальных конвейерах встречаются не только насыщенные текстом данные, и простейшие RAG-системы не всегда с ними справляются. Далее мы на конкретных примерах рассмотрим трудности при работе с данными и будем постепенно развивать решение по мере усложнения задачи.

Когда задача усложняется

  1. Когда данные состоят не только из текста (а это совсем не редкость)

Рассмотрим следующий фрагмент игровых данных:

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

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

Если бы у нас были только данные такого типа, проблемы бы не возникло: их всё ещё можно было бы находить по эмбеддингам немногих доступных описательных слов (или просто использовать преобразование текста в SQL). Но что, если этот фрагмент затерялся среди множества насыщенных текстом фрагментов, где встречаются те же слова? Например:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

Теперь представим, что нам нужно найти ответ на вопрос: «What is the attack range with Draconic Ascension?» Скорее всего, нужный фрагмент найти не удастся: он затеряется среди других фрагментов с теми же ключевыми словами.

Главная проблема в том, что мы не можем уверенно различать эти фрагменты, хотя они содержат разные типы информации по одной теме. Можно ли как-то обогатить или улучшить эти данные? Конечно можно:smile:

  1. Обогащайте данные с помощью кратких описаний — да, вы всё правильно прочитали

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

Для двух приведённых выше фрагментов можно создать примерно такие описания:

  1. Характеристики дальности, скорости и урона атаки (по умолчанию и с Draconic Ascension).

  2. Описание и подробности Draconic Ascension, включая условия активации, визуальные эффекты и историю мира.

Затем мы также дополняем запрос, чтобы «согласовать» его с описанием. Например, запрос «What is the attack range with Draconic Ascension?» мы преобразуем в «What is the statistics of attack range with Draconic Ascension?» Это особенно важно, когда поисковые запросы поступают от пользователей без технической подготовки, которые выражаются в ~~«свободной форме»~~ обычным человеческим языком. В конце концов, им не нужно знать, как работает RAG и как добиться максимальной точности и полноты поиска.

Схема, показывающая, как задача усложняется.

  1. Не вырывайте данные из контекста (да и в жизни это полезный совет)

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

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

Если придерживаться того же подхода, представьте запрос: «what is character X’s attack range?» С только что созданными описаниями нам пришлось бы полагаться на удачу и пытаться угадать, ведь они тоже выглядели бы очень похоже. Как же их различать?

Ответ прост: добавить контекст. Можно просто добавить во фрагмент данных ссылку на его исходный документ — например, в данном случае {”character”: “X”}. Тогда мы сможем точно найти нужные данные о персонаже X, даже если располагаем такими же данными о персонажах Y и Z.

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

  1. Этот фрагмент содержит подробные характеристики … персонажа X. В полном документе этот фрагмент показывает преимущество X в скорости атаки…

  2. Этот фрагмент содержит подробные характеристики … персонажа Y. В полном документе этот фрагмент показывает улучшенные характеристики Y при использовании её особой способности…

  3. Этот фрагмент содержит подробные характеристики … персонажа Z. В полном документе этот фрагмент показывает, что характеристики Z хорошо подходят для роли танка в командных матчах…

Для приведённого выше примера этот метод (отчасти вдохновлённый подходом Anthropic) может показаться избыточным. Однако он очень эффективен для фрагментов, которые легко неверно истолковать «вне контекста», а также предлагает единый подход ко всем фрагментам и помогает сохранить аккуратную архитектуру конвейера.

Схема, показывающая, как задача усложняется.

  1. Когда нужно быть ~~помешанным на контроле~~ предельно строгим

Обычно мы получаем цельные данные и разбиваем их на фрагменты для RAG-системы. В этом примере ситуация немного иная: данные уже разбиты, но неудачно. Это случайные части логически цельного фрагмента, которые необходимо снова объединить. Логический фрагмент — это содержимое, которое естественным образом должно оставаться единым целым, например подраздел документа или связный абзац.

Схема, показывающая, как задача усложняется.

Сначала мы попытались передать все данные в одном вызове LLM, попросить её сгруппировать их по своему усмотрению и вернуть объединённое содержимое. LLM должна неплохо с этим справиться, верно? И да и нет.

Здесь, как и в нескольких других случаях, мы обнаружили, что LLM склонны лениться и ненадёжны, когда требуется вернуть всё содержимое в точности, особенно при длинном контексте. И это вполне объяснимо. Но для этого сценария такой недостаток оказался критическим: нам требовалось дословно сохранить всё содержимое — без обобщений и пропусков каких-либо частей исходного материала. Нельзя было упустить ни одной детали.

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

Как же использовать сильные стороны LLM и при этом не зависеть от того, в чём она ненадёжна? Мы обратились к старому доброму другу — коду (то есть пользовательской функции Python). А ещё к «проще некуда» модели Pydantic. Вот наше решение:

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

  • Для каждого раздела спрашивать LLM: относится ли он к текущему логическому фрагменту? Ответ должен быть «да» или «нет» в соответствии с моделью Pydantic.

  • Если да — добавить раздел к фрагменту. Если нет — сохранить завершённый текущий логический фрагмент и начать новый с этого раздела.

Схема, показывающая, как задача усложняется.

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

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

Пользовательский код и функции вместе с моделями Pydantic позволяют получить предсказуемый и надёжный результат, не отказываясь от возможностей LLM.

Подведём итоги

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

Автор

Cynthia Yu