Архитектура Agentic AI в ERP: Реализация мультиагентной закупки на LangGraph
От теории к практической реализации интеллектуального модуля Procure-to-Pay с использованием паттерна Supervisor и параллельным выполнением агентов.
Ключевой вывод: Традиционные RAG-системы и RPA-роботы не способны на автономное принятие комплексных решений в закупках из-за отсутствия контекстуального понимания и итеративного планирования. Agentic AI с графом состояний решает эту проблему.
Проблема традиционной автоматизации
Современные ERP-системы (SAP, Oracle, 1С) используют RPA для автоматизации рутинных операций, но эти решения сталкиваются с фундаментальными ограничениями при обработке сложных закупочных сценариев:
- Отсутствие контекстуальной памяти: RPA-боты не хранят историю взаимодействий, что критично при переговорах с поставщиками.
- Жёсткие流程: Каждое отклонение от сценария (например, отсутствие товара) требует ручного вмешательства.
- Нет итеративного планирования: Системы не могут генерировать и оценивать альтернативные варианты (замена поставщика, изменение сроков).
- Изоляция данных: Информация о бюджете, контрактах и рисках хранится в разных модулях без единого контекста.
В результате время согласования сложной закупки занимает 2–5 рабочих дней, а не 5–10 минут, как в идеале. Agentic AI, построенный на графах состояний, решает эти проблемы через:
- Состояние (State): Единый контекст (запрос, бюджет, поставщики, решения) передаётся между агентами.
- Параллельное выполнение: Несколько агентов работают одновременно (например, Scout и Auditor).
- Итеративные циклы: Возможность возврата к предыдущим шагам при недостатке данных.
- 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:
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. Валидация перед записью
Никогда не доверяйте решениям агента без проверки. Используйте двухэтапную валидацию:
- Схемы данных (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:
| Поле | Пример значения |
|---|---|
timestamp | 2024-05-15T10:30:00Z |
agent | Auditor |
input_state | {"supplier_id": "SUP-123", ...} |
output_state | {"risk_score": 45, "reasons": ["стабильная финансовая отчётность"]} |
llm_prompt | Оцени риски поставщика SUP-123... |
llm_response | {"score": 45, "confidence": 0.87} |
tokens_used | 142 |
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. Это запускает пересчёт графа:
- Orchestrator получает уведомление и меняет состояние:
"delivery_at_risk": true. - Orchestrator повторно вызывает Scout с запросом: "Найди поставщиков с доставкой за 24 часа в регион Москва".
- 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. Логируйте промпты для анализа "глюков" модели.