Wybór ram agentowych w zależności od kształtu obciążenia: sześć osi i ścieżka decyzyjna
Wybierz ramy agenta w zależności od charakteru obciążenia: LangGraph, CrewAI, AutoGen, Temporal lub żaden, oceniane pod kątem determinizmu, czasu trwania, idempotencji, możliwości audytu oraz kosztu.
Źródła: Your AI Agent Framework Is Probably the Wrong One Here’s How to Actually Choose
Zadawanie pytania o to, który framework agentowy wybrać, to błędne pytanie początkowe. Lepsze jest pytanie o to, jaka jest struktura obciążenia pracy, ponieważ framework wynika z tej odpowiedzi i nie powinien być wybierany bez żadnego kontekstu na podstawie postów porównawczych. Zespoły, które już w pierwszym tygodniu podejmują decyzję na podstawie dyskusji w mediach społecznościowych, często spędzają miesiące od trzeciego do szóstego na odkrywaniu, że podstawowe założenia frameworka nie odpowiadają temu, co budują. Rzadko to framework jest winny. Nikt przed wyborem narzędzia przeznaczonego do realizacji określonych wymagań nie określił dokładnie, jakie właściwości te wymagają.
Zamiast porównywać funkcje, ten podejście wskazuje sześć osi, które określają dopasowanie, pokazuje, jak wyglądają one w kodzie, a następnie tworzy ścieżkę decyzyjną. Jako przykład podano agenta do triażu alertów w infrastrukturze – jest on prosty, realistyczny i dotyczy wszystkich osi. Agent musi ocenić, czy alert stanowi rzeczywisty problem, działać poprzez powiadomienie kogoś lub automatyczne naprawienie, czasami czekać godzinami na reakcję człowieka, a nigdy nie powiadamiać inżyniera dyżurnego dwukrotnie o 4 rano z powodu dwukrotnej próby wykonania zadania.
Osia pierwsza: na ile deterministyczne musi być rozgałęzianie?
Tę oś pomija się i rozważa później. Szczerym testem jest pytanie, czy ktoś ucierpi, jeśli ten sam wprowadzony danych przejdzie inną ścieżkę kodową w 2% przypadków. Niektóre rozgałęzienia tolerują zmienność, np. przy wyborze tonu e-maila podsumowującego.
# 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
)
Inne przypadki takie nie są, na przykład decyzja o tym, czy obudzić człowieka o 4 rano, czy zastosować automatyczną korektę. Ta decyzja musi być realizowana przez kod, a nie poprzez prośbę użytkownika.
# 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"
Gdy ważne decyzje w procesie pracy przypominają drugą funkcję – deterministyczną, poddającą się audytowi i taką, którą recenzent zgodności chciałby przeczytać jako zwykły kod – framework musi trzymać tę logikę z dala od LLM. Warunkowe krawędzie LangGraph doskonale do tego pasują, ponieważ funkcja routingu jest zwykłym kodem Pythona, a model dotyka jedynie tych części, które zostały mu celowo przekazane, takich jak klasyfikacja czy tworzenie projektów.
CrewAI i AutoGen są zbudowane w odwrotny sposób, pozwalając własnemu rozumowaniu agenta na decydowanie o dalszych krokach. Jest to zaleta, gdy celem jest kreatywna elastyczność, ale wada, gdy elastyczność czasami oznacza pominięcie sprawdzenia zgodności. Jeśli większość kluczowych ścieżek musi być deterministyczna, sama ta odpowiedź eliminuje znaczną część tego obszaru.
Druga osa: jak długo istnieje jednostka pracy?
Jednostka o krótkim czasie trwania rozpoczyna się i kończy w ramach jednej prośby HTTP. To obszar, w którym nie jest potrzebne żadne frameworki, ponieważ ich użycie stanowiłoby czystą stratę wydajności.
# 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)
Jednostka o długim czasie życia funkcjonuje inaczej. Alarm może czekać na potwierdzenie przez człowieka przez sześć godzin, podczas gdy inżynier na zmianie śpi, a następnie wznowić działanie w innym procesie po tym, jak aktualizacja zastąpiła znajdujące się pod nim kontenery.
# 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
...
Jeśli zadanie kończy się niezawodnie w ciągu kilku sekund i cały czas znajduje się w pamięci jednego procesu, to kwestia wykorzystania frameworka jest prawdopodobnie przeceniana. Zwykła funkcja asynchroniczna z dekoratorem powtórek radzi sobie lepiej niż każdy framework pod względem łatwości utrzymania, ponieważ nie ma potrzeby żadnego koordynowania, a nauka modelu stanu LangGraph lub uruchamianie klastra Temporal w celu stworzenia bezstanowego punktu końcowego do generowania streszczeń to koszt bez żadnych korzyści.
Gdy jednostka pracy może przetrwać dłużej niż proces, ponieważ czeka na osobę, zlecenie zbiorcze lub SLA innego zespołu, konieczna staje się trwała realizacja mierzona w godzinach. Żaden z trzech popularnych frameworków do orkiestracji LLM nie został zaprojektowany jako warstwa zapewniająca trwałość. Ten aspekt wskazuje na coś w rodzaju Temporal lub przynajmniej na ręcznie budowany system z zapisywanym stanem oraz kolejką zadań z możliwością kontynuacji kroków.
Trzeci aspekt: co jeśli jakiś krok zostanie wykonyany dwa razy?
Zazwyczaj zespoły znajdują to podczas przeglądu incydentu. Pierwsza wersja poniżej wygląda dobrze w demonstracji, ale stanowi incydent, który może się wydarzyć, ponieważ ponowna próba aktywności ponownie uruchamia usługę.
# 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?
Druga wersja jest z definicji idempotentna.
# 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
Klucz idempotencji musi pochodzić z czegoś stabilnego w danych biznesowych, takiego jak identyfikator alertu, a nigdy z UUID generowanego przy każdej próbie – w przeciwnym razie ponawiane próby przestają być bezpieczne. Dotyczy to funkcji narzędzia, a nie żadnego frameworku, jednak wpływa to na to, które frameworki są do przyjęcia, ponieważ niektóre ułatwiają zapomnienie o tym, że doszło do ponownej próby. Wykonywanie w AutoGen na podstawie czatu nie daje żadnego strukturalnego sygnału, że wezwanie narzędzia stanowi ponowną próbę, a nie nową decyzję. Z punktu widzenia transkrypcji agent po prostu zdecydował się ponownie wezwać narzędzie, a to, czy jest to ponowna próba, czy powtórzenie działania, pozostaje nierozstrzygnięte.
Gdy skutki uboczne są kosztowne, niebezpieczne lub wiążą się ze zmianami finansowymi lub infrastrukturalnymi, konieczny jest framework lub stworzona od podstaw warstwa pod nim, która traktuje kwestię tego, czy skutek uboczny już wystąpił, jako problem pierwszorzędny, a nie coś, co można wywnioskować z zapisu rozmowy.
Osia czwarta: kto czyta decyzję później i w jaki sposób?
W analizie postmortem liczy się zapis audytu niezależny od konkretnego frameworku, bez względu na to, co kierowało poszczególnymi krokami.
# 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))
Każda ramka umożliwia rejestrowanie pewnych informacji. Pytanie brzmi, czy jej struktura naturalnie zapewnia przejrzysty, możliwy do zbadania ślad decyzji, czy też uzyskanie takiego śladu wymaga walki z abstrakcją. Obiekt stanu LangGraph przechodzi przez nazwane węzły, więc zapytanie o to, jaki był stan tuż przed podjęciem decyzji o routingu, to bezpośrednie zapytanie do TypedDict, który nigdy nie został przekształcony w tekst wolny. Rozmowę AutoGen trzeba analizować, aby odtworzyć te same fakty, ponieważ jego naturalny format pamięci to proza, a nie ustrukturyzowany stan. W miejscach, gdzie istnieją obowiązki związane z przestrzeganiem regulacji lub zespół ds. audytu oraz zwyczaj dokonywania dokładnych analiz po zakończeniu projektów, ta kwestia zasługuje na większe znaczenie, niż sugeruje materiał marketingowy.
Osia piąta: co ogranicza szybkość działania zespołu?
Rzadko pojawia się to na wykresach porównawczych technologii, a często decyduje o ostatecznym wyborze. Startup składający się z czterech osób, który sprawdza, czy agenci w ogóle są w stanie wykonać zadanie, nie powinien tracić trzech tygodni na naukę ograniczeń determinizmu Temporal przed napisaniem jakiejkolwiek logiki biznesowej.
# 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})
To jest zupełnie normalny efekt pierwszego tygodnia pracy. Błędem nie było wybranie CrewAI do prototypu, lecz to, że zespół sześć miesięcy później nigdy nie ponownie nie rozważył tej decyzji, gdy w grę wchodziły rzeczywiste obowiązki i zmiany w infrastrukturze, ponieważ prototyp już funkcjonował. Wybór frameworku ma datę ważności zależną od liczby zapytań przetwarzanych w systemie, a zapisanie tej daty przy dokonywaniu wyboru zapobiega jej ustawieniu domyślnemu.
Osia szósta: jaka opóźniona reakcja i budżet kosztów na każdą decyzję?
Każda decyzja dotycząca routingu, która polega na wywołaniu modelu LLM, jest mnożona w kontekście rozmowy między kilkoma agentami obejmującej kilka rund, a przy setkach alertów dziennie oraz nagłych wzrostach do tysięcy podczas incydentów, rachunek szybko rośnie.
# 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
To raczej dyscyplina architektoniczna niż oś frameworku, która pomaga wszystkim z nich. Frameworky, które ułatwiają łączenie taniego, deterministycznego kodu z kosztownymi wywołaniami modeli, pozwalają zaoszczędzić realne pieniądze przy dużych ilościach pracy. LangGraph to robi, podobnie jak silnik workflow, np. Temporal, który pakuje wywołanie LLM jako jedną aktywność. Alternatywą jest natywny wzorzec przekierowywania wszystkiego przez kolejną rozmowę pośredniczoną przez model, co jest domyślnym podejściem AutoGen, gdy parametr speaker_selection_method jest ustawiony na auto.
Ścieżka decyzyjna, a nie drzewo decyzyjne
Schemat przepływu, który daje jedną odpowiedź, byłby niesprawiedliwy, ponieważ obszary się pokrywają. Poniższa ścieżka lepiej odzwierciedla sposób, w jaki faktycznie podejmowana jest decyzja.
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.
Stosując to rozwiązanie w agencie do triażu alertów, otrzymujemy projekt warstwowy. Dzięki temu może funkcjonować po ponownym uruchomieniu i czekać na interwencję ludzi, więc potrzebna jest trwała warstwa wykonywania. Jego najważniejszy węzeł ma charakter krytyczny pod względem bezpieczeństwa – dotyczy on rozróżnienia między przekierowywaniem a automatycznym naprawianiem, dlatego w każdym kroku konieczne jest wyraźne routowanie w stylu LangGraph. Ponadto, gdy już posiada mechanizm przekierowywania, jest daleko poza fazą prototypu. Temporal staje się tu filarem, a krok orkiestracji LLM w określonym zakresie stanowi jedną z aktywności w jego strukturze.
@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"]}
W praktyce wybór odpowiedniej ramy programistycznej wygląda właśnie w ten sposób i nie jest to satysfakcjonująca odpowiedź w jednej linii. To decyzja dotycząca składania elementów, a umiejętność takiego składania stanowi kluczową kompetencję.
Błędy, które się powtarzają
- Wybór na podstawie liczby gwiazdek na GitHubie. Popularność wskazuje na wsparcie społeczności oraz liczbę dostępnych rozwiązań w internecie. Nie mówi ona nic o tym, czy podstawowy model wykonywania zadań odpowiada wymaganiom dotyczącym trwałości i determinizmu w pracy systemu.
- Przekształcanie ramy prototypowej w rozwiązanie trwałe. Prototyp, który poprawnie funkcjonował, staje się systemem produkcyjnym, ponieważ przepisanie mechanizmów koordynacji wydaje się nie dodawać żadnych nowych funkcji. W rzeczywistości zwiększa to niezawodność, która pozostaje niewidoczna, dopóki nie zniknie.
Prawdziwa odpowiedź
Nie istnieje najlepsza ramowa architektura agentów, a udawanie, że taka istnieje, jest powodem, dla którego tak wiele z tych systemów musi być przebudowanych po osiemnastu miesiącach. Istnieje natomiast najlepsza ramowa architektura dla określonego obciążenia pracy posiadającego szereg sześciu właściwości. Dyscyplina wymagana przy pracy zespołowej polega na określeniu tych właściwości jeszcze przed rozpoczęciem pracy z jakąkolwiek ramową architekturą, a po tym kroku wybór zazwyczaj staje się oczywisty. Gdy tego nie zrobi się, warstwa orkiestracji zostaje przepisana w ciągu roku, niezależnie od wybranej ramowej architektury, ponieważ problem nie leżał nigdy w samej architekturze – wynikał z braku zrozumienia, co właściwie ma być zbudowane, zanim podjęto decyzję o sposobie jej budowy.
Literatura pokrewna
- Optymalizacja agenta Terraform za pomocą DSPy: Stwórz weryfikator przed optymalizatorem — Agenci Terraform tworzą schematy. Zbuduj deterministyczny weryfikator, ustal bazę dla agenta, a następnie użyj DSPy i GEPA do optymalizacji jego pliku z informacjami o umiejętnościach, uzyskując wiarygodne wyniki.
- Strumieniowanie agentów typu Deep Agents w LangChain: plany w czasie rzeczywistym, wywołania narzędzi i tokeny — Zastąp blokujące wywołania strumieniem, aby użytkownicy mogli obserwować pracę agenta typu Deep Agent: trzy tryby strumieniowania, nazwy przestrzeni dla podagentów oraz filtrowanie tokenów do streszczania.