Strona główna / Artykuły

Frameworki agentów rozumieją – Temporal przetrwa: trwała realizacja dla agentów

LangGraph, CrewAI i AutoGen koordynują wywołania modeli językowych, ale nie zapobiegają awariom ani tygodniowym oczekiwaniom. Jak trwała realizacja w Temporal wypełnia tę lukę i kiedy jest przesadą.

Wydawca: REACTAPP.TOP11 min czytania2460 słów

Źródła: Agentic AI Frameworks in 2026: The 3 Everyone’s Using and the 1 Nobody Talks About

Inżynierowie zajmujący się ankietami, którzy w 2026 roku budują systemy agentowe, mówią o swoich frameworkach – prawie wszyscy wymieniają LangGraph, CrewAI, AutoGen lub jakąś ich kombinację. Żaden z tych narzędzi nie jest zły. Problem polega na tym, że cała ta dyskusja toczy się na niewłaściwym poziomie całego stacku.

Systemy agentowe w środowisku produkcyjnym wykazują powtarzający się wzorzec. To, co budzi inżyniera dyżurnego o 3 rano, rzadko jest frameworkiem decydującym o tym, co myśli agent. Prawie zawsze chodzi o jedną grupę problemów: proces umiera w trakcie wykonywania zadania, jakiś krok jest wykonywany dwa razy, ponieważ jego próba ponownego wykonania nie była idempotentna, ewakuacja poda niszczy długo trwające zadanie agenta po 40 minutach bez możliwości jego wznowienia, albo stan procesu jest przechowywany w zmiennej Pythona, która znika po ponownym użyciu kontenera.

Żaden z systemów LangGraph, CrewAI ani AutoGen nie obsługuje tej całej ścieżki od początku do końca, i to jest zamierzone, ponieważ ich celem jest koordynowanie wywołań modeli językowych i używania narzędzi. Systemy agentów produkcyjnych naprawdę zawodzą z powodu braku trwałej realizacji zadań – to znacznie starsza i mniej rozwinięta dziedzina, która istniała już około dekady przed pojawieniem się terminu „agent”. Temporal, system, który ją tworzy, prawie w ogóle nie pojawia się w dyskusjach na temat frameworki agentów, a mimo to stał się cichym filarem coraz większej liczby poważnych zastosowań agentów. W dalszej części omówiono, w czym te trzy popularne frameworki radzą sobie dobrze, gdzie kończy się skuteczność poszczególnych abstrakcji oraz dlaczego ta mniej popularna czwarta opcja zasługuje na inwestycję biznesową.

LangGraph

Wkład LangGraph polega na przekształceniu agenta w wyraźny graf stanów typowanych, a nie w ukryty łańcuch poleceń. Ma to rzeczywistą wartość, ponieważ przepływ sterowania staje się sprawdzalny i testowalny. Przykładowy proces triażu może wyglądać w ten sposób:

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()

To dobry pomysł, dobrze napisany. Problemy pojawiają się wtedy, gdy coś musi funkcjonować dłużej niż jedna wywołanie invoke() wraz z jego punktem kontrolnym. Pomyśl o koordynacji między usługami, procesach trwających kilka dni lub krokach realizowanych tam, gdzie LangGraph nie ma wpływu: zadańie w Sparku, kolejce zatwierdzeń przechodzącej przez Slack i wracającej po kilku dniach, lub usłudze, którą inny zespół wdraża według własnego harmonogramu. Mechanizm punktów kontrolnych faktycznie działa, ale obejmuje jedynie własny model wykonywania zadań przez LangGraph. Po przekazaniu zadania infrastrukturze, która nie ma połączenia z modelami językowymi, trwałość funkcjonowania staje się czymś, co trzeba budować ręcznie.

CrewAI

Zespół składający się z wielu agentów można zadeklarować w kilku liniach:

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"})

Żaden narzędzie nie przekształca luźnej idei dotyczącej procesu pracy z wieloma agentami w działającą demonstrację w dwadzieścia minut szybciej niż CrewAI. Koszty pojawiają się później. Funkcja kickoff() blokuje działanie, nie oferuje możliwości pauzowania na godziny lub dni, brakuje mechanizmu ponawiania prób poza tym, co jest dostępne w poszczególnych narzędziach, a trwały stan istnieje tylko wtedy, gdy programista go zapisze. Narzędzie to koordynuje rozumowanie modeli językowych, ale nie jest silnikiem procesów pracy, i nigdy tak nie było reklamowane, chociaż wiele zespołów przypadkowo tak je traktuje.

AutoGen

Minimalna konfiguracja AutoGen łączy asystenta z proxy użytkownika:

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.")

Konwersacyjny, dynamiczny styl AutoGen nadaje się do pracy eksploracyjnej. Jego wersja z 2026 roku, wyposażona w bardziej ustrukturyzowane środowisko pracy oraz wzorce oparte na zdarzeniach, nawet dąży do oddzielenia procesu orkiestracji od samej konwersacji. Mimo to podstawą pozostaje transkrypcja wiadomości, a nie trwała, zapisana w formie tekstowej historia wykonywania zadań. Gdy proces obsługujący konwersację przestaje działać, pozostaje jedynie częściowy zapis rozmowy, a nie obliczenie, które mogłoby zostać wznowione.

Wspólna luka

Wszystkie trzy rozwiązania radzą sobie dobrze z tym, co jest rzeczywiście nowe: sprawianiem, by model językowy potrafił rozumować, korzystać z narzędzi i współpracować z innymi agentami opartymi na modelach językowych. Starszy, prostszy problem – zapewnienie prawidłowego zakończenia procesu wieloetapowego bez względu na to, co się pod nim zepsuje, restartuje, wywoła timeout lub inne problemy – nie jest czymś, co one próbują rozwiązać. To nie jest ich wina. Oczekiwanie tego od nich to błąd kategoryczny.

Pomyśl o rzeczywistym procesie pracy, a nie o demonstracji: pracownik, który weryfikuje faktury od dostawców. Wyodrębnia pozycje z pliku PDF, porównuje je z zamówieniem zakupowym w systemie ERP, zaznacza rozbieżności przekraczające określony próg do sprawdzenia przez człowieka, a następnie może czekać dni na odpowiedź, zanim wprowadzi zatwierdzone dane do księgowości. W przypadku niepowodzenia danej operacji należy ją powtórzyć, unikając podwójnego wprowadzania danych. Ponowne uruchomienie procesu w trakcie jego trwania nie powinno powodować restartu od zera ani niepostrzeżonego zniknięcia informacji.

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

Nikt nie projektował LangGraph, CrewAI ani AutoGen tak, aby gwarantować, że ten diagram przetrwa ponowne uruchomienie kontenera trzeciego dnia, podczas gdy ktoś wciąż ocenia pozycję 14. Nie chodzi tu o orkiestrację modeli językowych, lecz o trwałe wykonywanie zadań. Systemy, które radzą sobie z tym dobrze, przy czym Temporal stoi na czele, od lat obsługują mało efektowne zadania w tle: przetwarzanie płatności, realizację zamówień oraz mikrosługi w stylu saga, na długo przed tym, zanim termin „agent” zaczął być używany do opisywania takich programów.

Jak działa Temporal

Model Temporal wydaje się zwodniczo prosty. Przepływ pracy jest zapisany jako funkcja w zwykłym kodzie, zawierająca pętle, warunki i wywołania funkcji, a Temporal zapewnia trwałość historii wykonywania tej funkcji. Jeśli proces ulegnie awarii w dowolnym momencie, Temporal odtwarza funkcję na podstawie jej zapisanej historii zdarzeń aż do punktu awarii i kontynuuje działanie w sposób deterministyczny, bez ponownego uruchamiania efektów ubocznych. Tajemnicą jest to, że efekty uboczne, takie jak wywołanie LLM, żądanie do API czy zapisywanie danych w bazie danych, znajdują się w oddzielnych funkcjach zwanych aktywnościami. Temporal próbuje je uruchomić ponownie niezależnie, z możliwością konfiguracji opóźnień, podczas gdy funkcja przepływu pracy jedynie je koordynuje.

Agent do rozrachunków faktur poniżej traktuje wywołanie LLM jako jedną z kilku aktywności. Najpierw są to aktywności, czyli elementy, które mają kontakt z otoczeniem:

# 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.",
    )

Następnie przychodzi proces pracy, który przejmuje rolę orkiestracji, którą zwykle pełniłyby LangGraph lub CrewAI, przy czym trwałość jest wbudowana i nie zależy od włączenia punktu kontrolnego:

# 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}

Kilka szczegółów zasługuje na uwagę, ponieważ to właśnie one stanowią przyczyny niestabilności systemów opartych na agentach, a w tym przypadku są to właściwości środowiska wykonawczego, a nie funkcje, które trzeba pamiętać o dodaniu.

Siedmiodniowe oczekiwanie jest bezpłatne i przetrwa wszystko. workflow.wait_condition nie utrzymuje otwartego żadnego wątku, procesu ani kontenera przez cały tydzień. Temporal przechowuje stan workflow w swoim własnym trwałym magazynie, więc w międzyczasie pracownik może zostać ponownie rozmieszczony, zmniejszony do zera lub całkowicie awariować. Gdy sygnał w końcu przyjdzie, Temporal odtwarza historię i kontynuuje działanie dokładnie w tym miejscu, gdzie było await. Zrobienie tego samego z checkpointerem LangGraph lub zespołem CrewAI oznaczałoby w istocie ponowną budowę modelu opartego na źródłach zdarzeń Temporal.

Ponawiania są automatyczne, mają wąski zakres i nigdy nie powtarzają już zakończonych działań. Załóżmy, że post_to_erp zawodzi po tym, jak match_purchase_order zakończył się pomyślnie. Temporal nie uruchomi ponownie match_purchase_order, ponieważ jego wynik znajduje się już w trwałej historii. Porównaj to z prostym podejściem do tego samego problemu:

# 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

Ta naiwna wersja powtarza ekstrakcję za pomocą LLM przy każdej próbie, marnując tokeny i pieniądze na kroku, który już zadziałał, a wait_for_approval_somehow nie ma żadnej dobrego rozwiązania, jeśli musi przetrwać ponowną implementację. Wersja Temporal nie radzi sobie z tymi problemami za pomocą dodatkowego kodu. W takiej formie problemy w ogóle nie występują, dzięki sposobowi strukturyzacji wykonywania zadań.

Zwykły Python oznacza możliwość testowania. Temporal zawiera framework do testów, który może pomijać oczekiwanie trwające timedelta(days=7) i wykonywać je w milisekundach w ramach testu jednostkowego, a aktywności można symulować bez łączenia się z siecią:

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"

Zwróć uwagę na braki: brak rzeczywistego siedmiodniowego snu, brak ingerencji w wewnętrzne mechanizmy kontrolera, brak ręcznego rozumowania dotyczącego restartów. Trwałość to infrastruktura nośna, a nie coś, co każdy test musi na nowo udowadniać.

Wykorzystywanie obu warstw

Rozsądna odpowiedź nie polega na odrzuceniu LangGraph na rzecz Temporal. Działają one na różnych warstwach, a najlepsze rozwiązania produkcyjne z 2026 roku łączą je: LangGraph lub CrewAI do rozumowania w ramach jednego kroku, Temporal do warstwy przepływu pracy, która łączy procesy trwające kilka dni i obejmujące kilka usług. Aktywność może otaczać taki ograniczony krok rozumowania w następujący sposób:

@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

To jest architektura, którą zaleca się zespołom, których system agentów musi funkcjonować w środowisku produkcyjnym. Framework oparty na technologii LLM radzi sobie z tym, w czym jest dobry, czyli przeprowadzaniem rozumowań w ograniczonej liczbie kroków. Wytrzymały silnik wykonywania zajmuje się tym, do czego został stworzony, zapewniając, że cały proces – niezależnie od tego, czy trwa minuty, czy tygodnie, oraz czy obejmuje wywołania modeli LLM, czy zatwierdzenia przez ludzi – zostanie prawidłowo zakończony albo wyraźnie zawiedzie. Żaden z nich nie musi pełnić roli drugiego.

Gdzie koszty czasowe są zbyt wysokie

Temporal ma swoje wady, które sprawiają, że nie nadaje się jako domyślny wybór do wielu zadań realizowanych przez agenty. To kolejny system rozproszony, który trzeba uruchomić – czy to serwer własny, czy opłata za Temporal Cloud – plus procesory robocze i kolejki zadań, co oznacza znacznie większą infrastrukturę niż proces w Pythonie uruchamiający LangGraph. Kod workflow musi również przestrzegać ścisłych zasad determinizmu. Bezpośrednie wywoływanie datetime.now(), używanie niekontrolowanej losowości lub wykonywanie operacji wejścia/wyjścia bezpośrednio w funkcji workflow jest zabronione. Zespoły potrzebują czasu, aby to pojąć, a łamanie tych zasad powoduje zagmatwane błędy niedeterminizmu przy ponownym uruchamianiu. W przypadku krótkiego zadań realizowanych przez jeden proces, takich jak odpowiadanie na pytanie lub streszczanie dokumentu przy jednym wywołaniu LLM, trwałe wykonywanie jest jedynie zbędnym obciążeniem, ponieważ nie istnieje proces wieloetapowy, który mógłby zostać uszkodzony w przypadku awarii.

Reguła decyzyjna

Wybierz trwałą realizację jako podstawę wtedy, gdy przepływ pracy może przetrwać dłużej niż jeden proces, musi koordynować działania pomiędzy usługami lub czekać na osobę, albo obsługiwać dane, które nie mogą zostać przetworzone dwukrotnie – takie jak pieniądze, zapasy czy e-mail, który nie powinien zostać wysłany dwa razy. Niech preferowany framework LLM działa jako komponent w ramach jednego z jego kroków. Jeśli wszystko rozpoczyna się i kończy w ramach pojedynczego cyklu żądanie-odpowiedź, pomiń ten element, ponieważ jego dodanie rozwiązałoby problem, który jeszcze nie istnieje.

Wiele dyskusji na temat „którego frameworku agentów powinienem użyć” pomija ważniejsze pytanie: który warstwa systemu faktycznie wymaga wzmocnienia? LangGraph, CrewAI i AutoGen toczą wartościową debatę na temat tego, jak zorganizować rozumowanie modeli językowych i koordynację między agentami. Jednak kwestia tego, czy system agentów przetrwa nieudane wdrożenie, podział sieci lub sytuację, gdy ktoś potrzebuje trzech dni, by kliknąć „zatwierdź”, to odrębny problem, który silniki workflow rozwiązali już dawno, zajmując się niezawodną eksploatacją jeszcze przed pojawieniem się koncepcji sztucznej inteligencji agentowej. Popularne frameworki dostosowują to, co jest widoczne w demonstracjach. Ten pomijany aspekt decyduje o tym, czy cokolwiek nadal będzie funkcjonować po miesiącu.

Literatura pokrewna

  • Tożsamość dla długotrwałych agentów: tokeny delegacji i pętla ponownego nawadniania — Dlaczego statyczne klucze nie sprawdzają się w przypadku autonomicznych agentów oraz jak delegacja typu OBO, platforma audytowa i pętla ponownego nawadniania umożliwiają agentom z stanem bezpieczne działanie na krótkotrwałych serwerach typu serverless.