Архитектура Agentic AI в ERP: Реализация мультиагентной закупки на LangGraph

От теории к практической реализации интеллектуального модуля Procure-to-Pay с использованием паттерна Supervisor и параллельным выполнением агентов.

Ключевой вывод: Традиционные RAG-системы и RPA-роботы не способны на автономное принятие комплексных решений в закупках из-за отсутствия контекстуального понимания и итеративного планирования. Agentic AI с графом состояний решает эту проблему.

Проблема традиционной автоматизации

Современные ERP-системы (SAP, Oracle, 1С) используют RPA для автоматизации рутинных операций, но эти решения сталкиваются с фундаментальными ограничениями при обработке сложных закупочных сценариев:

  • Отсутствие контекстуальной памяти: RPA-боты не хранят историю взаимодействий, что критично при переговорах с поставщиками.
  • Жёсткие流程: Каждое отклонение от сценария (например, отсутствие товара) требует ручного вмешательства.
  • Нет итеративного планирования: Системы не могут генерировать и оценивать альтернативные варианты (замена поставщика, изменение сроков).
  • Изоляция данных: Информация о бюджете, контрактах и рисках хранится в разных модулях без единого контекста.

В результате время согласования сложной закупки занимает 2–5 рабочих дней, а не 5–10 минут, как в идеале. Agentic AI, построенный на графах состояний, решает эти проблемы через:

  1. Состояние (State): Единый контекст (запрос, бюджет, поставщики, решения) передаётся между агентами.
  2. Параллельное выполнение: Несколько агентов работают одновременно (например, Scout и Auditor).
  3. Итеративные циклы: Возможность возврата к предыдущим шагам при недостатке данных.
  4. Human-in-the-loop: Автоматический эскалация при низкой уверенности модели.

Архитектура мультиагентной системы: Supervisor + Specialized Agents

Мы используем паттерн Supervisor, где Orchestrator выступает как диспетчер, распределяющий задачи между специализированными агентами. Каждый агент — это LLM (например, GPT-4o или Claude 3.5 Sonnet) с системным промптом и набором инструментов (Tools).

Scout (Разведчик)

Отвечает за поиск альтернативных поставщиков и товаров через внешние API.

Инструменты:

  • search_suppliers(query, category) — запрос к каталогу поставщиков (например, через GraphQL API).
  • check_availability(sku, quantity) — проверка остатков у поставщиков.
  • get_price_history(sku) — анализ динамики цен.
  • compare_alternatives(sku_list) — сравнение альтернативных товаров.

Auditor (Аудитор)

Оценивает риски поставщиков: финансовую устойчивость, репутацию, compliance.

Инструменты:

  • get_credit_rating(supplier_id) — запрос к бюро кредитных историй (например, Experian API).
  • check_blacklist(supplier_id) — проверка по внутреннему чёрному списку.
  • analyze_reviews(supplier_id) — анализ отзывов из открытых источников.
  • calculate_risk_score(supplier_data) — агрегация метрик в единый скор (0–100).

Budget (Бюджет)

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

Инструменты:

  • check_budget_balance(cost_center, amount) — запрос к модулю бюджетирования.
  • validate_policy(item, amount) — проверка политик (например, тендер при суммы > 500к руб).
  • get_commitments(cost_center) — получение уже принятых обязательств.
  • forecast_impact(amount, period) — прогноз влияния на бюджет.

Orchestrator (Координатор)

Управляет потоком, синтезирует данные, принимает решение или эскалирует к человеку.

Функции:

  • Планирование: Разбивает задачу на подзадачи для агентов.
  • Параллельное выполнение: Запускает Scout и Auditor одновременно.
  • Синтез: Объединяет результаты, взвешивает факторы (цена, риск, бюджет).
  • Решение: Если уверенность > 85% — создаёт заказ, иначе запрашивает human-in-the-loop.

"Orchestrator не просто последовательно вызывает агентов, а управляет графом состояний: после получения данных от Scout он может повторно запросить информацию, если поставщики не прошли проверку Auditor. Это итеративный процесс, а не линейный пайплайн."

Реализация на LangGraph: StateGraph и параллельные узлы

LangGraph позволяет создавать циклические графы, где состояние (State) передаётся между узлами. Вот упрощённый пример для Procure-to-Pay:

procurement_graph.py
from langgraph.graph import StateGraph, END
from typing import TypedDict, List, Annotated
import operator

# Определение состояния
class ProcurementState(TypedDict):
    request: dict                    # Исходный запрос на закупку
    suppliers: List[dict]            # Найденные поставщики
    risks: dict                      # Оценка рисков
    budget_check: dict               # Результат проверки бюджета
    decision: str                    # "approve", "reject", "escalate"
    confidence: float                # Уверенность в решении (0-1)
    human_intervention: bool         # Нужно ли вмешательство человека

# Узлы (агенты)
def scout_node(state: ProcurementState):
    """Поиск поставщиков через внешние API"""
    suppliers = search_suppliers(state["request"]["item"])
    return {"suppliers": suppliers}

def auditor_node(state: ProcurementState):
    """Оценка рисков для каждого поставщика"""
    risks = {}
    for supplier in state["suppliers"]:
        risk_score = calculate_risk_score(supplier["id"])
        risks[supplier["id"]] = risk_score
    return {"risks": risks}

def budget_node(state: ProcurementState):
    """Проверка бюджета"""
    total = sum(s["price"] for s in state["suppliers"])
    check = check_budget_balance(state["request"]["cost_center"], total)
    return {"budget_check": check}

def orchestrator_node(state: ProcurementState):
    """Синтез и принятие решения"""
    # Взвешивание факторов
    avg_risk = sum(state["risks"].values()) / len(state["risks"])
    budget_ok = state["budget_check"]["available"] > state["budget_check"]["required"]
    
    # Простая логика (в реальности — ML-модель)
    if avg_risk < 30 and budget_ok:
        decision = "approve"
        confidence = 0.9
    elif avg_risk > 70:
        decision = "reject"
        confidence = 0.85
    else:
        decision = "escalate"
        confidence = 0.6
    
    return {
        "decision": decision,
        "confidence": confidence,
        "human_intervention": confidence < 0.8
    }

# Построение графа
workflow = StateGraph(ProcurementState)

# Добавление узлов
workflow.add_node("scout", scout_node)
workflow.add_node("auditor", auditor_node)
workflow.add_node("budget", budget_node)
workflow.add_node("orchestrator", orchestrator_node)

# Параллельное выполнение scout и auditor
workflow.add_edge("scout", "auditor")
workflow.add_edge("auditor", "budget")
workflow.add_edge("budget", "orchestrator")
workflow.add_edge("orchestrator", END)

# Компиляция
app = workflow.compile()

# Запуск
result = app.invoke({
    "request": {"item": "ноутбук Dell XPS 15", "quantity": 10, "cost_center": "IT-2024"}
})
print(f"Решение: {result['decision']} (уверенность: {result['confidence']:.2f})")

Ключевые особенности реализации

Управление состоянием (State)

Все агенты работают с единым объектом ProcurementState. Изменения одного агента видны другим. Это обеспечивает контекстуальную связность.

Параллельное выполнение

В примере Scout и Auditor работают последовательно, но LangGraph поддерживает add_conditional_edges для параллельных веток. Например, можно запустить Auditor и Budget одновременно после Scout.

Цикличность

Если после первого прохода бюджет не подтверждён, Orchestrator может вернуться к Scout для поиска более дешёвых аналогов (цикл по условию).

Human-in-the-loop

При низкой уверенности (confidence < 0.8) граф переходит в состояние ожидания, после чего оператор ERP принимает решение, и граф возобновляется.

Инструменты и API: Подключение внешних данных

Каждый агент использует набор инструментов (Tools) — функций, которые вызываются LLM для получения данных. Важно: инструменты должны быть идемпотентными и иметь валидацию.

Пример инструмента для Scout

from langchain.tools import tool
import requests

@tool
def search_suppliers(item: str, location: str = "RU") -> List[dict]:
    """Поиск поставщиков по каталогу. Возвращает список словарей с id, name, price, delivery_time."""
    response = requests.get(
        "https://api.supplier-catalog.com/v1/search",
        params={"q": item, "country": location},
        headers={"Authorization": "Bearer API_KEY"}
    )
    response.raise_for_status()
    suppliers = response.json()["suppliers"]
    
    # Фильтрация и сортировка
    filtered = [s for s in suppliers if s["in_stock"] > 0]
    return sorted(filtered, key=lambda x: x["price"])[:10]  # Топ-10

Интеграция с ERP-модулями

Инструменты должны взаимодействовать с существующими ERP-системами через:

  • REST/GraphQL API: Для модулей бюджетирования, склада, контрактов.
  • SOAP (legacy): Для старых ERP (SAP ECC). Используйте адаптеры.
  • Прямой доступ к БД: Только для чтения (реплика) с строгими правами.
  • События (Kafka/RabbitMQ): Для асинхронных уведомлений (например, "поставка задержана").

Важно: Инструменты должны быть стабильными. Добавьте retry-логику (например, через tenacity) и circuit breaker для защиты от падений внешних сервисов.

Безопасность и Аудит: Валидация и логирование

Agentic AI должен соответствовать требованиям compliance (GDPR, ФЗ-152, SOX). Ключевые практики:

1. Валидация перед записью

Никогда не доверяйте решениям агента без проверки. Используйте двухэтапную валидацию:

  1. Схемы данных (Pydantic): Все входные/выходные данные агентов должны описываться через Pydantic модели. Пример:
from pydantic import BaseModel, Field
from typing import List

class Supplier(BaseModel):
    id: str = Field(..., description="Идентификатор поставщика в ERP")
    name: str
    price: float = Field(gt=0, description="Цена за единицу")
    delivery_days: int = Field(ge=1, description="Срок поставки в днях")
    risk_score: int = Field(ge=0, le=100, description="Скор риска от Auditor")

2. Аудит всех шагов

Логируйте каждое действие агента в отдельную таблицу ai_audit_log:

Поле Пример значения
timestamp2024-05-15T10:30:00Z
agentAuditor
input_state{"supplier_id": "SUP-123", ...}
output_state{"risk_score": 45, "reasons": ["стабильная финансовая отчётность"]}
llm_promptОцени риски поставщика SUP-123...
llm_response{"score": 45, "confidence": 0.87}
tokens_used142

3. RBAC для эскалации

Human-in-the-loop запросы направляются не любому оператору, а только тем, у кого есть роль procurement_approver и права на конкретный центр затрат. Интеграция через workflow-систему (Camunda, Temporal):

  • Orchestrator создаёт задачу в Camunda с данными запроса.
  • Camunda назначает задачу approver'у на основе политик (например, сумма > 1M руб → CFO).
  • После решения оператора Camunda возвращает управление графу.

Кейс: Sudden Outage — как агенты реагируют на срыв поставки

Сценарий: Закупка критичного компонента для производства. За 2 дня до поставки основной поставщик сообщает о форс-мажоре (забастовка).

Шаг 1: Мониторинг и обнаружение

Система получает событие из ERP (shipment_delayed) через Kafka. Это запускает пересчёт графа:

  1. Orchestrator получает уведомление и меняет состояние: "delivery_at_risk": true.
  2. Orchestrator повторно вызывает Scout с запросом: "Найди поставщиков с доставкой за 24 часа в регион Москва".
  3. Scout проверяет каталог и возвращает 3 варианта (включая экспресс-доставку).

Шаг 2: Параллельная оценка

Orchestrator запускает Auditor и Budget параллельно для новых предложений:

  • Auditor: Проверяет репутацию новых поставщиков. Один из них имеет риск-скор 65 (средний), но проверенные отзывы о срочных поставках.
  • Budget: Экспресс-доставка увеличивает стоимость на 40%. Проверка показывает, что лимит бюджета превышен на 15%.

Результат: budget_check = {"available": false, "shortfall": 150000}.

Шаг 3: Принятие решения и эскалация

Orchestrator анализирует:

  • Риск срыва производства без компонента: очень высокий (стоимость простоя ~5M руб/день).
  • Допзатраты на экспресс: 150к руб.
  • Уверенность в поставщике: 70% (средняя).

Итоговый расчёт: confidence = 0.7 (ниже порога 0.8). Orchestrator создаёт задачу human-in-the-loop с рекомендацией: "Одобрить срочную закупку у SUP-456, несмотря на превышение бюджета, из-за критичности срыва".

Оператор видит в интерфейсе ERP уведомление с кейсом, данными по рискам и бюджетом, и принимает решение за 2 минуты.

Результат: Вместо 2 дней ручного поиска и согласований система за 5 минут нашла альтернативу, оценила риски, выявила бюджетное ограничение и эскалировала осмысленный запрос. Общее время — 15 минут (включая human-in-the-loop).

Часто задаваемые вопросы

Как обеспечить безопасность API-ключей для инструментов?

Не храните ключи в коде. Используйте vault-решения (HashiCorp Vault, AWS Secrets Manager) или переменные окружения. LangChain поддерживает get_secret_value() для безопасного доступа. Также настройте network policies в Kubernetes, чтобы поды агентов могли обращаться только к разрешённым хостам.

Можно ли использовать open-source LLM (Llama 3) вместо GPT-4?

Да, но с оговорками. Для Scout (поиск) достаточно 7B модели. Для Auditor (оценка рисков) нужна 70B+ из-за сложности анализа. Разверните модели локально (vLLM, Ollama) или используйте через API (Groq, Together). Важно: тестируйте качество на своих данных.

Как тестировать графы состояний?

Используйте моки для инструментов. LangGraph предоставляет app.get_graph().draw_mermaid_png() для визуализации. Пишите unit-тесты на каждый узел, проверяя переходы. Для интеграционных тестов запускайте граф с фикстурными данными и проверяйте итоговое состояние. Пример теста:

def test_orchestrator_approval():
        state = {
            "suppliers": [{"id": "S1", "price": 1000, "risk_score": 20}],
            "budget_check": {"available": 5000, "required": 3000}
        }
        result = orchestrator_node(state)
        assert result["decision"] == "approve"
        assert result["confidence"] > 0.8

Как масштабировать: десятки тысяч закупок в день?

Используйте асинхронные вызовы LLM (например, litellm.acompletion) и пуллы агентов. Запускайте графы в Kubernetes с horizontal pod autoscaling по метрике "очередь задач". Кэшируйте результаты поиска (Redis) для повторяющихся запросов. Мониторьте latency каждого узла (Prometheus + Grafana).

Что делать, если LLM возвращает некорректные JSON?

Используйте with_structured_output в LangChain, который гарантирует валидный JSON по схеме Pydantic. Добавьте retry с запасом токенов. Если после 3 попыток ответ некорректен, переходите в human-in-the-loop. Логируйте промпты для анализа "глюков" модели.