В этой главе
• Создание приложений с помощью LangChain.
• Решение сложных задач с помощью агентов.
• Запрос данных с помощью LlamaIndex.
До сих пор для взаимодействия с языковыми моделями мы в основном пользовались библиотекой Python компании OpenAI. Она предоставляет базовый функционал для отправки промптов и получения ответов от GPT и других моделей OpenAI (а также возможности настройки и дообучения). Библиотеки других провайдеров, таких как Anthropic и Cohere, предлагают схожий функционал. Пока ваши задачи анализа данных просты, этого, скорее всего, будет достаточно. Но что, если для анализа данных потребуется сложный многоступенчатый конвейер, потенциально объединяющий множество разных форматов данных?
Тогда вы, пожалуй, захотите перейти на более мощный фреймворк. В настоящее время активно развиваются несколько высокоуровневых фреймворков для создания сложных приложений на основе языковых моделей. В этой главе мы обсудим два наиболее популярных из них: LangChain и LlamaIndex. Первый представляет собой универсальный фреймворк для создания приложений с использованием больших языковых моделей. Кроме того, он поставляется с множеством полезных встроенных компонентов, реализующих популярные сценарии использования языковых моделей. LlamaIndex, в свою очередь, специализируется на сценариях, где языковым моделям необходимо взаимодействовать с большими наборами данных.
Чтобы понять, как работает LangChain, сначала напишем простой конвейер классификации текста с использованием этого фреймворка. Затем рассмотрим некоторые его расширенные возможности. В частности, мы обсудим механизм работы агентов, построенных на базе языковых моделей. Создание агента означает, что мы даем модели право самостоятельно выбирать методы выполнения задачи, используя набор инструментов, предоставленный пользователем. С помощью таких агентов вы научитесь самостоятельно решать сложные аналитические задачи, комбинируя разные инструменты для работы с данными из различных источников. Далее вы увидите, как LlamaIndex легко поглощает большие объемы данных в различных форматах и делает их пригодными для использования языковыми моделями. В основе работы этого фреймворка лежат недорогие модели, которые переводят фрагменты данных и задачи анализа в векторное представление, после чего сопоставляют задачи с данными на основе сходства векторов. Наконец, мы сравним два фреймворка и проанализируем их преимущества и недостатки по сравнению с библиотеками OpenAI и иных разработчиков языковых моделей.
Создать сложное приложение на основе языковых моделей можно с помощью LangChain. Фреймворк был запущен в октябре 2022 года и быстро набирает популярность (что привело к созданию соответствующего стартапа в апреле 2023 года). На момент написания книги LangChain все еще стремительно развивался. Обязательно запускайте код из этого раздела в корректной версии LangChain (поскольку в будущих версиях интерфейсы могут измениться).
Название LangChain говорит само за себя: оно отсылает к языковым моделям (Lang) и цепочкам (Chain). В терминологии LangChain цепочка — это просто последовательность действий. Каждое из них может соответствовать вызову языковой модели, обработке данных или вызову произвольного инструмента. Важный момент: мы больше не рассчитываем, что один вызов языковой модели решит нашу проблему (как было в большинстве случаев, которые мы обсуждали в этой книге). Вместо этого мы полагаем, что потребуется сложная сеть связанных компонентов. Именно в таких сценариях раскрываются сильные стороны LangChain.
Чтобы использовать этот фреймворк, его сначала необходимо установить. Перейдите в терминал и выполните следующую команду:
pip install langchain==0.1.13
Как уже говорилось, для запуска приведенных ниже примеров кода необходимо установить подходящую версию LangChain. Фреймворк меняется так быстро, что в другой версии код может не работать.
Помимо ядра LangChain, возможно, потребуется установить библиотеки, поддерживающие языковые модели от конкретных провайдеров. В разделах ниже мы будем использовать модели OpenAI. Выполните в терминале следующую команду (и еще раз убедитесь, что используете указанную версию):
pip install langchain-openai==0.1.1
Поддержка других провайдеров, таких как Anthropic и Cohere, также доступна.
После выполнения этих команд вы готовы запускать примеры проектов.
Одним из первых наших проектов был анализ текста с помощью языковых моделей. Помните главу 4? Мы использовали модели, чтобы классифицировать отзывы на основе их тональности (это рекомендация или предупреждение?). То же самое будем делать и в этом проекте, но в коде станем использовать LangChain. Сравнив код для этого фреймворка с исходным, вы сразу увидите, насколько он способен упростить создание приложений с помощью языковых моделей.
Мы создадим цепочку для классификации текстовых документов. Вообще, цепочка LangChain может состоять из множества действий, каждое из которых реализуется путем вызова языковой модели или общей функции Python (например, для приведения результатов вызовов языковой модели к заданному формату). На самом деле термин «цепочка» немного вводит в заблуждение. Цепочку можно представить как последовательность действий, однако цепочки LangChain этим не ограничиваются. Например, они могут содержать параллельные шаги, а также условное выполнение. Однако для простого приложения классификации текста нам не понадобятся такие расширенные возможности. Вместо этого мы ограничимся простой цепочкой, состоящей всего из нескольких действий.
Она будет интегрировать несколько стандартных компонентов, предлагаемых фреймворком LangChain. Первый компонент — шаблон промпта. Как и в главе 4, в нем описывается задача классификации и ожидаемый формат вывода. Наверное, вам любопытно, что изменилось по сравнению с предыдущей версией кода. В конце концов, мы ведь постоянно обсуждаем шаблоны промптов. Разница в том, что LangChain вводит специальный класс для представления этих шаблонов. Он предлагает различные удобные функции: например, для создания и заполнения таких шаблонов. В то же время в LangChain есть специальное хранилище — хаб, где можно выкладывать свои шаблоны промптов, а также скачивать чужие (и не только). В нашем простом сценарии эти расширенные возможности не потребуются. Достаточно будет просто создать экземпляр шаблона, передав в качестве входных данных один параметр (текст для классификации).
Второе действие в цепочке — вызов языковой модели. Мы так уже работали с моделями ранее, но LangChain предлагает ряд удобных возможностей, которые надстраиваются над стандартным объектом модели. К примеру, можно вести журнал всех обращений к модели, а еще фреймворк предлагает удобные функции для различных сценариев вызова (например, пакетную и потоковую обработку). Опять же, в данном случае мы не будем применять эти расширенные возможности. Просто используем промпт (первое действие в нашей цепочке), чтобы модель могла сгенерировать ответ.
Третье действие в цепочке — использование парсера, который извлекает строку ответа из вывода, сгенерированного языковой моделью. Из главы 3 вы наверняка помните, что языковые модели компании OpenAI генерируют подробные ответы, содержащие один или несколько вариантов, а также различные типы метаданных (например, информацию об использовании токенов). Парсер автоматически извлекает строку ответа, которую мы ищем, из объекта результата (это работает как для моделей OpenAI, так и для всех других провайдеров). Результатом конвейера становится единственный токен, указывающий, является ли входной отзыв рекомендацией. Все три этапа работы конвейера показаны на рис. 10.1.

Рис. 10.1. Компоненты в цепочке классификации LangChain
Пора реализовать нашу цепочку на Python! Прежде всего нам потребуется шаблон промпта. Возьмем тот же шаблон, что и в главе 4, но на этот раз укажем класс ChatPromptTemplate, используемый LangChain:
from langchain_core.prompts.chat import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template(
'{text}\n' ❶ 
'Отзыв положительный или отрицательный?\n'
'Ответ ("Положительный"/"Отрицательный")\n')
Вы могли заметить отсылку к чат-моделям (в конце концов, создаваемый класс называется ChatPromptTemplate). Как обсуждалось в главе 3, чат-модели обрабатывают историю предыдущих сообщений, а не одно входное сообщение. Многие из недавно выпущенных моделей являются чат-моделями. В LangChain чат-моделям требуется специализированный шаблон промпта (который создает экземпляр последовательности сообщений, а не одного текста). Именно такой тип шаблона мы здесь создаем. Такой же шаблон использовался в главе 4. Он содержит метку-заполнитель ❶ для входного текста, который нужно классифицировать. Для обозначения меток-заполнителей в шаблонах промптов обычно используются фигурные скобки ({}), которые заменяются конкретными значениями при заполнении промпта.
Далее, нам нужна языковая модель для обработки промптов. Этот код создает экземпляр модели GPT-4o компании OpenAI:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model='gpt-4o', temperature=0,
max_tokens=1)
Класс ChatOpenAI охватывает все чат-модели компании OpenAI. Он импортируется из пакета langchain_openai, который содержит функционал для поддержки использования моделей OpenAI в LangChain. У других провайдеров, таких как Anthropic и Cohere, есть собственные пакеты, предлагающие сопоставимый функционал для их моделей (обратите внимание, что эти пакеты необходимо устанавливать отдельно через систему управления пакетами pip). Параметры в конструкторе ChatOpenAI могут показаться знакомыми: мы выбираем модель (gpt-4o), задаем значение температуры (temperature), равное 0 (чтобы уменьшить степень рандомизации в выводе), и ограничиваем максимальное количество выходных токенов одним (поскольку обе возможные метки классов, Положительный и Отрицательный, состоят из одного токена).
Теперь необходимо извлечь строку ответа из (расширенного) вывода языковой модели. Это легко сделать с помощью класса StrOutputParser. Парсеры вывода LangChain позволяют выполнить широкий спектр преобразований над результатом вызова модели. В данном случае требуется очень простое преобразование — извлечение нужной строки ответа. Этот фрагмент кода создает соответствующий парсер:
from langchain_core.output_parsers.string import StrOutputParser
parser = StrOutputParser()
И наконец, объединим все компоненты в цепочку. Для этого можно использовать язык выражений LangChain (LangChain Expression Language, LCEL). Если вы пользователь Linux, то этот синтаксис наверняка вам знаком:
from langchain_core.runnables.passthrough import RunnablePassthrough
chain = ({'text':RunnablePassthrough()} | prompt | llm | parser)
Чтобы использовать вывод одной операции в качестве входных данных для следующего действия, соединим их с помощью символа |. Команда создает цепочку, связывающую ранее упомянутые компоненты. Дополнительно она указывает формат входных данных, требуемых цепочкой. В нашем случае шаблон промпта содержит метку-заполнитель для текста, который требуется классифицировать.
В начале цепочки мы помечаем этот параметр как RunnablePassthrough. Так мы получаем больше возможностей при передаче входных параметров в цепочку. Например, в коде ниже показано, как обработать список входных данных с помощью ранее созданной цепочки:
inputs = ['Это великолепный фильм!', 'Это плохой фильм!']
outputs = chain.batch(inputs)
Пора закончить код для классификации текста. Код в листинге 10.1 принимает в качестве входных данных путь к файлу в формате .csv, содержащему столбец text. После запуска кода генерируется файл вывода с дополнительным столбцом class, содержащим результаты классификации. Другими словами, код делает в точности то же самое, что и код из главы 4, но на этот раз с использованием LangChain.
Листинг 10.1. Классификация отзывов по тональности с помощью LangChain
from langchain_openai import ChatOpenAI
from langchain_core.prompts.chat import ChatPromptTemplate
from langchain_core.output_parsers.string import StrOutputParser
from langchain_core.runnables.passthrough import RunnablePassthrough
import argparse
import pandas as pd
def create_chain(): ❶ 
""" Создает цепочку для классификации текста.
Выводит:
цепочка для классификации текста.
''''''
prompt = ChatPromptTemplate.from_template( ❷ 
'{text}\n'
'Отзыв положительный или отрицательный?\n'
'Ответ ("Положительный"/"Отрицательный")\n')
llm = ChatOpenAI( ❸ 
model='gpt-4o', temperature=0,
max_tokens=1)
parser = StrOutputParser() ❹ 
❺ 
chain = ({'text':RunnablePassthrough()} | prompt | llm | parser)
return chain
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('file_path', type=str, help='Путь к входному файлу')
args = parser.parse_args()
df = pd.read_csv(args.file_path) ❻ 
chain = create_chain() ❼ 
results = chain.batch(list(df['text'])) ❽ 
df['class'] = results ❾ 
df.to_csv('result.csv')
Функция create_chain ❶ реализует шаги, которые мы обсуждали в предыдущем подразделе. Она генерирует шаблон промпта для классификации ❷, затем чат-модель ❸ и, наконец, парсер вывода ❹. Результатом является цепочка, соединяющая все эти компоненты ❺.
После считывания параметров командной строки код считывает входные данные ❻, создает соответствующую цепочку ❼ и, наконец, применяет цепочку к списку входных текстов ❽. Результаты классификации добавляются к входным данным и сохраняются на диске ❾.
Как обычно, код из листинга 10.1 доступен по ссылке Listing 1, размещенной на сайте книги, в разделе дополнительных материалов к этой главе. Скачайте код (файл listing1.py) и (по желанию) файл, содержащий отзывы для классификации (например, reviews.csv из главы 4). Откройте терминал и перейдите в папку с кодом. При условии, что файл reviews.csv находится в той же папке, выполните следующую команду:
python listing1.py reviews.csv
Проверьте папку с кодом. В ней должен появиться новый файл result.csv с желаемыми результатами классификации. Пока что мы лишь убедились, что с помощью LangChain можно решать те же задачи, что и напрямую через библиотеки OpenAI (хотя, пожалуй, код фреймворка LangChain аккуратнее). В следующем разделе мы убедимся, что возможности LangChain этим не ограничиваются.
Полагаю, до сих пор вы считали языковые модели инструментами (хоть и очень сложными). На основе предоставленных вами входных данных модель генерирует вывод. Когда возможностей модели недостаточно для обработки данных, разработчику приходится добавлять необходимую инфраструктуру самостоятельно. Представьте, что создаете набор вопросов и ответов по математике. Понимая, что языковые модели плохо справляются с вычислениями (звучит иронично, ведь они являются компьютерными программами, но это действительно так), вы можете использовать следующий подход: на основе вопроса пользователя языковая модель переводит ввод в математическую формулу. Затем простой инструмент-калькулятор разбирает эту формулу и вычисляет результат, который и отправляется пользователю.
Пока неплохо. Ситуация усложняется, когда у вас не один, а несколько математических инструментов. Допустим, один инструмент решает дифференциальные уравнения, а другой — вычисляет простые арифметические выражения. В таких случаях можно расширить подход, добавив этап классификации, сопоставляющий пользовательский ввод с наиболее подходящим инструментом. Однако этот подход перестает работать в ситуациях, когда для ответа на вопрос пользователя может потребоваться не один инструмент, а разные, причем их придется вызывать многократно, возможно используя результат работы одного инструмента в качестве входных данных для последующих вызовов. В таких случаях прописывать вручную каждую возможную последовательность необходимых вызовов инструментов просто нереально.
Именно для подобных сценариев использования и нужны агенты. Это сравнительно новый способ применения больших языковых моделей. В его основе лежит смена перспективы. Вместо того чтобы рассматривать языковую модель как инструмент, используемый на определенном этапе в конвейере, спроектированном разработчиком, мы даем ей возможность действовать самостоятельно. Мы не пытаемся выстроить порядок применения модели и других шагов обработки (как было в предыдущем разделе), а предоставляем ей самой решать, какие этапы обработки применять и в каком порядке. Преимущество этого подхода в том, что он гораздо более гибкий и освобождает нас, разработчиков, от необходимости предвидеть каждый возможный сценарий и создавать соответствующую ветвь в логике обработки.
Агенты могут быть полезны при решении сложных задач анализа данных, где заранее не ясно, какие источники данных или методы обработки потребуются для удовлетворения запроса пользователя.
В подходе с агентами центральными являются два понятия: агент и его инструменты.
Инструмент может инкапсулировать какую угодно функциональность. Это функция, которую языковая модель может использовать, если сочтет необходимым. Когда мы применяем для работы с агентами LangChain или подобные фреймворки, инструмент обычно реализуется как функция Python. Каждый инструмент должен быть связан с описанием на естественном языке. Оно предоставляется языковой модели как элемент промптов. На основе описания модель решает, будет ли инструмент полезен в текущем контексте. Для использования инструмента ей требуется описание входных параметров и семантики вывода. Языковые модели, как и программисты-люди, эффективнее работают с инструментами, если у параметров понятные названия, а к самому инструменту прилагается четкое описание. Агенты реализуются через языковые модели, поэтому полное описание всех доступных инструментов обычно предоставляется как часть входного промпта.
Агенты используют инструменты всякий раз, когда требуется решить сложную задачу, поставленную пользователем. Агенты реализуются через языковые модели. Дообучение, конечно, может сделать модель более эффективным агентом, но и обычные модели, в принципе, должны работать. Секрет превращения языковых моделей в агенты заключается не столько в самой модели, сколько в том, как формулируются промпты к ней.
Как минимум в соответствующих промптах содержатся следующие компоненты.
• Краткое описание задачи, которую должен решить агент (предоставляется пользователем).
• Список доступных инструментов вместе с описанием их функциональности, а также их входных и выходных параметров.
• Описание ожидаемого формата вывода. Оно позволяет сопоставить вывод языковой модели с вызовами инструментов.
При наличии подобного промпта языковая модель может сама решить, какие инструменты вызвать, и сформировать соответствующий вывод. Инфраструктура, реализующая агентный подход, разбирает вывод, сопоставляет его с соответствующими инструментами и значениями входных параметров и получает результат вызова. При следующей итерации результат вызова инструмента добавляется во входной промпт. Таким образом, языковая модель, по сути, может получить доступ к результатам вызовов инструментов. На основе этого она может либо применить дополнительные инструменты (возможно, используя результаты предыдущих вызовов как входные данные), либо завершить работу, если готовый ответ уже получен.
Этот процесс обобщенно показан на рис. 10.2. Поставленная пользователем задача вместе с подробным описанием всех инструментов формирует входные данные для языковой модели. Вывод, сгенерированный ею, разбирается и преобразуется в действие: вызов инструмента (в этом случае команда вызова содержит значения для всех входных параметров инструмента) или завершение (в этом случае команда завершения содержит данные, которые модель считает ответом на входную задачу). Когда действием является вызов инструмента, производится соответствующий запуск. Результат добавляется в промпт, используемый при следующей итерации. Итерации продолжаются, пока модель не решит завершить работу (или пока не будет достигнут заданный пользователем лимит на количество итераций).

Рис. 10.2. Использование языковых моделей в качестве агентов. На основе промпта, описывающего задачу и доступные инструменты, языковая модель принимает решения о завершении и вызовах инструментов. Результаты вызовов инструментов добавляются в промпт, используемый для следующей итерации
Сейчас вам, наверное, любопытно, как выглядят соответствующие промпты. Рассмотрим стандартный шаблон, используемый для агентов в LangChain. Его можно скачать из хаба LangChain. Для этого сначала установите хаб, введя в терминале следующую команду:
pip install langchainhub==0.1.15
Затем запустите приведенный ниже код на Python, чтобы вывести стандартный шаблон для одного из самых популярных типов агентов:
from langchain import hub
prompt = hub.pull('hwchase17/react')
print(prompt.template)
Должен отобразиться следующий вывод:
❶ 
Ответь на следующие вопросы как можно точнее.
У тебя есть доступ к следующим инструментам:
{tools} ❷ 
Используй следующий формат: ❸ 
Вопрос: входной вопрос, на который ты должен ответить
Мысль: ты всегда должен обдумывать свои действия
Действие: действие, которое нужно предпринять, должно быть одним из [{tool_names}]
Входные данные действия: входные данные для действия
Наблюдение: результат действия
... (эта последовательность Мысль/Действие/Входные данные действия/Наблюдение может повторяться N раз)
Мысль: теперь я знаю окончательный ответ
Окончательный ответ: окончательный ответ на исходный входной вопрос
Начнем!
Вопрос: {input} ❹ 
Мысль: {agent_scratchpad} ❺ 
В этом шаблоне промпта описываются общий сценарий ❶ (есть вопрос, на который модель должна ответить), доступные инструменты ❷ и процесс решения задачи ❸. Шаблон содержит несколько меток-заполнителей, представляющих собой описания инструментов ❷, ввод от пользователя ❹ и результаты предыдущих итераций ❺. Как мы увидим в следующих разделах, фреймворк LangChain предлагает различные удобные функции для создания и выполнения агентов на основе этого и похожих шаблонов промптов.
В этом разделе мы используем фреймворк LangChain, чтобы создать агент для анализа данных. Он сможет получать доступ к различным источникам, содержащим как структурированные, так и неструктурированные данные. Более того, агент будет сам решать, к каким из этих источников обращаться и в каком порядке. Он сможет даже использовать информацию, полученную из одного источника, для запроса ко второму источнику (например, получить доступ к структурированной базе данных о продажах видеоигр, чтобы определить самую продаваемую игру в конкретном году, а затем использовать название игры для поиска дополнительной информации в Интернете).
Наш агент для анализа данных реализует подход, который мы обсуждали в предыдущем разделе. Он использует языковую модель, которая определяет, какие инструменты вызывать, их порядок и значения входных параметров. В примере мы предоставим агенту инструменты для доступа к реляционной базе данных (а также для получения информации о ее структуре, такой как имена доступных таблиц). Мы также дадим агенту инструмент, позволяющий выполнять поиск в Интернете (используя существующие поисковые системы в фоновом режиме). В совокупности мы получим агент, который может отправлять запросы к реляционной базе данных и использовать Интернет для получения информации, связанной с ее содержимым.
Начнем с более подробного описания инструментов, предоставляемых агенту. У него будет доступ к следующим пяти инструментам:
• sql_db_list_tables — перечисляет все таблицы в реляционной базе данных;
• sql_db_schema — возвращает SQL-схему таблицы по ее имени;
• sql_db_query_checker — позволяет агенту проверить SQL-запрос;
• sql_db_query — выполняет SQL-запрос и возвращает результат;
• search — позволяет агенту выполнять поиск в Интернете по ключевым словам, возвращая текст из сети.
Первые четыре инструмента помогают агенту получить доступ к реляционной базе данных. Последний инструмент позволяет ему извлекать информацию из Интернета. Получив задачу от пользователя, агент (используя базовую языковую модель) определяет, какие из этих инструментов вызывать и в каком порядке. Этот сценарий показан на рис. 10.3.

Рис. 10.3. Агент использует несколько инструментов для изучения структуры и выполнения запросов к реляционной базе данных. Кроме того, он может получать текст из Интернета с помощью инструмента веб-поиска
LangChain позволяет быстро создавать агенты. И даже предлагает специализированные конструкторы для агентов, которые обращаются к структурированной базе данных. Мы используем эти возможности в коде ниже.
Агенты работают на базе языковых моделей. Для создания агента сначала необходимо создать объект модели:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
temperature=0, model='gpt-4o')
Мы создадим чат-модель OpenAI. Точнее, опять обратимся к модели GPT-4o.
Затем мы создадим объект, представляющий реляционную базу данных. Запросы будут выполняться к базе данных SQLite, хранящейся на диске. Предположим, что параметр dbpath содержит путь к соответствующему файлу базы данных (обычно такие файлы имеют расширение .db). Можно создать объект базы данных с помощью такого кода:
from langchain_community.utilities.sql_database import SQLDatabase
db = SQLDatabase.from_uri(f'sqlite:///{dbpath}')
Мы говорили о четырех инструментах для доступа к реляционной базе данных. К счастью, все они будут созданы автоматически из ее объекта. Однако нам еще нужно создать инструмент для веб-поиска.
Применим встроенный компонент LangChain — инструмент SerpAPI. Чтобы его использовать, сначала необходимо зарегистрировать учетную запись на сайте SerpAPI. Откройте браузер, перейдите по адресу https://serpapi.com/, нажмите кнопку Register (Регистрация) и создайте соответствующую учетную запись. Для выполнения приведенного ниже кода вам потребуется получить ключ доступа API (доступен на сайте https://serpapi.com/dashboard). Кроме того, необходимо установить расширение LangChain для активизации инструмента веб-поиска. Перейдите в терминал и выполните следующую команду:
pip install google-search-results==2.4.2
После этого достаточно следующего фрагмента кода на Python, чтобы создать инструмент для веб-поиска (при условии, что параметр llm содержит ранее созданный объект языковой модели, а serpaikey — ключ доступа SerpAPI):
from langchain.agents.load_tools import load_tools
extra_tools = load_tools(
['serpapi'], serpapi_api_key=serpaikey, llm=llm)
С помощью функции load_tools можно загрузить стандартные инструменты, передавая имена желаемых инструментов в качестве параметров. В данном случае нам нужен только инструмент веб-поиска, и мы передаем в списке имен инструментов (serpapi) только одну запись. После вызова функции load_tools мы сохраняем результат в параметре extra_tools: это список инструментов, содержащий одну запись (инструмент веб-поиска). Теперь у нас есть все компоненты, необходимые для создания агента с помощью фреймворка LangChain.
Предположим, что параметр db содержит созданный ранее объект базы данных, а llm — сгенерированную прежде языковую модель. С помощью кода ниже инициализируем агент, который обеспечивает доступ к данным на основе SQL:
from langchain_community.agent_toolkits.sql.base import create_sql_agent
agent = create_sql_agent(
llm=llm, db=db, verbose=True,
agent_type='openai-tools',
extra_tools=extra_tools)
Команда create_sql_agent — это удобная функция, предлагаемая LangChain и позволяющая создавать агенты для доступа к данным на основе SQL. Четыре ранее упомянутых инструмента для доступа к реляционной базе данных (используемые для получения имен таблиц, показа схем таблиц, проверки SQL-запросов и в конечном счете их выполнения) добавляются автоматически, их не нужно указывать отдельно. Помимо этих ориентированных на SQL инструментов, нам нужен еще только один — для выполнения веб-поиска. Подобные инструменты указываются в списке через входной параметр extra_tools. Установка параметра verbose в значение True позволяет следить за «ходом мыслей», приводящим агента к вызову конкретных инструментов (пример вывода будет показан ниже). Тип агента (в данном случае openai-tools) задает сразу два параметра: какой промпт отправлять модели и как именно разбирать ее ответ, чтобы понять, какой инструмент вызывать.
Создав агент, вызываем его для решения конкретной задачи, введя следующий код (предположим, что переменная task хранит описание задачи, которую требуется решить, на естественном языке):
agent.invoke({'input':task})
В листинге 10.2 объединены все элементы, о которых мы говорили выше: считав ключ доступа API SerpAPI, путь к файлу базы данных и вопрос из командной строки, код создает объект языковой модели ❶, затем базу данных ❷, инструмент веб-поиска ❸ и, наконец, агент ❹. Он запускает агент ❺, передавая ему вопрос на обработку. Вывод, произведенный агентом, завершается ответом на этот вопрос (или сообщением об ошибке, если агенту не удалось найти ответ).
Листинг 10.2. Агент для анализа данных с возможностью веб-поиска
import argparse
from langchain.agents.load_tools import load_tools
from langchain_community.utilities.sql_database import SQLDatabase
from langchain_community.agent_toolkits.sql.base import create_sql_agent
from langchain_openai import ChatOpenAI
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('serpaikey', type=str, help='Ключ доступа к SERP API')
parser.add_argument('dbpath', type=str, help='Путь к базе данных SQLite')
parser.add_argument('question', type=str, help='Вопрос, на который нужно ответить')
args = parser.parse_args()
llm = ChatOpenAI( ❶ 
temperature=0, model='gpt-4o')
❷ 
db = SQLDatabase.from_uri(f'sqlite:///{args.dbpath}')
extra_tools = load_tools( ❸ 
['serpapi'], serpapi_api_key=args.serpaikey, llm=llm)
agent = create_sql_agent( ❹ 
llm=llm, db=db, verbose=True,
agent_type='openai-tools',
extra_tools=extra_tools)
agent.invoke({'input':args.question}) ❺ 
Посмотрим, как код работает на практике. Скачайте код листинга 10.2, используя ссылку Listing 2 на сайте книги. Помимо кода, для тестирования агента потребуется база данных SQLite. Мы будем использовать базу SQLite из главы 5, хранящую информацию о видеоиграх (файл доступен по ссылке Games SQLite, размещенной на сайте книги, в разделе дополнительных материалов к главе 5).
Откройте терминал и перейдите в каталог, содержащий код. Предположим, что файл базы данных, games.db, находится в том же каталоге. Запустите приведенный ниже код (заменив [SerpAPI key] своим ключом поиска, который доступен по ссылке https://serpapi.com/dashboard):

Вывод будет похож на приведенный ниже (ваш вывод может немного отличаться из-за изменений веб-контента, небольших модификаций модели GPT-4o и некоторых других факторов):
[1m> Entering new SQL Agent Executor chain...[0m
[32;1m[1;3m
❶ 
Invoking: ``sql_db_list_tables` with `{'tool_input': "}`
[0m[38;5;200m[1;3mgames[0m[32;1m[1;3m
❷ 
Invoking: `sql_db_schema` with `{'table_names': 'games'}`
[0m[33;1m[1;3m
CREATE TABLE games (
rank INTEGER,
name TEXT,
platform TEXT,
year INTEGER,
genre TEXT,
publisher TEXT,
americasales NUMERIC,
eusales NUMERIC,
japansales NUMERIC,
othersales NUMERIC,
globalsales NUMERIC
)
/*
3 строки из таблицы игр:
rank name platform year genre publisher
americasales eusales japansales othersales globalsales
1 Wii Sports Wii 2006 Sports Nintendo 41.4900000000
29.0200000000 3.7700000000 8.4600000000 82.7400000000
2 Super Mario Bros. NES 1985 Platform Nintendo 29.0800000000
3.5800000000 6.8100000000 0.7700000000 40.2400000000
3 Mario Kart Wii Wii 2008 Racing Nintendo 15.8500000000
12.8800000000 3.7900000000 3.3100000000 35.8200000000
*/[0m[32;1m[1;3m
❸ 
Invoking: `sql_db_query_checker` with `{'query': 'SELECT name
FROM games WHERE year = 2016 ORDER BY globalsales DESC LIMIT 1'}`
responded: The games table contains the information we need.
I will query for the game with the highest global sales in 2016.
[0m[36;1m[1;3mSELECT name FROM games WHERE year = 2016
ORDER BY globalsales DESC LIMIT 1[0m[32;1m[1;3m
❹ 
Invoking: `sql_db_query` with `{'query': 'SELECT name
FROM games WHERE year = 2016 ORDER BY globalsales DESC LIMIT 1'}`
[0m[36;1m[1;3m[('FIFA 17',)][0m[32;1m[1;3m
❺ 
Invoking: `Search` with `How to play FIFA 17`
[0m[33;1m[1;3m["A Beginner's Guide To Complete FIFA 17 Domination ... The
main steps you should take are to jump right in with a quick play game. ...
EA Sports FIFA ...", 'Play FIFA 17 up to 5 days before launch for a full
10 hours when you join EA Access on Xbox One and Origin Access on PC.',
"1. Shield the ball in 360 degrees · 2. Use Driven Shots and Driven Headers
·
3. Use set piece upgrades to score with style · 4. Make Fifa 17's ...",
'Play FIFA 17 as much as you want with EA Access or Origin Access
for only $4.99 per month. Now available in The Vault.',
'Cautiously Start An Online Match. Score Early After Some Self-Proclaimed
Beautiful Build Up Play. Concede 4 Goals In A Row And Convince ...',
'FIFA 17 TUTORIALS & ULTIMATE TEAM ⊠
Twitter: https://twitter.com/KrasiFIFA ⊠
Instagram: http://instagram.com/KrasiFIFA How I record my ...',
"Draft mode is another way to play FIFA Ultimate Team,
giving you the ability to play with Players you don't own.
You'll have the opportunity to draft a random ..."]
❻ 
[0m[32;1m[1;3mThe most sold game in 2016 was FIFA 17.
To play FIFA 17, you can follow these steps:
1. Jump right in with a quick play game.
2. Shield the ball in 360 degrees.
3. Use Driven Shots and Driven Headers.
4. Use set piece upgrades to score with style.
5. Start an online match cautiously.
6. Score early after some self-proclaimed beautiful build-up play.
7. Draft mode is another way to play FIFA Ultimate Team,
giving you the ability to play with players you don't own.
You'll have the opportunity to draft a random team.
Remember, practice makes perfect![0m
[1m> Finished chain.[0m
Напомню, что мы включили в агенте подробный режим вывода. Это означает, что в выводе содержится полная запись того, какие инструменты вызывал агент, а также процесс его рассуждений. Внимательнее изучим вывод, чтобы понять, что произошло.
Сначала агент получает список таблиц, доступных в реляционной базе данных (используя инструмент sql_db_list_tables) ❶. Определенно, это разумное действие при знакомстве с новой базой. Результат вызова инструмента показывает, что база содержит только одну таблицу (games). Агенту «любопытно» ее содержимое. Он вызывает инструмент sql_db_schema, чтобы получить дополнительную информацию о таблице games ❷. Обратите внимание, что этому инструменту нужны входные параметры, в частности имя исследуемой таблицы. В журнале показаны значения всех входных параметров для каждого вызова инструмента.
Вызов инструмента sql_db_schema возвращает SQL-команду, которая использовалась для создания таблицы games, вместе с небольшим фрагментом содержимого таблицы. Далее агент изучает SQL-запрос для получения соответствующей информации о входном вопросе о том, какая игра была самой продаваемой в 2016 году и как в нее играть. На первом этапе он проверяет, корректен ли запрос с точки зрения синтаксиса, вызывая инструмент sql_db_query_checker ❸:
SELECT name FROM games WHERE year = 2016 ORDER BY globalsales DESC LIMIT 1
В то же время агент использует возможность «поразмышлять» о полезности рассматриваемого запроса, о чем свидетельствует вывод, в котором говорится, что в таблице games содержится нужная информация и инструмент выполнит запрос, чтобы найти игру с самыми высокими мировыми продажами в 2016 году. Может показаться странным, что языковая модель умеет извлекать пользу из такого рода монолога, вместо того чтобы просто напрямую вызвать нужный инструмент. Тем не менее было показано, что предоставление агентам возможности явно рассуждать о стоящей перед ними проблеме и действиях, которые они совершают для ее решения, может повысить их производительность [1]. То же самое происходит и здесь.
Далее агент решает использовать ранее проверенный запрос для получения информации из базы данных, используя инструмент sql_db_query ❹. SQL-запрос возвращает название игры, которая принесла наибольшую выручку в 2016 году: FIFA 17, симулятор игры в футбол компании Electronic Arts. Но вопрос во входных данных этим не ограничивался: «Какая игра была самой продаваемой в 2016 году и как в нее играть?» Ответ на вторую его часть не удастся получить из контента базы данных. Надо отдать должное агенту — он это понимает и пытается обратиться к Интернету: отправляет поисковый запрос с помощью инструмента Search для поисковой строки «Как играть в FIFA 17» ❺. Обратите внимание, что агент сумел автоматически сформулировать подходящую поисковую строку на основе результата SQL-запроса и входного вопроса. Результатом веб-поиска является коллекция текстовых фрагментов (показанных в выводе), которые содержат информацию о том, как играть в FIFA 17.
Наконец, агент использует информацию, полученную в результате веб-поиска (вместе с информацией из SQL-базы данных), чтобы сформулировать окончательный ответ ❻. В нем FIFA 17 называется самой популярной игрой в 2016 году и содержатся подробные инструкции, как в нее успешно играть. Мы видели, что агент может выполнять сложную последовательность вызовов инструментов, чтобы найти желаемый ответ, и для этого не нужно вручную прописывать необходимые этапы процесса. При желании вы можете задать агенту еще несколько, возможно, более сложных вопросов и посмотреть, сможет ли он ответить на них.
До сих пор мы использовали стандартные инструменты, предлагаемые LangChain, для наиболее распространенных сценариев использования. Но что произойдет, если появятся специализированные требования? Например, потребуется сделать так, чтобы источник данных был доступен через пользовательский API, или у вас есть специализированные аналитические функции, которые агент может применять к данным. В таких случаях можно определить собственные пользовательские инструменты и сделать их доступными для агента LangChain.
В предыдущем разделе мы анализировали набор данных о продажах видеоигр. Исходные данные сообщают о значениях продаж в долларах США. А сколько это в других валютах? Чтобы агенты могли анализировать продажи игр, используя несколько валют, мы добавим инструмент конвертации. Получив на вход сумму в долларах США и название целевой валюты, этот инструмент возвращает значение, эквивалентное этой валюте.
В листинге 10.3 показано, как добавить инструмент конвертации валют к агенту данных.
Листинг 10.3. Агент анализа данных с инструментом конвертации валют
import argparse
from langchain.agents.load_tools import load_tools
from langchain.tools import tool
from langchain_community.utilities.sql_database import SQLDatabase
from langchain_community.agent_toolkits.sql.base import create_sql_agent
from langchain_openai import ChatOpenAI
from typing import Union
@tool ❶ 
❷ 
def convert_currency(USD_amount: float, currency: str) -> Union[float, str]:
❸ 
""" Конвертирует сумму в долларах США в другую валюту.
Аргументы:
USD_amount: сумма в долларах США.
currency: название целевой валюты.
Выводит:
Ввести сумму в целевой валюте.
"""
conversion_factors = {
'Euro':0.93, 'Yen':151.28, 'Yun':0.14,
'Pound':1.26, 'Won':0.00074, 'Rupee':0.012}
if currency not in conversion_factors:
❹ 
error_message = (
f'Неизвестная валюта: {currency}!'
f'Используйте одну из {conversion_factors.keys()}')
return error_message
❺ 
conversion_factor = conversion_factors[currency]
converted_amount = USD_amount * conversion_factor
return converted_amount
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('serpaikey', type=str, help='Ключ доступа к SERP API')
parser.add_argument('dbpath', type=str, help='Путь к базе данных SQLite')
parser.add_argument('question', type=str, help='Вопрос, на который нужно ответить')
args = parser.parse_args()
llm = ChatOpenAI(
temperature=0, model='gpt-4o')
db = SQLDatabase.from_uri(f'sqlite:///{args.dbpath}')
extra_tools = load_tools(
['serpapi'], serpapi_api_key=args.serpaikey, llm=llm)
❻ 
extra_tools.append(convert_currency)
agent = create_sql_agent(
llm=llm, db=db, verbose=True,
agent_type='openai-tools',
extra_tools=extra_tools)
agent.invoke({'input':args.question})
По сути, инструмент — это не что иное, как функция Python. Наш конвертер валют запускается функцией convert_currency ❷. Как фреймворк LangChain узнает, что следует превратить функцию в инструмент? С помощью декоратора @tool ❶, который должен находиться непосредственно перед именем функции. Обычно для функций Python не требуется указывать типы параметров и возвращаемых значений (хотя это улучшает читаемость кода). Однако если вы планируете превратить функцию в инструмент, то все эти типы нужно обязательно прописать. Причина такова: чтобы правильно использовать функцию в качестве инструмента, агент должен вызывать ее с параметрами корректного типа. Все типы, которые указываются в заголовке функции, будут доступны агенту как часть описания инструмента. Следовательно, сопоставление параметров с типами поможет агенту избежать ненужных ошибок вызова.
Помимо типов параметров, агент должен иметь информацию о том, что может делать ваш инструмент. Первая важная информация — имя функции. По умолчанию инструмент будет назван в соответствии с ней. Не называйте функцию XYZ, ведь это сильно затруднит понимание выполняемого действия! Имя функции, приведенной в листинге 10.3, convert_currency, должно вполне четко указывать, что она делает. Аналогичным образом имена входных параметров, USD_amount (типа float) и currency (типа `str`), говорят сами за себя (и это хорошо!). Вывод функции — это сумма, конвертированная в целевой валюте, или сообщение об ошибке, если запрошенная валюта не поддерживается (поэтому тип вывода — это Union[float, str] — либо число, либо строка). Простое правило: если вы собираетесь использовать функцию Python как инструмент, то оформляйте ее так, чтобы другой разработчик-человек мог понять, что она делает, даже не заглядывая в реализацию.
Помимо имени и параметров функции, агент «видит» ее документацию ❸. Опять же, убедитесь, что ваша документация хорошо структурирована, а также объясните семантику своего инструмента и связанных параметров. В данном случае документация описывает назначение инструмента, семантику входных параметров (даже с примером допустимого значения для второго параметра) и семантику вывода.
Функция convert_currency использует небольшую базу данных валют с соответствующими коэффициентами конвертации. Например, она содержит коэффициенты конвертации для евро и иен, но в ней нет многих других валют. Создавая инструмент для своего агента, учитывайте случаи, когда агент будет использовать инструмент некорректно. Это может произойти, если описание инструмента неполное или если языковая модель допускает ошибку (что бывает даже с современными моделями). В этом случае мы добавляем специальную обработку для ситуации, когда целевая валюта не поддерживается (то есть отсутствует соответствующий коэффициент конвертации) ❹. Если целевая валюта не поддерживается, то функция возвращает информативное сообщение об ошибке, содержащее полный список доступных валют. Это помогает агенту ограничить параметр набором допустимых вариантов для последующих вызовов. Если целевая валюта поддерживается, то функция возвращает конвертированную сумму ❺.
Создав инструмент на основе функции Python, мы просто должны сделать его доступным для агента. В листинге 10.3 показывается создание практически такого же агента, как в листинге 10.2, с той лишь разницей, что мы добавляем инструмент конвертации валют ❻. Мы снова используем SQL-агент, поэтому инструменты конвертера и веб-поиска добавлены в список дополнительных инструментов (добавляемых поверх стандартных для доступа к SQL, которые автоматически предоставляются агенту). По умолчанию имя инструмента совпадает с именем функции, на основе которой он создан. Следовательно, мы просто добавляем convert_currency в список дополнительных инструментов ❻, чтобы наделить агент возможностью конвертировать валюты.
Посмотрим, сможет ли наш агент использовать новый инструмент. Скачайте код листинга 10.3, доступный по ссылке Listing 3, размещенной на сайте книги в разделе дополнительных материалов к главе. Можно взять тот же файл базы данных, что и раньше (предполагается, что файл games.db находится в одной папке с кодом). Затем откройте терминал и введите следующий код (вставив вместо [SerpAPI key] свой ключ доступа SerpAPI):

Очевидно, что для ответа на этот вопрос требуется инструмент конвертации валют. При запуске кода отобразится вывод, похожий на этот:
[1m> Entering new SQL Agent Executor chain...[0m
[32;1m[1;3m
❶ 
Invoking: `sql_db_list_tables` with `'tool_input': "`
[0m[38;5;200m[1;3mgames[0m[32;1m[1;3m
❷ 
Invoking: `sql_db_schema` with `'table_names': 'games'`
[0m[33;1m[1;3m
CREATE TABLE games (
rank INTEGER,
name TEXT,
platform TEXT,
year INTEGER,
genre TEXT,
publisher TEXT,
americasales NUMERIC,
eusales NUMERIC,
japansales NUMERIC,
othersales NUMERIC,
globalsales NUMERIC
)
/*
3 строки из таблицы игр:
rank name platform year genre publisher americasales
eusales japansales othersales globalsales
1 Wii Sports Wii 2006 Sports Nintendo 41.4900000000
29.0200000000 3.7700000000 8.4600000000 82.7400000000
2 Super Mario Bros. NES 1985 Platform Nintendo 29.0800000000
3.5800000000 6.8100000000 0.7700000000 40.2400000000
3 Mario Kart Wii Wii 2008 Racing Nintendo 15.8500000000
12.8800000000 3.7900000000 3.3100000000 35.8200000000
*/[0m[32;1m[1;3m
❸ 
Invoking: `sql_db_query_checker` with `{'query': 'SELECT SUM(globalsales) as
total_revenue FROM games WHERE year = 2015'}`
responded: The "games" table contains the information we need.
The "globalsales" column represents the global revenue generated by
each game. We can sum this column for the games released in 2015 to
get the total revenue. Let's write and check the SQL query.
[0m[36;1m[1;3mSELECT SUM(globalsales) as total_revenue
FROM games WHERE year = 2015[0m[32;1m[1;3m
❹ 
Invoking: `sql_db_query` with `{'query': 'SELECT SUM(globalsales) as
total_revenue FROM games WHERE year = 2015'}`
[0m[36;1m[1;3m[(264.43999999999795,)][0m[32;1m[1;3m
❺ 
Invoking: `convert_currency` with `{'USD_amount':
264.43999999999795, 'currency': 'Yen'}`
[0m[38;5;200m[1;3m40004.48319999969[0m[32;1m[1;3m
❻ 
The total revenue generated by computer games in 2015
was approximately $264.44 million.
In Japanese Yen, this is approximately ¥40,004,483,200.[0m
[1m> Finished chain.[0m
Аналогично предыдущему примеру агент сначала исследует базу данных, получая набор таблиц ❶, а затем, узнав, что она содержит только одну таблицу, получает схему для нее ❷. Агент делает верный вывод, что база содержит полезную информацию для входного вопроса, и сначала проверяет ❸, а затем выполняет ❹ соответствующий SQL-запрос. В результате возвращается общая стоимость продаж компьютерных игр в 2015 году, выраженная в долларах США. Чтобы ответить на вторую часть вопроса о стоимости в иенах, агент затем применяет инструмент конвертации валют ❺. Обратите внимание, что он выбирает соответствующие значения двух входных параметров на основе описания функции и типов. Наконец, агент формулирует ответ на входной вопрос ❻.
LangChain вовсе не единственный фреймворк, способный упростить использование языковых моделей для анализа данных. В этом разделе мы обсудим еще один фреймворк, который появился недавно и быстро набирает популярность, — LlamaIndex.
LlamaIndex в полной мере раскрывает свои возможности в тех сценариях, где языковым моделям необходимо обращаться к большим хранилищам данных, в том числе разноформатных. В таких случаях, как правило, нецелесообразно (или даже невозможно) напрямую загружать все данные в модель. Вместо этого нужен механизм, который быстро идентифицирует релевантные данные для конкретной задачи, передавая модели только их. Как следует из названия, LlamaIndex индексирует данные, чтобы быстро идентифицировать соответствующие подмножества. Точнее, платформа связывает фрагменты данных (например, части текста) с эмбеддингами. Мы кратко обсуждали такие векторы в главе 4. В двух словах: эмбеддинг представляет семантику текста в виде вектора, вычисляемого языковой моделью. Если у двух документов векторы похожи (расстояние между ними маленькое), то мы предполагаем, что они относятся к схожим темам.
Типичный конвейер обработки данных LlamaIndex состоит из следующих этапов. Сначала он загружает данные (возможно, в различных форматах) и выполняет предварительную обработку. Например, такая обработка может заключаться в разделении длинных текстовых документов на более мелкие фрагменты, с которыми удобнее работать. Далее LlamaIndex индексирует данные. Как я уже говорил, это означает сопоставление фрагментов данных с эмбеддингами. По умолчанию для вычисления эмбеддингов LlamaIndex использует небольшие языковые модели (например, ada от OpenAI). Благодаря этому этап индексации обходится недорого. Более того, LlamaIndex может сохранять сгенерированный индекс (эмбеддинги) на диск, чтобы их не приходилось генерировать повторно для каждой новой задачи.
Фреймворк предлагает поддержку различных сценариев использования на основе сгенерированного индекса. Например, он может использовать проиндексированные данные для ответов на вопросы на естественном языке. Получив вопрос в качестве входных данных, LlamaIndex сначала вычисляет эмбеддинг для текста вопроса. Затем сравнивает вектор, представляющий вопрос, с предварительно вычисленными векторами, представляющими фрагменты данных. Фреймворк выявляет элементы данных с наиболее похожими векторами. Связанные данные добавляются в промпт вместе с входным вопросом. Цель — сгенерировать ответ на вопрос, используя релевантные данные в качестве контекста. Для индексации используются маленькие модели, однако для генерации окончательного ответа обычно применяются модели большего размера. На рис. 10.4 показан такой конвейер обработки данных.

Рис. 10.4. Основные этапы типичного конвейера обработки данных LlamaIndex. Фреймворк загружает и индексирует данные, чтобы обеспечить быстрое извлечение. Получив вопрос, он идентифицирует релевантные элементы данных и передает их вместе с входным вопросом языковой модели для генерации ответа
Давайте реализуем конвейер, описанный в предыдущем подразделе, на языке Python. Для работы с LlamaIndex сначала необходимо установить несколько пакетов. Перейдите в терминал и выполните следующую команду:
pip install llama-index==0.10.25
Она установит основные пакеты LlamaIndex. Однако вы будете использовать платформу для анализа множества разных форматов данных. Чтобы LlamaIndex мог корректно обращаться ко всем этим форматам и анализировать их, вы должны установить несколько дополнительных пакетов. В терминале введите следующие команды:
pip install torch==2.1.2
pip install transformers==4.36.0
pip install python-pptx==0.6.23
pip install Pillow==10.2.0
Эти библиотеки необходимы для анализа документов в формате .pdf и презентаций PowerPoint — и те и другие понадобятся для следующего проекта.
Вы все еще работаете в компании Banana и столкнулись со сложной проблемой: так как компания международная, у нее есть множество филиалов. Ваш руководитель хочет, чтобы вы проанализировали данные из разных филиалов, например, в целях сравнения эффективности их работы. Однако разные филиалы используют совершенно разные форматы данных. Одни публикуют свои результаты в виде простых текстовых документов, тогда как другие регулярно выпускают подробные презентации PowerPoint. Как объединить все эти форматы? К счастью, LlamaIndex упрощает эту задачу.
Ее можно решить с помощью всего нескольких строк кода на Python (листинг 10.4).
Листинг 10.4. Простая система вопросов и ответов с помощью фреймворка LlamaIndex
import argparse
import openai
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
if __name__ == '__main__':
❶ 
parser = argparse.ArgumentParser()
parser.add_argument('datadir', type=str, help='Путь к папке с данными')
parser.add_argument('question', type=str, help='Вопрос, на который нужно ответить')
args = parser.parse_args()
❷ 
documents = SimpleDirectoryReader(args.datadir).load_data()
❸ 
index = VectorStoreIndex.from_documents(documents)
❹ 
engine = index.as_query_engine()
❺ 
answer = engine.query(args.question)
print(answer)
Код принимает следующие входные параметры.
• Ссылку на репозиторий данных. Этот репозиторий может содержать файлы различных типов.
• Вопрос, на который нужно ответить. LlamaIndex будет искать ответ на него среди данных в репозитории.
После получения этих параметров из командной строки ❶ загружаются данные из входного репозитория ❷. К счастью, в LlamaIndex это сделать очень просто: не нужно добавлять обработку разных типов файлов и т.п. Вместо этого достаточно передать путь к каталогу. Далее только что загруженные данные индексируются ❸. По умолчанию для вычисления эмбеддингов LlamaIndex использует модели ada компании OpenAI. Преобразование данных и разделение на фрагменты (например, разбиение больших текстовых документов на части, достаточно мелкие для обработки моделями ada от OpenAI) — все это выполняется автоматически. Далее на основе индекса создается механизм, который будет отвечать на запросы ❹. Он будет автоматически извлекать данные, связанные с входным вопросом, используя индекс. Наконец, применяется ранее сгенерированный механизм, чтобы ответить на входной вопрос ❺, и выводится результат.
Несмотря на то что фреймворк LlamaIndex предлагает различные способы настройки и специализации каждого шага данного конвейера (и создания других конвейеров), именно заданные по умолчанию параметры на каждом шаге позволяют добиться предельной краткости кода.
Протестируем наш конвейер на демонстрационных данных. Вы можете скачать код листинга 10.4, используя ссылку Listing 4 на сайте книги. Кроме того, скачайте файл bananareports.zip (доступен по ссылке Banana Reports, размещенной там же на сайте, в разделе дополнительных материалов к этой главе) и распакуйте его в той же папке, что и код. В папке вы найдете (короткие) бизнес-отчеты в форматах текстовых, PDF-документов и презентаций PowerPoint. Пора ответить на несколько вопросов! Откройте терминал и перейдите в каталог, содержащий код и папку bananareports (после распаковки). Теперь выполните следующую команду, которая задает вопрос о прибыли филиала Plantain за 2023 год:
python listing4.py bananareports 'Сколько филиал Plantain заработал в 2023 году?'
Должен отобразиться примерно такой вывод:
За 2023 год филиал Plantain заработал 30 миллионов долларов США.
Просмотрите файл. Вы увидите, что филиал Plantain действительно заработал 30 миллионов долларов США. Спросите модель о других филиалах (Pisang Raja и Cavendish); должны отобразиться корректные результаты. Можно задавать более сложные вопросы (например, «Какой филиал принес наибольшую прибыль в 2023 году?»), для ответа на которые требуется сравнить данные разных файлов. Опять же, система должна быть способна выдать правильный ответ.
Итак, LangChain и LlamaIndex — два популярных фреймворка для работы с языковыми моделями. Мы убедились, что реализация даже сложных приложений, например на основе агентов, при использовании этих фреймворков выполняется довольно быстро. LangChain и LlamaIndex имеют схожую функциональность: к примеру, оба фреймворка предоставляют поддержку для реализации агентов на основе языковых моделей. LlamaIndex особенно эффективен в сценариях, где используется общий шаблон, описанный в предыдущем разделе (предоставление функциональности на основе индексов данных). LangChain имеет более общую направленность, помогая разработчикам создавать сложные приложения с использованием языковых моделей.
На момент написания книги оба фреймворка были относительно новыми, поэтому описанные выше характеристики могут не соответствовать будущим версиям. Кроме того, интерфейсы фреймворков быстро развиваются, и для работы с новейшими версиями, возможно, придется внести изменения в код. Если ваше приложение не требует сложной логики, то можете использовать низкоуровневые библиотеки наподобие библиотек компании OpenAI.
Конечно, в этой главе не приводилось подробное описание фреймворков. Я хотел показать их возможности, чтобы вы могли осознанно выбрать, какой из них стоит изучить более глубоко.
• Фреймворки LangChain и LlamaIndex позволяют создавать сложные приложения на основе языковых моделей.
• LangChain и LlamaIndex упрощают процесс создания агентов. Агенты используют языковые модели для управления вызовами различных инструментов (стандартных, а также пользовательских). Они могут решать сложные задачи при наличии доступа к подходящим инструментам.
• В LangChain для превращения функций в инструменты следует использовать декоратор @tool.
• LlamaIndex индексирует различные типы данных, создавая эмбеддинги.
• LlamaIndex упрощает исследование и запрос проиндексированных данных.
1. Wei J., Wang X., Schuurmans D. et al. Chain-of-Thought Prompting Elicits Reasoning in Large Language Models // Advances in Neural Information Processing Systems, 2022. — № 35. — Рр. — 24 824–24 837.
Это не просто эмбеддинги. LlamaIndex строит индекс из набора данных, их эмбеддингов и дополнительных метаданных, которые используются для быстрого поиска. — Примеч. науч. ред.