Auswahl eines Agentenframeworks nach der Arbeitslaststruktur: Sechs Achsen und ein Entscheidungsweg
Wählen Sie ein Agentenframework entsprechend der Arbeitslaststruktur aus: LangGraph, CrewAI, AutoGen, Temporal oder keines, wobei die Auswahl an Determinismus, Lebensdauer, Idempotenz, Prüfbarkeit und Kosten gemessen wird.
Quellen: Your AI Agent Framework Is Probably the Wrong One Here’s How to Actually Choose
Die Frage, welches Agent-Framework verwendet werden soll, ist die falsche Eröffnungsfrage. Besser ist die Frage nach der Struktur der Arbeitslast, denn daraus ergibt sich das passende Framework – es sollte nicht isoliert auf Grundlage von Vergleichsbeiträgen ausgewählt werden. Teams, die bereits in der ersten Woche aufgrund eines Beitrags in den sozialen Medien entscheiden, verbringen oft Monate drei bis sechs damit herauszufinden, dass die grundlegenden Annahmen des Frameworks nicht mit dem übereinstimmen, was sie tatsächlich entwickeln. Selten liegt das Problem am Framework selbst. Niemand hat vor der Auswahl des Tools, das diese Eigenschaften liefern soll, genau definiert, welche Eigenschaften die Arbeitslast eigentlich erfordert.
Anstatt Funktionen zu vergleichen, nennt dieser Ansatz sechs Achsen, die die Passgenauigkeit bestimmen, zeigt, wie jede davon im Code aussieht, und erstellt anschließend einen Entscheidungsweg. Ein laufendes Beispiel hierfür ist ein Agent zur Priorisierung von Infrastrukturwarnungen. Er ist nüchtern, realistisch und berücksichtigt alle Achsen. Der Agent muss beurteilen, ob eine Warnung ein echtes Problem darstellt, entweder jemanden benachrichtigen oder automatisch Abhilfe schaffen, manchmal stundenlang auf eine menschliche Reaktion warten – und niemals um 4 Uhr morgens denselben Bereitschaftsingenieur zweimal benachrichtigen, nur weil ein Versuch bereits zweimal durchgeführt wurde.
Achse eins: Wie deterministisch muss die Verzweigung sein?
Bei dieser Achse wird vorerst verzichtet und sie wird später berücksichtigt. Der ehrliche Test besteht darin zu prüfen, ob jemand Schaden nimmt, wenn derselbe Eingabewert 2 Prozent der Zeit einen anderen Codepfad nimmt. Einige Verzweigungen tolerieren Abweichungen, wie beispielsweise die Auswahl eines Tonfalls für eine Zusammenfassungse-Mail.
# 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
)
Andere Funktionen tun das nicht, wie beispielsweise die Entscheidung, ob man einen Menschen um 4 Uhr morgens wecken soll oder automatisch abhilfe schaffen muss. Diese Entscheidung muss in Code enthalten sein und nicht in einer Anweisung.
# 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"
Wenn die wichtigen Entscheidungen in einem Workflow wie die zweite Funktion aussehen – deterministisch, überprüfbar und so, dass ein Compliance-Prüfer sie gerne als reinen Code lesen würde – muss das Framework diese Logik vom LLM trennen. LangGraphs bedingte Kanten eignen sich hier hervorragend, denn die Routenfunktion besteht aus gewöhnlichem Python, und das Modell berührt nur die ihm absichtlich übergebenen Teile, wie beispielsweise Klassifizierung oder Erstellung von Entwürfen.
CrewAI und AutoGen sind anders aufgebaut: Sie lassen das eigene Denken des Agenten entscheiden, was als Nächstes geschieht. Das ist vorteilhaft, wenn kreative Flexibilität das Ziel ist, aber nachteilig, wenn Flexibilität gelegentlich dazu führt, dass Compliance-Prüfungen übersprungen werden. Wenn die wichtigsten Entscheidungspfade deterministisch sein müssen, schließt allein diese Voraussetzung einen großen Teil dieses Bereichs aus.
Achse zwei: Wie lange existiert eine Arbeitseinheit?
Eine kurzlebige Einheit beginnt und endet innerhalb einer einzigen HTTP-Anfrage. Das ist der Bereich, in dem kein Framework erforderlich ist, da eines lediglich reine Overhead-Kosten darstellen würde.
# 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)
Eine langlebige Einheit ist anders. Eine Meldung kann bis zu sechs Stunden lang auf die Bestätigung durch einen Menschen warten, während der diensthabende Ingenieur schläft, und anschließend in einem anderen Prozess fortgesetzt werden, nachdem ein Deployment die darunterliegenden Pods ersetzt hat.
# 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
...
Falls die Aufgabe zuverlässig in Sekunden abgeschlossen wird und vollständig im Speicher eines Prozesses bleibt, wird die Frage nach einem Framework vermutlich überdacht. Eine einfache asynchrone Funktion mit einem Retry-Decorator ist in Bezug auf Wartbarkeit allen Frameworks überlegen, da nichts orchestriert werden muss. Das Erlernen des State-Modells von LangGraph oder das Betreiben eines Temporal-Clusters für einen statelessen Summarisierungs-Endpunkt bedeutet lediglich Kosten ohne Nutzen.
Sobald eine Arbeitseinheit länger bestehen kann als ein Prozess – weil sie auf einen Menschen, einen Batch-Job oder die SLA eines anderen Teams wartet – wird eine dauerhafte Ausführung notwendig, die in Stunden gemessen wird. Keines der drei beliebten LLM-Orchestrierungsframeworks wurde als Durabilitätslayer konzipiert. In diesem Fall ist etwas wie Temporal oder zumindest ein selbst erstelltes System mit persistierendem State sowie einer Job-Queue mit wiederaufnahmefähigen Schritten angebracht.
Achse drei: Was passiert, wenn ein Schritt zweimal ausgeführt wird?
Teams finden diesen Fall in der Regel bei der Überprüfung von Vorfällen. Die erste Version unten funktioniert in einer Demo einwandfrei, stellt jedoch ein drohendes Problem dar, da eine erneute Ausführung der Aktion den Dienst erneut startet.
# 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?
Die zweite Version ist durch ihre Konstruktion idempotent.
# 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
Der Idempotenzschlüssel muss aus etwas Stabilem in den Geschäftsdaten stammen, wie beispielsweise der Alarm-ID, und niemals aus einer bei jedem Versuch generierten UUID – sonst hören die erneuten Versuche auf, sicher zu sein. Dies gehört zur Funktionalität des Tools selbst und nicht zu irgendeinem Framework, beeinflusst aber dennoch, welche Frameworks geeignet sind, da einige es leicht machen, zu vergessen, dass bereits ein erneuter Versuch stattgefunden hat. Die chatbasierte Ausführung von AutoGen gibt kein strukturelles Signal dafür, ob ein Toolaufruf ein erneuter Versuch oder eine neue Entscheidung ist. Aus Sicht der Transkriptierung hat der Agent einfach beschlossen, das Tool erneut aufzurufen, wobei unklar bleibt, ob es sich um einen erneuten Versuch oder eine doppelte Aktion handelt.
Wenn Nebenwirkungen teuer, gefährlich oder Geld- oder Infrastrukturänderungen beinhalten, benötigt die Einrichtung ein Framework – oder eine selbst entwickelte Schicht darunter –, das die Frage, ob die Nebenwirkung bereits aufgetreten ist, als zentrales Kriterium behandelt und nicht etwas, das aus einem Chatprotokoll abgeleitet wird.
Achse vier: Wer liest die Entscheidung später und wie?
Bei einer Nachbesprechung ist ein auditfreies Protokoll das Wichtigste, unabhängig davon, was die Schritte ausgelöst hat.
# 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))
Jedes Framework ermöglicht das Protokollieren von Informationen. Die Frage ist, ob seine Struktur von Natur aus einen klaren, abfragbaren Entscheidungsverlauf liefert oder ob man dafür gegen die Abstraktion ankämpfen muss. Ein LangGraph-Zustandsobjekt verarbeitet benannte Knoten, wodurch eine Abfrage nach dem Zustand unmittelbar vor einer Routenentscheidung eine direkte Abfrage auf einem TypedDict darstellt, das niemals in freien Text umgewandelt wurde. Eine AutoGen-Unterhaltung muss zur Wiederherstellung derselben Fakten analysiert werden, da ihr natives Speicherformat Prosa und kein strukturierter Zustand ist. In Bereichen mit Compliance-Pflichten oder bei Prüfungsteams sowie der Gewohnheit gründlicher Nachbesprechungen verdient diese Dimension mehr Bedeutung, als es Marketingmaterialien nahelegen.
Dimension fünf: Was begrenzt die Geschwindigkeit des Teams?
Das taucht selten in einer technischen Vergleichstabelle auf und entscheidet oft über den Ausgang. Ein vierköpfiges Startup, das prüft, ob Agenten die Aufgabe überhaupt bewältigen können, sollte nicht drei Wochen damit verbringen, die Determinismus-Beschränkungen von Temporal zu erlernen, bevor es irgendwelche Geschäftslogik entwickelt.
# 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})
Das ist ein völlig legitimes Phänomen in der ersten Woche. Der Fehler liegt nicht darin, für das Prototyp nicht CrewAI gewählt zu haben. Vielmehr hat das Team sechs Monate später die Entscheidung nie noch einmal überprüft, als echte Wachdienste und tatsächliche Infrastrukturänderungen auf dem Spiel standen – schließlich funktionierte das Prototyp bereits. Die Wahl eines Frameworks hat ein Ablaufdatum, das von der Anzahl der Nutzungsfälle im System abhängt; dieses Datum festzuhalten, verhindert, dass es standardmäßig gesetzt wird.
Achse sechs: Welche Latenz und Kostenbudget pro Entscheidung?
Jede Routenentscheidung, die eine eigene LLM-Aufrufung erfordert, wird in einem mehragentigen Dialog mit mehreren Schritten berücksichtigt. Bei Hunderten von Alarmen pro Tag sowie Spitzenwerten von Tausenden während eines Incidents steigen die Kosten schnell an.
# 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
Es handelt sich weniger um eine Achse eines Frameworks als vielmehr um eine Architekturdisziplin, die allen Frameworks zugutekommt. Frameworks, die es ermöglichen, billigen, deterministischen Code mit teuren Modellaufrufen zu kombinieren, sparen bei großer Menge an Daten echtes Geld. LangGraph tut dies, genauso wie ein Workflow-Engine wie Temporal, der einen LLM-Aufruf als eine Aktivität abfasst. Die Alternative besteht darin, alles über einen weiteren, vom Modell vermittelten Konversationsschritt zu leiten – das ist die Standardmethode von AutoGen, wenn speaker_selection_method auf auto gesetzt ist.
Ein Entscheidungsweg, kein Entscheidungsbaum
Ein Flussdiagramm, das nur eine Antwort liefert, wäre unaufrichtig, da die Bereiche überschneiden. Der unten dargestellte Weg entspricht eher der tatsächlichen Vorgehensweise bei der Entscheidungsfindung.
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.
Wenn dieses Konzept auf den Alert-Triage-Agent angewandt wird, ergibt sich ein schichtweises Design. Da dieser Agent nach Neustarts weiterhin funktionsfähig sein muss und auf menschliches Eingreifen wartet, ist eine zuverlässige Ausführungsschicht erforderlich. Der wichtigste Zweig dieses Systems ist sicherheitskritisch – es geht dabei um das Abrufen von Informationen durch einen Benutzer im Vergleich zur automatischen Behebung – weshalb innerhalb jeder Schritt ein explizites Routing im LangGraph-Stil notwendig ist. Zudem befindet sich das System längst über der Prototypenphase, sobald es über einen Benutzer zur Informationsabfrage verfügt. Temporal wird dabei zur Grundstruktur, wobei eine auf bestimmte Bereiche beschränkte LLM-Orchestrierungsschritt als eine Aktivität darin enthalten ist.
@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"]}
In der Praxis sieht die tatsächliche Auswahl eines Frameworks so aus – und es handelt sich dabei nicht um eine zufriedenstellende Antwort auf eine einzige Frage. Es geht um eine Entscheidung bezüglich der Kombination verschiedener Komponenten, und genau diese Fähigkeit zur Kombination ist entscheidend.
Immer wieder auftretende Fehler
- Auswahl anhand der GitHub-Stars. Die Beliebtheit gibt Aufschluss über die Unterstützung durch die Community sowie über die Anzahl der online verfügbaren Lösungen. Sie sagt jedoch nichts darüber aus, ob das zugrundeliegende Ausführungsmodell den Anforderungen an Langlebigkeit und Determinismus entspricht.
- Das Prototyp-Framework dauerhaft verwenden. Das ursprünglich nur zur Probe genutzte Framework wird zum Produktivsystem, weil das Umschreiben der Orchestrierungstechniken keine neuen Funktionen zu bieten scheint. Tatsächlich wird dadurch Zuverlässigkeit erreicht, die jedoch erst dann sichtbar wird, wenn sie fehlt.
Die eigentliche Lösung
Es gibt kein bestes Agentenframework – und gerade das Verhalten, als gäbe es eines, ist der Grund, warum so viele dieser Systeme bereits nach achtzehn Monaten neu architektiert werden müssen. Für eine bestimmte Arbeitslast mit einem vorgegebenen Satz von sechs Eigenschaften gibt es jedoch ein optimales Framework. Die Disziplin, bei der Entwicklung im Team zu wahren, besteht darin, diese Eigenschaften bereits vor dem Umgang mit einem Framework zu definieren; sobald das erledigt ist, fällt die Wahl in der Regel von selbst. Wird dieser Schritt übersprungen, wird die Orchestrierungsschicht innerhalb eines Jahres erneut geschrieben – unabhängig vom gewählten Framework –, denn das Problem lag niemals am Framework selbst, sondern daran, dass nicht zuerst geklärt wurde, was eigentlich gebaut werden soll, bevor entschieden wurde, wie es gebaut werden soll.
Verwandte Literatur
- Optimierung eines Terraform Agents mit DSPy: Erstellen Sie zunächst den Verifier, bevor Sie den Optimierer nutzen — Terraform Agents erfinden Schablonen. Erstellen Sie einen deterministischen Verifier, legen Sie eine Baseline für den Agenten fest und nutzen Sie anschließend DSPy sowie GEPA, um dessen Skill-Datei mit zuverlässigen Ergebnissen zu optimieren.
- Streaming von LangChain Deep Agents: Echtzeit-Pläne, Toolaufrufe und Tokens — Ersetzen Sie einen blockierenden Aufruf durch ein Streaming, damit Nutzer sehen können, wie ein Deep Agent arbeitet: die drei Streaming-Modi, Namensräume für Subagenten sowie das Filtern und Zusammenfassen von Tokens.