Вступление

Одними из наиболее интересных подходов в создании AI-систем в 2024 году являются агенты (agents, agentic AI systems) и мультиагентные системы (multi-agent systems).

Оба они показывают большую эффективность при решении многих задач и позволяют решать более сложные задачи с помощью LLM

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

Если хотите разбираться глубже в том, как использовать LLM и AI-агентов в продуктивности, работе, бизнесе и повседневной жизни — подписывайтесь на наш Telegram-канал “Вкалывают роботы”.

Поговорим сегодня про Агентов.

Ключевая черта, отличающая агентов от других AI-систем — это автономность

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

Решение реальной бизнес-задачи

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

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

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

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

Требования к агенту

Наш агент должен уметь:

  • Задавать уточняющие вопросы клиенту.

  • Искать информацию в поисковике.

Для этого используются инструменты (tools), которые становятся своего рода «руками» LLM, позволяя взаимодействовать с внешним миром.

Приблизительно это работает так:

  1. Мы объясняем LLM, что есть в её распоряжении и просим выполнить какую-то задачу.

USER: У тебя есть следующие Инструменты: search(query) — для поиска в интернете, human(question) — чтобы задать вопрос пользователю

Какой адрес большого театра?

  1. LLM просит нас выполнить функцию с заданными параметрами (сама она этого сделать не может)

LLM: run search('большой театр адрес')

  1. Мы на своей стороне выполняем функцию search и отдаем LLM результаты

USER: run search('большой театр адрес') results = {результаты поиска, например первая страница выдачи гугла}

  1. LLM используя эти данные отвечает 

LLM: Адрес большого театра: ул. Большая Дмитровка, 4/2, Москва

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

Реализация агента

Для создания агента будем использоваться langgraph — это очень крутая библиотека, которая упрощает работу с агентами и инструментами. В качестве LLM будем использовать chatgpt-4o.

Основа приложения на langgraph — это, собственно граф: несколько нод, объединенных связями.

В нашем случае это будет 2 ноды:

  • Нода взаимодействия с LLM

  • Нода вызова инструментов

Также нам потребуется 2 инструмента:

  1. Поиск в интернете – будем использовать Tavily

  2. Взаимодействие с пользователем – human tool из langchain

Настройка модели и инструментов

from langchain_openai import ChatOpenAI 
from langgraph.graph import StateGraph, END, MessagesState 
from langgraph.prebuilt import ToolNode 
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_community.tools import TavilySearchResults 
from langchain.agents import load_tools

# Tavily для поиска в интернете
search_tool = TavilySearchResults(max_results=10, 
                                  include_answer=True, 
                                  include_raw_content=False)
# Human tool для взаимодействия с пользователем
human_tool = load_tools(["human"])[0]

tools = [search_tool, human_tool]

# Настраиваем модель и биндим к ней инструменты
llm = ChatOpenAI(model="gpt-4o", max_tokens=None).bind_tools(tools)

Теперь у нас определены инструменты и модель, которая знает про эти инструменты

Далее создадим ноду для взаимодействия с LLM и граф:

# функция, которая определяет нужно ли вызывать инструменты 
# или результат уже получен
def should_continue(state: MessagesState):
    messages = state["messages"]
    last_message = messages[-1]
    if last_message.tool_calls:
        return "tools"
    return END

# функция для ноды взаимодейтсвия с LLM
def gather_data(state: MessagesState):

    messages = state["messages"]

    messages.append(SystemMessage(content='''
    Ты – ассистент юриста. 
    Клиент обратился с проблемой. Твоя задача - собрать данные о клиенте, 
    компании и покупке
    
    Тебе нужно собрать следующие данные:
    * о компании
    ** юридическое наименование (ООО, ИП и другие правовые формы)
    ** Юридический адрес
    ** ИНН, ОГРН(ИП)
    ** контактный email 
    * Данные клиента:
    ** ФИО
    ** Паспортные данные (номер, кем и когда выдан)
    * Данные о покупке:
    ** дата
    ** номер (если есть)
    ** сумма

    
    Ответь JSON в формате 
    {{"case_data": все данные о клиенте и компании текстовом виде}}.
    Никакий других данных в ответе быть не должно, 
    только JSON, который можно распарсить

    '''))

    response = llm.invoke(messages)

    # информация для отладки
    print(json.dumps(response.tool_calls, indent=2,ensure_ascii=False))
    print(json.dumps(response.content, indent=2,ensure_ascii=False))
   
    return {"messages": [response]}

# встроенная в langgraph нода вызова инструментов
tool_node = ToolNode(tools)

workflow = StateGraph(MessagesState)

# задаём ноды
workflow.add_node("gather_data_node", gather_data)
workflow.add_node("tools", tool_node)

# задаём переходы между нодами
# входная нода - gather_data_node
workflow.set_entry_point("gather_data_node")
# после gather_data_node вызываем should_continue, 
# чтобы определить что делать дальше
workflow.add_conditional_edges("gather_data_node", 
                               should_continue, 
                               ["tools", END])
# после вызова инструментов всегда возвращаемся к ноде LLM, 
# чтобы отдать ей результат вызова инструментов
workflow.add_edge("tools", "gather_data_node")

graph = workflow.compile()


# изначальное описание ситуации от клиента
prompt = """
    Вот описание ситуации от клиента:
    Я купил абонемент в спортзале Wordclass на лени��ском 109 на год. 
    Через месяц я захотел его вернуть и на ресепшене мне отказали.
    """



input_messages = [HumanMessage(prompt)]
output = graph.invoke({"messages": input_messages})

В итоге у нас получился вот такой граф:

Как это работает

У графов в langgraph есть стейт, доступный при выполнении каждой ноды, куда можно записывать данные. Стейт можно задать произвольно, в нашем случае используется MessagesState. Этот стейт хранит историю всех сообщений между LLM и пользователем, аналогично тому, как это реализовано в веб-версии ChatGPT. При вызове LLM доступна вся история взаимодействий.

Изначально вызывается gather_data_node. После её выполнения LLM либо возвращает конечный результат, либо «просит» выполнить один из доступных инструментов. Решение о дальнейшем действии принимает функция should_continue: она либо направляет граф в ноду инструментов, либо завершает его выполнение.

После выполнения инструментов граф всегда возвращается к ноде взаимодействия с LLM (gather_data_node), чтобы передать результаты выполнения инструментов для дальнейшей обработки.

Изначальное описание проблемы клиента будет таким:

Я купил абонемент в спортзале Wordclass на Ленинском 109 на год. Через месяц я захотел его вернуть, и на ресепшене мне отказали.

Запускаем

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

Запуск 1

Первый запуск выдал такое:

По умолчанию LLM просто “ленится” и просит человека предоставить абсолютно все данные 🙂

Давайте объясним ему, как правильно пользоваться инструментами. Немного поправим промпт:

Ты – ассистент юриста. Клиент обратился с проблемой. Твоя задача - собрать данные о клиенте, компании и покупке.

Тебе нужно собрать следующие данные:

    * о компании

    ** юридическое наименование (ООО, ИП и другие правовые формы)

    ** Юридический адрес

    ** ИНН, ОГРН(ИП)

    ** контактный email 

    * Данные клиента:

    ** ФИО

    ** Паспортные данные (номер, кем и когда выдан)

    * Данные о покупке:

    ** дата

    ** номер (если есть)

    ** сумма

Для поиска данных о компании используй инструмент TavilySearchResults.

С помощью HumanInputRun ты можешь уточнить у клиента его данные и данные о покупке    

Ответь JSON в формате {{"case_data": все данные о клиенте и компании текстовом виде}}.

Никакий других данных в ответе быть не должно, только JSON, который можно распарсить

Запуск 2

[
  {
    "name": "tavily_search_results_json",
    "args": {
      "query": "Worldclass Ленинский 109 юридическая информация"
    },
    "id": "call_RIOMHjxV5qoQjTXVrdV8orNB",
    "type": "tool_call"
  },
  {
    "name": "human",
    "args": {
      "query": "Пожалуйста, предоставьте ваши ФИО, паспортные данные, а также информацию о покупке (дата, номер, сумма), если у вас есть эти данные."
    },
    "id": "call_DhUwVdO14d7dUFH1fyPp0YvG",
    "type": "tool_call"
  }
]

В первой итерации наш агента решил сделать поисковый запрос «Worldclass Ленинский 109 юридическая информация» и попросил предоставить нас данные

LLM: Пожалуйста, предоставьте ваши ФИО, паспортные данные, а также информацию о покупке (дата, номер, сумма), если у вас есть эти данные.

USER: Иванов Иван Ивановч 4444 444444 (тут я намеренно сделал опечатку и дал ему не все данные)

[
  {
    "name": "tavily_search_results_json",
    "args": {
      "query": "Worldclass юридическая информация"
    },
    "id": "call_LujnH75FUxTEpOim9QZCoXEI",
    "type": "tool_call"
  }
]
""
[
  {
    "name": "human",
    "args": {
      "query": "Я нашел информацию, что клубы World Class управляются ООО «Спорт Форум». Пожалуйста, уточните, кем и когда был выдан ваш паспорт, а также данные о покупке: дата покупки, номер договора (если есть) и сумма."
    },
    "id": "call_lbKtbYuzCUnz4JMGDxaQsmNA",
    "type": "tool_call"
  }
]

Видимо он нашёл не всю информацию и решил во второй итерации сделать еще один поиск с другим запросом и дозапросить недостающие данные у нас

LLM: Я нашел информацию, что клубы World Class управляются ООО «Спорт Форум». Пожалуйста, уточните, кем и когда был выдан ваш паспорт, а также данные о покупке: дата покупки, номер договора (если есть) и сумма.

USER: абонемент куплен 01.10.24 за 100 000 руб.

После этого наш агент решил, что собрал всю информацию и вернул конечный результат:

{
  "case_data": "Компания:
  - Юридическое наименование: ООО «Спорт Форум»
  - Юридический адрес: 117420, Москва, улица Наметкина д6 к1
  - ИНН, ОГРН: информация не найдена
  - Контактный email: информация не найдена
  
  Клиент:
  - ФИО: Иванов Иван Иванович
  - Паспортные данные: номер 4444 444444, кем и когда выдан: информация не предоставлена
  
  Покупка:
  - Дата: 01.10.2024
  - Номер: не указан
  - Сумма: 100 000 руб."
}

Видим что  он нашёл только часть данных по компании.

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

Ты – ассистент юриста. 

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

    

    Тебе нужно собрать следующие данные:

    * о компании

    ** юридическое наименование (ООО, ИП и другие правовые формы)

    ** Юридический адрес

    ** ИНН, ОГРН(ИП)

    ** контактный email 

    * Данные клиента:

    ** ФИО

    ** Паспортные данные (номер, кем и когда выдан)

    * Данные о покупке:

    ** дата

    ** номер (если есть)

    ** сумма

    Для поиска данных о компании используй инструмент TavilySearchResults. При использовании поиска ищи каждый параметр отдельным поисковым запросом.

    С помощью HumanInputRun ты можешь уточнить у клиента его данные и данные о покупке.

    Продолжай задавать вопросы и пользоваться поиском до тех пор, пока не соберешь все данные

Запуск 3

[
  {
    "name": "tavily_search_results_json",
    "args": {
      "query": "Wordclass Ленинский 109 юридическое наименование"
    },
    "id": "call_j2QfMPhLUrr3u3k6CTCAnzFo",
    "type": "tool_call"
  },
  {
    "name": "tavily_search_results_json",
    "args": {
      "query": "Wordclass Ленинский 109 юридический адрес"
    },
    "id": "call_5GgJB1bUg3jziGvM5JthfJGq",
    "type": "tool_call"
  },
  {
    "name": "tavily_search_results_json",
    "args": {
      "query": "Wordclass Ленинский 109 ИНН"
    },
    "id": "call_6IMxybQovBXJXsZGrKjk9Dhs",
    "type": "tool_call"
  },
  {
    "name": "tavily_search_results_json",
    "args": {
      "query": "Wordclass Ленинский 109 ОГРН"
    },
    "id": "call_erXpeIrcxi185cOnLhEITxwi",
    "type": "tool_call"
  },
  {
    "name": "tavily_search_results_json",
    "args": {
      "query": "Wordclass Ленинский 109 контактный email"
    },
    "id": "call_k10Q1YreIWa8ElPllnpOC7kk",
    "type": "tool_call"
  }
]

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

Смотрим дальше:

  {
    "name": "human",
    "args": {
      "query": "Пожалуйста, предоставьте ваши данные:\n\n1. ФИО\n2. Паспортные данные (номер, кем и когда выдан)\n3. Данные о покупке абонемента:\n   - Дата покупки\n   - Номер (если есть)\n   - Сумма покупки"
    },
    "id": "call_kw7nB2z3MZh3ZMCBZXljvHWF",
    "type": "tool_call"
  }
]

На второй цикл уже нет поисковых запросов, но есть вопросы к пользователю

LLM: Пожалуйста, предоставьте ваши данные:

1. ФИО

2. Паспортные данные (номер, кем и когда выдан)

3. Данные о покупке абонемента:

   - Дата покупки

   - Номер (если есть)

   - Сумма покупки


USER: Иванов Иван Ивановч 4444 444444

Намеренно отвечаем ему только частично, смотрим что происходит

[
  {
    "name": "human",
    "args": {
      "query": "Пожалуйста, укажите дату покупки абонемента, его номер (если есть) и сумму покупки."
    },
    "id": "call_XKuza4NR2K2b0K3ODa8wm4de",
    "type": "tool_call"
  }
]

Он понял, что данных не хватает �� задал еще вопрос

LLM: Пожалуйста, укажите дату покупки абонемента, его номер (если есть) и сумму покупки.

USER: абонемент куплен 01.10.24 за 100 000 руб.

После этого, наш агент решил, что собрал все данные и выдал конечный результат:

{
     "case_data":{
            "company":{
                   "юридическое наименование":"ООО Спорт Форум",
                   "юридический адрес":"Москва, Ленинский проспект, 109",
                   "ИНН":"7710309880",
                   "ОГРН":"1027700488493",
                   "контактный email":"info@worldclass.ru"       
     },
            "client":{
                   "ФИО":"Иванов Иван Иванович",
                   "паспортные данные":{
                          "номер":"4444 444444"           
        }       
     },
            "purchase":{
                   "дата":"01.10.24",
                   "сумма":"100000 руб"      
     }   
  }
}

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

Финульную версию скрипта можно взять тут.

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

Что дальше?

Конечно, это пока только proof-of-concept.

Но он уже демонстрирует, что LLM может автономно решать несложные задачи. Этот пример можно доработать: добавить обработку ошибок, интегрировать дополнительные инструменты, такие как проверка компании в ЕГРЮЛ, парсинг сайтов для получения информации и поиск релевантных статей закона. В итоге получится полноценный инструмент, способный значительно повысить эффективность бизнеса.

Кроме этого, я в этой статье я не затронул важный аспект – реалзиация долгосрочной памяти и RAG, про это на хабре есть хорошая статья.

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

Перспективы

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

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

---

Если хотите разбираться глубже в том, как использовать LLM и AI-агентов в продуктивности, работе, бизнесе и повседневной жизни — подписывайтесь на наш Telegram-канал “Вкалывают роботы”.