Друзья, приветствую! Надеюсь, успели соскучиться.

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

На повестке дня — LangGraph и MCP: инструменты, с помощью которых можно создавать действительно полезных ИИ-агентов.

Если раньше мы спорили о том, какая нейросеть лучше отвечает на русском языке, то сегодня поле битвы сместилось в сторону более прикладных задач: кто лучше справляется с ролью ИИ-агента? Какие фреймворки действительно упрощают разработку? И как интегрировать всё это добро в реальный проект?

Но прежде чем нырнуть в практику и код, давайте разберёмся с базовыми понятиями. Особенно с двумя ключевыми: ИИ-агенты и MCP. Без них разговор про LangGraph будет неполным.

ИИ-агенты простыми словами

ИИ-агенты — это не просто «прокачанные» чат‑боты. Они представляют собой более сложные, автономные сущности, которые обладают двумя важнейшими особенностями:

  1. Умение взаимодействовать и координироваться

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

  2. Доступ к внешним ресурсам

    ИИ-агент больше не ограничен рамками диалога. Он может обращаться к базам данных, выполнять вызовы к 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.

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

? Быстрая установка (сводка шагов)

  1. Установите Docker + Docker Compose

  2. Создайте структуру проекта:

    mkdir qwen-local && cd qwen-local
  3. Создайте 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
    
  4. Запустите контейнер:

    docker compose up -d
  5. Загрузите модель:

    docker exec -it ollama_qwen ollama pull qwen2.5:32b
  6. Проверьте работу через API:

    curl http://localhost:11434/api/generate \
      -H "Content-Type: application/json" \
      -d '{"model": "qwen2.5:32b", "prompt": "Привет!", "stream": false}'
    
  1. Интеграция с 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-платформ и бирж фриланса.

Задача агента

Наш агент принимает на вход текстовое описание вакансии или услуги и выполняет трёхуровневую классификацию:

  1. Тип работы: проектная работа или постоянная вакансия

  2. Категория профессии: из 45+ предопределённых специальностей

  3. Тип поиска: ищет ли человек работу или ищет исполнителя

Результат возвращается в структурированном 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
}

Ключевые преимущества архитектуры

  1. Модульность — каждый узел решает одну задачу, легко тестировать и улучшать отдельно

  2. Расширяемость — можно добавлять новые узлы анализа без изменения существующих

  3. Прозрачность — весь процесс принятия решений документирован и отслеживаем

  4. Производительность — асинхронная обработка множественных запросов

  5. Надёжность — 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

Ключевые отличия от простого чат-бота

  1. Реальные действия — агент не просто говорит, что делать, а делает сам

  2. Контекстная память — помнит всю историю и может ссылаться на предыдущие действия

  3. Актуальные данные — может получать свежую информацию из интернета

  4. Обработка ошибок — graceful degradation при проблемах с инструментами

  5. Адаптивность — поведение зависит от доступных инструментов

От примера к продакшену

Этот код демонстрирует архитектурные паттерны для создания продакшн-готовых агентов:

  • Модульная конфигурация — легко менять поведение без изменения кода

  • Абстракция провайдеров — поддержка множества 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)


  1. Exception92
    22.07.2025 10:21

    Спасибо большое за статью, написано очень доступно и понятно!

    Но хотелось бы более расширенного варианта, с более сложными сценариями :)

    И отдельно статью про MCP ;)


    1. yakvenalex Автор
      22.07.2025 10:21

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


  1. Roma97
    22.07.2025 10:21

    С нетерпением ожидаю каждую новую вашу статью)


  1. Denwer_py
    22.07.2025 10:21

    А я правильно понял, что без МСР не получится реализовать работу с файловой системой (читать, создавать, изменять файлы)? Потому что это идет в пункте про МСП как раз. И для этих действий обязательно ключ BRAVE нужен, верно?
    За статью спасибо, очень злободневно!