Главная / Статьи / Агенты возврата средств: LangGraph выжил там, где CrewAI и AutoGen потерпели неудачу

Агенты возврата средств: LangGraph выжил там, где CrewAI и AutoGen потерпели неудачу

Одни и те же инструменты и политики во всех трех фреймворках — только явное указание состояния, идемпотентность и контрольные точки выжили после уничтожения хаосом.

2754 слов

Нагрузка, разрушающая агентов-демо

Фреймворки с несколькими агентами выглядят идеально на демо-версиях для исследований и блогов. Не имеет значения, если инструмент запустится дважды. Однако агент, обрабатывающий возвраты средств, менее терпим к таким ситуациям. Получив сообщение от клиента, он должен определить его цель, загрузить информацию о заказе, проанализировать правила (сроки, категория, предыдущие возвраты), при наличии условий вызвать платежный шлюз не более одного раза, в противном случае передать дело на рассмотрение вышестоящих служб с письменным обоснованием и оставить запись для аудита.

Такая структура характерна для производственных систем: принятие решений, взаимодействие с состоянийными системами и ответственность за действия. Это требует сохранения состояния на протяжении всех этапов, детерминированного выбора между возвратом средств и передачей дела на рассмотрение, идемпотентных побочных эффектов, а также возможности для перерыва со стороны человека, который сохраняется после перезапуска процесса — а не просто временного ожидания.

Три реализации использовали одинаковые инструменты, модель и логику правил. Только одна из них выдержала тесты на хаос, при которых процесс прерывался во время выполнения.

Общие инструменты

Соблюдайте честные условия соревнования: одинаковые функции инструментов, включая шаблонный шлюз и ключ идемпотентности, связанный с идентификатором заказа.

# tools.py — identical across all three implementations
import time
import uuid
from dataclasses import dataclass
from typing import Literal

class PaymentGatewayError(Exception):
    pass

@dataclass
class Order:
    order_id: str
    customer_id: str
    item_category: str
    amount_cents: int
    purchased_at: float
    refund_count: int

# Fake DB — in prod this is Postgres behind a repository class
_ORDERS = {
    "ORD-4471": Order("ORD-4471", "CUST-991", "electronics", 8999, time.time() - 86400 * 5, 0),
    "ORD-2210": Order("ORD-2210", "CUST-102", "electronics", 4200, time.time() - 86400 * 45, 1),
}

_PROCESSED_REFUNDS: set[str] = set()  # idempotency ledger

def get_order(order_id: str) -> Order | None:
    return _ORDERS.get(order_id)

def check_refund_policy(order: Order) -> tuple[bool, str]:
    days_since_purchase = (time.time() - order.purchased_at) / 86400
    if days_since_purchase > 30:
        return False, f"Purchase was {days_since_purchase:.0f} days ago, outside the 30-day window."
    if order.refund_count >= 1:
        return False, "Customer has already received a refund on this order."
    return True, "Eligible: within window, no prior refund."

def issue_refund(order_id: str, idempotency_key: str) -> dict:
    """Calls the payment gateway. MUST be idempotent — retries are expected."""
    if idempotency_key in _PROCESSED_REFUNDS:
        return {"status": "already_processed", "idempotency_key": idempotency_key}
    order = _ORDERS[order_id]
    # simulate a flaky gateway — this matters later
    if uuid.uuid4().int % 5 == 0:
        raise PaymentGatewayError("gateway timeout, retry with same idempotency_key")
    _PROCESSED_REFUNDS.add(idempotency_key)
    return {"status": "refunded", "amount_cents": order.amount_cents, "idempotency_key": idempotency_key}

Идемпотентность — это не декоративный элемент; именно она определяет разницу между рабочей платформой для агентов и простым инструментом для них.

CrewAI: мощные демонстрации, слабый контроль

Модели CrewAI определяют роли и задачи в рамках структуры Crew. Презентации продуктов любят метафору организационной структуры.

Наивный подход

from crewai import Agent, Task, Crew, Process
from crewai.tools import tool

@tool("Get Order")
def get_order_tool(order_id: str) -> str:
    """Fetch order details by ID."""
    order = get_order(order_id)
    return str(order) if order else "NOT_FOUND"

@tool("Check Policy")
def check_policy_tool(order_id: str) -> str:
    """Check refund eligibility for an order."""
    order = get_order(order_id)
    if not order:
        return "NOT_FOUND"
    eligible, reason = check_refund_policy(order)
    return f"eligible={eligible}, reason={reason}"

@tool("Issue Refund")
def issue_refund_tool(order_id: str) -> str:
    """Issue a refund for an order."""
    result = issue_refund(order_id, idempotency_key=f"refund-{order_id}")
    return str(result)

triage_agent = Agent(
    role="Refund Triage Specialist",
    goal="Decide whether a customer refund request should be approved or escalated",
    backstory="You are an experienced support agent who follows policy strictly.",
    tools=[get_order_tool, check_policy_tool, issue_refund_tool],
    verbose=True,
)

triage_task = Task(
    description="A customer says: '{customer_message}'. Order ID: {order_id}. "
                "Decide if this qualifies for a refund and act accordingly.",
    expected_output="A short summary of the action taken.",
    agent=triage_agent,
)

crew = Crew(agents=[triage_agent], tasks=[triage_task], process=Process.sequential)
result = crew.kickoff(inputs={"customer_message": "I want a refund, item broke", "order_id": "ORD-4471"})

Все выглядит нормально с точки зрения успешного выполнения. Однако сервис трижды сбойдал. Порядок вызова инструментов был недетерминированным — иногда метод issue_refund выполнялся раньше, чем check_policy, поскольку модель свободно ассоциирует инструменты между собой. Повторные попытки после возникновения ошибки PaymentGatewayError могли приводить к вызову новых инструментов и использованию новых ключей, если только сам инструмент не обеспечивал идемпотентность через жестко заданные параметры. Метод kickoff() выполнялся до конца без каких-либо промежуточных остановок со стороны человека; подделка продолжения работы требовала воссоздания состояния вне рамок фреймворка.

Усиленная иерархическая структура попыток

manager_agent = Agent(
    role="Refund Process Manager",
    goal="Enforce strict order: lookup, then policy check, then refund or escalate. Never skip steps.",
    backstory="You strictly enforce process compliance and never let steps be skipped.",
    allow_delegation=True,
)

crew = Crew(
    agents=[triage_agent],
    tasks=[triage_task],
    process=Process.hierarchical,
    manager_agent=manager_agent,
)

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

AutoGen: гибкий чат, нечеткое управление

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

import autogen

config_list = [{"model": "gpt-4o", "api_key": "..."}]

llm_config = {"config_list": config_list, "temperature": 0}

triage_agent = autogen.AssistantAgent(
    name="TriageAgent",
    system_message=(
        "You triage refund requests. Look up the order, check policy, "
        "then either call issue_refund or hand off to EscalationAgent."
    ),
    llm_config=llm_config,
)

escalation_agent = autogen.AssistantAgent(
    name="EscalationAgent",
    system_message="You write a human-readable escalation note explaining why a refund needs manual review.",
    llm_config=llm_config,
)

user_proxy = autogen.UserProxyAgent(
    name="ToolExecutor",
    human_input_mode="NEVER",
    code_execution_config=False,
    function_map={
        "get_order": lambda order_id: str(get_order(order_id)),
        "check_refund_policy_tool": lambda order_id: str(check_refund_policy(get_order(order_id))),
        "issue_refund": lambda order_id: str(issue_refund(order_id, f"refund-{order_id}")),
    },
)

groupchat = autogen.GroupChat(
    agents=[user_proxy, triage_agent, escalation_agent],
    messages=[],
    max_round=10,
    speaker_selection_method="auto",  # an LLM call decides who speaks next
)
manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=llm_config)

user_proxy.initiate_chat(manager, message="Customer wants a refund on ORD-2210, item broke on arrival.")

Проблемы: циклические диалоги между этапами обработки запросов и усиления реакции без структурированного состояния «решено» — только транскрипции, что вынуждало использовать жесткие ограничения по количеству раундов через параметр max_round. Вопросы вроде «Мы вернули деньги?» решались с помощью поиска в свободном тексте. Недетерминизм влиял на всю структуру выполнения, поэтому инциденты восстанавливались из логов чата, а не из записей ввода.

LangGraph: тот, кто выжил

Структурированное состояние, явные связи между элементами, инструменты для сохранения состояния и возможность повторных попыток соответствовали характеру проблемы.

from typing import TypedDict, Literal, Optional
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import interrupt, Command
import uuid

class RefundState(TypedDict):
    order_id: str
    customer_message: str
    order: Optional[dict]
    eligible: Optional[bool]
    policy_reason: Optional[str]
    decision: Optional[Literal["refund", "escalate", "denied"]]
    refund_result: Optional[dict]
    audit_log: list[str]

def lookup_order_node(state: RefundState) -> RefundState:
    order = get_order(state["order_id"])
    log = state["audit_log"] + [f"Looked up {state['order_id']}: {'found' if order else 'not found'}"]
    if not order:
        return {**state, "decision": "escalate", "audit_log": log}
    return {**state, "order": order.__dict__, "audit_log": log}

def policy_check_node(state: RefundState) -> RefundState:
    order = Order(**state["order"])
    eligible, reason = check_refund_policy(order)
    log = state["audit_log"] + [f"Policy check: eligible={eligible}, reason={reason}"]
    return {**state, "eligible": eligible, "policy_reason": reason, "audit_log": log}

def route_after_policy(state: RefundState) -> str:
    # Plain Python. No LLM call decides this branch. This is the whole point.
    if state.get("decision") == "escalate":
        return "escalate"
    return "refund" if state["eligible"] else "escalate"

def human_approval_node(state: RefundState) -> RefundState:
    # Durable pause: this literally suspends the graph run and persists state
    # via the checkpointer. It can resume hours or days later, across restarts.
    decision = interrupt({
        "reason": "Ambiguous or ineligible refund needs human sign-off",
        "order": state["order"],
        "policy_reason": state["policy_reason"],
    })
    return {**state, "decision": decision, "audit_log": state["audit_log"] + [f"Human decision: {decision}"]}

def issue_refund_node(state: RefundState) -> RefundState:
    idempotency_key = f"refund-{state['order_id']}"  # stable across retries — this is the whole trick
    try:
        result = issue_refund(state["order_id"], idempotency_key)
    except PaymentGatewayError as e:
        # LangGraph re-raises into the node; retry policy (below) handles this,
        # and because the key is stable, a retried call is safe.
        raise
    log = state["audit_log"] + [f"Refund issued: {result}"]
    return {**state, "decision": "refund", "refund_result": result, "audit_log": log}

def escalate_node(state: RefundState) -> RefundState:
    log = state["audit_log"] + ["Escalated to human queue"]
    return {**state, "audit_log": log}
from langgraph.pregel.retry import RetryPolicy

graph = StateGraph(RefundState)

graph.add_node("lookup_order", lookup_order_node)
graph.add_node("policy_check", policy_check_node)
graph.add_node(
    "issue_refund",
    issue_refund_node,
    retry=RetryPolicy(max_attempts=3, retry_on=PaymentGatewayError),
)
graph.add_node("human_approval", human_approval_node)
graph.add_node("escalate", escalate_node)

graph.set_entry_point("lookup_order")
graph.add_edge("lookup_order", "policy_check")
graph.add_conditional_edges("policy_check", route_after_policy, {
    "refund": "issue_refund",
    "escalate": "human_approval",
})
graph.add_edge("human_approval", "issue_refund")  # human can still approve
graph.add_edge("issue_refund", END)
graph.add_edge("escalate", END)

checkpointer = SqliteSaver.from_conn_string("refunds.db")
app = graph.compile(checkpointer=checkpointer)

Конфигурации тредов возобновляются после сбоев:

config = {"configurable": {"thread_id": "order-2210-refund-req"}}

# Kick off the run — it will pause at human_approval_node
result = app.invoke(
    {"order_id": "ORD-2210", "customer_message": "second refund please", "audit_log": []},
    config=config,
)
# result contains an interrupt payload; the process can now exit entirely.

# ... hours later, possibly a different process, different machine ...
final_result = app.invoke(Command(resume="escalate"), config=config)

Детерминистичные тесты подтверждают, что неподходящие запросы должны переходить на более высокий уровень обработки:

def test_ineligible_order_escalates():
    state = {"eligible": False, "decision": None}
    assert route_after_policy(state) == "escalate"

def test_eligible_order_refunds():
    state = {"eligible": True, "decision": None}
    assert route_after_policy(state) == "refund"

Механизм преодоления хаоса позволял останавливать процесс во время возврата средств и возобновлять его с использованием того же ключа идемпотентности, что было сохранено в точке контроля. Именно этот тест определил победителя соревнования.

Когда CrewAI или AutoGen всё равно побеждают

CrewAI подходит для совместной разработки с гибкой организацией работы. AutoGen — для исследовательской деятельности с участием нескольких агентов, где результатом является диалог. Ни один из них не заменяет машину состояний, когда речь идёт о финансовых операциях.

Урок

Соотносите уровень абстракции с возможными сценариями сбоев. Если неправильный порядок использования инструментов или двойные побочные эффекты неприемлемы, предпочтите явные графы с устойчивым состоянием вместо структур, основанных на промптах. Фреймворки — это не просто заменяемые оболочки для «агентов»; они отражают разные подходы к управлению, хранению информации и восстановлению работоспособности.

Чек-лист для производства после соревнования

Прежде чем внедрять любой агент, имитирующий возврат средств, необходимо: указание статуса с флагом already_refunded (или эквивалентным); ключи идемпотентности, сгенерированные на основе бизнес-идентификаторов; проверки политик в виде кодовых узлов, а не предложений из интерфейса; прерывания типа HITL, размещаемые после точки контроля; тесты на хаос, приводящие к остановке процессов во время выполнения побочных эффектов; журналы аудита, не зависящие от поиска текста. Если фреймворк не может реализовать эти требования без дополнительного движка рабочих процессов, то на самом деле именно этот движок является основным оркестратором, а фреймворк — лишь дорогостоящим связующим элементом.

Измеряйте показатели сбоев на этапе тестирования: пропуск проверок политик, многократные попытки возврата средств, потеря прерываний после перезагрузки и неразборчивые записи в журналах аудита. Прототипы Crew и AutoGen, которые не могут превзойти LangGraph по этим показателям, должны оставаться в лаборатории. Радуйтесь их устареванию — чрезмерное расширение приводит к значительным затратам.

Задокументируйте это решение для будущих команд, чтобы следующий конкурс стал ненужным. Приложите ссылку на документ с описанием процедур в файле README. Лучше использовать скучные графики, которые остаются актуальными после любых изменений, чем умные комментарии, пригодные лишь для демонстраций. Этот стандарт применим не только к возвратам средств, но и ко всем агентам, которые изменяют внешние системы под давлением аудита — что и является основной целью большинства компаний от «ИИ-агентов» после того, как временные решения исчезают, а учетная отчетность снова начинает играть решающую роль каждый квартал.

Сопоставление сбоев с предположениями фреймворка

CrewAI предполагает гибкое разделение задач. AutoGen считает, что сам диалог является достаточной основой для управления. LangGraph предполагает, что вы сами будете создавать структуру управления. Автоматизация возвратов средств нарушает первые два предположения: критерии соответствия не подлежат обсуждению, а выбор собеседника не является механизмом авторизации платежа. Когда предположения противоречат законам данной области, побеждает та система, которая содержит меньше предположений — даже если в первую неделю она кажется менее эффективной.

Инженеры иногда пытаются «исправить» CrewAI или AutoGen с помощью всё более длинных системных команд. Это всё равно что использовать забор против циклона в качестве двери сейфа. Необходимо переместить требования к соответствию правилам в отдельные узлы и оставить языковые модели внутри узлов, отвечающих за классификацию или составление текста, а не в узлах, принимающих решения о переводе средств.

Требования к общей возможности наблюдения

Какой бы вариант вы ни выбрали, необходимо генерировать данные для каждого вызова инструмента с идентификатором порядка, ключом идемпотентности и результатом применения правил. Без этого дискуссии в рамках фреймворков превращаются в споры, а производственные процессы остаются без контроля. LangGraph сделал эти механизмы очевидными, поскольку узлы представляют собой функции; такую же дисциплину можно применить и в других местах, но тестирование показало, что это наименее сложный подход для данной нагрузки.

Создавайте агента, соответствующего способу сбоев вашей системы. Для обработки возвратов таким агентом является граф с механизмами хранения данных, а не команда людей. Используйте другие инструменты только для решения тех проблем, к которым они действительно подходят, и перестаньте притворяться, что одна абстракция может решить все задачи в списке ожидания.

Успешная структура графа сохраняла категории классификации, получения данных, политик, возврата средств, перевода проблемы на более высокий уровень и аудита в виде отдельных узлов. Ребра обозначали единственно допустимые юридические переходы. Модель никогда не решала, следует ли пропустить этап применения политики; она лишь заполняла структурированные поля, которые интерпретировал код политики. Повторные попытки в узле шлюза использовали тот же ключ идемпотентности, хранящийся в состоянии. Для перевода проблемы на более высокий уровень применялась функция interrupt() с механизмом контроля, что позволяло менеджеру одобрить действия спустя несколько часов на другой копии системы.

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

Сравнение процедур реагирования на инциденты

Когда CrewAI дважды запускал инструмент на этапе тестирования, в отчете о причинах ошибки указывалось на «смещение запроса». Когда AutoGen заходил в бесконечный цикл, причиной называли «выбор говорящего агента». При сбое LangGraph в отчете указывали на конкретный узел и отсутствие функции сокращения данных — проблему можно было решить без лишних дискуссий. Уже сама классификация инцидентов оправдывала выбор рабочего процесса, связанного с обработкой платежей.

Замечания о затратах и задержках

Использование LLM для говорящего агента в AutoGen увеличивало задержки и количество токенов. Повторные попытки CrewAI иногда приводили к множественным вызовам инструментов. LangGraph требовал небольших постоянных затрат на сохранение состояний, но обеспечивал предсказуемость расходов. При объеме поддержки в тысячи запросов в день предсказуемость важнее эпизодических экономий.

Навыки команды и найм

Количество вакансий с требованием опыта работы с CrewAI меньше, чем вакансий, требующих знаний состояний машин и больших языков моделей. Навыки работы с LangGraph можно применить в любом инструменте оркестрации. Если компания стандартизирует процессы, лучше использовать универсальные концепции: состояния, идемпотентность, HITL, оценки. Циклы развития фреймворков происходят быстрее, чем эти концепции.

Расширение набора инструментов для управления хаосом

Помимо простого перезапуска: необходимо учитывать ситуации с кодом ошибки 503 на шлюзах, дублирование доставок webhook-сообщений, отклонения времени в рамках политик обработки и сотрудников, отклоняющих запросы на уровень выше. Графы, функционирующие только в идеальных условиях, по-прежнему остаются учебными инструментами. Необходимо автоматизировать этот набор инструментов в процессах CI с использованием детерминированных фиктивных данных, чтобы при рефакторинге не удалялись механизмы обеспечения безопасности.

Безопасная замена прототипов

Используйте песочницы CrewAI/AutoGen для рабочих процессов с контентом при участии человеческих редакторов. Не блокируйте возможность экспериментирования — блокируйте лишь учетные данные для производственного использования. Политика платформы, запрещающая использование инструментов оплаты в сценариях, организованных с помощью промптов, предотвращает новые почти инциденты, не подавляя любознательность.

Заключительный акцент

Утверждение статьи конкретно и убедительно: при автоматизации возвратов с реальными побочными эффектами LangGraph справился там, где CrewAI и AutoGen не смогли это сделать, при использовании одинаковых инструментов. Будьте осторожны при обобщениях. Однако можно свободно использовать мета-урок — оценивайте фреймворки с точки зрения ваших способов сбоев, а не с точки зрения внешнего вида демо-версий, — и тогда эта работа оправдает потраченную на нее неделю.

Подробная хронология сбоев на стадии тестирования

Первая неделя: демонстрация CrewAI произвела впечатление на заинтересованные стороны. Вторая неделя: две попытки двойного возврата средств на этапе тестирования из-за сбоев гейтвэя. Третья неделя: пилотный проект AutoGen тратил токены в циклах воспроизведения звука при отклоненных заказах. Четвертая неделя: механизм управления хаосом в LangGraph активировал процедуру прерывания возврата средств на середине процесса. Календарный план важен, поскольку организационная инициатива часто застывает после первой демонстрации; необходимо записать хронологию сбоев в документ ADR, чтобы инициатива не могла стереть доказательства.

Идемпотентность как межсферное требование

Каждая из рассмотренных здесь фреймворков может вызывать соответствующие инструменты. Только те решения, которые используют стабильный ключ при повторных попытках, безопасны при обработке платежей. Храните ключ в состоянии графа до первой попытки. Откажитесь от создания нового ключа на узлах повторных попыток. Записывайте ключ, идентификатор заказа и коды ответов гейтвэя вместе. Если фреймворк затрудняет это, такая сложность является сигналом проблемы, а не вопросом оформления документов.

Эргономика передачи задач человеку

Текст для эскалации должен включать идентификаторы пунктов политики, временные метки заказа и количество предыдущих возвратов — структурированную информацию, а не просто свободный текст. Руководители должны видеть те же поля, что и узел политики. LangGraph облегчил это, поскольку состояние представляет собой TypedDict; Crew/AutoGen требовали воссоздания фактов из транскрипций, что и приводит к пробелам в аудите.

Что мы сохранили из менее удачных решений

Метафоры ролей в CrewAI способствовали эффективным продуктовым обсуждениям — их было преобразовано в названия узлов LangGraph. Четкий список агентов в AutoGen способствовал более ясному определению ответственности за инструменты в каждом узле. Разрешается заимствовать идеи UX при одновременном отклонении небезопасных механизмов управления.

Расширенный чек-лист для производства

  • Хаос: сбои во время использования инструмента, во время ожидания прерывания, во время попыток повтора.
  • Тесты свойств: неквалифицированные пользователи никогда не достигают узла возврата средств.
  • Загрузка: одновременная обработка 100 идентичных ID заказов; проверка успешности работы через единственный шлюз.
  • Безопасность: учетные данные платежных инструментов хранятся только на работниках узла возврата средств.
  • Наблюдаемость: панели управления для отслеживания частоты применения политики пропуска (должна быть равна нулю).
  • Управление: контроль изменений в коде узла политик должен соответствовать процедурам изменений в движке правил.
  • Почему подход «просто добавить ещё один агент» не сработал

    Добавление агента «PolicyEnforcer» в CrewAI всё равно оставляло процесс применения правил вероятностным. Добавление агента «RefundGuardian» в AutoGen продолжало функционировать в рамках чата. Агенты, не способные полностью заблокировать определённые пути, являются лишь декоративными элементами. Полная блокировка должна реализовываться на уровне топологии графа.

    Те же инструменты, та же модель, но разная философия управления. Только явные графы выдерживают сбои, имеющие значение для возврата средств. Используйте группы и чаты там, где ошибка в порядке действий стоит недорого; используйте графы там, где ошибка в порядке действий является событием в учетной книге. Опубликуйте это правило внутри организации, чтобы сэкономить следующей команде четыре недели на решение проблем.

    Дополнительные рекомендации по усилению безопасности графов для возврата средств

    Политики, зависящие от времени, должны использовать время сервера, сохраненное при получении данных, а не даты, указанные в модели. Проверки категорий следует реализовывать через проверку членства в коде. Количество предыдущих возвратов берется из учетной книги, а не из памяти чата. Каждый из этих выборов устраняет определенный тип попыток внедрения команд, направленных на изменение критериев подходящести с помощью естественного языка. Графы делают эти выборы очевидными; группы же прячут их в контекстной информации агентов, где рецензенты перестают их обращать внимание.