Головна / Статті

Вибір фреймворку агента за формою навантаження: шість осей та шлях прийняття рішень

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

Видавець: REACTAPP.TOP11 хв читання2439 слів

Джерела: Your AI Agent Framework Is Probably the Wrong One Here’s How to Actually Choose

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

Замість порівняння функцій цей підхід визначає шість осей, які впливають на придатність рішення, показує, як вони виглядають у коді, а потім створює шлях прийняття рішень. Одним із прикладів є агент для триажу попереджень в інфраструктурі – він простий, реалістичний та охоплює всі осі. Агенту потрібно вирішувати, чи є попередження справжньою проблемою, діяти шляхом надсилання повідомлення комусь чи автоматичного усунення проблеми, іноді чекати години на відповідь людини, і ніколи не надсилати повідомлення інженеру, який чергує, двічі о 4 ранку через подвійну спробу.

Перша ось: наскільки детермінованим має бути розгалуження?

Цю ось пропускають та розглядають пізніше. Чесним тестом є запитання: чи постраждає хтось, якщо однаковий вхідний даний 2% часу буде проходити іншим кодовим шляхом? Деякі розгалуження допускають варіації, наприклад, при виборі тону для підсумкового електронного листа.

# A branch where non-determinism is FINE — picking a tone for a summary email.
# If the agent occasionally phrases things slightly differently, nobody's paged.
def draft_summary_tone(context: dict) -> str:
    return llm_call(
        prompt=f"Summarize this incident in a {context['audience']}-appropriate tone.",
        temperature=0.7,  # variability here is a feature, not a bug
    )

Інші функції такими не є, наприклад, прийняття рішення про те, чи потрібно розбудити людину о 4 ранку чи автоматично вирішити проблему. Це рішення має бути частиною коду, а не запитом користувача.

# A branch where non-determinism is NOT fine — deciding whether to page a human
# at 4am versus auto-remediating. This must be code, not a prompt.
def route_alert(alert: dict) -> str:
    if alert["severity"] == "critical" and alert["service"] in PAGE_ALWAYS_SERVICES:
        return "page_oncall"
    if alert["auto_remediation_available"] and alert["confidence"] > 0.9:
        return "auto_remediate"
    if alert["severity"] == "critical":
        return "page_oncall"
    return "log_and_monitor"

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

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

Друга ось: як довго існує одиниця роботи?

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

# Short-lived: starts and finishes inside one HTTP request.
# This is the "no framework needed" zone — a framework here is pure overhead.
async def handle_summarize_request(request: SummarizeRequest) -> SummarizeResponse:
    text = await fetch_document(request.doc_id)
    summary = await llm_summarize(text, max_tokens=300)
    return SummarizeResponse(summary=summary)

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

# Long-lived: this alert might sit in "awaiting human ack" for six hours
# while the on-call engineer is asleep, then resume on a completely
# different process after a deploy rotated the pods underneath it.
class AlertTriageWorkflow:
    async def run(self, alert: dict) -> dict:
        decision = await self.classify_and_route(alert)
        if decision == "page_oncall":
            await self.page(alert)
            await self.wait_for_ack(timeout_hours=1)  # this line is the whole ballgame
        ...

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

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

Третій вимір: що, якщо крок виконується двічі?

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

# BEFORE — looks fine in a demo, is a live incident waiting to happen
async def auto_remediate(alert: dict):
    await restart_service(alert["service"])  # what if this activity gets retried?

Друга версія за своєю конструкцією є ідемпотентною.

# AFTER — idempotent by construction
async def auto_remediate(alert: dict, idempotency_key: str):
    if await remediation_ledger.already_applied(idempotency_key):
        logger.info("remediation already applied, skipping", key=idempotency_key)
        return await remediation_ledger.get_result(idempotency_key)
    result = await restart_service(alert["service"])
    await remediation_ledger.record(idempotency_key, result)
    return result

Ключ ідемпотентності має походити з чогось стабільного в даних бізнесу, наприклад з ідентифікатора сповіщення, і ніколи не з UUID, який генерується з кожної спроби, інакше автоматичні повторні спроби втрачають свою безпеку. Це стосується функції інструменту, а не будь-якої фреймворк-системи, проте це впливає на те, які фреймворки є прийнятними, адже деякі ускладнюють відстеження того, чи відбулася повторна спроба. Функція виконання на основі чату в AutoGen не надає жодних структурних сигналів про те, що виклик інструменту є повторною спробою, а не новим рішенням. З точки зору запису розмови агент просто вирішив знову викликати інструмент, і чи є це повторною спробою чи дублюванням дії, залишається незареєстрованим.

Коли побічні ефекти є дорогими, небезпечними або пов’язані з грошима чи змінами інфраструктури, системі потрібен фреймворк або самостійно створений шар під ним, який розглядає питання про те, чи вже відбувся побічний ефект, як проблему першого класу, а не щось, що виводиться з журналу чату.

Четвертий вимір: хто читає рішення пізніше та як?

У аналізі після події важливим є запис аудиту, незалежний від конкретної структури, яка керувала процесом.

# A framework-agnostic audit record — this is what actually matters
# in a postmortem, regardless of what orchestrated the steps.
@dataclass
class DecisionRecord:
    alert_id: str
    timestamp: float
    step: str
    reasoning: str        # what the LLM said, verbatim
    decision: str         # the structured outcome, not prose
    confidence: float | None
    human_override: bool

async def log_decision(record: DecisionRecord):
    await audit_store.insert(record)
    # Also emit as a structured log line — cheap insurance for when
    # the audit store itself is the thing that's down during an incident.
    logger.info("agent_decision", **asdict(record))

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

П’ятий вимір: що обмежує швидкість роботи команди?

Це рідко зустрічається у технічних таблицях порівняння, але часто вирішує справу. Стартап із чотирма співробітниками, який перевіряє, чи зможуть агенти взагалі виконати завдання, не повинен витрачати три тижні на вивчення обмежень детермінізму Temporal перед тим, як написати будь-яку бізнес-логіку.

# Week-one prototype: prove the concept fast, accept the debt knowingly.
from crewai import Agent, Task, Crew

triage_agent = Agent(role="Alert Triage", goal="Decide how to handle infra alerts")
crew = Crew(agents=[triage_agent], tasks=[Task(description="Triage: {alert}", agent=triage_agent)])
crew.kickoff(inputs={"alert": alert_payload})

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

Шостий вимір: яка затримка та бюджет на витрати за кожне рішення?

Кожне рішення щодо маршрутизації, яке передбачає використання LLM, множиться під час багатоагентних діалогів із кількома етапами, а при обсязі сповіщень у сотні на день з піками у тисячі під час інцидентів, рахунок швидко зростає.

# Expensive pattern: every routing decision is its own LLM call,
# multiplied across a multi-agent conversation with several turns.
# At alert volumes (hundreds/day, sometimes bursts of thousands during
# a real incident), this is a real line item, not a rounding error.
async def route_via_llm(alert: dict) -> str:
    return await llm_call(f"How should we handle this alert? {alert}")

# Cheaper, faster, and more auditable: cheap deterministic pre-filtering
# in code, LLM reserved for genuinely ambiguous cases.
async def route_alert_efficiently(alert: dict) -> str:
    if alert["service"] in KNOWN_NOISY_SERVICES and alert["severity"] == "low":
        return "log_and_monitor"          # zero LLM calls for the common case
    if alert["signature"] in KNOWN_REMEDIATION_PLAYBOOK:
        return "auto_remediate"           # deterministic lookup, zero LLM calls
    return await llm_call(f"Novel alert, needs judgment: {alert}")  # LLM only when genuinely needed

Це скоріше дисципліна архітектури, яка допомагає будь-якому з цих підходів, ніж вісь фреймворку. Фреймворки, які полегшують поєднання дешевого детерміністичного коду з дорогими викликами моделей, дозволяють заощадити реальні гроші при великих обсягах роботи. LangGraph це робить, так само як і двигун робочих процесів на кшталт Temporal, який обгортає виклик LLM у одну дію. Альтернативою є нативний підхід, при якому все направляється через ще один етап розмови, посередником у якому є інша модель — це стандартний режим AutoGen, коли параметр speaker_selection_method встановлений на auto.

Шлях прийняття рішення, а не дерево рішень

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

Is this unit of work stateless and finishes in seconds?
  └─ YES → skip the framework entirely. Plain functions + retries. Ship it.
  └─ NO, continue.

Does it need to survive process restarts / wait on humans for hours-to-days?
  └─ YES → you need durable execution (Temporal or equivalent) as the backbone,
           regardless of what else you pick for the reasoning layer.
  └─ NO, continue.

Are the important branches safety- or compliance-critical
(money, infra changes, irreversible external actions)?
  └─ YES → LangGraph-style explicit graphs, keep LLM scoped to narrow nodes.
  └─ NO, mostly exploratory/creative → CrewAI or AutoGen are legitimate defaults.

Is this still a prototype whose findings might get thrown away?
  └─ YES → optimize for speed of iteration over long-term correctness,
           but write down when you'll revisit that tradeoff.

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

@activity.defn
async def classify_alert_activity(alert: dict) -> dict:
    # LangGraph-style graph runs here — bounded reasoning, deterministic routing —
    # inside an activity Temporal will retry and time-box like any other side effect.
    result = alert_triage_graph.invoke({"alert": alert, "audit_log": []})
    return {"decision": result["decision"], "confidence": result["confidence"]}

@workflow.defn
class AlertTriageWorkflow:
    def __init__(self):
        self._acked = False

    @workflow.signal
    async def acknowledge(self):
        self._acked = True

    @workflow.run
    async def run(self, alert: dict) -> dict:
        classification = await workflow.execute_activity(
            classify_alert_activity, alert,
            start_to_close_timeout=timedelta(seconds=20),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )
        if classification["decision"] == "page_oncall":
            await workflow.execute_activity(page_oncall, alert, start_to_close_timeout=timedelta(seconds=10))
            await workflow.wait_condition(lambda: self._acked, timeout=timedelta(hours=1))
            if not self._acked:
                await workflow.execute_activity(escalate_to_secondary, alert, start_to_close_timeout=timedelta(seconds=10))
        elif classification["decision"] == "auto_remediate":
            await workflow.execute_activity(
                auto_remediate, alert, f"remediate-{alert['id']}",
                start_to_close_timeout=timedelta(minutes=2),
                retry_policy=workflow.RetryPolicy(maximum_attempts=2),
            )
        return {"alert_id": alert["id"], "decision": classification["decision"]}

Реальний вибір фреймворку виглядає ось так на практиці, і це не є простою однорядковою відповіддю. Це рішення щодо складання системи, а саме складання є ключовою навичкою.

Поширені помилки

  • Вибір за кількістю зірок у GitHub. Популярність вказує на підтримку спільноти та кількість існуючих онлайн-рішень. Вона нічого не говорить про те, чи відповідає основна модель виконання вимогам до надійності та детермінізму, необхідних для завдань.
  • Перетворення прототипного фреймворку на постійний. Прототип, який просто працював, поступово перетворюється на систему для продакшну, оскільки переписування механізмів оркестрації здається безперспективним з точки зору додавання нових функцій. Насправді це підвищує надійність, яка залишається непомітною, доки не зникне.
  • Початок із потужного двигуна робочих процесів. Існує також протилежна проблема: команда читає занадто багато матеріалів про стійке виконання завдань та впроваджує Temporal під ботом для відповідей на запитання, який не має шансів прослужити довше, ніж перезапуск процесу. Інструмент має відповідати обсягу роботи як у одному, так і в іншому напрямку.
  • Розглядання фреймворку агентів як повної відповіді. Він охоплює рівень міркувань та використання інструментів, але рідко — рівні стійкості, ідемпотентності та спостережуваності, саме тому згадані вище аспекти майже не зустрічаються у маркетингових текстах фреймворків.
  • Справжня відповідь

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

    Пов’язана література

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