Одними из наиболее интересных подходов в создании AI-систем в 2024 году являются агенты (agents, agentic AI systems) и мультиагентные системы (multi-agent systems).
Оба они показывают большую эффективность при решении многих задач и позволяют решать более сложные задачи с помощью LLM
Эти подходы не только увеличивают эффективность, а в принципе позволяют по другому взглянуть на то как можно подходить к дизайну AI систем в бизнесе и помогают решать задачи с бОльшей степенью неопределенности. По сути, вместо того, чтобы давать четкие инструкции по решению задачи, мы объясняем AI системе каким образом нужно думать, чтобы решить её.
Ключевая черта, отличающая агентов от других AI-систем – это автономность
То есть мы можем говорить что имеем дело с Агентом, если наша система обладает автономностью в решении задачи. Степень автономности может быть разная: от выбора действий в рамках заданных шагов и инструментов до самостоятельного определения плана и последовательности выполнения.
Большинство гайдов по агентам ограничиваются теорией и супер-абстрактными примерами, игнорируя реальные проблемы, которые возникают в процессе разработки.
Сегодня мы решим задачу, приближенную к реальности и актуальную для многих бизнесов – сделаем агента-ассистента для сбора данных.
Представим, что у нас есть юридическая компания, помогающая клиентам защищать права потребителей. Когда клиент обращается с проблемой, прежде чем передать задачу юристу, наш ассистент должен собрать полную информацию о клиенте, компании и покупке.
Если бы мы просто автоматизировали этот процесс, то сделали бы стандартный опросник и набор скриптов для сбора данных из интернета. Но мы создадим агента, который будет автономно решать эту задачу, адаптируясь в процессе.
Наш агент должен уметь:
Задавать уточняющие вопросы клиенту.
Искать информацию в поисковике.
Для этого используются инструменты (tools), которые становятся своего рода «руками» LLM, позволяя взаимодействовать с внешним миром.
Приблизительно это работает так:
Мы объясняем LLM, что есть в её распоряжении и просим выполнить какую-то задачу.
USER: У тебя есть следующие Инструменты: search(query) - для поиска в интернете, human(qustion) - чтобы задать вопрос пользователю
Какой адрес большого театра?
LLM просит нас выполнить функцию с заданными параметрами (сама она этого сделать не может)
LLM: run search('большой театр адрес')
Мы на своей стороне выполняем функцию search и отдаем LLM результаты
USER: run search('большой театр адрес') results = {результаты поиска, например первая страница выдачи гугла}
LLM используя эти данные отвечает
LLM: Адрес большого театра: ул. Большая Дмитровка, 4/2, Москва
Т.е. есть мы можем имплементировать какое-то количество функций, объяснить LLM как и для чего их использовать, и она будет по своему усмотрению просить нас их выполнять.
Для создания агента будем использоваться langgraph – это очень крутая библиотека, которая упрощает работу с агентами и инструментами. В качестве LLM будем использовать chatgpt-4o.
Основа приложения на langgraph – это, собственно граф: несколько нод, объединенных связями.
В нашем случае это будет 2 ноды:
Нода взаимодействия с LLM
Нода вызова инструментов
Также нам потребуется 2 инструмента:
Поиск в интернете – будем использовать Tavily
Взаимодействие с пользователем – 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":"[email protected]"
},
"client":{
"ФИО":"Иванов Иван Иванович",
"паспортные данные":{
"номер":"4444 444444"
}
},
"purchase":{
"дата":"01.10.24",
"сумма":"100000 руб"
}
}
}
Теперь результат уже достаточно хороший, чтобы отдавать его юристу, который не будет тратить время на поиск информации.
Финульную версию скрипта можно взять тут.
В процессе создания подобных AI-систем основное время уходит на проектирование графа, стейта и промпт-инжиниринг. Это некоторая смесь программирования и творчества, которое требует накопления опыта и множества проб и ошибок, чтобы развить интуицию и научиться эффективно проектировать такие системы.
Конечно, это пока только proof-of-concept.
Но он уже демонстрирует, что LLM может автономно решать несложные задачи. Этот пример можно доработать: добавить обработку ошибок, интегрировать дополнительные инструменты, такие как проверка компании в ЕГРЮЛ, парсинг сайтов для получения информации и поиск релевантных статей закона. В итоге получится полноценный инструмент, способный значительно повысить эффективность бизнеса.
Кроме этого, я в этой статье я не затронул важный аспект – реалзиация долгосрочной памяти и RAG, про это на хабре есть хорошая статья.
Следующим шагом может стать создание ещё одного агента — юриста, который сможет автоматически составлять жалобы. Таким образом, мы получим мультиагентную систему, о которой я расскажу в следующей статье.
Текущий уровень развития LLM уже позволяет в некоторых задачах перейти от традиционной автоматизации к созданию автономных агентов, которые работают гораздо эффективнее. Эта эффективность будет только увеличиваться.
Буквально несколько дней назад OpenAI анонсировала новую модель o3 которая, уверен, ещё лучше проявит себя в подходах с использованием агентов. Изменения происходят очень быстро, поэтому важно уже сейчас пересмотреть своё отношение к LLM и воспринимать их не только как инструменты для выполнения чётких инструкций.