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

Agent Frameworks Reason, Temporal Survives: Стійка реалізація для агентів

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

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

Джерела: Agentic AI Frameworks in 2026: The 3 Everyone’s Using and the 1 Nobody Talks About

Інженери, які розробляють агентські системи у 2026 році, обговорюють свої фреймворки, і майже всі згадують LangGraph, CrewAI, AutoGen або їхні поєднання. Жоден з цих інструментів не є поганим. Проблема полягає у тому, що вся дискусія відбувається на неправильному рівні архітектури.

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

Жоден із LangGraph, CrewAI чи AutoGen не обробляє цю сімку завдань повністю від початку до кінця, і це є частиною їхньої концепції, адже вони створені для координації викликів ШІ та використання інструментів. Системи продакшн-агентів справді зазнають невдач через відсутність надійної реалізації — це значно старіша та менш приваблива галузь, яка існує приблизно на десять років раніше, ніж з’явилось поняття „агент“. Temporal, система, яка є основою цієї галузі, майже не згадується у дискусіях про фреймворки агентів, проте вона тихо стала основою для все більшої кількості серйозних реалізацій агентів. У наступному описано, що добре роблять ці три популярні фреймворки, де кожна абстракція має свої обмеження, та чому непопулярний четвертий варіант заслуговує на увагу з боку бізнесу.

LangGraph

Внесок LangGraph полягає у тому, що агент стає експліцитною графікою типових станів, а не імпліцитною послідовністю запитів. Це має справжню цінність, оскільки потік керування стає піддаємим перевірці та тестуванню. Наприклад, процес триажу може виглядати ось так:

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END

class TriageState(TypedDict):
    ticket_id: str
    category: Literal["billing", "technical", "abuse", "unknown"]
    priority: int
    assigned_queue: str | None

def classify_node(state: TriageState) -> TriageState:
    category = llm_classify(state["ticket_id"])  # the only probabilistic step
    return {**state, "category": category}

def route_node(state: TriageState) -> str:
    # deterministic, testable, no LLM involved
    return {
        "billing": "billing_queue",
        "technical": "tech_queue",
        "abuse": "trust_and_safety_queue",
        "unknown": "human_review",
    }[state["category"]]

graph = StateGraph(TriageState)
graph.add_node("classify", classify_node)
graph.set_entry_point("classify")
graph.add_conditional_edges("classify", route_node)
app = graph.compile()

Це гарна ідея, добре сформульована. Проблеми виникають тоді, коли щось має існувати довше, ніж один виклик invoke() та його контрольний пункт. Уявіть координацію між сервісами, робочі процеси, що тривають кілька днів, або кроки виконання, над якими LangGraph не має контролю: завдання в Spark, черга схвалень, яка проходить через Slack та повертається через кілька днів, або сервіс, який інша команда розгортає за власним графіком. Контрольні пункти справді працюють, але вони охоплюють лише власну модель виконання LangGraph. Після передачі на інфраструктуру, яка не має зв’язку з LLM, стійкість даних стає чимось, що потрібно будувати вручну.

CrewAI

Багатоагентну команду можна оголосити за кілька рядків:

from crewai import Agent, Task, Crew

researcher = Agent(role="Research Analyst", goal="Find relevant market data", tools=[search_tool])
writer = Agent(role="Report Writer", goal="Turn research into a client-ready summary")

crew = Crew(agents=[researcher, writer], tasks=[
    Task(description="Research {company}'s Q2 earnings", agent=researcher),
    Task(description="Write a 3-paragraph summary for the client", agent=writer),
])
crew.kickoff(inputs={"company": "Acme Corp"})

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

AutoGen

Мінімальна конфігурація AutoGen поєднує асистента з проксі користувача:

import autogen

assistant = autogen.AssistantAgent(name="assistant", llm_config={"config_list": config_list})
user_proxy = autogen.UserProxyAgent(name="user_proxy", human_input_mode="NEVER")
user_proxy.initiate_chat(assistant, message="Summarize this quarter's incident reports.")

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

Спільна прогалина

Усі три рішення добре справляються з тим, що справді є новим: змушувати ШІ міркувати, використовувати інструменти та працювати з іншими агентами, керованими ШІ. Старіша, простіша проблема — забезпечення належного завершення багатоетапного процесу незалежно від того, що може зламатися, перезапуститися, вийти за час або відбутися під час його виконання — не є тим, що вони намагаються вирішити. Це не їхня провина. Очікувати цього від них — це категорична помилка.

Уявіть собі реальний процес роботи, а не демонстрацію: працівник, який узгоджує рахунки-фактури від постачальників. Він видобуває елементи списку з PDF-файлу, порівнює їх із замовленням на покупку в системі ERP, позначає розбіжності, що перевищують певний поріг, для подальшого перегляду людиною, а потім може чекати кілька днів на відповідь, перш ніж записати схвалений елемент у бухгалтерію. У разі невдачі кроку необхідно спробувати знову, не створюючи дублікатів записів. Перезапуск процесу посеред його виконання не повинен призводити до початку роботи з нуля чи до непомітного зникнення даних.

┌────────────┐     ┌───────────────┐     ┌─────────────────┐     ┌───────────────┐
│  Extract   │ --> │ Match against │ --> │  Human approval │ --> │  Post to ERP  │
│  line items│     │ purchase order│     │  (can take days)│     │  (idempotent) │
└────────────┘     └───────────────┘     └─────────────────┘     └───────────────┘
      ▲                                          │
      └───────────── retry on failure ───────────┘

Ніхто не розробляв LangGraph, CrewAI чи AutoGen з метою гарантувати, що ця діаграма залишиться функціональною після перезапуску контейнера на третій день, поки хтось ще оцінює пункт 14. Йдеться не про оркестрацію LLM, а про стабільну роботу систем, які ефективно це здійснюють — на чолі з Temporal — протягом багатьох років виконуючи непримітну роботу на бекенді: обробку платежів, виконання замовлень та мікросервіси за схемою saga, задовго до того, як термін «агент» почав використовуватися для опису таких програм.

Як працює Temporal

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

Агент для узгодження рахунків нижче розглядає виклик LLM як одну з кількох активностей. Спочатку йдуть активності, які взаємодіють із зовнішнім світом:

# activities.py — the parts that touch the outside world (LLMs, APIs, DBs)
from temporalio import activity
from dataclasses import dataclass

@dataclass
class Invoice:
    invoice_id: str
    vendor: str
    line_items: list[dict]

@activity.defn
async def extract_line_items(pdf_url: str) -> list[dict]:
    # calls a vision-capable LLM, or a dedicated OCR model — doesn't matter to the workflow
    text = await download_and_ocr(pdf_url)
    return await llm_extract_structured(text, schema=LINE_ITEM_SCHEMA)

@activity.defn
async def match_purchase_order(invoice: Invoice) -> dict:
    po = await erp_client.get_purchase_order(invoice.vendor)
    discrepancies = diff_line_items(invoice.line_items, po["line_items"])
    return {"po_id": po["id"], "discrepancies": discrepancies}
@activity.defn
async def post_to_erp(invoice_id: str, po_id: str) -> dict:
    # idempotency is enforced with a deterministic key derived from workflow inputs,
    # not a random UUID generated per attempt — this is the same lesson as always
    return await erp_client.post_journal_entry(
        idempotency_key=f"invoice-post-{invoice_id}",
        invoice_id=invoice_id,
        po_id=po_id,
    )

@activity.defn
async def notify_human_reviewer(invoice_id: str, discrepancies: list[dict]) -> None:
    await slack_client.post_message(
        channel="#ap-review",
        text=f"Invoice {invoice_id} has {len(discrepancies)} discrepancies needing review.",
    )

Далі йде робочий процес, який бере на себе функцію оркестрації, яку зазвичай виконували б LangGraph чи CrewAI, проте його стійкість є вбудованою та не залежить від використання чекпоїнта:

# workflows.py
from temporalio import workflow
from datetime import timedelta

@workflow.defn
class InvoiceReconciliationWorkflow:
    def __init__(self) -> None:
        self._approval_received = False
        self._approved = False

    @workflow.signal
    async def submit_human_decision(self, approved: bool) -> None:
        # A signal is how the outside world talks to a running, possibly-days-old workflow
        self._approval_received = True
        self._approved = approved

    @workflow.run
    async def run(self, pdf_url: str, invoice_id: str, vendor: str) -> dict:
        line_items = await workflow.execute_activity(
            extract_line_items, pdf_url,
            start_to_close_timeout=timedelta(minutes=2),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )

        invoice = Invoice(invoice_id=invoice_id, vendor=vendor, line_items=line_items)
        match_result = await workflow.execute_activity(
            match_purchase_order, invoice,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=5),
        )

        if match_result["discrepancies"]:
            await workflow.execute_activity(
                notify_human_reviewer, invoice_id, match_result["discrepancies"],
                start_to_close_timeout=timedelta(seconds=10),
            )
            # This is the line that would be genuinely hard to write safely in the
            # other three frameworks: wait, durably, for up to 7 days, for a signal
            # that might arrive from a Slack button, a web form, anything —
            # while the workflow's entire state sits safely in Temporal's history,
            # costing zero compute while it waits.
            await workflow.wait_condition(
                lambda: self._approval_received,
                timeout=timedelta(days=7),
            )
            if not self._approved:
                return {"status": "rejected", "invoice_id": invoice_id}

        result = await workflow.execute_activity(
            post_to_erp, invoice_id, match_result["po_id"],
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=5),
        )
        return {"status": "posted", "invoice_id": invoice_id, "erp_result": result}

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

Сімденний час очікування є безкоштовним та переживає все. workflow.wait_condition не утримує жодної нитки, процесу чи контейнера відкритим протягом тижня. Temporal зберігає стан робочого процесу у власному надійному сховищі, тож працівник може бути перерозгорнутий, зменшений до нуля або повністю зламаний протягом цього часу. Коли сигнал нарешті надходить, Temporal відтворює історію та продовжує виконання саме на цьому місці await. Робити те саме з контролером LangGraph чи командою CrewAI фактично означало б перебудувати модель походження подій Temporal.

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

# BEFORE — naive retry loop, the kind you write at 11pm under deadline pressure
async def reconcile_invoice_naive(pdf_url, invoice_id, vendor):
    for attempt in range(3):
        try:
            line_items = await extract_line_items(pdf_url)
            match_result = await match_purchase_order(Invoice(invoice_id, vendor, line_items))
            if match_result["discrepancies"]:
                await notify_human_reviewer(invoice_id, match_result["discrepancies"])
                approved = await wait_for_approval_somehow(invoice_id)  # good luck
                if not approved:
                    return {"status": "rejected"}
            return await post_to_erp(invoice_id, match_result["po_id"])
        except Exception:
            continue  # retries EVERYTHING from the top, including re-extracting the PDF

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

Простий Python означає можливість тестування. Temporal містить фреймворк тестів, який може пропустити чекання timedelta(days=7) у мілісекундах всередині тесту одиниці, а також дозволяє імітувати дії без звернення до мережі:

import pytest
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker

@pytest.mark.asyncio
async def test_rejected_invoice_does_not_post_to_erp():
    async with await WorkflowEnvironment.start_time_skipping() as env:
        async with Worker(
            env.client, task_queue="test-queue",
            workflows=[InvoiceReconciliationWorkflow],
            activities=[extract_line_items, match_purchase_order, notify_human_reviewer, post_to_erp],
        ):
            handle = await env.client.start_workflow(
                InvoiceReconciliationWorkflow.run,
                args=["fake.pdf", "INV-001", "Acme"],
                id="test-invoice-001",
                task_queue="test-queue",
            )
            await handle.signal(InvoiceReconciliationWorkflow.submit_human_decision, False)
            result = await handle.result()
            assert result["status"] == "rejected"

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

Використання обох шарів

Справедлива відповідь полягає не у відмові від LangGraph на користь Temporal. Вони працюють на різних шарах, і найкращі продакшн-налаштування 2026 року поєднують їх: LangGraph або CrewAI для обґрунтування в межах одного кроку, Temporal для шару робочого процесу, який об’єднує багатоденні, багатосервісні операції. Одна дія може охоплювати один обмежений крок обґрунтування ось так:

@activity.defn
async def run_agent_reasoning_step(ticket_text: str) -> dict:
    # LangGraph handles the bounded, single-turn reasoning —
    # classify, decide, draft — inside an activity that Temporal
    # retries and times out like any other side effect.
    result = triage_graph.invoke({"ticket_id": ticket_text, "audit_log": []})
    return {"category": result["category"], "priority": result["priority"]}

@workflow.defn
class SupportTicketWorkflow:
    @workflow.run
    async def run(self, ticket_id: str, ticket_text: str) -> dict:
        triage = await workflow.execute_activity(
            run_agent_reasoning_step, ticket_text,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )
        if triage["priority"] >= 8:
            await workflow.execute_activity(page_oncall_engineer, ticket_id, ...)
            await workflow.wait_condition(lambda: self._acknowledged, timeout=timedelta(minutes=15))
        # ... routing, SLA timers, escalation chains — all durable, all testable

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

Де часові витрати є занадто високими

Temporal має недоліки, які роблять його поганою стандартною альтернативою для багатьох завдань агентів. Це ще одна дистрибуйована система, яку потрібно керувати — чи то самостійний сервер, чи оплата послуг Temporal Cloud, плюс працівники та черги завдань; це значно більше інфраструктури, ніж процес на Python, який використовує LangGraph. Код робочого процесу також підкоряється суворим правилам детермінізму. Пряме викликання datetime.now(), використання неконтрольованої випадковості чи виконання операцій введення-виведення безпосередньо у функції робочого процесу — все це заборонено. Командам потрібен час, щоб звикнути до цього, а порушення правил призводить до заплутаних помилок недетермінізму під час повторного виконання. Для короткого завдання з одним процесом, наприклад для відповіді на запитання чи узагальнення документа за один виклик LLM, стійке виконання є просто зайвим, оскільки не існує багатоетапного процесу, який міг би постраждати від збою.

Правило прийняття рішень

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

Багато дискусій на тему „Який фреймворк агентів мені використовувати“ ігнорують більш важливе питання: який шар системи насправді потребує посилення безпеки? LangGraph, CrewAI та AutoGen ведуть корисні дебати щодо того, як структурувати міркування ШІ та координацію кількох агентів. Проте питання про те, чи зможе агентська система вижити після невдалої реалізації, розриву мережі чи ситуації, коли людині потрібно три дні, щоб натиснути „схвалити“, — це окреме питання, і двигуни робочих процесів фактично вже вирішили його, забезпечуючи надійну роботу ще до того, як хтось заговорив про агентське ШІ. Популярні фреймворки налаштовують те, що видно під час демонстрації. Той, який ігнорують, вирішує, чи щось все ще буде функціонувати через місяць.