Игорь Градов
Игорь Градов
7 мин
ai

Что такое ИИ-агент с ожиданием: локальный граф на LangGraph без облака за 2 часа

Большинство разговоров об ИИ-агентах (agent, программа, которая сама решает, какой инструмент вызвать и когда остановиться) заканчиваются на уровне «подключите API и дайте промпт», но реальный локальный агент требует управления состоянием, контрольных точек и умения ждать ответа человека, и именно этому посвящена третья часть практического руководства разработчика Владимира на Хабре.

Что такое ИИ-агент с ожиданием: локальный граф на LangGraph без облака за 2 часа
Почему это важно

Это готовый рецепт сборки агентного графа на LangGraph с чекпоинтерами и прерываниями, который работает без облачных сервисов и целиком на локальном стеке, а значит, подходит для проектов, где данные нельзя отправлять за периметр.

Руководство является третьей частью серии. В первой части автор поднял контейнер с моделью, настроил Langfuse (инструмент трассировки, то есть записи каждого шага работы агента) и подключил MCP-инструменты (Model Context Protocol, протокол, через который агент вызывает внешние функции). Во второй добавил планировщик, оценщики и защиту от зацикливания. Теперь всё это собирается в работающий граф, где агент умеет останавливаться и ждать подтверждения от человека.

Что понадобится?

  • Знакомство с первой и второй частями серии на Хабре (ссылки в оригинале)
  • Python 3.10+ и установленные библиотеки: LangGraph, LangChain, Langfuse
  • Локально развёрнутая языковая модель (контейнер из первой части)
  • Настроенный Langfuse для трассировки
  • MCP-инструменты, подключённые по протоколу из первой части
  • Примерно 2 часа на сборку и отладку графа

Пошаговая инструкция

1. Добавьте чекпоинтер в конструктор агента.

Чекпоинтер (checkpointer) сохраняет состояние графа после каждого узла. Если агент остановился и ждёт ответа пользователя, состояние не теряется. Дополнительно формируется session_id на основе даты и времени, чтобы трассировки в Langfuse не смешивались между сессиями.

class MCPAgent:
    def __init__(self):
        self.llm_with_tools: ChatOpenAI | None = None
        self.tools: list[BaseTool] | None = None
        self.graph = None
        self.checkpointer = InMemorySaver()
        self.lf_handler = CallbackHandler(
            public_key=settings.langfuse.public_key
        )
        self.init_time = datetime.now().strftime('%Y%m%d_%H%M%S')

2. Инициализируйте граф: подключите инструменты и привяжите их к модели.

Метод init_graph загружает MCP-инструменты, привязывает их к языковой модели (с отключённым параллельным вызовом, чтобы агент выполнял действия последовательно) и компилирует граф.

async def init_graph(self):
    try:
        self.tools = await init_tools()
    except Exception as e:
        logger.error(f'Ошибка инициализации инструментов: {e}')
        raise
    self.llm_with_tools = settings.llm.chat.llm.bind_tools(
        self.tools, parallel_tool_calls=False
    )
    self.graph = self._compile_graph()

3. Настройте условные рёбра графа.

Условные рёбра определяют, куда двигается агент после каждого узла. Два ключевых маршрутизатора проверяют флаг is_approved в состоянии.

Первый решает: если план подтверждён пользователем, переходим к выполнению, если нет, возвращаемся к планировщику.

@staticmethod
def need_adjust_plan_router(state: AgentState) -> Literal['injector', 'planer']:
    if state.get('is_approved', False):
        return 'injector'
    return 'planer'

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

@staticmethod
def need_modify_step_router(state: AgentState) -> Literal['agent_node', 'compressor']:
    if state.get('is_approved', False):
        return 'compressor'
    return 'agent_node'

4. Добавьте маршрутизатор перехода по шагам плана.

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

@staticmethod
def next_step_router(state: AgentState) -> Literal['injector', 'finalizer']:
    if state['current_step'] < len(state['plan']):
        return 'injector'
    return 'finalizer'

5. Соберите и скомпилируйте граф.

Здесь все узлы соединяются рёбрами. Два ключевых параметра компиляции: interrupt_after=['planer'] останавливает граф после формирования плана и ждёт подтверждения, interrupt_before=['agent_solver'] останавливает перед оценкой результата шага.

def _compile_graph(self):
    workflow = StateGraph(AgentState)
    workflow.add_node('agent_node', AgentNode(llm=self.llm_with_tools).node)
    workflow.add_node('compressor', ContextCompressorNode().node)
    workflow.add_node('finalizer', FinalizerNode(llm=settings.llm.chat.llm).node)
    workflow.add_node('planer', PlanerNode(llm=settings.llm.chat.llm).node)
    workflow.add_node('plan_solver', PlanSolverNode().node)
    workflow.add_node('agent_solver', AgentSolverNode().node)
    workflow.add_node('injector', StepInjectorNode().node)
    workflow.add_node('tools', ToolNode(self.tools))

    workflow.set_entry_point(key='planer')
    workflow.add_edge(start_key='planer', end_key='plan_solver')
    workflow.add_conditional_edges(source='plan_solver', path=self.need_adjust_plan_router)
    workflow.add_edge(start_key='injector', end_key='agent_node')
    workflow.add_conditional_edges(source='agent_node', path=self.agent_router)
    workflow.add_edge(start_key='tools', end_key='agent_node')
    workflow.add_conditional_edges(source='agent_solver', path=self.need_modify_step_router)
    workflow.add_conditional_edges(source='compressor', path=self.next_step_router)
    workflow.set_finish_point(key='finalizer')

    graph = workflow.compile(
        checkpointer=self.checkpointer,
        interrupt_after=['planer'],
        interrupt_before=['agent_solver'],
    )
    return graph

6. Реализуйте запуск и возобновление диалога.

Метод run запускает граф с нуля. Метод resume продолжает работу после паузы, оборачивая пользовательский ввод в Command и извлекая trace_id из сохранённого состояния, чтобы Langfuse не плодил отдельные трейсы на каждый возврат.

async def run(self, user_messages: str, request_id: str | None = None) -> dict[str, Any]:
    self._check_graph_available()
    trace_id = Langfuse.create_trace_id()
    return await self._ainvoke_with_tracing(
        data={'user_request': user_messages, 'user_input': user_messages, 'trace_id': trace_id},
        request_id=request_id, trace_id=trace_id, span_name='agent_run')

async def resume(self, user_messages: str, request_id: str | None = None) -> dict[str, Any]:
    self._check_graph_available()
    trace_id = self._get_trace_id(request_id)
    return await self._ainvoke_with_tracing(
        data=Command(update={'user_input': user_messages}),
        request_id=request_id, trace_id=trace_id, span_name='agent_resume')

7. Добавьте служебные методы: проверку инициализации и извлечение trace_id.

Первый метод не даёт вызвать агента до init_graph. Второй достаёт trace_id из сохранённого состояния по thread_id, чтобы вся сессия попала в один трейс.

def _check_graph_available(self):
    if self.graph is None:
        raise RuntimeError('Агент не инициализирован. Запустите `initialize()`.')

def _get_trace_id(self, request_id: str) -> str | None:
    return self.graph.get_state(
        {'configurable': {'thread_id': request_id}}
    ).values.get('trace_id', None)

Что такое ИИ-агент в контексте этого графа?

Когда говорят «что такое ИИ-агент», обычно подразумевают программу, которая сама выбирает инструменты и решает, когда остановиться. Но в реальной сборке агент без чекпоинтера и прерываний превращается в чёрный ящик: он либо молча делает всё подряд, либо теряет контекст при любой паузе. Именно механизм interrupt_after и interrupt_before превращает набор узлов в управляемого агента, который спрашивает, прежде чем действовать.

Как это работает на практике

Допустим, вы отправляете агенту задачу: «Создай REST-эндпоинт для загрузки файлов». Агент формирует план из трёх шагов (создать маршрут, написать валидацию, добавить тесты) и останавливается на interrupt_after=['planer']. Вы видите план, корректируете второй шаг, отправляете подтверждение через resume. Агент продолжает, выполняет первый шаг, останавливается перед agent_solver для оценки результата. Вы подтверждаете, агент переходит к суммаризации контекста и следующему шагу. Весь процесс записывается в Langfuse одним трейсом, потому что trace_id передаётся между вызовами.

Частые ошибки
  • Забыли вызвать init_graph перед run. Агент выбросит RuntimeError. Проверка _check_graph_available ловит это, но лучше вызывать инициализацию явно при старте приложения.
  • Используете InMemorySaver в продакшене. Он хранит состояние в памяти процесса. Перезапуск сервиса обнуляет все сессии. Для боевого использования замените на персистентный чекпоинтер (Redis, PostgreSQL).
  • Не передаёте thread_id при вызове resume. Без него граф не найдёт сохранённое состояние и начнёт с нуля или упадёт.
  • Включаете parallel_tool_calls=True. При параллельном вызове инструментов агент может запустить конфликтующие операции с файловой системой одновременно, что приведёт к непредсказуемым результатам.
  • Путаете interrupt_after и interrupt_before. interrupt_after останавливает граф после выполнения узла, пользователь видит результат. interrupt_before останавливает до входа в узел, пользователь может изменить входные данные. Перепутали местами, и агент либо покажет план до его формирования, либо выполнит шаг до вашего одобрения.

Кому это пригодится и что делать прямо сейчас?

Разработчику, который собирает бота или автоматизацию. Весь стек работает локально: модель в контейнере, Langfuse для трассировки, MCP для инструментов. Данные не уходят в облако, что критично для корпоративных проектов в РФ. Возьмите код из статьи и начните с InMemorySaver, затем замените на персистентный чекпоинтер под свою инфраструктуру.

Автору Дзена или контент-маркетологу. Сам код вам не нужен, но механика «агент делает, останавливается, спрашивает» это то, к чему идут все пользовательские ИИ-продукты. Понимание, что такое ИИ-агент на уровне архитектуры, поможет точнее писать о технологиях и не путать «чат-бот» с «агентом».

Предпринимателю. Если ваша команда обсуждает внедрение ИИ-агентов, покажите им эту серию статей как пример реализации без зависимости от OpenAI, Anthropic и других зарубежных API. Локальная модель плюс LangGraph плюс MCP равно полный контроль над данными и предсказуемыми затратами.

Совет редакции dzen.guru

Серия Владимира на Хабре, один из немногих русскоязычных примеров, где ИИ-агент собирается не «в теории на слайдах», а с рабочим кодом, чекпоинтерами и трассировкой. По моим наблюдениям, большинство туториалов по агентам заканчиваются на вызове одного инструмента и не доходят до управления состоянием, а именно там начинается настоящая сложность. Честная оговорка: InMemorySaver не подходит для продакшена, а MCP-протокол ещё молод и может измениться. Но как учебный стенд для понимания, что такое ИИ-агент изнутри, это ценный материал.

Попробуйте ИИ-инструменты dzen.guru

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

Попробовать

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

По материалам Habr AI

Поделиться:TelegramVK
Игорь Градов
Игорь Градов

Основатель dzen.guru. Эксперт по монетизации и продвижению на Дзен. Автор курса «Старт на Дзен 2026».

Комментарии

Читайте также

Яндекс ГПТ в ИИ-агентах за 15 минут: готовые конфиги для OpenCode, Pi и Hermes
ai

Яндекс ГПТ в ИИ-агентах за 15 минут: готовые конфиги для OpenCode, Pi и Hermes

Яндекс ГПТ подключается к инструментам разработки через стандартный OpenAI-совместимый протокол, и автор Habr собрал готовые конфиги для OpenCode, Pi и Hermes,…

5 мин
ИИ-агенты сливают секреты компаний через поиск: безопасность падает при росте точности
ai

ИИ-агенты сливают секреты компаний через поиск: безопасность падает при росте точности

Microsoft, OpenAI, Google, другие компании активно встраивают ИИ-агентов в корпоративные сервисы, но исследователи из ServiceNow обнаружили системную проблему:…

5 мин
Что такое ИИ-агент SpatialClaw: NVIDIA набрала 59,9% на 20 бенчмарках без дообучения
ai

Что такое ИИ-агент SpatialClaw: NVIDIA набрала 59,9% на 20 бенчмарках без дообучения

NVIDIA выпустила SpatialClaw, ИИ-агента для пространственного мышления, который не требует дообучения и работает через код: на 20 бенчмарках он набрал 59,9%…

5 мин