Друзья, приветствую! Надеюсь, успели соскучиться.
Последние пару месяцев я с головой ушёл в исследование интеграции ИИ-агентов в собственные Python-проекты. В процессе накопилось немало практических знаний и наблюдений, которыми просто грех не поделиться. Поэтому сегодня я возвращаюсь на Хабр — с новой темой, свежим взглядом и с намерением писать чаще.
На повестке дня — LangGraph и MCP: инструменты, с помощью которых можно создавать действительно полезных ИИ-агентов.
Если раньше мы спорили о том, какая нейросеть лучше отвечает на русском языке, то сегодня поле битвы сместилось в сторону более прикладных задач: кто лучше справляется с ролью ИИ-агента? Какие фреймворки действительно упрощают разработку? И как интегрировать всё это добро в реальный проект?
Но прежде чем нырнуть в практику и код, давайте разберёмся с базовыми понятиями. Особенно с двумя ключевыми: ИИ-агенты и MCP. Без них разговор про LangGraph будет неполным.
ИИ-агенты простыми словами
ИИ-агенты — это не просто «прокачанные» чат‑боты. Они представляют собой более сложные, автономные сущности, которые обладают двумя важнейшими особенностями:
Умение взаимодействовать и координироваться
Современные агенты способны делить задачи на подзадачи, вызывать других агентов, запрашивать внешние данные, работать в команде. Это уже не одиночный ассистент, а распределённая система, где каждый компонент может вносить свой вклад.Доступ к внешним ресурсам
ИИ-агент больше не ограничен рамками диалога. Он может обращаться к базам данных, выполнять вызовы к API, взаимодействовать с локальными файлами, векторными хранилищами знаний и даже запускать команды в терминале. Всё это стало возможным благодаря появлению MCP — нового уровня интеграции между моделью и средой.
Что такое MCP (Model Context Protocol)
Если говорить просто: MCP — это мост между нейросетью и её окружением. Он позволяет модели «понимать» контекст задачи, получать доступ к данным, выполнять вызовы и формировать обоснованные действия, а не просто выдавать текстовые ответы.
Представим аналогию:
У вас есть нейросеть — она умеет рассуждать и генерировать тексты.
Есть данные и инструменты — документы, API, базы знаний, терминал, код.
И есть MCP — это интерфейс, который позволяет модели взаимодействовать с этими внешними источниками так, как если бы они были частью её внутреннего мира.
Без MCP:
Модель — это изолированный диалоговый движок. Вы подаёте ей текст — она отвечает. И всё.
С MCP:
Модель становится полноценным исполнителем задач:
получает доступ к структурам данных и API;
вызывает внешние функции;
ориентируется в текущем состоянии проекта или приложения;
может запоминать, отслеживать и изменять контекст по мере диалога;
использует расширения, такие как инструменты поиска, код-раннеры, базу векторных эмбеддингов и пр.
В техническом смысле MCP — это протокол взаимодействия между LLM и её окружением, где контекст подаётся в виде структурированных объектов (вместо «сырого» текста), а вызовы оформляются как интерактивные операции (например, function calling, tool usage или agent actions). Именно это и превращает обычную модель в настоящего ИИ-агента, способного делать больше, чем просто "поговорить".
А теперь — к делу!
Теперь, когда мы разобрались с базовыми понятиями, логично задаться вопросом: «Как всё это реализовать на практике в Python?»
Вот здесь и вступает в игру LangGraph — мощный фреймворк для построения графов состояний, поведения агентов и цепочек мышления. Он позволяет "прошивать" логику взаимодействия между агентами, инструментами и пользователем, создавая живую архитектуру ИИ, адаптирующуюся к задачам.
В следующих разделах мы посмотрим, как:
строится агент с нуля;
создаются состояния, переходы и события;
интегрируются функции и инструменты;
и как вся эта экосистема работает в реальном проекте.
Немного теории: что такое LangGraph
Прежде чем приступить к практике, нужно сказать пару слов о самом фреймворке.
LangGraph — это проект от команды LangChain, тех самых, кто первыми предложили концепцию «цепочек» (chains) взаимодействия с LLM. Если раньше основной упор делался на линейные или условно‑ветвящиеся пайплайны (langchain.chains
), то теперь разработчики делают ставку на графовую модель, и именно LangGraph они рекомендуют как новое «ядро» для построения сложных ИИ‑сценариев.
LangGraph — это фреймворк для построения конечных автоматов и графов состояний, в которых каждый нода (или узел) представляет собой часть логики агента: вызов модели, внешний инструмент, условие, пользовательский ввод и т.д.
Как это работает: графы и узлы
Концептуально, LangGraph строится на следующих идеях:
Граф — это структура, которая описывает возможные пути выполнения логики. Можно думать о нём как о карте: из одной точки можно перейти в другую в зависимости от условий или результата выполнения.
Узлы (ноды) — это конкретные шаги внутри графа. Каждый узел выполняет какую-то функцию: вызывает модель, вызывает внешний API, проверяет условие или просто обновляет внутреннее состояние.
Переходы между узлами — это логика маршрутизации: если результат предыдущего шага такой-то, то идём туда-то.
Состояние (state) — передаётся между узлами и накапливает всё, что нужно: историю, промежуточные выводы, пользовательский ввод, результат работы инструментов и т. д.
Таким образом, мы получаем гибкий механизм управления логикой агента, в котором можно описывать как простые, так и очень сложные сценарии: циклы, условия, параллельные действия,вложенные вызовы и многое другое.
Почему это удобно?
LangGraph позволяет строить прозрачную, воспроизводимую и расширяемую логику:
легко отлаживать;
легко визуализировать;
легко масштабировать под новые задачи;
легко интегрировать внешние инструменты и MCP‑протоколы.
По сути, LangGraph — это «мозг» агента, где каждый шаг задокументирован, контролируем и может быть изменён без хаоса и «магии».
Ну а теперь — хватит теории!
Можно ещё долго рассказывать о графах, состояний, композиции логики и преимуществах LangGraph над классическими пайплайнами. Но, как показывает практика, лучше один раз увидеть в коде.
Пора перейти к практике. Дальше — пример на Python: создадим простого, но полезного ИИ‑агента на базе LangGraph, который будет использовать внешние инструменты, память и принимать решения сам.
Подготовка: облачные и локальные нейросети
Для того чтобы приступить к созданию ИИ‑агентов, нам в первую очередь нужен мозг — языковая модель. Здесь есть два подхода:
использовать облачные решения, где всё готово «из коробки»;
или поднять модель локально — для полной автономии и конфиденциальности.
Рассмотрим оба варианта.
Облачные сервисы: быстро и удобно
Самый простой путь — воспользоваться мощностями крупных провайдеров: OpenAI, Anthropic, DeepSeek, Groq, Mistral и других. Вам достаточно приобрести API-ключ и начать использовать модели через стандартные HTTP-запросы.
Где взять ключи и токены:
OpenAI — ChatGPT и другие продукты;
Anthropic — Claude;
OpenRouter.ai — десятки моделей (один токен — множество моделей через OpenAI-совместимый API);
Amvera Cloud — возможность подключить LLaMA с оплатой рублями и встроенным проксированием до OpenAI и Anthropic.
Этот путь удобен, особенно если вы:
не хотите настраивать инфраструктуру;
разрабатываете с упором на скорость;
работаете с ограниченными ресурсами.
Локальные модели: полный контроль
Если вам важна приватность, работа без интернета или вы хотите строить полностью автономные агенты, то имеет смысл развернуть нейросеть локально.
Основные преимущества:
Конфиденциальность — данные остаются у вас;
Работа без интернета — полезно в изолированных сетях;
Отсутствие подписок и токенов — бесплатно после настройки.
Недостатки очевидны:
Требования к ресурсам (особенно к видеопамяти);
Настройка может занять время;
Некоторые модели сложно развернуть без опыта.
Тем не менее, есть инструменты, которые делают локальный запуск проще. Один из лучших на сегодня — это Ollama.
Развёртывание локальной LLM через Ollama + Docker
Мы подготовим локальный запуск модели Qwen 2.5 (qwen2.5:32b) с использованием Docker-контейнера и системы Ollama. Это позволит интегрировать нейросеть с MCP и использовать её в собственных агентах на базе LangGraph.
Если вычислительных ресурсов вашего компьютера или сервера окажется недостаточно для работы с данной версией модели, вы всегда можете выбрать менее "прожорливую" нейросеть — процесс установки и запуска останется аналогичным.
? Быстрая установка (сводка шагов)
Установите Docker + Docker Compose
-
Создайте структуру проекта:
mkdir qwen-local && cd qwen-local
-
Создайте
docker-compose.yml
(универсальный вариант, GPU определяется автоматически)services: ollama: image: ollama/ollama:latest container_name: ollama_qwen ports: - '11434:11434' volumes: - ./ollama_data:/root/.ollama - /tmp:/tmp environment: - OLLAMA_HOST=0.0.0.0 - OLLAMA_ORIGINS=* restart: unless-stopped
-
Запустите контейнер:
docker compose up -d
-
Загрузите модель:
docker exec -it ollama_qwen ollama pull qwen2.5:32b
-
Проверьте работу через API:
curl http://localhost:11434/api/generate \ -H "Content-Type: application/json" \ -d '{"model": "qwen2.5:32b", "prompt": "Привет!", "stream": false}'

-
Интеграция с Python:
import requests def query(prompt): res = requests.post("http://localhost:11434/api/generate", json={ "model": "qwen2.5:32b", "prompt": prompt, "stream": False }) return res.json()['response'] print(query("Объясни квантовую запутанность"))
Теперь у вас полноценная локальная LLM, готовая к работе с MCP и LangGraph.
Что дальше?
У нас есть выбор между облачными и локальными моделями, и мы научились подключать обе. Самое интересное впереди — создание ИИ-агентов на LangGraph, которые используют выбранную модель, память, инструменты и собственную логику.
Переходим к самому вкусному — коду и практике!
Подготовка к написанию кода
Перед тем как перейти к практике, важно подготовить рабочее окружение. Я предполагаю, что вы уже знакомы с основами Python, знаете, что такое библиотеки и зависимости, и понимаете, зачем использовать виртуальное окружение.
Если всё это вам в новинку — рекомендую сначала пройти короткий курс или гайд по Python-базе, а затем возвращаться к статье.
Шаг 1: Создание виртуального окружения
Создайте новое виртуальное окружение в папке проекта:
python -m venv venv
source venv/bin/activate # для Linux/macOS
venv\Scripts\activate # для Windows
Шаг 2: Установка зависимостей
Создайте файл requirements.txt
и добавьте в него следующие строки:
langchain==0.3.26
langchain-core==0.3.69
langchain-deepseek==0.1.3
langchain-mcp-adapters==0.1.9
langchain-ollama==0.3.5
langchain-openai==0.3.28
langgraph==0.5.3
langgraph-checkpoint==2.1.1
langgraph-prebuilt==0.5.2
langgraph-sdk==0.1.73
langsmith==0.4.8
mcp==1.12.0
ollama==0.5.1
openai==1.97.0
⚠️ Актуальные версии указаны на 21 июля 2025 года. С момента публикации они могли измениться — проверяйте актуальность перед установкой.
Затем установите зависимости:
pip install -r requirements.txt
Шаг 3: Конфигурация переменных окружения
Создайте в корне проекта файл .env
и добавьте в него нужные API-ключи:
OPENAI_API_KEY=sk-proj-1234
DEEPSEEK_API_KEY=sk-123
OPENROUTER_API_KEY=sk-or-v1-123
BRAVE_API_KEY=BSAj123KlbvBGpH1344tLwc
Назначение переменных:
OPENAI_API_KEY
— ключ для доступа к GPT-моделям от OpenAI;DEEPSEEK_API_KEY
— ключ для использования моделей Deepseek;OPENROUTER_API_KEY
— единый ключ для доступа к множеству моделей через OpenRouter (можно получить бесплатно);BRAVE_API_KEY
— API-ключ для MCP-модуля web-поиска через Brave Search (можно получить бесплатно).
Некоторые MCP-инструменты (например,
brave-web-search
) требуют ключ для работы. Без него они просто не активируются.
А если у вас нет API-ключей?
Не проблема. Вы можете начать разработку с локальной моделью (например, через Ollama), не подключая ни одного внешнего сервиса. В этом случае файл .env
можно не создавать вовсе.
Готово! Теперь у нас есть всё необходимое для начала — изолированное окружение, зависимости, и, при необходимости, доступ к облачным нейросетям и MCP-интеграциям.
Далее - запустим нашего LLM-агента разными способами.
Простой запуск LLM-агентов через LangGraph: базовая интеграция
Начнём с самого простого: как «подключить мозг» к будущему агенту. Мы разберём базовые способы запуска языковых моделей (LLM) с помощью LangChain, чтобы в следующем шаге перейти к интеграции с LangGraph и построению полноценного ИИ-агента.
Импорты
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_ollama import ChatOllama
from langchain_deepseek import ChatDeepSeek
os
иload_dotenv()
— для загрузки переменных из.env
-файла.ChatOpenAI
,ChatOllama
,ChatDeepSeek
— обёртки для подключения языковых моделей через LangChain.
? Если вы используете альтернативные подходы к работе с конфигурациями (например, Pydantic Settings), можете заменить
load_dotenv()
на свой привычный способ.
Загрузка переменных окружения
load_dotenv()
Это подгрузит все переменные из .env
, включая ключи для доступа к API OpenAI, DeepSeek, OpenRouter и другим.
Простые функции для получения LLM
OpenAI
def get_openai_llm():
return ChatOpenAI(model="gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
Если переменная OPENAI_API_KEY
корректно задана, LangChain
подставит её автоматически — явное указание api_key=...
здесь опционально.
DeepSeek
def get_deepseek_llm():
return ChatDeepSeek(model="deepseek-chat", api_key=os.getenv("DEEPSEEK_API_KEY"))
Аналогично, но используем обёртку ChatDeepSeek
.
OpenRouter (и другие совместимые API)
def get_openrouter_llm(model="moonshotai/kimi-k2:free"):
return ChatOpenAI(
model=model,
api_key=os.getenv("OPENROUTER_API_KEY"),
base_url="https://openrouter.ai/api/v1",
temperature=0
)
Особенности:
ChatOpenAI
используется, несмотря на то что модель не от OpenAI — потому что OpenRouter использует тот же протокол.base_url
обязателен: он указывает на OpenRouter API.Модель
moonshotai/kimi-k2:free
выбрана как один из наиболее сбалансированных вариантов по качеству и скорости на момент написания статьи.API-ключ OpenRouter нужно передавать явно — автоматическая подстановка здесь не работает.
Мини-тест: проверка работы модели
if __name__ == "__main__":
llm = get_openrouter_llm(model="moonshotai/kimi-k2:free")
response = llm.invoke("Кто ты?")
print(response.content)

Если всё настроено правильно, вы получите осмысленный ответ от модели. Поздравляю — первый шаг сделан!
Но это ещё не агент
На текущем этапе мы подключили LLM и сделали простой вызов. Это больше похоже на консольного чат-бота, чем на ИИ-агента.
Почему?
Мы пишем синхронный, линейный код без логики состояния или целей.
Агент не принимает решений, не запоминает контекст и не использует инструменты.
MCP и LangGraph пока не задействованы.
Что дальше?
Далее мы реализуем полноценного ИИ-агента с использованием LangGraph — сначала без MCP, чтобы сфокусироваться на архитектуре, состояниях и логике самого агента.
Погружаемся в настоящую агентную механику. Поехали!
Агент классификации вакансий: от теории к практике
Создадим реального ИИ-агента, который будет решать конкретную бизнес-задачу — автоматическую классификацию описаний вакансий и услуг. Этот пример покажет, как применить концепции LangGraph на практике и создать полезный инструмент для HR-платформ и бирж фриланса.
Задача агента
Наш агент принимает на вход текстовое описание вакансии или услуги и выполняет трёхуровневую классификацию:
Тип работы: проектная работа или постоянная вакансия
Категория профессии: из 45+ предопределённых специальностей
Тип поиска: ищет ли человек работу или ищет исполнителя
Результат возвращается в структурированном JSON-формате с оценкой уверенности для каждой классификации.
?️ Архитектура агента на LangGraph
Следуя принципам LangGraph, создаём граф состояний из четырёх узлов:
? Входное описание
↓
? Узел классификации типа работы
↓
? Узел классификации категории
↓
? Узел определения типа поиска
↓
? Узел расчёта уверенности
↓
✅ JSON-результат
Каждый узел — это специализированная функция, которая:
Получает текущее состояние агента
Выполняет свою часть анализа
Обновляет состояние и передаёт его дальше
Управление состоянием
Определяем структуру памяти агента через TypedDict:
class State(TypedDict):
"""Состояние агента для хранения информации о процессе классификации"""
description: str
job_type: str
category: str
search_type: str
confidence_scores: Dict[str, float]
processed: bool
Это рабочая память агента — всё, что он помнит и накапливает в процессе анализа. Подобно тому, как человек-эксперт держит в уме контекст задачи при анализе документа.
Давайте рассмотрим полный код, а после сконцентрируемся на основных моментах.
import asyncio
import json
from typing import TypedDict, Dict, Any
from enum import Enum
from langgraph.graph import StateGraph, END
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage
# Категории профессий
CATEGORIES = [
"2D-аниматор", "3D-аниматор", "3D-моделлер",
"Бизнес-аналитик", "Блокчейн-разработчик", ...
]
class JobType(Enum):
PROJECT = "проектная работа"
PERMANENT = "постоянная работа"
class SearchType(Enum):
LOOKING_FOR_WORK = "поиск работы"
LOOKING_FOR_PERFORMER = "поиск исполнителя"
class State(TypedDict):
"""Состояние агента для хранения информации о процессе классификации"""
description: str
job_type: str
category: str
search_type: str
confidence_scores: Dict[str, float]
processed: bool
class VacancyClassificationAgent:
"""Асинхронный агент для классификации вакансий и услуг"""
def __init__(self, model_name: str = "gpt-4o-mini", temperature: float = 0.1):
"""Инициализация агента"""
self.llm = ChatOpenAI(model=model_name, temperature=temperature)
self.workflow = self._create_workflow()
def _create_workflow(self) -> StateGraph:
"""Создает рабочий процесс агента на основе LangGraph"""
workflow = StateGraph(State)
# Добавляем узлы в граф
workflow.add_node("job_type_classification", self._classify_job_type)
workflow.add_node("category_classification", self._classify_category)
workflow.add_node("search_type_classification", self._classify_search_type)
workflow.add_node("confidence_calculation", self._calculate_confidence)
# Определяем последовательность выполнения узлов
workflow.set_entry_point("job_type_classification")
workflow.add_edge("job_type_classification", "category_classification")
workflow.add_edge("category_classification", "search_type_classification")
workflow.add_edge("search_type_classification", "confidence_calculation")
workflow.add_edge("confidence_calculation", END)
return workflow.compile()
async def _classify_job_type(self, state: State) -> Dict[str, Any]:
"""Узел для определения типа работы: проектная или постоянная"""
prompt = PromptTemplate(
input_variables=["description"],
template="""
Проанализируй следующее описание и определи тип работы.
Описание: {description}
Ответь только одним из двух вариантов:
- "проектная работа" - если это временная задача, проект, фриланс, разовая работа
- "постоянная работа" - если это постоянная должность, штатная позиция, долгосрочное трудоустройство
Тип работы:
"""
)
message = HumanMessage(content=prompt.format(description=state["description"]))
response = await self.llm.ainvoke([message])
job_type = response.content.strip().lower()
# Нормализуем ответ
if "проектная" in job_type or "проект" in job_type or "фриланс" in job_type:
job_type = JobType.PROJECT.value
else:
job_type = JobType.PERMANENT.value
return {"job_type": job_type}
async def _classify_category(self, state: State) -> Dict[str, Any]:
"""Узел для определения категории профессии"""
categories_str = "\n".join([f"- {cat}" for cat in CATEGORIES])
prompt = PromptTemplate(
input_variables=["description", "categories"],
template="""
Проанализируй описание вакансии/услуги и определи наиболее подходящую категорию из списка.
Описание: {description}
Доступные категории:
{categories}
Выбери ТОЧНО одну категорию из списка выше, которая лучше всего соответствует описанию.
Ответь только названием категории без дополнительных пояснений.
Категория:
"""
)
message = HumanMessage(content=prompt.format(
description=state["description"],
categories=categories_str
))
response = await self.llm.ainvoke([message])
category = response.content.strip()
# Проверяем, есть ли категория в списке доступных
if category not in CATEGORIES:
# Ищем наиболее похожую категорию
category = self._find_closest_category(category)
return {"category": category}
async def _classify_search_type(self, state: State) -> Dict[str, Any]:
"""Узел для определения типа поиска"""
prompt = PromptTemplate(
input_variables=["description"],
template="""
Проанализируй описание и определи, кто и что ищет.
Описание: {description}
Ответь только одним из двух вариантов:
- "поиск работы" - если соискатель ищет работу/заказы
- "поиск исполнителя" - если работодатель/заказчик ищет исполнителя
Обрати внимание на ключевые слова:
- "ищу работу", "резюме", "хочу работать" = поиск работы
- "требуется", "ищем", "вакансия", "нужен специалист" = поиск исполнителя
Тип поиска:
"""
)
message = HumanMessage(content=prompt.format(description=state["description"]))
response = await self.llm.ainvoke([message])
search_type = response.content.strip().lower()
# Нормализуем ответ
if "поиск работы" in search_type or "ищу работу" in search_type:
search_type = SearchType.LOOKING_FOR_WORK.value
else:
search_type = SearchType.LOOKING_FOR_PERFORMER.value
return {"search_type": search_type}
async def _calculate_confidence(self, state: State) -> Dict[str, Any]:
"""Узел для расчета уровня уверенности в классификации"""
prompt = PromptTemplate(
input_variables=["description", "job_type", "category", "search_type"],
template="""
Оцени уверенность классификации по шкале от 0.0 до 1.0 для каждого параметра:
Описание: {description}
Тип работы: {job_type}
Категория: {category}
Тип поиска: {search_type}
Ответь в формате JSON:
{{
"job_type_confidence": 0.0-1.0,
"category_confidence": 0.0-1.0,
"search_type_confidence": 0.0-1.0
}}
"""
)
message = HumanMessage(content=prompt.format(
description=state["description"],
job_type=state["job_type"],
category=state["category"],
search_type=state["search_type"]
))
response = await self.llm.ainvoke([message])
try:
confidence_scores = json.loads(response.content.strip())
except:
# Fallback значения если парсинг не удался
confidence_scores = {
"job_type_confidence": 0.7,
"category_confidence": 0.7,
"search_type_confidence": 0.7
}
return {
"confidence_scores": confidence_scores,
"processed": True
}
def _find_closest_category(self, predicted_category: str) -> str:
"""Находит наиболее похожую категорию из списка доступных"""
# Простая эвристика поиска по вхождению ключевых слов
predicted_lower = predicted_category.lower()
for category in CATEGORIES:
category_lower = category.lower()
if predicted_lower in category_lower or category_lower in predicted_lower:
return category
# Если ничего не найдено, возвращаем первую категорию как fallback
return CATEGORIES[0]
async def classify(self, description: str) -> Dict[str, Any]:
"""Основной метод для классификации вакансии/услуги"""
initial_state = {
"description": description,
"job_type": "",
"category": "",
"search_type": "",
"confidence_scores": {},
"processed": False
}
# Запускаем рабочий процесс
result = await self.workflow.ainvoke(initial_state)
# Формируем итоговый ответ в формате JSON
classification_result = {
"job_type": result["job_type"],
"category": result["category"],
"search_type": result["search_type"],
"confidence_scores": result["confidence_scores"],
"success": result["processed"]
}
return classification_result
async def main():
"""Демонстрация работы агента"""
agent = VacancyClassificationAgent()
# Тестовые примеры
test_cases = [
"Требуется Python разработчик для создания веб-приложения на Django. Постоянная работа, полный рабочий день.",
"Ищу заказы на создание логотипов и фирменного стиля. Работаю в Adobe Illustrator.",
"Нужен 3D-аниматор для краткосрочного проекта создания рекламного ролика.",
"Резюме: опытный маркетолог, ищу удаленную работу в сфере digital-маркетинга",
"Ищем фронтенд-разработчика React в нашу команду на постоянную основе"
]
test_cases = []
print("? Демонстрация работы агента классификации вакансий\n")
for i, description in enumerate(test_cases, 1):
print(f"? Тест {i}:")
print(f"Описание: {description}")
try:
result = await agent.classify(description)
print("Результат классификации:")
print(json.dumps(result, ensure_ascii=False, indent=2))
except Exception as e:
print(f"❌ Ошибка: {e}")
print("-" * 80)
if __name__ == "__main__":
asyncio.run(main())
Асинхронная архитектура
В отличие от простых примеров, наш агент работает полностью асинхронно:
async def _classify_job_type(self, state: State) -> Dict[str, Any]:
"""Узел для определения типа работы: проектная или постоянная"""
response = await self.llm.ainvoke([message])
# Обработка ответа...
return {"job_type": job_type}
Это позволяет:
Обрабатывать множественные запросы параллельно
Интегрироваться с асинхронными веб-фреймворками
Эффективно использовать ресурсы при масштабировании
Специализированные промпты для каждого узла
Каждый узел использует целевой промпт, оптимизированный под свою задачу:
For job type classification:
template="""
Проанализируй следующее описание и определи тип работы.
Описание: {description}
Ответь только одним из двух вариантов:
- "проектная работа" - если это временная задача, проект, фриланс
- "постоянная работа" - если это постоянная должность, штатная позиция
Тип работы:
"""
Такой подход обеспечивает высокую точность каждого этапа классификации, так как модель получает чёткие, специфичные инструкции.
Обработка ошибок и fallback-механизмы
Агент включает умную обработку неожиданных ситуаций:
def _find_closest_category(self, predicted_category: str) -> str:
"""Находит наиболее похожую категорию из списка доступных"""
predicted_lower = predicted_category.lower()
for category in CATEGORIES:
category_lower = category.lower()
if predicted_lower in category_lower or category_lower in predicted_lower:
return category
# Fallback: возвращаем первую категорию
return CATEGORIES[0]
Это делает агента устойчивым к ошибкам — даже если модель вернёт неточный результат, система найдёт наиболее подходящий вариант из допустимых.
Оценка уверенности
Финальный узел рассчитывает метрики качества классификации:
async def _calculate_confidence(self, state: State) -> Dict[str, Any]:
"""Узел для расчета уровня уверенности в классификации"""
# Запрос к LLM для оценки уверенности по шкале 0.0-1.0
# Возврат структурированного JSON с метриками
Это позволяет:
Отслеживать качество работы агента
Выделять случаи, требующие ручной проверки
Оптимизировать промпты на основе статистики
Практическое применение
async def main():
agent = VacancyClassificationAgent()
description = "Требуется Python разработчик для создания веб-приложения"
result = await agent.classify(description)
print(json.dumps(result, ensure_ascii=False, indent=2))
Результат:
{
"job_type": "постоянная работа",
"category": "Бэкенд-разработчик (Node.js, Python, PHP, Ruby)",
"search_type": "поиск исполнителя",
"confidence_scores": {
"job_type_confidence": 0.95,
"category_confidence": 0.88,
"search_type_confidence": 0.92
},
"success": true
}
Ключевые преимущества архитектуры
Модульность — каждый узел решает одну задачу, легко тестировать и улучшать отдельно
Расширяемость — можно добавлять новые узлы анализа без изменения существующих
Прозрачность — весь процесс принятия решений документирован и отслеживаем
Производительность — асинхронная обработка множественных запросов
Надёжность — fallback-механизмы и обработка ошибок
Реальная польза
Такой агент может использоваться в:
HR-платформах для автоматической категоризации резюме и вакансий
Биржах фриланса для улучшения поиска и рекомендаций
Внутренних системах компаний для обработки заявок и проектов
Аналитических решениях для исследования рынка труда
MCP в действии: создаём агента с файловой системой и веб-поиском
После того как мы разобрались с базовыми принципами LangGraph и создали простого классификатора, пора перейти к настоящей магии — интеграции агента с внешним миром через MCP (Model Context Protocol).
Сейчас мы создадим полноценного ИИ-помощника, который сможет:
Работать с файловой системой (читать, создавать, изменять файлы)
Искать актуальную информацию в интернете
Запоминать контекст диалога
Обрабатывать ошибки и восстанавливаться после сбоев
От теории к реальным инструментам
Помните, как в начале статьи мы говорили о том, что MCP — это мост между нейросетью и её окружением? Сейчас вы увидите это на практике. Наш агент получит доступ к реальным инструментам:
# Инструменты файловой системы
- read_file — чтение файлов
- write_file — запись и создание файлов
- list_directory — просмотр содержимого папок
- create_directory — создание папок
# Инструменты веб-поиска
- brave_web_search — поиск в интернете
- get_web_content — получение содержимого страниц
Это уже не «игрушечный» агент — это рабочий инструмент, который может решать реальные задачи.
Кода и текста уже накопилось достаточно много, поэтому далее я приведу лишь общее описание принципов и концепции разработки подобных ИИ-агентов с интеграцией MCP. Полный пример кода интеграции с MCP — как для одного сервера, так и для нескольких — вы найдете в моём бесплатном телеграм-канале «Лёгкий путь в Python». Сообщество уже насчитывает около 4000 участников, присоединяйтесь.
?️ Архитектура: от простого к сложному
1. Конфигурация как основа стабильности
@dataclass
class AgentConfig:
"""Упрощенная конфигурация AI-агента"""
filesystem_path: str = "/path/to/work/directory"
model_provider: ModelProvider = ModelProvider.OLLAMA
use_memory: bool = True
enable_web_search: bool = True
def validate(self) -> None:
"""Валидация конфигурации"""
if not os.path.exists(self.filesystem_path):
raise ValueError(f"Путь не существует: {self.filesystem_path}")
Почему это важно? В отличие от примера с классификацией, здесь агент взаимодействует с внешними системами. Одна ошибка в пути к файлам или отсутствующий API-ключ — и весь агент перестаёт работать. Валидация на старте экономит часы отладки.
2. Фабрика моделей: гибкость выбора
class ModelFactory:
"""Упрощенная фабрика моделей"""
@staticmethod
def create_model(config: AgentConfig):
"""Создает модель согласно конфигурации"""
provider = config.model_provider.value
if provider == "ollama":
return ChatOllama(model="qwen2.5:32b", base_url="http://localhost:11434")
elif provider == "openai":
return ChatOpenAI(model="gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
# ... другие провайдеры
Один код — множество моделей. Хотите бесплатную локальную модель? Используйте Ollama. Нужна максимальная точность? Переключитесь на GPT-4. Важна скорость? Попробуйте DeepSeek. Код остаётся тем же.
3. MCP-интеграция: подключение к реальному миру
async def _init_mcp_client(self):
"""Инициализация MCP клиента"""
mcp_config = {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", self.filesystem_path],
"transport": "stdio"
},
"brave-search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search@latest"],
"transport": "stdio",
"env": {"BRAVE_API_KEY": os.getenv("BRAVE_API_KEY")}
}
}
self.mcp_client = MultiServerMCPClient(mcp_config)
self.tools = await self.mcp_client.get_tools()
Здесь происходит ключевая работа MCP: мы подключаем к агенту внешние MCP-серверы, которые предоставляют набор инструментов и функций. Агент при этом получает не просто отдельные функции, а полноценное контекстное понимание того, как работать с файловой системой и интернетом.
Для корректной работы указанных MCP-серверов потребуется установить Node.js и npm последней версии. После этого глобально установите необходимые MCP-серверы с помощью команд:
npm install -g @modelcontextprotocol/server-filesystem
npm install -g @modelcontextprotocol/server-brave-search@latest
Если вы планируете использовать сервер server-brave-search, обязательно получите бесплатный API-ключ на сайте Brave и установите его в переменную окружения BRAVE_API_KEY.
Таким образом, MCP-сервера предоставляют вашему ИИ-агенту расширенные возможности через стандартизированный протокол, позволяющий интегрировать разные внешние сервисы и инструменты для работы с реальным миром.
Устойчивость к ошибкам
В реальном мире всё ломается: сеть недоступна, файлы заблокированы, API-ключи просрочены. Наш агент готов к этому:
@retry_on_failure(max_retries=2, delay=1.0)
async def process_message(self, user_input: str, thread_id: str = "default") -> str:
"""Обработка сообщения пользователя с повторными попытками"""
try:
config = {"configurable": {"thread_id": thread_id}}
message_input = {"messages": [HumanMessage(content=user_input)]}
response = await self.agent.ainvoke(message_input, config)
return response["messages"][-1].content
except Exception as e:
error_msg = f"❌ Ошибка обработки: {e}"
logger.error(error_msg)
return error_msg
Декоратор @retry_on_failure
автоматически повторяет операции при временных сбоях. Пользователь даже не заметит, что что-то пошло не так.
Контекстная память
if self.config.use_memory:
self.checkpointer = InMemorySaver()
logger.info("Память агента включена")
Агент помнит всю историю разговора. Вы можете сказать «создай файл config.py», затем «добавь в него настройки базы данных», и агент поймёт, что речь идёт о том же файле. Это кардинально меняет пользовательский опыт.
Умный системный промпт
def _get_system_prompt(self) -> str:
base_prompt = (
"Ты — умный AI-ассистент, который помогает пользователю работать с файлами "
"и искать информацию в интернете. Всегда внимательно анализируй запрос "
"и выбирай наиболее подходящий инструмент."
)
if self.config.enable_web_search:
base_prompt += (
" Если требуется найти актуальные данные (погоду, новости, цены), "
"обязательно используй веб-поиск и предоставляй самую свежую информацию."
)
return base_prompt
Промпт адаптируется к возможностям агента. Если веб-поиск отключён, агент не будет предлагать найти что-то в интернете. Если включён — будет активно использовать эту возможность.
? Практические сценарии использования
Работа с кодом:
> Создай структуру проекта Flask с папками templates, static и models
✅ Создание папок...
? Создал директорию /project/templates
? Создал директорию /project/static
? Создал директорию /project/models
? Создал базовый app.py с настройками Flask
Актуальная информация:
> Какая сейчас погода в Москве?
? Ищу актуальную информацию о погоде...
?️ В Москве сейчас +25°C, переменная облачность, ветер 3 м/с
Анализ и обработка файлов:
> Прочитай все .py файлы в папке и создай документацию
? Читаю Python файлы...
? Найдено 5 файлов: app.py, models.py, views.py, utils.py, config.py
? Создаю документацию на основе анализа кода...
✅ Документация сохранена в README.md
Ключевые отличия от простого чат-бота
Реальные действия — агент не просто говорит, что делать, а делает сам
Контекстная память — помнит всю историю и может ссылаться на предыдущие действия
Актуальные данные — может получать свежую информацию из интернета
Обработка ошибок — graceful degradation при проблемах с инструментами
Адаптивность — поведение зависит от доступных инструментов
От примера к продакшену
Этот код демонстрирует архитектурные паттерны для создания продакшн-готовых агентов:
Модульная конфигурация — легко менять поведение без изменения кода
Абстракция провайдеров — поддержка множества LLM из коробки
Graceful error handling — система не падает при проблемах
Расширяемость — новые инструменты добавляются декларативно
Наблюдаемость — подробное логирование для отладки
Итоги: от теории к практике ИИ-агентов
Сегодня мы прошли путь от базовых концепций до создания работающих ИИ-агентов. Давайте подведём итоги того, что мы изучили и чего достигли.
Что мы освоили
1. Фундаментальные концепции
Разобрались с различием между чат-ботами и настоящими ИИ-агентами
Поняли роль MCP (Model Context Protocol) как моста между моделью и внешним миром
Изучили архитектуру LangGraph для построения сложной логики агентов
2. Практические навыки
Настроили рабочее окружение с поддержкой облачных и локальных моделей
Создали агента-классификатора с асинхронной архитектурой и управлением состояниями
Построили MCP-агента с доступом к файловой системе и веб-поиску
3. Архитектурные паттерны
Научились проектировать графы состояний для сложной логики
Освоили модульную конфигурацию и фабрики моделей
Внедрили обработку ошибок и retry-механизмы для продакшн-готовых решений
Ключевые преимущества подхода
LangGraph + MCP дают нам:
Прозрачность — каждый шаг агента документирован и отслеживаем
Расширяемость — новые возможности добавляются декларативно
Надёжность — встроенная обработка ошибок и восстановление
Гибкость — поддержка множества моделей и провайдеров из коробки
Практическая ценность
Созданные примеры — это не просто демонстрация технологий. Это готовые решения для реальных задач:
Агент-классификатор можно внедрить в HR-платформы и биржи фриланса
MCP-агент подходит для автоматизации рабочих процессов и анализа данных
Архитектурные паттерны масштабируются на проекты любой сложности
Уровень подготовки
Сегодня я намеренно не усложнял материал глубокими техническими деталями и сложной терминологией. Цель была простая — преодолеть базовый порог входа в тему интеграции ИИ-агентов с собственными проектами.
Этой информации достаточно, чтобы:
Понять принципы работы современных ИИ-агентов
Начать экспериментировать с собственными решениями
Оценить потенциал технологий для ваших задач
Сделать осознанный выбор инструментов для проекта
Что дальше?
Мы затронули лишь верхушку айсберга. LangGraph и MCP предлагают гораздо более широкие возможности:
Мультиагентные системы — координация команд специализированных агентов
Продвинутые MCP-серверы — интеграция с базами данных, CRM, API сервисов
Сложные графы состояний — циклы, условные переходы, параллельное выполнение
Production deployment — масштабирование, мониторинг, A/B тестирование
Где найти больше материалов
Полный исходный код всех примеров, а также эксклюзивные материалы, которые я не публикую на Хабре, вы найдёте в моём телеграм-канале «Легкий путь в Python».
Обратная связь важна
Если вам интересны более глубокие темы:
LLM и ИИ-агенты в интеграции с собственными проектами
MCP-серверы и создание кастомных инструментов
Детальное рассмотрение LangGraph — продвинутые паттерны и оптимизации
Дайте знать своим:
? Лайком этой статьи
? Подпиской на телеграм-канал
? Комментарием с вопросами и предложениями тем
Ваша активность показывает, что тема востребована, и мотивирует создавать ещё больше качественного контента.
Заключение
ИИ-агенты — это не футуристическая фантастика, а реальная технология сегодняшнего дня. С помощью LangGraph и MCP мы можем создавать системы, которые решают конкретные бизнес-задачи, автоматизируют рутину и открывают новые возможности.
Главное — начать. Возьмите код из примеров, адаптируйте под свои задачи, экспериментируйте. Каждый проект — это новый опыт и шаг к мастерству в области ИИ-разработки.
Удачи в ваших проектах!
Amvera Cloud – облако для простого запуска проектов со встроенным CI/CD (деплой идёт через Git или загрузку файлов в интерфейсе), встроенным проксированием до ведущих LLM и собственным инференсом LLaMA. Вам не нужно думать о настройке NGINX, виртуальных машин и другой инфраструктуры. Зарегистрируйтесь и получите 111 рублей на тест.
Комментарии (4)
Denwer_py
22.07.2025 10:21А я правильно понял, что без МСР не получится реализовать работу с файловой системой (читать, создавать, изменять файлы)? Потому что это идет в пункте про МСП как раз. И для этих действий обязательно ключ BRAVE нужен, верно?
За статью спасибо, очень злободневно!
Exception92
Спасибо большое за статью, написано очень доступно и понятно!
Но хотелось бы более расширенного варианта, с более сложными сценариями :)
И отдельно статью про MCP ;)
yakvenalex Автор
Спасибо за обратную связь. В планах есть более детальное раскрытие темы LangGraph. В частности, сами графы, которые в этой статье почти не раскрыл или те же Tools. Так что если звезды сложатся - с меня мини курс по данному фреймворку.