Подключение внешнего источника знаний (RAG)

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

Есть способ подключить внешний источник знаний к LLM, чтобы она справлялась с задачей лучше. В этом уроке вы разберёте, как это работает и на что обратить внимание.

Дообучение моделей и In-Context Learning

Существует два способа внести в модель новые знания:

1️⃣ Дообучение языковой модели на своём корпусе текстовых документов. Речь идёт о создании большого датасета (обычно десятки или сотни тысяч записей) и проведении дообучения (fine-tuning) исходной языковой модели. Это трудоёмкий процесс, который требует значительных вычислительных ресурсов и высокой квалификации. На практике дообучение редко помогает модели полноценно усвоить новые знания, поскольку объём оригинальных обучающих данных превышает размер дополнительных датасетов. Зато дообучение эффективно при адаптации модели под нужный формат или стиль ответа.

2️⃣ Добавление предметной информации в запрос (контекст) модели (In-Context Learning). В этом подходе исходная модель остаётся без изменений — необходимая информация добавляется в запрос. Используется формат вроде: Посмотри на контекст ниже и ответь на вопрос: <вопрос>. Контекст: <контекст>. Модель эффективно учитывает данные из контекста, но его объём — это ключевое ограничение.Сравнение двух подходов:

Возникает вопрос — можно ли преодолеть недостатки и добавить существенный объём знаний в модель без дообучения?

RAG-архитектура

💡АрхитектураRAG: Retrieval-Augmented Generation позволяет добавить существенный объём знаний в модель без дообучения. Она основана на In-Context Learning, но данные для добавления в контекст динамически выбираются из текстовой (или более сложной: графовой, иерархической) базы знаний с помощью алгоритма поиска.

Простейший конвейер RAG состоит из следующих шагов:1️⃣ Получение запроса от пользователя или от языковой модели.2️⃣ Поиск текстовых фрагментов в базе знаний, которые максимально соответствуют запросу.3️⃣ Вызов LLM для получения финального ответа на вопрос.В современных системах RAG чаще всего реализуется с использованием Function Calling:

Для реализации RAG-подхода необходимо решить несколько вопросов:

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

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

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

Дополнительную информацию можно представить в виде текстовой базы знаний и разбить её на небольшие файлы по смыслу.В дальнейших примерах файлы с расширением *.txt будут находиться в каталоге data/text-kb.💡 Примеры текстовых файлов и других материалов вы сможете найти в репозитории на GitHub

Как осуществляется поиск

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

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

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

Выбор метода зависит от задачи. Если нужно отвечать на широкий круг вопросов, эффективнее комбинировать подходы: добавлять в контекст и точные совпадения, и тексты, близкие по смыслу.💡 Существуют и более сложные варианты RAG, например Graph RAG. В нём база знаний строится как граф, где фрагменты связаны отношениями вроде целое – часть. Такой подход открывает доступ к расширенным алгоритмам поиска и помогает извлекать информацию глубже.

Семантический поиск и векторные базы знаний

В RAG обычно применяют семантический поиск на основе эмбеддингов. Для каждого фрагмента текста вычисляется смысловой вектор. Затем система сравнивает вектор запроса с векторами в базе и находит самые близкие по косинусному расстоянию — то есть те, что совпадают по смыслу. Для такого поиска используют векторные базы данных, например Chroma DB или Qdrant.Если нужно применить комбинированный поиск, подойдут решения вроде OpenSearch, особенно его облачная версия — Managed OpenSearch.

Но во многих случаях можно использовать встроенную в Responses API версию векторной базы данных. Далее расскажем, как это сделать по шагам.1️⃣ Создайте поисковый индекс:

vector_store = client.vector_stores.create(name='rag_store')   

💡 Здесь и далее переменная client — это объект OpenAI для работы с Responses API, который мы уже научились создавать ранее в курсе.2️⃣ Загрузите все файлы из текстовой базы знаний в облако и добавьте каждый файл в поисковый индекс:

for fn in glob('data/text-kb/*.txt'):
   # Загрузите файл в облако
   f = client.files.create(file=open(fn,'rb'),purpose='assistants')
   # Добавьте файл в векторное хранилище
   client.vector_stores.files.create(
        vector_store_id=vector_store.id, 
        file_id=f.id) 

При загрузке файла мы указали purpose='assistants', чтобы подчеркнуть, что файл будет использоваться в качестве файлового индекса. Существуют и другие возможные значения этого параметра, которые описаны в документации.3️⃣ После добавления файлов в хранилище запускается асинхронная индексация. Обычно она занимает несколько секунд, реже — десятки секунд. Если нужно вручную проверить, что процесс завершён, можно обратиться к объекту, который возвращает функция files.create.

Разбиение текста на фрагменты

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

  • Слишком короткие фрагменты не содержат завершённую мысль: контекста недостаточно, смысл неполный. Например, если описание техники выполнения упражнения отделить от его названия, ассистент не поймёт, к чему относится инструкция.
  • Слишком длинные фрагменты, которые включают несколько мыслей, затрудняют векторный поиск. Эмбеддинг формируется один на весь фрагмент, что снижает точность сопоставления.
  • Дополнительный фактор — эффективный размер контекста модели. Хотя современные LLM поддерживают контексты длиной 32 000 токенов и больше, качество ответа обычно падает при работе с очень длинными фрагментами.

Например, если у модели контекст на 8000 токенов, стоит зарезервировать около 2000 под промт и сам ответ. Тогда для базы знаний остаётся 6000 токенов. При использовании пяти фрагментов оптимальный размер каждого — примерно 1000 токенов (около 3000 символов).💡 Такое разбиение большой базы знаний на удобные фрагменты называют чанкованием.Существуют разные стратегии чанкования:1️⃣ Разбиение на фрагментывручную Позволяет контролировать содержание каждого фрагмента, сохранять завершённость мыслей и включать ключевые слова. При таком подходе важно следить, чтобы размер каждого фрагмента не превышал заданный порог.2️⃣ Автоматическое разбиениес перекрытием Текст делится по смысловым разделителям (например, по абзацам или предложениям) с учётом заданной длины. Перекрытие (overlap) помогает не потерять смысл, если граница разделения проходит через важную часть текста.

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

client.vector_stores.files.create(
        vector_store_id=vector_store.id, 
        file_id=file.id,
        chunking_strategy={
            "type": "static",
            "static" : { "max_chunk_size_tokens" : 1000, "chunk_overlap_tokens" : 100 }
        }
) 

3️⃣ Умное разбиение с учётом особенностей текста. Например, нужно добавить ассистенту информацию о пищевых добавках следующего вида:

Название добавкиКатегорияНазначениеДозаРейтинг
КреатинДля наращивания мышечной массы, силы и ускорения восстановленияУвеличивает физическую силу, ускоряет восстановление; увеличивает массу2‒20 г в день*****
ГлютаминДля наращивания мышечной массы, силы и ускорения восстановленияПредотвращает распад мышечной ткани; укрепляет иммунную систему5‒20 г в день*****

(В этой таблице представлены только первые две строки в качестве примера)Поскольку языковые модели работают с текстовыми данными, табличку лучше всего представить в формате Markdown:

| Название | Категория                      | Назначение                                                              | Доза          | Рейтинг |
|----------|--------------------------------|-------------------------------------------------------------------------|---------------|---------|
| Креатин  | Для наращивания мышечной массы | Увеличивает физическую силу, ускоряет восстановление; увеличивает массу | 2-20 г в день | *****   |
| Глютамин | Для наращивания мышечной массы | Предотвращает распад мышечной ткани; укрепляет иммунную систему         | 5-20 г в день | *****   | 

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

with open("data/additives.md", encoding="utf-8") as f:
    additives = f.readlines()

# Отделите заголовок таблицы
header = additives[:2]

chunk_size = 2048 # Размер чанка в символах
s = header.copy()
i = 0
for x in additives[2:]:
    s.append(x)
    if len("".join(s)) > chunk_size:
        f = client.files.create(
            purpose="assistants",
            file = (f'table_{i}.txt',io.BytesIO("".join(s).encode("utf-8")),'text/markdown')
        )
        client.vector_stores.files.create(file_id=f.id, vector_store_id=vector_store.id)
        i+=1
        s = header.copy() 

💡 Responses API поддерживает загрузку документов в разных форматах — не только текстовых. Например, можно загрузить файлы DOCX или PDF. Но внутри облака они всё равно преобразуются в текст, поэтому часть форматирования, изображения или таблицы могут потеряться. Лучше заранее преобразовать нетекстовые документы в текст самостоятельно, проверить результат и при необходимости исправить ошибки. После этого текст можно загрузить в облако и провести чанкование.Кроме того, стоит вручную разбивать текст на небольшие смысловые фрагменты и использовать Markdown-разметку для выделения заголовков и других важных элементов структуры.Чем лучше подготовлена текстовая база знаний для RAG, тем точнее и полезнее будут ответы ассистента.Операция добавления файла в индекс быстро завершается, но сам процесс индексирования на этом не останавливается. Время, необходимое для построения индекса, зависит от объёма документов, их типа и сложности, а также общей загруженности сервиса. Пользоваться файловым поиском можно лишь после окончания индексирования. Если вам необходимо явно дождаться завершения индексирования, это можно сделать программно – соответствующий пример содержится в документации.

Добавление инструмента поиска в ассистента

Теперь нужно добавить к языковой модели инструмент поиска. Для этого опишите инструмент с требуемыми параметрами и передайте его в параметре tools:

search_tool = {
    "type" : "file_search",
    "vector_store_ids" : [vector_store.id],
    "max_num_results" : 5
}

res = client.responses.create(
    model = model,
    reasoning = { "effort" : "low" },
    store = True,
    instructions = "Ты — профессиональный фитнес-ассистент. Отвечай как энергичный молодой человек со спортивным задором",
    input = "С чего начать тренировки в зале?",
    tools = [search_tool],
    tool_choice = 'required'
) 

Также был добавлен параметр tool_choice='required' для гарантии, что инструмент поиска всегда будет вызван. В настоящий момент большинство моделей в AI Studio не поддерживают указание tool_choiceи игнорируют эту установку, поэтому стоит также использовать дополнительный вариант — указать требование вызвать поиск прямо в системном промте.В поле res.output формируются два объекта:

  • ResponseFileSearchToolCall — первоначальный вызов инструмента поиска. В поле queries хранятся перефразированные моделью поисковые запросы к базе знаний.
  • ResponseOutputMessage — текстовый ответ модели. В этом объекте доступно поле annotations — список файлов, включённых в контекст модели. Их можно посмотреть через код:
o = res.output[-1]
for x in o.content[0].annotations:
  if x.type == "file_citation":
    print(f"{x.filename}, idx={x.index}") 

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

Оптимизация RAG

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

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

Автоматизированные методы оценки позволяют проводить больше экспериментов, тестировать параметры и добавлять этапы оптимизации в RAG-конвейер. Это ускоряет работу и улучшает итоговое качество.

Расширение RAG-конвейера

Простейший RAG-конвейер состоит из поиска релевантных фрагментов и формирования ответа с помощью промта. В этот конвейер можно внести ещё два промежуточных этапа:1️⃣ Pre-Retrieval — дообработка запроса перед передачей в поиск. Основные приёмы:

  • Суммаризация, или выделение сути. Если запрос слишком длинный, его сокращают до главного смысла. Так эмбеддинги будут точнее, поиск эффективнее, а опечатки меньше мешают при поиске по ключевым словам. Во встроенной реализации поискового инструмента в Yandex AI Studio Responses API уже заложена суммаризация и перефразировка запроса пользователя.
  • Классификация по темам. Запросы можно разделять на несколько фиксированных категорий. Это ограничивает поиск определёнными разделами базы знаний и повышает точность. Полезно, когда есть разные классы обращений.
  • Перефразировка контекста. Работает, если запрос связан с контекстом диалога. Например, пользователь спрашивает: Какое самое эффективное упражнение для потери веса?, а затем: А для набора мышц?. LLM перефразирует второй вопрос так, чтобы восстановить полный контекст.

2️⃣ Post-Retriveal — обработка результатов поиска перед их помещением в контекст запроса.

  • Суммаризация. Позволяет уместить в контекст больше информации, чем влезает напрямую. Иногда суммаризацию делают сразу при индексировании базы.
  • Ранжирование. Фрагменты располагаются так, чтобы самые релевантные были в приоритете для модели.

Расширение RAG-конвейера с помощью Pre- и Post-Retriever💡 Встроенный поиск подходит для типовых задач, но почти не позволяет настраивать RAG-конвейер под конкретные нужды. В более сложных случаях лучше реализовать конвейер самостоятельно — через Function Calling или без него, направляя запросы напрямую в поисковую систему. Для этого подойдут специализированные фреймворки, например LangChain.

Источник: https://practicum.yandex.ru/trainer/yc-ml-aiagents/lesson/6e021361-29cc-46d9-8689-058861601a78/

Рубрики: Uncategorized

0 комментариев

Добавить комментарий

Заполнитель аватара

Ваш адрес email не будет опубликован.