В своей предыдущей статье я написал о многообещающем фреймворке LangChain. Туториал был достаточно коротким; удалось охватить только самые базовые концепции проекта (и то не все). В этой части предстоит более глубокое погружение. Разберемся, как можно добавить память в диалоги с LLM, а также задействуем мощь агентов. С помощью агентов вы сможете подключить вашу языковую модель к внешним ресурсам: поисковой машине, Википедии или даже к собственной базе данных. На мой взгляд, это killer-feature всего фреймворка. Агенты - это следующий эволюционный шаг развития ИИ, и это не только моя больная фантазия. Например, о перспективах использования агентов можно почитать здесь.

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

Настраиваем окружение для проекта

Как обычно, вам понадобится получить ключ API OpenAI. Можно, конечно, в качестве LLM использовать и другие модели (загрузить из Hagging Face), но их качество пока сильно отстает от ChatGPT. Далее предполагается, что ключ у вас уже есть.

  1. Создаем директорию mkdir tshirt_bot и переходим в нее cd tshirt_bot

  2. Настраиваем виртуальное окружение.

    conda create --name tshirt_bot python=3.10
    conda activate tshirt_bot
  3. Также зарегистрируем наш кернел в jupyter

    pip install jupyterlab
    pip install --user ipykernel
    python -m ipykernel install --name tshirt_bot --user
  4. Устанавливаем LangChain и сопутствующие библиотеки

    pip install openai langchain pandas tiktoken google-api-python-client

Разбираемся с памятью

Для начала научим бота работать с памятью. Память позволяет LLM запоминать предыдущие взаимодействия с пользователем. По умолчанию LLM является stateless, что означает независимую обработку каждого входящего запроса от других взаимодействий. На самом деле, когда мы вводим запросы в интерфейсе ChatGPT, под капотом там работает память, иначе поведение модели напоминало бы героя фильма Memento. В LangChain есть различные реализации памяти, посмотрим как они функционируют.

ConversationBufferMemory

Это самый простой тип памяти. Запустим код:

ConversationBufferMemory
ConversationBufferMemory

Пояснения:

  • Фреймворк не зря называется LangChain: цепочка - это один из базовых объектов, с которым вам предстоит работать, в данном случае мы создаем ConversationChain. В эту цепочку мы передаем языковую модель и память.

  • Видно, что при использовании буферной памяти, в history просто записываются все предыдущие сообщения.

Теперь посмотрим, что происходит под капотом. При каждом новом сообщении на вход модели подается специально подготовленный промпт, в который подаются две переменные input и template. А дальше уже работает магия ChatGPT.

prompt buffer
prompt buffer

Такой тип памяти достаточно хорош. Но есть нюансы. Чем длиннее история, тем больше токенов вы будете подавать на вход. Это накладно по финансам, и, даже если вы не бережете свои деньги, возможности модели не безграничны - около 4k токенов. А значит пора познакомиться со следующим типом памяти.

ConversationSummaryMemory

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

prompt summary
prompt summary

Теперь у нас появляется этап предобработки истории. На вход LLM мы подаем предыдущую историю (в виде summary), добавляем к ней текущий вывод модели (new_lines) и получаем новое состояние summary. Да, это очень похоже на обновление состояния ячейки в рекуррентных нейросетях. Вот как теперь выглядит наш предыдущий диалог.

ConversationSummaryMemory
ConversationSummaryMemory

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

  • ConversationSummaryBufferMemory - позволяет задать окно (в токенах), в рамках которого мы сохраняем диалоги в неизменной форме, а при превышении - суммаризируем.

  • ConversationBufferWindowMemory - сохраняет только последние k диалогов.

  • ConversationKGMemory - использует граф знаний для сохранения памяти.

  • ConversationEntityMemory - сохраняет в памяти знания об определенном объекте.

Переходим к агентам.

Теперь, когда мы немного разобрались, как работает память, пора переходить к логике взаимодействия с агентами. Ее можно разложить на три компонента:

  1. Языковая модель (LLM) - тут все понятно.

  2. Инструмент (Tool) - дополнительный функционал, который вы добавляете к LLM, например калькулятор или поиск. В фреймворке есть уже готовые реализации, но также можно создать свой инструмент.

  3. Агент (Agent) - это посредник между LLM и Tool. В одном агенте вы можете использовать сразу несколько инструментов и вызывать их в зависимости от контекста задачи.

Начнем с простых реализаций. Я думаю большинство читателей в курсе, что ChatGPT(да и любая другая языковая модель) не имеет доступа к актуальной информации. Поэтому попробуем создать агента, который добывает для LLM информацию из гугла.

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

Ключи к API
Ключи к 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.

Простой поиск
Простой поиск

Запускаем поиск. Теперь он не ломается при указании в запросе необходимости получения последних новостей.

Новости о LangChain
Новости о LangChain

Заключение

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

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

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

Комментарии (2)