Создавайте рабочие процессы ИИ с помощью агентных фреймворков
Делиться

С появлением мощных моделей ИИ, таких как GPT-5 и Gemini 2.5 Pro, мы также наблюдаем рост числа агентных фреймворков, использующих эти модели. Эти фреймворки упрощают работу с моделями ИИ, абстрагируясь от множества проблем, таких как вызов инструментов, управление состоянием агентов и настройка с участием человека.
В этой статье я подробнее расскажу о LangGraph, одном из доступных фреймворков для агентного ИИ. Я использую его для разработки простого агентного приложения, последовательно демонстрируя преимущества пакетов для агентного ИИ. Я также рассмотрю плюсы и минусы использования LangGraph и других подобных фреймворков.
LangGraph никоим образом не спонсирует мою работу над этой статьей. Я просто выбрал этот фреймворк, поскольку он один из самых распространённых. Существует множество других вариантов, например:
- LangChain
- LlamaIndex
- CrewAI

Зачем вам нужна агентская структура?
Существует множество пакетов, призванных упростить программирование приложений. Во многих случаях эти пакеты дают прямо противоположный эффект: они загромождают код, плохо работают в продакшене и иногда затрудняют отладку.
Однако вам необходимо найти пакеты, которые упрощают ваше приложение, абстрагируясь от шаблонного кода. Этот принцип часто подчёркивается в мире стартапов цитатой, подобной приведённой ниже:
Сосредоточьтесь на решении именно той проблемы, которую вы пытаетесь решить. Все остальные (ранее решённые проблемы) следует передать на аутсорсинг другим приложениям.
Агентная структура необходима, поскольку она абстрагирует множество сложностей, с которыми вам не хотелось бы иметь дело:
- Поддержание состояния. Не только история сообщений, но и вся другая информация, которую вы собираете, например, при выполнении RAG.
- Использование инструментов. Вам не нужно настраивать собственную логику для выполнения инструментов. Вместо этого следует просто определить их и позволить агентской платформе управлять их вызовом. (Это особенно актуально для параллельного и асинхронного вызова инструментов.)
Таким образом, использование агентной структуры позволяет избежать множества сложностей, что позволяет вам сосредоточиться на основной части вашего продукта.
Основы LangGraph
Чтобы приступить к внедрению LangGraph, я начинаю с чтения документации, охватывающей:
- Базовая реализация чат-бота
- Использование инструмента
- Поддержание и обновление состояния
LangGraph, как следует из названия, основан на построении графов и их выполнении по запросу. В графе можно определить:
- Состояние (текущая информация, хранящаяся в памяти)
- Узлы. Обычно это LLM или вызов инструмента, например, классифицирующий намерение пользователя или отвечающий на его вопрос.
- Рёбра. Условная логика определяет, к какому узлу перейти следующим.
Все это вытекает из базовой теории графов.
Реализация рабочего процесса

Я считаю, что один из лучших способов обучения — это просто пробовать всё самостоятельно. Поэтому я реализую простой рабочий процесс в LangGraph. Подробнее о построении этих рабочих процессов можно узнать в документации по рабочим процессам, основанной на блоге Anthropic «Building Effective Agents» (одна из моих любимых статей об агентах, о которой я писал в нескольких предыдущих статьях). Настоятельно рекомендую её прочитать.
Я создам простой рабочий процесс для определения приложения, в котором пользователь может:
- Создавайте документы с текстом
- Удалить документы
- Поиск в документах
Для этого я создам следующий рабочий процесс:
- Определите намерение пользователя. Хочет ли он создать документ, удалить документ или выполнить поиск в документе?
- Учитывая результат шага 1, у меня будут разные процессы для обработки каждого из них.
Вы также можете сделать это, просто определив все инструменты и предоставив агенту доступ для создания, удаления и поиска документов. Однако, если вы хотите выполнять больше действий в зависимости от намерения, лучше сначала выполнить этап маршрутизации с классификацией намерений.
Загрузка импорта и LLM
Сначала я загружу необходимые импорты и используемый мной LLM. Я буду использовать AWS Bedrock, хотя вы можете использовать и других поставщиков, как показано в шаге 3 этого руководства.
«»» Создайте рабочий процесс обработчика документов, в котором пользователь может создать новый документ в базе данных (в настоящее время это просто словарь), удалить документ из базы данных, задать вопрос о документе «»» из typing_extensions import TypedDict, Literal из langgraph.checkpoint.memory import InMemorySaver из langgraph.graph import StateGraph, START, END из langgraph.types import Command, interrupt из langchain_aws import ChatBedrockConverse из langchain_core.messages import HumanMessage, SystemMessage из pydantic import BaseModel, Field из IPython.display import display, Image из dotenv import load_dotenv import os load_dotenv() aws_access_key_id = os.getenv(«AWS_ACCESS_KEY_ID») или «» aws_secret_access_key = os.getenv(«AWS_SECRET_ACCESS_KEY») или «» os.environ[«AWS_ACCESS_KEY_ID»] = aws_access_key_id os.environ[«AWS_SECRET_ACCESS_KEY»] = aws_secret_access_key llm = ChatBedrockConverse( model_id=»us.anthropic.claude-3-5-haiku-20241022-v1:0″, # это идентификатор модели (добавлено us. перед идентификатором в платформе) region_name=»us-east-1″, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key, ) document_database: dict[str, str] = {} # словарь с ключом: имя файла, значением: текст в документе
Я также определил базу данных как словарь файлов. В рабочей среде вы, естественно, использовали бы полноценную базу данных, однако для этого руководства я упрощаю её.
Определение графа
Теперь пора определить граф. Сначала я создаю объект Router, который будет классифицировать запрос пользователя по одному из трёх намерений:
- добавить_документ
- удалить_документ
- ask_document
# Определить класс состояния State(TypedDict): ввод: str решение: str | None вывод: str | None # Схема для структурированного вывода, используемая в качестве логики маршрутизации class Route(BaseModel): step: Literal[«add_document», «delete_document», «ask_document»] = Field( description=»Следующий шаг в процессе маршрутизации» ) # Дополнить LLM схемой для структурированного вывода router = llm.with_structured_output(Route) def llm_call_router(state: State): «»»Направить пользовательский ввод в соответствующий узел»»» # Запустить дополненный LLM со структурированным выводом, который будет служить логикой маршрутизации decision = router.invoke( [ SystemMessage( content=»»»Направить пользовательский ввод в одно из следующих 3 намерений: — 'add_document' — 'delete_document' — 'ask_document' Вам нужно вернуть только намерение, а не любой другой текст. «»» ), HumanMessage(content=state[«input»]), ] ) return {«decision»: decision.step} # Условная функция ребра для маршрутизации к соответствующему узлу def route_decision(state: State): # Верните имя узла, который вы хотите посетить следующим if state[«decision»] == «add_document»: return «add_document_to_database_tool» elif state[«decision»] == «delete_document»: return «delete_document_from_database_tool» elif state[«decision»] == «ask_document»: return «ask_document_tool»
Я определяю состояние, в котором мы сохраняем пользовательский ввод, решение маршрутизатора (одно из трёх намерений), а затем обеспечиваю структурированный вывод от LLM. Структурированный вывод гарантирует, что модель ответит одним из трёх намерений.
Продолжая, я определю инструменты, которые мы используем в этой статье, по одному для каждого намерения.
# Узлы def add_document_to_database_tool(state: State): «»»Добавление документа в базу данных. По заданному пользовательскому запросу извлекаем имя файла и содержимое документа. Если не указано, документ в базу данных не добавляется.»»» user_query = state[«input»] # извлечение имени файла и содержимого из пользовательского запроса filename_prompt = f»По заданному пользовательскому запросу извлекаем имя файла документа: {user_query}. Возвращает только имя файла, а не какой-либо другой текст.» output = llm.invoke(filename_prompt) filename = output.content content_prompt = f»По заданному пользовательскому запросу извлекаем содержимое документа: {user_query}. Возвращает только содержимое, а не какой-либо другой текст.» output = llm.invoke(content_prompt) content = output.content # добавить документ в базу данных document_database[filename] = content return {«output»: f»Документ {filename} добавлен в базу данных»} def delete_document_from_database_tool(state: State): «»»Удаляет документ из базы данных. По заданному пользовательскому запросу извлекает имя файла документа для удаления. Если не указано, документ из базы данных не удаляется.»»» user_query = state[«input»] # извлекает имя файла из пользовательского запроса filename_prompt = f»По заданному пользовательскому запросу извлекает имя файла документа для удаления: {user_query}. Возвращает только имя файла, а не какой-либо другой текст.» output = llm.invoke(filename_prompt) filename = output.content # удалить документ из базы данных, если он существует, если нет, вернуть информацию об ошибке, если filename отсутствует в document_database: return {«output»: f»Документ {filename} не найден в базе данных»} document_database.pop(filename) return {«output»: f»Документ {filename} удален из базы данных»} def ask_document_tool(state: State): «»»Задать вопрос о документе. По заданному запросу пользователя извлечь имя файла и вопрос для документа. Если не указано, вопрос о документе не задается.»»» user_query = state[«input»] # извлечь имя файла и вопрос из запроса пользователя filename_prompt = f»По заданному следующему запросу пользователя извлечь имя файла документа, о котором нужно задать вопрос: {user_query}. Возвращает только имя файла, а не какой-либо другой текст.» output = llm.invoke(filename_prompt) filename = output.content question_prompt = f»Данный следующий запрос пользователя, извлеките вопрос о документе: {user_query}. Верните только вопрос, а не какой-либо другой текст.» output = llm.invoke(question_prompt) question = output.content # задать вопрос о документе, если имя файла отсутствует в document_database: return {«output»: f»Документ {filename} не найден в базе данных»} result = llm.invoke(f»Документ: {document_database[filename]}nnВопрос: {question}») return {«output»: f»Результат запроса документа: {result.content}»}
И наконец, строим граф с узлами и ребрами:
# Построение рабочего процесса router_builder = StateGraph(State) # Добавление узлов router_builder.add_node(«add_document_to_database_tool», add_document_to_database_tool) router_builder.add_node(«delete_document_from_database_tool», delete_document_from_database_tool) router_builder.add_node(«ask_document_tool», ask_document_tool) router_builder.add_node(«llm_call_router», llm_call_router) # Добавление ребер для соединения узлов router_builder.add_edge(START, «llm_call_router») router_builder.add_conditional_edges( «llm_call_router», route_decision, { # Имя, возвращаемое route_decision : Имя следующего узла для посещения «add_document_to_database_tool»: «add_document_to_database_tool», «delete_document_from_database_tool»: «delete_document_from_database_tool», «ask_document_tool»: «ask_document_tool», }, ) router_builder.add_edge(«add_document_to_database_tool», END) router_builder.add_edge(«delete_document_from_database_tool», END) router_builder.add_edge(«ask_document_tool», END) # Компиляция рабочего процесса memory = InMemorySaver() router_workflow = router_builder.compile(checkpointer=memory) config = {«configurable»: {«thread_id»: «1»}} # Показать рабочий процесс дисплей(Изображение(router_workflow.get_graph().draw_mermaid_png())
Последняя функция отображения должна отобразить график, как показано ниже:

Теперь вы можете опробовать рабочий процесс, задавая вопрос для каждого намерения.
Добавить документ:
user_input = «Добавить документ «test.txt» с содержимым «Это тестовый документ» в базу данных» state = router_workflow.invoke({«input»: user_input}, config) print(state[«output»] # -> Документ test.txt добавлен в базу данных
Поиск документа:
user_input = «Дайте мне краткое описание документа «test.txt»» state = router_workflow.invoke({«input»: user_input}, config) print(state[«output»]) # -> Краткий, общий тестовый документ с простым описательным предложением.
Удалить документ :
user_input = «Удалить документ test.txt из базы данных» state = router_workflow.invoke({«input»: user_input}, config) print(state[«output»]) # -> Документ test.txt удален из базы данных
Отлично! Видно, что рабочий процесс работает с различными вариантами маршрутизации. Вы можете добавить больше намерений или узлов для каждого намерения, чтобы создать более сложный рабочий процесс.
Более сильные варианты использования агентов
Разница между агентными рабочими процессами и полностью агентными приложениями иногда сбивает с толку. Однако, чтобы разделить эти два термина, я воспользуюсь цитатой из книги «Создание эффективных агентов» издательства Anthropic:
Рабочие процессы — это системы, в которых LLM и инструменты координируются посредством предопределённых путей кода. Агенты же — это системы, в которых LLM динамически управляют собственными процессами и использованием инструментов, сохраняя контроль над тем, как они выполняют задачи.
Большинство задач, которые вы решаете с помощью LLM, будут использовать шаблон рабочего процесса, поскольку большинство задач (по моему опыту) предопределены и должны иметь заранее установленный набор ограничений. В приведённом выше примере при добавлении/удалении/поиске документов вам обязательно следует настроить предопределённый рабочий процесс, определив классификатор намерений и действия, которые нужно выполнить для каждого намерения.
Однако иногда требуются и более автономные сценарии использования агентов. Представьте, например, Cursor, которому нужен агент-кодировщик, способный просматривать ваш код, проверять актуальную документацию онлайн и изменять его. В таких случаях сложно создавать заранее определённые рабочие процессы, поскольку существует множество различных сценариев.
Если вы хотите создать более автономные агентные системы, вы можете прочитать об этом подробнее здесь.
Плюсы и минусы LangGraph
Плюсы
Три моих главных плюса LangGraph:
- Легко настроить
- С открытым исходным кодом
- Упрощает ваш код
Настроить LangGraph и быстро запустить его было просто. Особенно если следовать документации или передавать её в Cursor и задавать ему задачи по реализации определённых рабочих процессов.
Более того, исходный код LangGraph открыт, что означает, что вы можете продолжать использовать его, независимо от того, что происходит с компанией-разработчиком или какие изменения она решит внести. Я думаю, это критически важно, если вы хотите развернуть его в рабочей среде. Наконец, LangGraph также упрощает значительную часть кода и абстрагирует значительную часть логики, которую пришлось бы писать на Python самостоятельно.
Минусы
Однако у LangGraph есть и некоторые недостатки, которые я заметил в ходе внедрения.
- Все еще удивительно много шаблонного кода
- Вы столкнетесь с ошибками, характерными для LangGraph.
При реализации собственного рабочего процесса я чувствовал, что мне всё равно придётся добавить много шаблонного кода. Хотя объём кода был определённо меньше, чем если бы я реализовал всё с нуля, я был удивлён объёмом кода, который пришлось добавить для создания относительно простого рабочего процесса. Однако, думаю, отчасти это связано с тем, что LangGraph пытается позиционировать себя как инструмент с меньшим объёмом кода, чем, например, многие функции LangChain (что, на мой взгляд, хорошо, поскольку LangChain, на мой взгляд, слишком абстрагируется, что затрудняет отладку кода).
Более того, как и в случае со многими внешними пакетами, при реализации пакета вы столкнётесь с проблемами, специфичными для LangGraph. Например, когда я хотел просмотреть график созданного мной рабочего процесса, у меня возникла проблема, связанная с функцией draw_mermaid_png. Подобные ошибки неизбежны при использовании внешних пакетов, и это всегда будет компромиссом между полезными абстракциями кода, предоставляемыми пакетом, и различными видами ошибок, с которыми вы можете столкнуться при использовании таких пакетов.
Краткое содержание
В целом, я считаю LangGraph полезным пакетом при работе с агентными системами. Настройка моего рабочего процесса путем предварительной классификации намерений и последующего применения различных потоков в зависимости от намерения была относительно простой. Более того, я думаю, что LangGraph нашёл хорошую золотую середину между отсутствием абстрагирования всей логики (что затеняет код и затрудняет отладку) и фактическим абстрагированием проблем, с которыми я не хочу сталкиваться при разработке своей агентной системы. Реализация подобных агентных фреймворков имеет как положительные, так и отрицательные стороны, и, на мой взгляд, лучший способ найти этот компромисс — самостоятельно реализовать простые рабочие процессы.
👉 Найдите меня в соцсетях:
🧑💻 Свяжитесь с нами
🐦 X / Твиттер
✍️ Средний
Если вы хотите узнать больше об агентных рабочих процессах, прочтите мою статью «Создание эффективных ИИ-агентов для обработки миллионов документов». Подробнее о программах LLM можно узнать в моей статье «Валидация LLM».
Источник: towardsdatascience.com



























