Могут ли LLM заменить аналитиков данных? Создание аналитика, оснащенного LLM

Могут ли аналитики данных быть заменены LLM? Создание аналитика с использованием LLM

Часть 1: дополнение ChatGPT инструментами

Изображение DALL-E 3

Думаю, каждый из нас хотя бы раз за последний год задавался вопросом, сможет ли (или, скорее, когда) ChatGPT заменить вашу роль. И я здесь не исключение.

У нас есть некоторое согласие на то, что последние достижения в области генеративного искусственного интеллекта сильно повлияют на нашу личную жизнь и работу. Однако пока нет четкого представления о том, как наши роли будут меняться со временем.

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

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

Итак, пусть начнется путешествие.

Что такое анализ данных?

Прежде чем перейти к LLM, давайте попробуем определить, что такое аналитика и какие задачи мы выполняем в роли аналитиков.

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

Мне нравится предложенная модель от Gartner. Она выделяет четыре различные техники работы с данными и аналитикой:

  • Описательный анализ отвечает на вопросы вроде “Что случилось?”. Например, каковы были доходы в декабре? В этот подход входят задачи отчетности и работа с BI-инструментами.
  • Диагностический анализ идет дальше и задает вопросы вроде “Почему что-то произошло?”. Например, почему доходы снизились на 10% по сравнению с предыдущим годом? Эта техника требует более подробного анализа и манипулирования данными.
  • Прогнозирующий анализ позволяет нам получить ответы на вопросы вроде “Что произойдет?”. Два ключевых элемента этого подхода: прогнозирование (предсказание будущего для ситуаций “как обычно”) и моделирование различных возможных результатов.
  • Предписывающий анализ влияет на конечные решения. Обычные вопросы: “На что мы должны сосредоточиться?” или “Как мы можем увеличить объем на 10%?”.

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

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

Если мы вернемся к нашему аналитику, усиленному LLM, мы должны сосредоточиться на описательном анализе и задачах отчетности. Лучше начать с основ. Поэтому мы сосредоточимся на изучении LLM для понимания основных вопросов о данных.

Мы определили нашу цель для первого прототипа. Итак, мы готовы перейти к техническим вопросам и обсудить концепцию агентов LLM и инструментов.

Агенты LLM и инструменты

Когда мы раньше использовали LLM (например, для тематического моделирования), мы сами описывали точные шаги в коде. Например, давайте рассмотрим приведенную ниже цепочку действий. Сначала мы просим модель определить настроение отзыва клиента. Затем, в зависимости от настроения, извлекаем из отзыва упоминания о преимуществах или недостатках.

Иллюстрация автора

В этом примере мы четко определили поведение LLM и LLM справился с этой задачей довольно хорошо. Однако такой подход не сработает, если мы создаем что-то более высокоуровневое и неопределенное, например, LLM-питаемый аналитик.

Если вы когда-либо работали в качестве аналитика хотя бы один день, то знаете, что аналитики получают широкий спектр различных вопросов и запросов, начиная от базовых вопросов (например, “Сколько клиентов было на нашем сайте вчера?” или “Можете сделать график для нашего заседания совета завтра?”) до очень высокоуровневых (например, “Какие основные проблемы у клиентов?” или “На каком рынке мы должны запуститься дальше?”). Само собой разумеется, что невозможно описать все возможные сценарии.

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

Важная концепция, связанная с агентами (которую я уже упоминал выше), – это инструменты. Инструменты – это функции, которые LLM может вызывать для получения недостающей информации (например, выполнение SQL-запроса, использование калькулятора или вызов поисковой системы). Инструменты крайне важны, потому что они позволяют вам поднять LLM на новый уровень и взаимодействовать с миром. В этой статье мы в основном сосредоточимся на функциях OpenAI в качестве инструментов.

OpenAI настроила модели так, чтобы они могли работать с функциями и:

  • Вы можете передать в модель список функций с описаниями;
  • Если это относится к вашему запросу, модель вернет вам вызов функции – имя функции и входные параметры для ее вызова.

Вы можете найти более подробную информацию и актуальный список моделей, поддерживающих функции, в документации.

Есть два важных случая использования функций с LLM:

  • Маркировка и извлечение – в этих случаях функции используются для обеспечения формата вывода модели. Вместо обычного вывода с контентом вы получите структурированный вызов функции.
  • Инструменты и маршрутизация – это более интересный случай использования, который позволяет вам создать агента.

Начнем с более простого случая использования извлечения, чтобы узнать, как использовать функции OpenAI.

Случай использования №1: Маркировка и извлечение

Вам может быть интересно, в чем разница между маркировкой и извлечением. Эти термины довольно близки. Единственное отличие состоит в том, извлекает ли модель информацию, представленную в тексте, или маркирует текст, предоставляя новую информацию (т.е. определяет язык или настроение).

В иллюстрации автора

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

В иллюстрации автора

Это будет пример извлечения, поскольку нам нужна только информация, содержащаяся в тексте.

Основной пример работы с OpenAI Completion API

Сначала нам нужно определить функцию. OpenAI ожидает описание функции в формате JSON. Этот JSON будет передан LLM, поэтому нам нужно сообщить ему весь контекст: что делает эта функция и как ее использовать.

Вот пример JSON-функции. Мы указали:

  • name и description для самой функции,
  • type и description для каждого аргумента,
  • список необходимых входных параметров для функции.
extraction_functions = [    {        "name": "extract_information",        "description": "извлекает информацию",        "parameters": {            "type": "object",            "properties": {                "metric": {                    "type": "string",                    "description": "основная метрика, которую необходимо рассчитать, например, 'количество пользователей' или 'количество сеансов'",                },                "filters": {                    "type": "string",                    "description": "фильтры, применяемые для расчета (не включайте фильтры по датам здесь)",                },                "dimensions": {                    "type": "string",                    "description": "параметры для разделения вашей метрики",                },                "period_start": {                    "type": "string",                    "description": "начальный день периода для отчета",                },                "period_end": {                    "type": "string",                    "description": "конечный день периода для отчета",                },                "output_type": {                    "type": "string",                    "description": "желаемый вид вывода",                    "enum": ["число", "визуализация"]                }            },            "required": ["metric"],        },    }]

Для этого использования нет необходимости реализовывать саму функцию, потому что мы не будем ее использовать. Мы получаем структурированные LLM-ответы в виде вызовов функций.

Теперь мы можем использовать стандартный OpenAI Chat Completion API для вызова функции. Мы передали в вызов API следующее:

  • модель – Я использовал последнюю версию ChatGPT 3.5 Turbo, которая может работать с функциями,
  • список сообщений – одно системное сообщение для настройки контекста и запрос пользователя,
  • список ранее определенных функций.
import openaimessages = [    {        "role": "system",        "content": "Извлечь соответствующую информацию из предоставленного запроса."    },    {        "role": "user",        "content": "Как менялось количество пользователей iOS со временем?"    }]response = openai.ChatCompletion.create(    model = "gpt-3.5-turbo-1106",     messages = messages,    functions = extraction_functions)print(response)

В результате мы получили следующий JSON.

{  "id": "chatcmpl-8TqGWvGAXZ7L43gYjPyxsWdOTD2n2",  "object": "chat.completion",  "created": 1702123112,  "model": "gpt-3.5-turbo-1106",  "choices": [    {      "index": 0,      "message": {        "role": "assistant",        "content": null,        "function_call": {          "name": "extract_information",          "arguments": "{\"metric\":\"количество пользователей\",\"фильтры\":\"платформа='iOS'\",\"измерения\":\"дата\",\"начало_периода\":\"2021-01-01\",\"конец_периода\":\"2021-12-31\",\"тип_вывода\":\"визуализация\"}"        }      },      "finish_reason": "function_call"    }  ],  "usage": {    "prompt_tokens": 159,    "completion_tokens": 53,    "total_tokens": 212  },  "system_fingerprint": "fp_eeff13170a"}

Помните, что функции и вызовы функций берутся в пределы лимита на токены и оплачиваются.

Модель вернула вызов функции вместо обычного ответа: можно увидеть, что content пуст, а finish_reason равняется function_call. В ответе также приведены параметры входных данных для вызова функции:

  • metric = "количество пользователей",
  • filters = "платформа = 'iOS'",
  • dimensions = "дата",
  • period_start = "2021-01-01",
  • period_start = "2021-12-31",
  • output_type = "визуализация".

Модель справилась довольно хорошо. Единственная проблема в том, что она предположила период исходя из ничего. Мы можем исправить это, добавив более явные указания в системное сообщение, например, "Извлечь соответствующую информацию из предоставленного запроса. Извлечь ТОЛЬКО информацию, представленную в исходном запросе; не добавлять ничего лишнего. Вернуть частичную информацию, если что-то отсутствует."

По умолчанию модели самостоятельно решают, использовать функции или нет (function_call = 'auto'). Мы можем требовать от нее всегда возвращать конкретный вызов функции или не использовать функции вообще.

# всегда вызывать функцию extract_informationresponse = openai.ChatCompletion.create(    model = "gpt-3.5-turbo-1106",    messages = messages,    functions = extraction_functions,    function_call = {"name": "extract_information"})# без вызовов функцийresponse = openai.ChatCompletion.create(    model = "gpt-3.5-turbo-1106",    messages = messages,    functions = extraction_functions,    function_call = "none")

Мы получили первую работающую программу, использующую функции LLM. Это потрясающе. Однако описывать функции в формате JSON не очень удобно. Давайте обсудим, как сделать это проще.

Использование Pydantic для определения функций

Чтобы удобнее определять функции, мы можем воспользоваться Pydantic. Pydantic является самой популярной библиотекой Python для валидации данных.

Мы уже использовали Pydantic для определения парсера вывода LangChain.

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

from pydantic import BaseModel, Fieldfrom typing import Optionalclass RequestStructure(BaseModel):  """extracts information"""  metric: str = Field(description = "основная метрика, которую нам нужно рассчитать, например, 'количество пользователей' или 'количество сеансов'")  filters: Optional[str] = Field(description = "фильтры, применяемые к расчету (не включайте здесь фильтры по датам)")  dimensions: Optional[str] = Field(description = "параметры для разделения метрики")  period_start: Optional[str] = Field(description = "начальный день периода для отчета")  period_end: Optional[str] = Field(description = "конечный день периода для отчета")  output_type: Optional[str] = Field(description = "желаемый вывод", enum = ["количество", "визуализация"])

Затем мы можем использовать LangChain, чтобы преобразовать класс Pydantic в функцию OpenAI.

from langchain.utils.openai_functions import convert_pydantic_to_openai_functionextract_info_function = convert_pydantic_to_openai_function(RequestStructure,     name = 'extract_information')

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

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

{'name': 'extract_information', 'description': 'extracts information', 'parameters': {'title': 'RequestStructure',  'description': 'extracts information',  'type': 'object',  'properties': {'metric': {'title': 'Metric',    'description': "основная метрика, которую нам нужно рассчитать, например, 'количество пользователей' или 'количество сеансов'",    'type': 'string'},   'filters': {'title': 'Filters',    'description': 'фильтры, применяемые к расчету (не включайте здесь фильтры по датам)',    'type': 'string'},   'dimensions': {'title': 'Dimensions',    'description': 'параметры для разделения метрики',    'type': 'string'},   'period_start': {'title': 'Period Start',    'description': 'начальный день периода для отчета',    'type': 'string'},   'period_end': {'title': 'Period End',    'description': 'конечный день периода для отчета',    'type': 'string'},   'output_type': {'title': 'Output Type',    'description': 'желаемый вывод',    'enum': ['количество', 'визуализация'],    'type': 'string'}},  'required': ['metric']}}

Теперь мы можем использовать его в нашем вызове OpenAI. Давайте перейдем от OpenAI API к LangChain, чтобы сделать наши вызовы API более модульными.

Определение цепочки LangChain

Давайте определим цепочку для извлечения необходимой информации из запросов. Мы будем использовать LangChain, поскольку это самый популярный фреймворк для LLM. Если вы раньше с ним не работали, я рекомендую вам ознакомиться с основами в одной из моих предыдущих статей.

Наша цепочка проста. Она состоит из модели Open AI и промпт с одной переменной request (сообщение пользователя).

Мы также использовали функцию bind для передачи аргумента functions модели. Функция bind позволяет задавать постоянные аргументы для наших моделей, которые не являются частью входных данных (например, functions или temperature).

from langchain.prompts import ChatPromptTemplatefrom langchain.chat_models import ChatOpenAImodel = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\  .bind(functions = [extract_info_function])prompt = ChatPromptTemplate.from_messages([    ("system", "Извлечь соответствующую информацию из предоставленного запроса. \            Извлекайте ТОЛЬКО информацию, представленную в исходном запросе. \            Не добавляйте ничего еще. \            Возвращайте частичную информацию, если что-то отсутствует."),    ("human", "{request}")])extraction_chain = prompt | model

Теперь пришло время попробовать нашу функцию. Нам нужно использовать метод invoke и передать запрос.

extraction_chain.invoke({'request': "Сколько клиентов посетило наш сайт на iOS в апреле 2023 года из разных стран?"})

В выводе мы получили AIMessage без содержимого, но с вызовом функции.

AIMessage(  content='',   additional_kwargs={    'function_call': {       'name': 'extract_information',        'arguments': '''{         "metric":"количество клиентов", "фильтры":"устройство = 'iOS'",         "измерения":"страна", "начало_периода":"2023-04-01",         "конец_периода":"2023-04-30", "тип_вывода":"число"}        '''}  })

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

Случай использования #2: Инструменты и маршрутизация

Пришло время использовать инструменты и усилить нашу модель внешними возможностями. Модели в этом подходе являются рассуждающими механизмами и могут решать, какие инструменты использовать и когда (это называется маршрутизацией).

У LangChain есть концепция инструментов – интерфейсов, которыми агенты могут взаимодействовать с миром. Инструменты могут быть функциями, цепочками LangChain или даже другими агентами.

Мы легко можем преобразовать инструменты в функции OpenAI, используя format_tool_to_openai_function, и продолжать передавать аргументы functions в LLM.

Определение пользовательского инструмента

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

Чтобы определить инструмент, нам нужно создать функцию и использовать декоратор @tool.

from langchain.agents import tool@tooldef percentage_difference(metric1: float, metric2: float) -> float:    """Вычисляет процентное отличие между метриками"""    return (metric2 - metric1)/metric1*100

Теперь у этой функции есть параметры name и description, которые будут переданы LLM.

print(percentage_difference.name)# percentage_difference.nameprint(percentage_difference.args)# {'metric1': {'title': 'Метрика1', 'type': 'число'},# 'metric2': {'title': 'Метрика2', 'type': 'число'}}print(percentage_difference.description)# 'percentage_difference(metric1: float, metric2: float) -> float - Вычисляет процентное отличие между метриками'

Эти параметры будут использованы для создания спецификации функции OpenAI. Давайте преобразуем наш инструмент в функцию OpenAI.

from langchain.tools.render import format_tool_to_openai_functionprint(format_tool_to_openai_function(percentage_difference))

Мы получили следующий JSON в качестве результата. Он описывает структуру, но отсутствуют описания полей.

{'name': 'percentage_difference', 'description': 'percentage_difference(metric1: float, metric2: float) -> float - Вычисляет процентное отличие между метриками', 'parameters': {'title': 'percentage_differenceSchemaSchema',  'type': 'object',  'properties': {'metric1': {'title': 'Метрика1', 'type': 'число'},   'metric2': {'title': 'Метрика2', 'type': 'число'}},  'required': ['metric1', 'metric2']}}

Мы можем использовать Pydantic для указания схемы аргументов.

class Metrics(BaseModel):    metric1: float = Field(description="Базовое значение метрики для вычисления разницы")    metric2: float = Field(description="Новое значение метрики, с которым мы сравниваем базовое")@tool(args_schema=Metrics)def percentage_difference(metric1: float, metric2: float) -> float:    """Вычисляет процентное отличие между метриками"""    return (metric2 - metric1)/metric1*100

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

{'name': 'percentage_difference', 'description': 'percentage_difference(metric1: float, metric2: float) -> float - Вычисляет процентное отличие между метриками', 'parameters': {'title': 'Метрики',  'type': 'object',  'properties': {'metric1': {'title': 'Метрика1',    'description': 'Базовое значение метрики для расчета отличия',    'type': 'number'},   'metric2': {'title': 'Метрика2',    'description': 'Новое значение метрики, с которым мы сравниваем базовое значение',    'type': 'number'}},  'required': ['metric1', 'metric2']}}

Итак, мы определили инструмент, которым сможет воспользоваться LLM. Давайте попробуем его в практике.

Использование инструмента в практике

Определим цепочку и передадим наш инструмент функции. Затем мы сможем протестировать его на пользовательском запросе.

model = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\
  .bind(functions = [format_tool_to_openai_function(percentage_difference)])prompt = ChatPromptTemplate.from_messages([    ("system", "Ты - аналитик продукта, готовый помочь своей команде. Ты очень строгий и точный. Ты используешь только факты, не изобретая информацию."),    ("user", "{request}")])analyst_chain = prompt | modelanalyst_chain.invoke({'request': "В апреле у нас было 100 пользователей, а в мае только 95. Какая разница в процентах?"})

Мы получили вызов функции с правильными аргументами, значит, она работает.

AIMessage(content='', additional_kwargs={    'function_call': {      'name': 'percentage_difference',       'arguments': '{"metric1":100,"metric2":95}'}  })

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

from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParseranalyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()result = analyst_chain.invoke({'request': "В апреле было 100 пользователей, а в мае стало 110 пользователей. Как изменилось количество пользователей?"})

Теперь мы получили вывод в более структурированном виде и легко можем получить аргументы для нашего инструмента, такие как result.tool_input.

AgentActionMessageLog(   tool='percentage_difference',    tool_input={'metric1': 100, 'metric2': 110},    log="\nВызов: `percentage_difference` с `{'metric1': 100, 'metric2': 110}`\n\n\n",    message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'percentage_difference', 'arguments': '{"metric1":100,"metric2":110}'}})])

Итак, мы можем выполнить функцию, как запросил LLM, вот так.

observation = percentage_difference(result.tool_input)print(observation)# 10

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

from langchain.prompts import MessagesPlaceholdermodel = ChatOpenAI(temperature=0.1, model = 'gpt-3.5-turbo-1106')\
  .bind(functions = [format_tool_to_openai_function(percentage_difference)])prompt = ChatPromptTemplate.from_messages([    ("system", "Ты - аналитик продукта, готовый помочь своей команде. Ты очень строгий и точный. Ты используешь только факты, не изобретая информацию."),    ("user", "{request}"),    MessagesPlaceholder(variable_name="observations")])analyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()result1 = analyst_chain.invoke({    'request': "В апреле было 100 пользователей, а в мае стало 110 пользователей. Как изменилось количество пользователей?",    "observations": []})observation = percentage_difference(result1.tool_input)print(observation)# 10

Затем нам нужно добавить наблюдение в нашу переменную observations. Мы можем использовать функцию format_to_openai_functions, чтобы отформатировать наши результаты в ожидаемом формате для модели.

from langchain.agents.format_scratchpad import format_to_openai_functionsformat_to_openai_functions([(result1, observation), ])

В результате мы получили такое сообщение, которое ЛЛМ может понять.

[AIMessage(content='', additional_kwargs={'function_call': {'name': 'percentage_difference',                                            'arguments': '{"metric1":100,"metric2":110}'}}), FunctionMessage(content='10.0', name='percentage_difference')]

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

result2 = analyst_chain.invoke({    'request': "В апреле было 100 пользователей, а в мае - 110 пользователей. Как изменилось количество пользователей?",    "observations": format_to_openai_functions([(result1, observation)])})

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

AgentFinish(  return_values={'output': 'Количество пользователей увеличилось на 10%.'},   log='Количество пользователей увеличилось на 10%.')

Если бы мы работали с обычным OpenAI Chat Completion API, мы могли бы просто добавить еще одно сообщение с ролью = инструмент. Подробный пример можно найти здесь.

Если мы включим отладку, то сможем увидеть точное сообщение, которое было передано в OpenAI API.

System: Вы - аналитик продукта, готовый помочь своей команде продукта. Вы очень точны и точны. Вы используете только факты, не изобретая информацию.Human: В апреле было 100 пользователей, а в мае - 110 пользователей. Как изменилось количество пользователей?AI: {'name': 'percentage_difference', 'arguments': '{"metric1":100,"metric2":110}'}Function: 10.0

Чтобы включить отладку LangChain, выполните следующий код и вызовите свою цепочку, чтобы увидеть, что происходит под капотом.

import langchainlangchain.debug = True

Мы попробовали работать с одним инструментом, но давайте расширим наш арсенал и посмотрим, как ЛЛМ может с этим справиться.

Маршрутизация: использование нескольких инструментов

Добавим несколько инструментов в наш аналитический набор инструментов:

  • получить количество активных пользователей в месяц
  • использование Wikipedia.

Сначала определим фиктивную функцию для подсчета аудитории с фильтрами по месяцу и городу. Мы снова будем использовать Pydantic, чтобы задать входные аргументы для нашей функции.

import datetimeimport randomclass Filters(BaseModel):    month: str = Field(description="Активность клиента в заданном месяце в формате %Y-%m-%d")    city: Optional[str] = Field(description="Город проживания клиентов (по умолчанию без фильтра)",                     enum = ["Лондон", "Берлин", "Амстердам", "Париж"])@tool(args_schema=Filters)def get_monthly_active_users(month: str, city: str = None) -> int:    """Возвращает количество активных клиентов за указанный месяц"""    dt = datetime.datetime.strptime(month, '%Y-%m-%d')    total = dt.year + 10*dt.month    if city is None:        return total    else:        return int(total*random.random())

Затем давайте используем пакет Python wikipedia, чтобы позволить модели обращаться к Wikipedia.

import wikipediaclass Wikipedia(BaseModel):    term: str = Field(description="Термин для поиска")@tool(args_schema=Wikipedia)def get_summary(term: str) -> str:    """Возвращает базовые знания о заданном термине, предоставленные Wikipedia"""    return wikipedia.summary(term)

Определим словарь со всеми функциями, которые наша модель теперь знает. Этот словарь поможет нам в дальнейшем при маршрутизации.

toolkit = {    'percentage_difference': percentage_difference,    'get_monthly_active_users': get_monthly_active_users,    'get_summary': get_summary}analyst_functions = [format_tool_to_openai_function(f)   for f in toolkit.values()]

Я внес некоторые изменения в наше предыдущее настроение:

  • Я немного изменил системное сообщение, чтобы заставить ЛЛМ обратиться к Wikipedia, если ему нужны некоторые базовые знания.
  • Я изменил модель на GPT 4, потому что она лучше справляется с задачами, требующими рассуждений.
from langchain.prompts import MessagesPlaceholdermodel = ChatOpenAI(temperature=0.1, model = 'gpt-4-1106-preview')\  .bind(functions = analyst_functions)prompt = ChatPromptTemplate.from_messages([    ("system", "Вы — аналитик продукта, готовый помочь вашей команде. Вы очень строги и точны в своих решениях. \
        Вы используете только информацию, предоставленную в начальном запросе. \
        Если вам необходимо узнать какую-то информацию, например, название столицы, вы можете использовать Википедию."),    
    ("user", "{request}"),    
    MessagesPlaceholder(variable_name="observations")])analyst_chain = prompt | model | OpenAIFunctionsAgentOutputParser()

Мы можем вызвать нашу цепочку со всеми функциями. Давайте начнем с довольно простого запроса.

result1 = analyst_chain.invoke({    'request': "Сколько пользователей было в апреле 2023 года в Берлине?",    "observations": []})print(result1)

Мы получили вызов функции get_monthly_active_users с входными параметрами — {'month': '2023–04–01', 'city': 'Berlin'}, что выглядит правильно. Модель смогла найти правильный инструмент и решить задачу.

Давайте попробуем сделать задачу немного сложнее.

result1 = analyst_chain.invoke({    'request': "Как менялось количество пользователей в столице Германии\        между апрелем и маем 2023 года?",    "observations": []})

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

  • вызвать Википедию, чтобы узнать столицу Германии
  • вызвать функцию get_monthly_active_users дважды, чтобы получить активных пользователей в апреле и мае
  • вызвать percentage_difference для вычисления разницы между метриками.

Это выглядит достаточно сложно. Давайте посмотрим, сможет ли ChatGPT справиться с этим вопросом.

На первом вызове LLM вернул обратно вызов функции Википедии со следующими параметрами — {'term': 'столица Германии'}. Пока что он следует нашему плану.

Предоставим наблюдение и посмотрим, какие будут следующие шаги.

observation1 = toolkit[result1.tool](result1.tool_input)print(observation1)# Столицей Германии является город-штат Берлин. В нем находится # резиденция Президента Германии, которым является Шлосс Бельвю. # Бундесрат (федеральный совет) представляет федеральные земли Германии # и имеет свои офисы в бывшем Прюсском Доме Лордов в Берлине. # Хотя большинство министерств расположены в Берлине, # некоторые из них, а также некоторые вспомогательные отделы, расположены в Бонне, # бывшей столице Западной Германии.# Хотя Берлин официально является столицей Федеративной Республики Германия, # 8 000 из 18 000 сотрудников, занятых в федеральной бюрократии, # все еще работают в Бонне, что находится примерно в 600 км (370 милях) от Берлина.# источник: https://en.wikipedia.org/wiki/Столица_Германии result2 = analyst_chain.invoke({    'request': "Как менялось количество пользователей в столице Германии между апрелем и маем 2023 года?",    "observations": format_to_openai_functions([(result1, observation1)])})

Модель хочет выполнить get_monthly_active_users с аргументами {'month': '2023–04–01', 'city': 'Berlin'}. Давайте сделаем это и вернем информацию модели снова.

observation2 = toolkit[result2.tool](result2.tool_input)print(observation2)# 168result3 = analyst_chain.invoke({    'request': "Как менялось количество пользователей в столице Германии между апрелем и маем 2023 года?",    "observations": format_to_openai_functions([(result1, observation1), (result2, observation2)])})

Затем модель просит вызвать get_monthly_active_users снова с аргументами {'month': '2023–05–01', 'city': 'Berlin'}. Пока что она справляется отлично. Давайте следуем ее логике.

observation3 = toolkit[result3.tool](result3.tool_input)print(observation3)# 1046result4 = analyst_chain.invoke({    'request': "Как изменялось количество пользователей из столицы Германии между апрелем и маем 2023 года?",    "observations": format_to_openai_functions(      [(result1, observation1), (result2, observation2),       (result3, observation3)])})

Последующий результат – вызов функции percentage_difference со следующими аргументами {'metric1': 168, 'metric2': 1046}. Давайте вычислим наблюдение и вызовем нашу цепочку еще раз. Надеюсь, это будет последним шагом.

observation4 = toolkit[result4.tool](result4.tool_input)print(observation4)# 523.27result5 = analyst_chain.invoke({    'request': "Как изменялось количество пользователей из столицы Германии между апрелем и маем 2023 года?",    "observations": format_to_openai_functions(      [(result1, observation1), (result2, observation2),       (result3, observation3), (result4, observation4)])})

В конце концов, мы получили следующий ответ от модели: Количество пользователей из Берлина, столицы Германии, увеличилось примерно на 523.27% между апрелем и маем 2023 года.

Вот полная схема вызовов LLM для этого вопроса.

Иллюстрация от автора

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

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

Вы можете найти полный код на GitHub.

Резюме

В этой статье мы узнали, как автоматизировать LLM с помощью внешних инструментов, используя функции OpenAI. Мы рассмотрели два случая использования: извлечение структурированного вывода и маршрутизацию для использования внешней информации для задач. Конечный результат меня вдохновляет, поскольку LLM может отвечать на довольно сложные вопросы, используя три различных инструмента.

Вернемся к исходному вопросу о том, могут ли LLM заменить аналитиков данных. Наш текущий прототип является базовым и далеким от возможностей младших аналитиков, но это только начало. Следите за обновлениями! Мы погрузимся глубже в различные подходы к агентам LLM. В следующий раз мы попробуем создать агента, который может получить доступ к базе данных и отвечать на базовые вопросы.

Ссылки

Эта статья вдохновлена курсом “Функции, инструменты и агенты с LangChain” от DeepLearning.AI