В своей предыдущей статье я написал о многообещающем фреймворке LangChain. Туториал был достаточно коротким; удалось охватить только самые базовые концепции проекта (и то не все). В этой части предстоит более глубокое погружение. Разберемся, как можно добавить память в диалоги с LLM, а также задействуем мощь агентов. С помощью агентов вы сможете подключить вашу языковую модель к внешним ресурсам: поисковой машине, Википедии или даже к собственной базе данных. На мой взгляд, это killer-feature всего фреймворка. Агенты - это следующий эволюционный шаг развития ИИ, и это не только моя больная фантазия. Например, о перспективах использования агентов можно почитать здесь.
Постигать новые знания лучше всего в рамках практики, поэтому фишки LangChain будем сразу опробовать в боевых условиях. Будем двигаться от простого к сложному. В первой части расскажу о реализации памяти, а также создам простого агента, который будет подключаться к поисковику Google.
Настраиваем окружение для проекта
Как обычно, вам понадобится получить ключ API OpenAI. Можно, конечно, в качестве LLM использовать и другие модели (загрузить из Hagging Face), но их качество пока сильно отстает от ChatGPT. Далее предполагается, что ключ у вас уже есть.
Создаем директорию
mkdir tshirt_bot
и переходим в нееcd tshirt_bot
-
Настраиваем виртуальное окружение.
conda create --name tshirt_bot python=3.10 conda activate tshirt_bot
-
Также зарегистрируем наш кернел в jupyter
pip install jupyterlab pip install --user ipykernel python -m ipykernel install --name tshirt_bot --user
-
Устанавливаем LangChain и сопутствующие библиотеки
pip install openai langchain pandas tiktoken google-api-python-client
Разбираемся с памятью
Для начала научим бота работать с памятью. Память позволяет LLM запоминать предыдущие взаимодействия с пользователем. По умолчанию LLM является stateless, что означает независимую обработку каждого входящего запроса от других взаимодействий. На самом деле, когда мы вводим запросы в интерфейсе ChatGPT, под капотом там работает память, иначе поведение модели напоминало бы героя фильма Memento. В LangChain есть различные реализации памяти, посмотрим как они функционируют.
ConversationBufferMemory
Это самый простой тип памяти. Запустим код:
Пояснения:
Фреймворк не зря называется LangChain: цепочка - это один из базовых объектов, с которым вам предстоит работать, в данном случае мы создаем ConversationChain. В эту цепочку мы передаем языковую модель и память.
Видно, что при использовании буферной памяти, в
history
просто записываются все предыдущие сообщения.
Теперь посмотрим, что происходит под капотом. При каждом новом сообщении на вход модели подается специально подготовленный промпт, в который подаются две переменные input
и template
. А дальше уже работает магия ChatGPT.
Такой тип памяти достаточно хорош. Но есть нюансы. Чем длиннее история, тем больше токенов вы будете подавать на вход. Это накладно по финансам, и, даже если вы не бережете свои деньги, возможности модели не безграничны - около 4k токенов. А значит пора познакомиться со следующим типом памяти.
ConversationSummaryMemory
Как можно понять из названия, этот вид памяти суммаризует историю разговора перед передачей её в параметр history
. Таким образом, можно сократить объем текста, обрабатываемого моделью, и улучшить скорость обработки запросов без существенной потери качества.
Теперь у нас появляется этап предобработки истории. На вход LLM мы подаем предыдущую историю (в виде summary), добавляем к ней текущий вывод модели (new_lines) и получаем новое состояние summary. Да, это очень похоже на обновление состояния ячейки в рекуррентных нейросетях. Вот как теперь выглядит наш предыдущий диалог.
Видно, что размер истории теперь не растет линейно по мере продвижения диалога.
Этих двух типов вполне достаточно, чтобы уловить логику работы с памятью; однако, в LangChain этим все не исчерпывается. Приведу еще несколько видов:
ConversationSummaryBufferMemory - позволяет задать окно (в токенах), в рамках которого мы сохраняем диалоги в неизменной форме, а при превышении - суммаризируем.
ConversationBufferWindowMemory - сохраняет только последние k диалогов.
ConversationKGMemory - использует граф знаний для сохранения памяти.
ConversationEntityMemory - сохраняет в памяти знания об определенном объекте.
Переходим к агентам.
Теперь, когда мы немного разобрались, как работает память, пора переходить к логике взаимодействия с агентами. Ее можно разложить на три компонента:
Языковая модель (LLM) - тут все понятно.
Инструмент (Tool) - дополнительный функционал, который вы добавляете к LLM, например калькулятор или поиск. В фреймворке есть уже готовые реализации, но также можно создать свой инструмент.
Агент (Agent) - это посредник между LLM и Tool. В одном агенте вы можете использовать сразу несколько инструментов и вызывать их в зависимости от контекста задачи.
Начнем с простых реализаций. Я думаю большинство читателей в курсе, что ChatGPT(да и любая другая языковая модель) не имеет доступа к актуальной информации. Поэтому попробуем создать агента, который добывает для LLM информацию из гугла.
Чтобы работать с поиском, нужно будет предварительно получить ключ API и зарегистрировать свой движок. Инструкцию можно посмотреть здесь. Да, это опять моя статья.
Теперь создаем все необходимые элементы агента.
Пояснения:
При инициализации инструмента в параметре
name
нужно обязательно указать Intermediate Answer (в официальных доках этого нет), иначе при запуске возникнет ошибка. Почему? Читайте дальше :)При создании агента, нужно указать его тип, в данном случае мы используем
self-ask-with-search
. Но вообще их множество, например:zero-shot-react-description
,react-docstore
,conversational-react-description
Ну и наконец запускаем нашего агента.
Работает!
Вопрос можно задать и на русском, но тогда после нескольких шагов он тормозит и пытается перевести на английский. Однако, как оказалось, его довольно легко сломать – достаточно указать магическое слово "latest".
Самое время заглянуть под капот. Посмотрим на шаблон промпта модели.
Выглядит он довольно странно. Как оказалось все не случайно, а основано на последних достижениях науки. Такая форма обеспечивает лучшее качество в вопросно-ответных задачах. Статью с разбором можно почитать здесь
Хорошо, допустим мы не хотим так заморачиваться, а желаем реализовать простой поиск. Тогда надо сменить тип агента, можно взять zero-shot-react-description
. Это агент без использования памяти. При создании tool в параметре name
указываем цель для вызова, например Google search.
Запускаем поиск. Теперь он не ломается при указании в запросе необходимости получения последних новостей.
Заключение
Первую часть знакомства с агентами можно завершать. Пока мы протестировали простую реализацию - научились включать поиск. В следующей части я расскажу, как научить агента работать с собственной базой данных, а также попробуем создать кастомный инструмент для анализа данных.
Спасибо за внимание!
Пишу про AI и NLP в телеграм.