Головна / Статті / Агенти з повернення коштів: LangGraph вижив там, де зазнали невдачі CrewAI та AutoGen

Агенти з повернення коштів: LangGraph вижив там, де зазнали невдачі CrewAI та AutoGen

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

2754 слів

Обсяг роботи, який ламає демонстраційні агенти

Фреймворки з кількома агентами виглядають ідеально на демонстраціях у дослідженнях та блогах. Якщо інструмент виконується двічі, нічого серйозного не стається. Але агент, який обробляє повернення грошей, не настільки толерантний. Отримавши повідомлення від клієнта, він мусить класифікувати його мету, завантажити інформацію про замовлення, оцінити правила (терміни, категорія, попередні повернення), звернутися до платіжного шлюзу щонайбільше один раз, якщо це дозволено, у разі проблем — надіслати письмове пояснення, а також залишити запис для аудиту.

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

Три реалізації використовували однакові інструменти, модель та логіку правил. Лише одна витримала тести на хаос, які зупиняли процес під час виконання.

Спільні інструменти

Зберігайте чесну боротьбу: ідентичні функції інструментів, включаючи „шкірястий“ шлюз та ключ ідемпотентності, прив’язаний до ID замовлення.

# 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 (або еквівалентним); використовувати ключі ідемпотентності, створені на основі бізнес-ID; перевірки політик у вигляді кодових вузлів, а не пропозицій у форматі запитань; переривання типу HITL після виконання checkpointer; тести на хаос, які зупиняють виконання скриптів під час виникнення побічних ефектів; журнали аудиту, які не залежать від пошуку тексту за шаблонами. Якщо фреймворк не може реалізувати ці вимоги без додаткового двигуна роботи у тіні, то саме цей двигун є справжнім оркеструвальником, а фреймворк — лише дорогий з’єднувальний елемент.

Вимірюйте різні показники невдач на етапі тестування: пропуск перевірок політик, кілька спроб повернення коштів, втрачені переривання після перезапуску та нерозбірливі журнали аудиту. Прототипи Crew та AutoGen, які не можуть перевершити LangGraph за цими показниками, повинні залишатися в лабораторії. Святкуйте їх виведення з експлуатації — неконтрольоване розширення має свою ціну.

Запишіть рішення для майбутніх команд, щоб наступний конкурс був зайвим. Посилання на інструкції щодо управління хаосом розмістіть у файлі README. Віддавайте перевагу нудним графікам, які залишаються актуальними після випробувань, над кмітливими чатами, які існують лише під час демонстрацій. Цей стандарт поширюється не лише на повернення коштів, а й на будь-якого агента, який змінює зовнішні системи під тиском аудиту, що саме і хочуть більшість компаній від «AI-агентів», коли тимчасові рішення закінчуються, а бухгалтерський облік знову починає мати реальне значення щокварталу.

Пов’язування невдач із припущеннями фреймворку

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

Інженери іноді намагаються «виправити» CrewAI чи AutoGen за допомогою все довших системних запитів. Це все одно, що використовувати огорожу проти циклону як двері сейфа. Перенесіть вимоги до відповідності стандартам у типовані вузли та тримайте мовні моделі всередині вузлів, які класифікують чи розробляють текст, а не вузлів, які вирішують, чи буде здійснена оплата.

Вимоги до спільної наявності інформації

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

Заключні зауваження

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

Огляд структури LangGraph, яка виявилася ефективною

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

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

Порівняння процедур реагування на інциденти

Коли CrewAI двічі використав інструмент на етапі підготовки, у аналізі причин виникнення проблеми звинуватили «зміщення запиту». Коли AutoGen увійшов у цикл, причиною назвали «вибір спікера». Коли LangGraph не спрацював, у аналізі вказали на конкретний вузол та відсутність засобу для оптимізації — це можна було виправити без дискусій. Сама таксономія інцидентів обґрунтувала вибір робочого процесу, пов’язаного з оплатами.

Примітки щодо витрат та затримок з конкурсу

Система LLM для вибору спікера у AutoGen збільшувала затримки та кількість токенів. Повторні спроби CrewAI іноді призводили до багаторазових викликів інструментів. LangGraph мав невеликі постійні витрати на збереження контрольних точок, але перевершував інші за прогнозованістю витрат. Для обсягу підтримки у тисячах запитів на день прогнозованість важливіша за епізодичну економію.

Навички команди та найм

Кількість вакансій з вимогою досвіду роботи з “CrewAI” менша, ніж вакансій із вимогою знань “state machines plus LLMs”. Навички роботи з LangGraph можна застосувати в будь-якому іншому оркестраторі. Якщо компанія стандартизує процеси, краще використовувати універсальні концепції: стани, ідемпотентність, HITL, оцінки. Цикли розвитку фреймворків відбуваються швидше, ніж ці концепції.

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

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

М’яка посадка прототипів

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

Остаточний наголос

Твердження статті є конкретним та переконливим: у сфері автоматизації повернення грошей із реальними побічними ефектами LangGraph витримав там, де CrewAI та AutoGen не змогли цього зробити, попри ідентичні інструменти. Будьте обережні під час розширення висновків. Вільно застосовуйте метапідсумок — оцінюйте фреймворки за вашими сценаріями провалу, а не за естетикою демонстрацій — і тоді цей конкурс справді вартий того часу, який він зайняв.

Детальний часовий план провалів з етапу тестування

Перший тиждень: демонстрація CrewAI справила враження на зацікавлених сторін. Другий тиждень: дві спроби подвійного повернення грошей на стадії тестування через проблеми з гейтвеєм. Третій тиждень: пілотний проект AutoGen витрачав токени під час повторних спроб обробки замовлень. Четвертий тиждень: система FilangGraph пройшла тест на можливість припинення процесу повернення грошей на середині. Календарний план важливий, оскільки організаційний імпульс часто зупиняється після першої демонстрації; необхідно записувати хронологію невдач у документ ADR, щоб імпульс не міг стерти докази.

Ідемпотентність як універсальна вимога

Кожна з тут розглянутих фреймворків може викликати інструменти. Лише ті рішення, які використовують стабільний ключ під час повторних спроб, є безпечними під час оплат. Зберігайте ключ у стані графа перед першою спробою. Відмовляйтесь створювати новий ключ на вузлах повторних спроб. Записуйте разом ключ, ідентифікатор замовлення та коди відповідей гейтвею. Якщо фреймворк ускладнює це, така складність є сигналом проблеми, а не питанням документообігу.

Ергономіка підвищення рівня підтримки до людини

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

Що ми зберегли з невдалих рішень

Метафори ролей у CrewAI допомогли у продуктових діалогах — їх переклали на назви вузлів LangGraph. Чіткий список агентів у AutoGen сприяв більш чіткому визначенню власності на інструменти для кожного вузла. Дозволяється запозичувати ідеї UX, відкидаючи при цьому небезпечні механізми керування.

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

  • Хаос: зупинка під час використання інструменту, під час очікування переривання, під час повторної спроби.
  • Тести властивостей: некваліфіковані користувачі ніколи не доходять до вузла повернення грошей.
  • Завантаження: одночасне оброблення 100 ідентичних ID замовлень; перевірка успішності операції через єдиний шлюз.
  • Безпека: дані облікових записів платіжних інструментів зберігаються лише на вузлах, що обробляють повернення коштів.
  • Оперативний моніторинг: панелі керування для відстеження частоти застосування політики пропуску (цей показник має дорівнювати нулю).
  • Керування: контроль змін у коді вузла політики має відповідати процедурам змін у двигуні правил.
  • Чому підхід „просто додати ще одного агента“ зазнав невдачі

    Додавання агента „PolicyEnforcer“ у CrewAI все ще залишало процес забезпечення дотримання правил імовірнісним. Додавання агента „RefundGuardian“ у AutoGen все ще обмежувалося чатом. Агенти, які не можуть жорстко заблокувати певні шляхи, є лише декоративними елементами. Жорстке блокування має відбуватися на рівні топології графа.

    Заключні зауваження

    Ті самі інструменти, та сама модель, але різна філософія керування. Лише явний граф вижив після збоїв, які мають значення для повернення коштів. Використовуйте команди та чати, де неправильний порядок є дешевим рішенням; використовуйте графи, де неправильний порядок є подією в бухгалтерському обліку. Оприлюдніть це правило внутрішньо, щоб заощадити наступній команді чотири тижні на роботу.

    Додаткові поради щодо зміцнення графів для повернення коштів

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