Наконец мы добрались до заключительной части сериала о фреймворке LangChain. Небольшое напоминание, о чем шла речь в предыдущих статьях:
LangСhain: создаем свой AI в несколько строк Вводный обзор. Знакомились с ключевыми компонентами, включая модели, промпты, цепочки и агенты.
LangChain для бывалых: память и агенты. часть 1 Здесь в основном разбирались в различных видах памяти.
LangChain для бывалых: память и агенты. часть 2 Погрузились в концепцию агентов, и попробовали реализовать агента для запросов к собственной базе данных.
Сегодня попробуем разобраться в инструментах(Tool). Инструменты являются исполнительной частью агента, которая как раз и добавляет языковой модели дополнительную функциональность. Например, LLM может наврать в базовых арифметических операциях, и лучше доверить вычисления калькулятору. В этот момент и приходит на помощь tool. В самом фреймворке уже есть готовые реализации для популярных задач, но они, естественно, не могут покрыть весь спектр потребностей, поэтому разработчики предусмотрели создание пользовательских типов.
Как и всегда, будем двигаться от простого к сложному. Сначала реализуем простой кейс, а потом попробуем создать инструмент для визуализации спроса в нашем вымышленном магазине.
Решаем уравнения с помощью LLM
Попробуем обучить ChatGPT(в агентах я пока буду использовать только эту модель) решать кубические уравнения.
Сначала зададим вопрос напрямую:
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import HumanMessage
llm = ChatOpenAI(openai_api_key=open_ai_api_key, model_name='gpt-3.5-turbo', temperature=0.0)
answer = llm([HumanMessage(content='Реши кубическое уравнение 3*x**3 - 5*x**2 + 2*x - 13')])
Ответ ChatGPT:
Для далеких от математики людей приведу секретную формулу 16 века. Из которой видно, что бот врет не только в финальном ответе, но и в промежуточных вычислениях.
Попробуем теперь создать инструмент для этой цели. Исходя из соображений лени, я не буду реализовывать формулу Кардано в коде, а воспользуюсь готовым решением из numpy
, тем более, что с помощью него можно решать уравнения и более высоких степеней.
from langchain.tools import BaseTool
from numpy.polynomial import Polynomial
from langchain.agents import initialize_agent
class PolynomRootsTool(BaseTool):
name = 'Решатель полиномиальных уравнений'
# В описании указываем модели, когда следует вызывать этот инструмент
description = 'Используй этот иструмент для решения линейных, квадратных и уравнений более высоких степеней.\
На вход подавай коэффициенты уравнения(только числа). Если какого-то коэффициента нет, значит его значение равно 0'
def _run(self, inp):
coefs = [int(coef.strip()) for coef in inp.split(',')]
# В Polynomial надо подавать коэффициенты в обратном порядке
p = Polynomial(coefs[::-1])
return p.roots()
def _arun(self, *coefs):
raise NotImplementedError('Does not support async')
Здесь стоит обратить внимание на description
- подсказка для LLM о том, как правильно использовать инструмент. Можете проэкспериментировать с разными вариациями промпта и самостоятельно убедиться, насколько важно грамотно продумывать такие описания.
Теперь можно создать агента и вызвать наш инструмент:
tools = [PolynomRootsTool()]
agent = initialize_agent(
agent='zero-shot-react-description',
tools=tools,
llm=llm,
verbose=True,
max_iterations=3,
early_stopping_method='generate',
)
agent.run('Реши кубическое уравнение x**3 + 5*x**2 + 2*x - 12')
Неплохо справился. Правда иногда он не воспринимает комплексные числа, поэтому промпт требует доработки. Напомню, что можно всегда вытащить ваш шаблон для модели и отредактировать.
Учим бота рисовать графики
В прошлый раз наш агент получил доступ к python и попробовал написать простенькую модельку для анализа продаж. Вроде даже что-то получилось, но качество модели было так себе. Будет значительно удобней, если мы явно укажем агенту, какую модель стоит использовать для прогноза. Но сначала нам нужно получить данные:
from langchain.agents import create_sql_agent
from langchain.agents.agent_toolkits import SQLDatabaseToolkit
from sqlalchemy import create_engine
engine = create_engine('sqlite:///tshirt_store.db')
db = SQLDatabase(engine)
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
db_agent_toolkit = create_sql_agent(
llm=llm,
toolkit=toolkit,
verbose=True
)
db_data = db_agent_toolkit.run('Выведи количество заказов за каждый день мая 2023 года, игнорируй лимит. В качестве ответа предоставь список из кортежей (дата, количество)')
Отлично. Данные в кармане. Теперь попробуем визуализировать цифры:
import matplotlib.pyplot as plt
from ast import literal_eval
class SalesVisualizerTool(BaseTool):
name = 'Визуализатор данных о продажах'
description = 'Используй этот инструмент для визуализации данных о продажах магазина. В качестве входных данных используй данные о продажах'
def _run(self, inp):
# Преобразование строки в список кортежей
sales_data = literal_eval(inp)
# Распаковка данных в два списка для дат и продаж
dates, sales = zip(*sales_data)
# Создание графика
plt.figure(figsize=(10, 6))
plt.plot(dates, sales, marker='o')
plt.xlabel('Дата')
plt.xticks(range(0, len(dates), 5), dates[::5])
plt.ylabel('Количество заказов')
plt.title('Данные о продажах')
plt.grid(True)
plt.show()
def _arun(self, *sales_data):
raise NotImplementedError('Does not support async')
Хорошо, инструмент мы создали, но как передать в него данные, которые мы запрашиваем из базы данных?
Когда мы создаем агента, LangChain позволяет добавить в него не один, а множество инструментов : tools = [PolynomRootsTool()]
. Однако наш случай немного отличается. Дело в том, что мы воспользовались специальным типом агента create_sql_agent
и добавили в него тулкит из коробки toolkit = SQLDatabaseToolkit(db=db, llm=llm)
. Это уже не обычный список, в который можно просто так добавить новый элемент.
Давайте посмотрим на объект SQLDatabaseToolkit
:
class SQLDatabaseToolkit(BaseToolkit):
"""Toolkit for interacting with SQL databases."""
db: SQLDatabase = Field(exclude=True)
llm: BaseLanguageModel = Field(exclude=True)
@property
def dialect(self) -> str:
"""Return string representation of dialect to use."""
return self.db.dialect
class Config:
"""Configuration for this pydantic object."""
arbitrary_types_allowed = True
def get_tools(self) -> List[BaseTool]:
"""Get the tools in the toolkit."""
return [
QuerySQLDataBaseTool(db=self.db),
InfoSQLDatabaseTool(db=self.db),
ListSQLDatabaseTool(db=self.db),
QueryCheckerTool(db=self.db, llm=self.llm),
]
Ага, в методе get_tools
как раз и передаются все инструменты для работы с базой данных. Давайте попробуем просто создать дочерний класс и переопределить метод get_tools
:
class SQLDatabaseToolkitViz(SQLDatabaseToolkit):
def get_tools(self):
return [
QuerySQLDataBaseTool(db=self.db),
InfoSQLDatabaseTool(db=self.db),
#ListSQLDatabaseTool(db=self.db),
QueryCheckerTool(db=self.db, llm=self.llm),
SalesVisualizerTool()
]
К сожалению пришлось исключить ListSQLDatabaseTool
из пайплайна, так как агент постоянно падал - надо будет подогнать промпт нового инструмента для согласованности с остальными.
Пора пробовать обновленный инструмент:
toolkit = SQLDatabaseToolkitViz(db=db, llm=llm)
db_agent_toolkit = create_sql_agent(
llm=llm,
toolkit=toolkit,
verbose=True
)
db_agent_toolkit.run('сначала найди, а потом визуализируй количество заказов за каждый день мая 2023 года, не используй LIMIT')
Работает. Но в конце агент все равно падает. Дело в том, что при добавлении нашего инструмента мы нарушили "когерентность" промптов, которые закладывали разработчики, и теперь выводы модели не согласуются с запрограмированной логикой.
Нужно будет либо переписывать SQLDatabaseToolkit
дальше, либо создавать свой собственный пайплайн инструментов. В конце концов, можно просто передать вывод агента базы на вход другого агента.
Аналогично можно создать инструмент и для моделирования наших данных для получения прогноза. Оставим это в качестве домашнего задания.
Наш сериал пора завершать. Я постарался раскрыть все возможности фрейворка, но верю, что энтузиасты наверняка найдут еще множество скрытых фишек. Надеюсь было полезно, спасибо за внимание!
Пишу про AI и NLP в телеграм.