Советы

LlamaIndex: как разбивать документ на ноды

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

Часть 3

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

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

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

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

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

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

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

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

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

После генерации текста договора я сохраняю его в формате pdf для большего соответствия реальной ситуации.

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

LlamaIndex: как разбивать документ на ноды Образец договора Интересно, перейти к каналу

Работа с 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')

LlamaIndex: как разбивать документ на ноды Загружаем файлы

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

LlamaIndex: как разбивать документ на ноды Уникальные названия

Коннекторов данных в ламаиндекс существует огромное количество, можно подгружать данные из Википедии, Jira, даже из 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))

LlamaIndex: как разбивать документ на ноды Количество нод

Теперь мы можем проиндексировать наши документы

from llama_index import GPTVectorStoreIndex

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

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

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

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

print(response)

LlamaIndex: как разбивать документ на ноды Ответ про сумму договора

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

LlamaIndex: как разбивать документ на ноды Ответ про контрагента

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

LlamaIndex: как разбивать документ на ноды Используемые ноды

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

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

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

  1. TitleExtractor: этот класс предназначен для извлечения заголовков, особенно полезен для длинных документов. Он извлекает поле метаданных document_title
  2. KeywordExtractor: извлекает ключевые слова на уровне ноды.
  3. QuestionsAnsweredExtractor: генерирует вопросы, на которые может ответить данный узел.
  4. 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])

LlamaIndex: как разбивать документ на ноды Метаданные ноды

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

Правильный ответ new_index = GPTVectorStoreIndex(nodes_with_meta)
new_engine = new_index.as_query_engine()

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

print(response)

LlamaIndex: как разбивать документ на ноды

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

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)

LlamaIndex: как разбивать документ на ноды Метаданные с ключевыми словами

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

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

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

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

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

Продолжаем изучать фреймворк для создания AI-ботов. В этой части узнаем про тонкости индексирования собственной базы документов.

← Часть 1

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

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

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

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

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

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

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

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

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

Читайте также:  Список List

После генерации текста договора я сохраняю его в формате pdf для большего соответствия реальной ситуации.

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

LlamaIndex: как разбивать документ на нодыОбразец договора

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека data scientist’а»

Интересно, перейти к каналу

Работа с 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')

LlamaIndex: как разбивать документ на нодыЗагружаем файлы

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

LlamaIndex: как разбивать документ на нодыУникальные названия

Коннекторов данных в ламаиндекс существует огромное количество, можно подгружать данные из Википедии, Jira, даже из 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))

LlamaIndex: как разбивать документ на нодыКоличество нод

Теперь мы можем проиндексировать наши документы

from llama_index import GPTVectorStoreIndex

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

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

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

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

print(response)

LlamaIndex: как разбивать документ на нодыОтвет про сумму договора

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

LlamaIndex: как разбивать документ на нодыОтвет про контрагента

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

LlamaIndex: как разбивать документ на нодыИспользуемые ноды

Статья по теме

???? Промпт-инжиниринг: как правильно писать запросы нейросетям

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

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

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

  1. TitleExtractor: этот класс предназначен для извлечения заголовков, особенно полезен для длинных документов. Он извлекает поле метаданных document_title
  2. KeywordExtractor: извлекает ключевые слова на уровне ноды.
  3. QuestionsAnsweredExtractor: генерирует вопросы, на которые может ответить данный узел.
  4. 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])

LlamaIndex: как разбивать документ на нодыМетаданные ноды

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

Правильный ответ new_index = GPTVectorStoreIndex(nodes_with_meta)
new_engine = new_index.as_query_engine()

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

print(response)

LlamaIndex: как разбивать документ на ноды

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

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)

LlamaIndex: как разбивать документ на нодыМетаданные с ключевыми словами

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

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

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

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

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

Python Magazine

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

LlamaIndex: как разбивать документ на ноды

Привет исследователям AI!

Меня зовут Марк Конаков, я развиваю NLP в компании Самолет. Мы разрабатываем чат-боты, занимаемся мэтчингом, строим модели для анализа звонков и многим другим. NLP – это только одна из голов DS-гидры Самолета, ребята из моей команды крутят классические модельки на табличках, строят рекомендательные системы, есть направление и по CV.

В последнее время я много писал о Langchain — мощном инструменте для создания приложений с использованием больших языковых моделей.

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

Lamaindex – это фреймворк, который предоставляет удобные инструменты для работы с большими языковыми моделями. Он позволяет индексировать большие объемы текстовых данных и выполнять по ним эффективный поиск. Это особенно полезно при работе с большими языковыми моделями, которые могут генерировать текст на основе предоставленного контекста.

https://www.youtube.com/watch?v=XGBQ_f-Yy48\u0026pp=ygVETGxhbWFJbmRleDog0LrQsNC6INGA0LDQt9Cx0LjQstCw0YLRjCDQtNC-0LrRg9C80LXQvdGCINC90LAg0L3QvtC00Ys%3D

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

Я планирую написать несколько статей об этом крутом инструменте. Сегодня будет первое знакомство и helloworld примеры.

Установка и настройка окружения

Прежде чем мы начнем, нам нужно установить llamaindex и подготовить наше виртуальное окружение:

  • Создание виртуального окружения с помощью conda: Для создания нового окружения с помощью conda выполните следующую команду:

conda create —name llamaindex_env python=3.10

  • Активация виртуального окружения: После создания виртуального окружения, его нужно активировать:

conda activate llamaindex_env

  • Установка llamaindex: После активации виртуального окружения, мы можем установить llamaindex с помощью pip:

pip install llama-index

  • Регистрация ядра в Jupyter Notebook: Если вы планируете использовать Jupyter Notebook, вам нужно зарегистрировать ваше новое виртуальное окружение как ядро:

pip install jupyterlab ipykernel
python -m ipykernel install —user —name=llamaindex_env

Основные концепции llamaindex

llamaindex основан на концепции Retrieval Augmented Generation (RAG). В основе RAG лежит идея о том, что для генерации ответа на запрос можно использовать не только контекст запроса, но и дополнительные данные, найденные по индексу. Это позволяет генерировать более точные и информативные ответы.

Схема работы llamaindex

В рамках llamaindex основные элементы RAG представлены следующими модулями:

  1. Data Connectors: Это модули, которые отвечают за загрузку и обработку данных. Они позволяют загрузить данные, создать из них узлы и построить индекс.
  2. Retrievers: Это модули, которые отвечают за поиск по индексу. Они позволяют выполнить поиск по индексу и получить наиболее релевантные узлы.
  3. Query Engines: Это модули, которые отвечают за генерацию ответа на запрос. Они используют информацию, полученную от retrievers, для генерации ответа.
Читайте также:  ТОП-27 плагинов для Visual Studio Code в 2023

Быстрый старт

Попробуем сразу ворваться в мир llamaindex и оценить возможности

Для начала создадим папку data и поместим туда один файл со стихотворением Пушкина лукоморье:

mkdir data
touch lukomorie.txt

Теперь можно создать список документов на основе нашего файла

from llama_index import VectorStoreIndex, SimpleDirectoryReader

documents = SimpleDirectoryReader('data').load_data()

LlamaIndex: как разбивать документ на ноды

Как и следовало ожидать, в списке у нас один документ с типом llama_index.schema.Document

Теперь попробуем проиндексировать наш документ в векторной базе, за это отвечает класс ChatGPTRetrievalPluginIndex В нем есть метод для создания индекса на основе списка документов from_documents. Векторных баз доступно множество:

GPTSimpleVectorIndex

GPTFaissIndex

GPTWeaviateIndex

GPTPineconeIndex

GPTQdrantIndex

GPTChromaIndex

GPTMilvusIndex

GPTDeepLakeIndex

GPTMyScaleIndex

Пока используем простой вариант GPTSimpleVectorIndex, но предварительно надо задать ключ API Open AI для использования модели эмбеддингов.

import os
os.environ['OPENAI_API_KEY'] = 'sk-L0xrKrmzb2KufEIi0***'

index = GPTVectorStoreIndex.from_documents(documents)
# переводим индекс в режим поискового движка
query_engine = index.as_query_engine()

# запрос к базе знаний
response = query_engine.query(
'сколько богатырей выходят из моря?'
)
print(response.response)

LlamaIndex: как разбивать документ на ноды

Запрос под капотом

В llamaIndex, query_engine — это объект, который управляет процессом поиска в индексе. Индекс представляет собой структуру данных, которая хранит «узлы» (Nodes). Узел соответствует фрагменту текста из документа. LlamaIndex принимает объекты документов и внутренне разбивает их на объекты узлов.

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

Когда вы вызываете query_engine.query('ваш запрос'), вы задаете вопрос ('ваш запрос'), и query_engine ищет наиболее релевантные узлы в индексе, которые могут ответить на ваш вопрос. Это делается путем сравнения вашего запроса с каждым узлом в индексе и выбора наиболее релевантных узлов.

LlamaIndex: как разбивать документ на ноды

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

В результате query_engine.query('ваш запрос') возвращает ответ на ваш запрос, который сгенерирован на основе наиболее релевантных узлов.

LlamaIndex: как разбивать документ на ноды

В LlamaIndex есть разные типы индексов, каждый из которых работает немного по-разному. Например, индекс векторного хранилища (VectorStoreIndex) хранит каждый узел и соответствующее ему векторное представление, и при поиске возвращает топ-k наиболее похожих узлов.

Другой тип индекса, индекс таблицы ключевых слов (KeywordTableIndex), извлекает ключевые слова из каждого узла и строит отображение от каждого ключевого слова к соответствующим узлам этого ключевого слова.

При поиске он извлекает релевантные ключевые слова из запроса и ищет соответствующие узлы.

Генерация ответа

После извлечения релевантной ноды данные попадают в Response Synthesis. Этот процесс берет данные из выбранных узлов и использует языковую модель для генерации окончательного ответа. Это может включать в себя и объединение информации из разных узлов, переформулирование информации, чтобы она была более понятной и т. д.

Процесс Response Synthesis может варьироваться в зависимости от конкретной конфигурации.

  1. default: В этом режиме для каждого узла делается отдельный вызов языковой модели. Это позволяет создавать и уточнять ответ, последовательно проходя через каждый узел. Этот режим хорош для более подробных ответов.
  2. compact: В этом режиме во время каждого вызова языковой модели «упаковывается» максимальное количество текстовых блоков узлов, которые могут поместиться в пределах максимального размера запроса. Если блоков слишком много, чтобы поместить их в один запрос, ответ «создается и уточняется», проходя через несколько запросов.
  3. tree_summarize: В этом режиме, учитывая набор узлов и запрос, рекурсивно строится дерево и в качестве ответа возвращается корневой узел.

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

Модифицируем ответ

Попробуем теперь модифицировать ответ на наш вопрос. Для этого нам понадобится инструмент ServiceContext.

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

  1. llm_predictor: Это объект LLMPredictor, который используется для предсказания ответов языковой моделью.
  2. prompt_helper: Это объект PromptHelper, который помогает формировать запросы к языковой модели.
  3. embed_model: Это объект BaseEmbedding, который используется для создания векторных представлений узлов.
  4. node_parser: Это объект NodeParser, который используется для разбиения документов на узлы.
  5. llama_logger: Это объект LlamaLogger, который используется для логирования различных аспектов работы LlamaIndex.
  6. chunk_size_limit: Это ограничение на размер блока, которое определяет максимальное количество токенов, которые могут быть включены в один запрос к языковой модели.

Вы можете создать ServiceContext, используя метод from_defaults.

from llama_index import ServiceContext
from llama_index.llms import OpenAI

# Изменим модель
llm = OpenAI(temperature=0, model='gpt-3.5-turbo')

service_context = ServiceContext.from_defaults(llm=llm)

index = GPTVectorStoreIndex.from_documents(
documents,
service_context=service_context
)

engine = index.as_query_engine()

response = engine.query('сколько богатырей выходят из моря?')

print(response)

LlamaIndex: как разбивать документ на ноды

Видно, что ответ модели немного изменился. Вывод также можно менять через параметр temperature при инициализации LLM.

LlamaIndex: как разбивать документ на нодыLlamaIndex: как разбивать документ на нодыИзменяем температуру LLM

Заключение

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

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

В качестве учебного кейса рассмотрим создание чат-бота по собственной базе договоров.

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

LlamaIndex Usage Pattern

The general usage pattern of LlamaIndex is as follows:

  1. Load in documents (either manually, or through a data loader)

  2. Parse the Documents into Nodes

  3. Construct Index (from Nodes or Documents)

  4. [Optional, Advanced] Building indices on top of other indices

  5. Query the index

The first step is to load in data. This data is represented in the form of Document objects.
We provide a variety of data loaders which will load in Documents
through the load_data function, e.g.:

from llama_index import SimpleDirectoryReader documents = SimpleDirectoryReader('data').load_data()

You can also choose to construct documents manually. LlamaIndex exposes the Document struct.

from llama_index import Document text_list = [text1, text2, …]
documents = [Document(t) for t in text_list]

A Document represents a lightweight container around the data source. You can now choose to proceed with one of the
following steps:

  1. Feed the Document object directly into the index (see section 3).

  2. First convert the Document into Node objects (see section 2).

The next step is to parse these Document objects into Node objects. Nodes represent “chunks” of source Documents,
whether that is a text chunk, an image, or more. They also contain metadata and relationship information
with other nodes and index structures.

Nodes are a first-class citizen in LlamaIndex. You can choose to define Nodes and all its attributes directly. You may also choose to “parse” source Documents into Nodes through our NodeParser classes.

For instance, you can do

from llama_index.node_parser import SimpleNodeParser parser = SimpleNodeParser() nodes = parser.get_nodes_from_documents(documents)

You can also choose to construct Node objects manually and skip the first section. For instance,

from llama_index.data_structs.node_v2 import Node, DocumentRelationship node1 = Node(text=»», doc_id=»»)
node2 = Node(text=»», doc_id=»»)
# set relationships
node1.relationships[DocumentRelationship.NEXT] = node2.get_doc_id()
node2.relationships[DocumentRelationship.PREVIOUS] = node1.get_doc_id()

We can now build an index over these Document objects. The simplest high-level abstraction is to load-in the Document objects during index initialization (this is relevant if you came directly from step 1 and skipped step 2).

from llama_index import GPTSimpleVectorIndex index = GPTSimpleVectorIndex.from_documents(documents)

You can also choose to build an index over a set of Node objects directly (this is a continuation of step 2).

from llama_index import GPTSimpleVectorIndex index = GPTSimpleVectorIndex(nodes)

Depending on which index you use, LlamaIndex may make LLM calls in order to build the index.

If you have multiple Node objects defined, and wish to share these Node
objects across multiple index structures, you can do that. Simply
define a DocumentStore object, add the Node objects to the DocumentStore,
and pass the DocumentStore around.

from gpt_index.docstore import SimpleDocumentStore docstore = SimpleDocumentStore()
docstore.add_documents(nodes) index1 = GPTSimpleVectorIndex(nodes, docstore=docstore)
index2 = GPTListIndex(nodes, docstore=docstore)

NOTE: If the docstore argument isn’t specified, then it is implicitly
created for each index during index construction. You can access the docstore
associated with a given index through index.docstore.

You can also take advantage of the insert capability of indices to insert Document objects
one at a time instead of during index construction.

from llama_index import GPTSimpleVectorIndex index = GPTSimpleVectorIndex([])
for doc in documents: index.insert(doc)

If you want to insert nodes on directly you can use insert_nodes function
instead.

from llama_index import GPTSimpleVectorIndex # nodes: Sequence[Node]
index = GPTSimpleVectorIndex([])
index.insert_nodes(nodes)

See the Update Index How-To for details and an example notebook.

By default, we use OpenAI’s text-davinci-003 model. You may choose to use another LLM when constructing
an index.

from llama_index import LLMPredictor, GPTSimpleVectorIndex, PromptHelper, ServiceContext
from langchain import OpenAI … # define LLM
llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name=»text-davinci-003″)) # define prompt helper
# set maximum input size
max_input_size = 4096
# set number of output tokens
num_output = 256
# set maximum chunk overlap
max_chunk_overlap = 20
prompt_helper = PromptHelper(max_input_size, num_output, max_chunk_overlap) service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper) index = GPTSimpleVectorIndex.from_documents( documents, service_context=service_context
)

See the Custom LLM’s How-To for more details.

Читайте также:  Создаем API на Docker, Node.js, Nginx, Postgres

Depending on the index used, we used default prompt templates for constructing the index (and also insertion/querying).
See Custom Prompts How-To for more details on how to customize your prompt.

For embedding-based indices, you can choose to pass in a custom embedding model. See
Custom Embeddings How-To for more details.

Creating an index, inserting to an index, and querying an index may use tokens. We can track
token usage through the outputs of these operations. When running operations,
the token usage will be printed.
You can also fetch the token usage through index.llm_predictor.last_token_usage.
See Cost Predictor How-To for more details.

To save to disk and load from disk, do

# save to disk
index.save_to_disk('index.json')
# load from disk
index = GPTSimpleVectorIndex.load_from_disk('index.json')

NOTE: If you had initialized the index with a custom
ServiceContext object, you will also need to pass in the same
ServiceContext during load_from_disk.

service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor) # when first building the index
index = GPTSimpleVectorIndex.from_documents(documents, service_context=service_context) … # when loading the index from disk
index = GPTSimpleVectorIndex.load_from_disk(«index.json», service_context=service_context)

You can build indices on top of other indices!

from llama_index import GPTSimpleVectorIndex, GPTListIndex index1 = GPTSimpleVectorIndex.from_documents(documents1)
index2 = GPTSimpleVectorIndex.from_documents(documents2) # Set summary text
# you can set the summary manually, or you can
# generate the summary itself using LlamaIndex
index1.set_text(«summary1»)
index2.set_text(«summary2») index3 = GPTListIndex([index1, index2])

Composability gives you greater power in indexing your heterogeneous sources of data. For a discussion on relevant use cases,
see our Query Use Cases. For technical details and examples, see our Composability How-To.

After building the index, you can now query it. Note that a “query” is simply an input to an LLM —
this means that you can use the index for question-answering, but you can also do more than that!

To start, you can query an index without specifying any additional keyword arguments, as follows:

response = index.query(«What did the author do growing up?»)
print(response) response = index.query(«Write an email to the user given their background information.»)
print(response)

However, you also have a variety of keyword arguments at your disposal, depending on the type of
index being used. A full treatment of all the index-dependent query keyword arguments can be
found here.

An index can have a variety of query modes. For instance, you can choose to specify
mode=»default» or mode=»embedding» for a list index. mode=»default» will a
create and refine an answer sequentially through the nodes of the list.
mode=»embedding» will synthesize an answer by fetching the top-k
nodes by embedding similarity.

index = GPTListIndex.from_documents(documents)
# mode=»default»
response = index.query(«What did the author do growing up?», mode=»default»)
# mode=»embedding»
response = index.query(«What did the author do growing up?», mode=»embedding»)

The full set of modes per index are documented in the Query Reference.

Note: This option is not available/utilized in GPTTreeIndex.

An index can also have the following response modes through response_mode:

  • default: For the given index, “create and refine” an answer by sequentially going through each Node;
    make a separate LLM call per Node. Good for more detailed answers.
  • compact: For the given index, “compact” the prompt during each LLM call by stuffing as
    many Node text chunks that can fit within the maximum prompt size. If there are
    too many chunks to stuff in one prompt, “create and refine” an answer by going through
    multiple prompts.
  • tree_summarize: Given a set of Nodes and the query, recursively construct a tree
    and return the root node as the response. Good for summarization purposes.

index = GPTListIndex.from_documents(documents)
# mode=»default»
response = index.query(«What did the author do growing up?», response_mode=»default»)
# mode=»compact»
response = index.query(«What did the author do growing up?», response_mode=»compact»)
# mode=»tree_summarize»
response = index.query(«What did the author do growing up?», response_mode=»tree_summarize»)

You can set required_keywords and exclude_keywords on most of our indices (the only exclusion is the GPTTreeIndex). This will preemptively filter out nodes that do not contain required_keywords or contain exclude_keywords, reducing the search space
and hence time/number of LLM calls/cost.

index.query( «What did the author do after Y Combinator?», required_keywords=[«Combinator»], exclude_keywords=[«Italy»]
)

The object returned is a Response object.
The object contains both the response text as well as the “sources” of the response:

response = index.query(«») # get response
# response.response
str(response) # get sources
response.source_nodes
# formatted sources
response.get_formatted_sources()

LlamaIndex: как разбивать документ на ноды

Basic Usage Pattern

  1. Load in documents (either manually, or through a data loader)

  2. Parse the Documents into Nodes

  3. Construct Index (from Nodes or Documents)

  4. [Optional, Advanced] Building indices on top of other indices

  5. Query the index

  6. Parsing the response

The first step is to load in data. This data is represented in the form of Document objects.
We provide a variety of data loaders which will load in Documents
through the load_data function, e.g.:

from llama_index import SimpleDirectoryReader documents = SimpleDirectoryReader('./data').load_data()

You can also choose to construct documents manually. LlamaIndex exposes the Document struct.

from llama_index import Document text_list = [text1, text2, …]
documents = [Document(text=t) for t in text_list]

A Document represents a lightweight container around the data source. You can now choose to proceed with one of the
following steps:

  1. Feed the Document object directly into the index (see section 3).

  2. First convert the Document into Node objects (see section 2).

The next step is to parse these Document objects into Node objects. Nodes represent “chunks” of source Documents,
whether that is a text chunk, an image, or more. They also contain metadata and relationship information
with other nodes and index structures.

Nodes are a first-class citizen in LlamaIndex. You can choose to define Nodes and all its attributes directly. You may also choose to “parse” source Documents into Nodes through our NodeParser classes.

For instance, you can do

from llama_index.node_parser import SimpleNodeParser parser = SimpleNodeParser.from_defaults() nodes = parser.get_nodes_from_documents(documents)

You can also choose to construct Node objects manually and skip the first section. For instance,

from llama_index.schema import TextNode, NodeRelationship, RelatedNodeInfo node1 = TextNode(text=»», id_=»»)
node2 = TextNode(text=»», id_=»»)
# set relationships
node1.relationships[NodeRelationship.NEXT] = RelatedNodeInfo(node_id=node2.node_id)
node2.relationships[NodeRelationship.PREVIOUS] = RelatedNodeInfo(node_id=node1.node_id)
nodes = [node1, node2]

The RelatedNodeInfo class can also store additional metadata if needed:

node2.relationships[NodeRelationship.PARENT] = RelatedNodeInfo(node_id=node1.node_id, metadata={«key»: «val»})

We can now build an index over these Document objects. The simplest high-level abstraction is to load-in the Document objects during index initialization (this is relevant if you came directly from step 1 and skipped step 2).

from_documents also takes an optional argument show_progress. Set it to True to display a progress bar during index construction.

from llama_index import VectorStoreIndex index = VectorStoreIndex.from_documents(documents)

You can also choose to build an index over a set of Node objects directly (this is a continuation of step 2).

from llama_index import VectorStoreIndex index = VectorStoreIndex(nodes)

Depending on which index you use, LlamaIndex may make LLM calls in order to build the index.

If you have multiple Node objects defined, and wish to share these Node
objects across multiple index structures, you can do that.
Simply instantiate a StorageContext object,
add the Node objects to the underlying DocumentStore,
and pass the StorageContext around.

from llama_index import StorageContext storage_context = StorageContext.from_defaults()
storage_context.docstore.add_documents(nodes) index1 = VectorStoreIndex(nodes, storage_context=storage_context)
index2 = SummaryIndex(nodes, storage_context=storage_context)

NOTE: If the storage_context argument isn’t specified, then it is implicitly
created for each index during index construction. You can access the docstore
associated with a given index through index.storage_context.

You can also take advantage of the insert capability of indices to insert Document objects
one at a time instead of during index construction.

from llama_index import VectorStoreIndex index = VectorStoreIndex([])
for doc in documents: index.insert(doc)

If you want to insert nodes on directly you can use insert_nodes function
instead.

from llama_index import VectorStoreIndex # nodes: Sequence[Node]
index = VectorStoreIndex([])
index.insert_nodes(nodes)

See the Document Management How-To for more details on managing documents and an example notebook.

When creating documents, you can also attach useful metadata. Any metadata added to a document will be copied to the nodes that get created from their respective source document.

document = Document( text='text', metadata={ 'filename': '', 'category': '' }
)

More information and approaches to this are discussed in the section Customizing Documents.

By default, we use OpenAI’s text-davinci-003 model. You may choose to use another LLM when constructing
an index.

from llama_index import VectorStoreIndex, ServiceContext, set_global_service_context
from llama_index.llms import OpenAI … # define LLM
llm = OpenAI(model=»gpt-4″, temperature=0, max_tokens=256) # configure service context
service_context = ServiceContext.from_defaults(llm=llm)
set_global_service_context(service_context) # build index
index = VectorStoreIndex.from_documents( documents
)

To save costs, you may want to use a local model.

from llama_index import ServiceContext
service_context = ServiceContext.from_defaults(llm=»local»)

This will use llama2-chat-13B from with LlamaCPP, and assumes you have llama-cpp-python installed. Full LlamaCPP usage guide is available in a notebook here.

See the Custom LLM’s How-To for more details.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *