Как стать автором
Обновить

LlamaIndex: создаем чат-бота без боли и страданий. Часть 2

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров5.9K

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

Современные чат-боты становятся все более интеллектуальными, однако, чтобы действительно раскрыть их потенциал, необходимо обеспечить их доступом к обширным базам данных и документации. Именно здесь и кроется основное преимущество llamaindex.

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

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

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

Создание синтетических данных

Мы будем строить базу данных для учебного чат-бота на основе договоров в формате pdf. Я возьму реальный пример, с которым я столкнулся в моей практике - договоры ПИР (проектные и изыскательские работы). Конечно, настоящие документы я, пожалуй, воздержусь здесь публиковать, поэтому создам синтетические примеры с помощью чатгпт. После генерации текста договора, я сохраняю его в формате pdf для большего соответствия реальной ситуации.

Вот как выглядит один из примеров:

nlp_daily/llamaindex/1.png
Образец договора

Работа с PDF в LlamaIndex

LlamaIndex предоставляет инструменты для работы с различными форматами данных, включая pdf. Для этого используется специальный коннектор. Коннектор в контексте llamaIndex — это инструмент, который позволяет интегрировать и обрабатывать данные из различных источников или форматов. Это своего рода адаптер, который обеспечивает совместимость между llamaIndex и внешними данными.

Например SimpleDirectoryReader позволяет загружать данные в форматах: .pdf.jpg.png.docx. Для чтения pdf надо будет еще дополнительно поставить пакет pypdf

import os
from llama_index import SimpleDirectoryReader

# Не забываем указать ключ к апи
os.environ['OPENAI_API_KEY'] = 'sk-L0xrKrmzb2KufE*'

# Создаем объект для работы с PDF
reader = SimpleDirectoryReader(input_dir='./pir_samples/')

# Загружаем наши документы
docs = reader.load_data()
print(f'Loaded {len(docs)} docs')
nlp_daily/llamaindex/2.png
Загружаем файлы

На самом деле я подгрузил только 5 договоров, но некоторые разбились на 2 страницы:

nlp_daily/llamaindex/3.png
Уникальные названия

Коннекторов данных в ламаиндекс существует огромное количество, можно подгружать данные из википедии, жиры, даже из youtube. Все коннекторы можно поcмотреть здесь

Разбиение документов на ноды

После того как мы загрузили наши документы в llamaIndex, следующий шаг — это разбиение их на ноды.

Что такое нода?
Нода — это базовая единица информации в llamaIndex. Каждая нода представляет собой фрагмент текста из документа, который может быть использован для ответа на запрос пользователя. Например, если у нас есть договор, то каждый пункт или подраздел этого договора может быть представлен в виде отдельной ноды.

Зачем разбивать документы на ноды?
Разбиение документов на ноды позволяет улучшить качество и точность ответов чат-бота. Вместо того чтобы анализировать весь документ целиком, фреймворк может быстро найти и использовать конкретную ноду, которая наиболее релевантна запросу пользователя.

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

  1. Определить ключевые разделы документа

    Обычно в документах есть четко выделенные разделы или подразделы. В контексте договора ПИР, это могут быть пункты, например такие как "Обязанности Сторон", "Стоимость работ" итд.

  2. Размер ноды имеет значение

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

  3. Использовать метаданные

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

Для начала попробуем самое простое деление на ноды:

from llama_index.node_parser import SimpleNodeParser

# Cоздаем парсер
parser = SimpleNodeParser()

# Разбиваем на ноды
nodes = parser.get_nodes_from_documents(docs)

print(len(nodes))
nlp_daily/llamaindex/4.png
Количество нод
from llama_index import GPTVectorStoreIndex

# Создаем индекс
index = GPTVectorStoreIndex([])

# Индексируем ноды
index.insert_nodes(nodes)

# Создаем движок запросов
engine = index.as_query_engine()

# Пробуем задать вопрос
response = engine.query('Какова сумма договора с ТатарГеоCтрой?')

print(response)
nlp_daily/llamaindex/5.png
Ответ про сумму договора

Нам соврали, тк в тексте фигурирует другая сумма - 900 тысяч, попробуем задать еще один вопрос:

6.png
Ответ про контрагента

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

7.png
Используемые ноды

Первая нода была выбрана правильно, но вот вторая взята из другого договора, поэтому и сумма получилась другой.

Добавляем метаданные в ноды.

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

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

  • TitleExtractor: этот класс предназначен для извлечения заголовков, особенно полезен для длинных документов. Он извлекает поле метаданных document_title

  • KeywordExtractor: извлекает ключевые слова на уровне ноды.

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

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

Для начала попробуем самое простое решение - добавим заголовок документа в ноды:

from llama_index.node_parser.extractors import (
  MetadataExtractor,
  TitleExtractor
)

# Создаем тип сборщика метаинформации
metadata_extractor = MetadataExtractor(
  extractors=[
  TitleExtractor(nodes=5) # указываем количество нод с одним title
  ]
)

# Создаем парсер для нод с нужным свойством
node_parser = SimpleNodeParser(
  metadata_extractor=metadata_extractor
)

# Получаем ноды
nodes_with_meta = node_parser.get_nodes_from_documents(docs)

print(nodes_with_meta[0])
8.png
Метаданные ноды

Ну а теперь попробуем еще раз задать тот же самый вопрос:

new_index = GPTVectorStoreIndex(nodes_with_meta)

new_engine = new_index.as_query_engine()

response = new_engine.query('Какова сумма договора с ТатарГеоCтрой?')

print(response)
9.png

Сработало! Попробуем добавить еще больше метаинформации:

from llama_index.node_parser.extractors import KeywordExtractor

metadata_extractor = MetadataExtractor(
  extractors=[
  TitleExtractor(nodes=5),
  KeywordExtractor(keywords=10) # задаем количество ключевых слов
  ]
)

node_parser = SimpleNodeParser(
  metadata_extractor=metadata_extractor,
)

nodes_with_meta = node_parser.get_nodes_from_documents(docs)

print(nodes_with_meta[0].metadata)
10.png
Метаданные с ключевыми словами

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

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

Спасибо за внимание!

Пишу про AI и NLP в телеграм.

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии9

Публикации

Истории

Работа

Data Scientist
78 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань