Harness, Loop, Graph: Drei Schichten hinter den meisten Fehlern von Produktion-Agenten
Fehler bei Produktionsagenten gehen selten auf das Modell zurück. Trennen Sie das Tool-Harness, die Stoppschranke der Schleife sowie die Graph-Topologie von LangGraph voneinander – jede Komponente hat einen eigenen Verantwortlichen.
Ein Agent stirbt während der Ausführung, und das Team gibt dem Modell die Schuld. Sie ersetzen es durch ein intelligenteres Modell – doch der Agent stirbt erneut. Der Prompt wird umgeschrieben, doch es passiert wieder dasselbe. Es wird eine Wiederholungslogik eingeführt, doch nichts ändert sich. Ein Ticket geht an den Anbieter des LLM, der mitteilt, dass sich das Modell wie vorgesehen verhält. Alle sind verwirrt.
Das Muster wiederholt sich. Was wie ein Defekt des Modells aussieht, ist in Wirklichkeit ein Fehler in der umgebenden Technik, die sich in drei Schichten unterteilt, die Teams selten voneinander trennen: die Tools, die der Agent nutzen kann (das Harness), der Zyklus, der entscheidet, wann die Arbeit beendet wird (der Loop), sowie die Karte der Orte, die der Agent aufsuchen kann (der Graph). Sobald diese Schichten verschwimmen, wird ein Fehler im Loop oder in den Tools dem Modell zugeschrieben, und die Anstrengungen konzentrieren sich auf die sichtbare Schicht, während der eigentliche Fehler darunter liegt.
Zwei Schichten, obwohl drei nötig wären
Die meisten Produktionsagenten-Stacks im Jahr 2026 laufen auf zwei Schichten. Sie verfügen über ein Graphenmodell, das mit LangGraph, CrewAI oder dem OpenAI Agents SDK erstellt wird, sowie über eine Beständigkeitsschicht wie Temporal – oder überhaupt keine solche Schicht. Was weggelassen wird, ist das Steuerungssystem: die Schicht, die festlegt, welche Tools der Agent verwenden darf, welche URLs er aufrufen darf, was er zurückgeben darf und wann er stoppen muss.
Auch die Anbieter bewegen sich in diese Richtung. Claude Code verfügt nun über eine Einstellung sandbox.network.deniedDomains. GitHub führte auf Organisationsebene Firewall-Steuerungen für seinen Copilot-Agent ein, und VS Code erhielt Gruppenrichtlinien, die steuern, worauf der Agent im Netzwerk zugreifen kann. Ein öffentlicher Katalog von Agentmustern listet die Ausgangspolitik als eigenständiges Muster auf. Forschungsergebnisse deuten in dieselbe Richtung: Eine arXiv-Studie aus dem Jahr 2026 behielt das Modell bei jeder Ausführung unverändert, ersetzte lediglich die Infrastruktur bei sechs fortschrittlichen Modellen und erzielte dadurch 41 Prozent geringere Kosten pro Aufgabe sowie 44 Prozent geringere Latenz. Nur wenige Teams haben in diese Ebene investiert.
Nichts davon existiert isoliert. Um die drei Kernschichten herum befinden sich ein Gateway, das Anfragen leitet und Budgets durchsetzt, ein Speichersystem für das Abrufen von Daten sowie ein semantischer Cache, eine Überwachung jedes Schritts, eine Steuerung, die Quoten und Geschwindigkeitsbeschränkungen anwendet, sowie eine Bewertung, die den Einsatz auf einem „goldenen“ Datensatz kontrolliert. Jedes dieser Elemente ist nützlich. Doch keines davon ist in der Regel der Grund dafür, dass ein Agent ausfällt. Verwirrende Grenzen zwischen Harness, Schleifen und Graphen führen stattdessen dazu, da der entstehende Fehler leicht fälschlicherweise als Fehler des Modells interpretiert werden kann.
Wie die drei Schichten entdeckt wurden
Der Auslöser war ein Sourcing-Agent, der immer wieder einen Aufruf an query_suppliers wiederholte. Das LLM gab ständig an, dass es mehr Daten benötige, der Loop wiederholte sich ununterbrochen und die Token-Rechnung wuchs stetig an. Anstatt Anleitungen zu lesen, erwiesen sich Post-Mortems, Architekturdokumente und Incidentsberichte als nützlicher Lesestoff, und in unzusammenhängenden Projekten tauchten immer wieder dieselben drei Begriffe auf: Harness, Loop, Graph.
Eine häufig zitierte Beschreibung aus der Literatur zur Agentenarchitektur beschreibt diese Aufteilung folgendermaßen. Der Harness umfasst die Beziehung eines Agenten zu seiner Umgebung. Der Loop beschreibt, wie die Ausführung eines einzelnen Agenten im Laufe der Zeit gesteuert wird. Der Graph zeigt, wie viele Agenten, Tools und Kontrollpunkte in einer gesamten Anwendung miteinander verbunden sind. Jede Schicht beruht auf der darunterliegenden, und jede tritt erst dann in Erscheinung, wenn die untere Schicht recht stabil ist.
Sobald die Namen bekannt waren, wurde der ursprüngliche Fehler offensichtlich: Die Abbruchbedingung befand sich innerhalb des Tools und nicht im Schleifenaufruf. Harness und Schleife wurden verwechselt. Das Modell war nicht kaputt, sondern die Grenzen der Schichten. Andere berichten vom gleichen Muster. Jemand formulierte es unverblümt: Ein Agent, der aufgrund eines falschen Modells als unzuverlässig gilt, hat in der Regel die falsche Schicht als Ursache. Ein Newsletter beschrieb einen Gründer, der ein Modell entfernte, ein angeblich besseres einsetzte, aber wieder denselben fehlerhaften Agenten erhielt, weil das Problem in der umgebenden Mechanik lag.
Die drei Schichten wurden nicht hier erfunden. Sie wurden von Personen benannt, die Agenten in großem Maßstab betreiben, und tauchen in unabhängigen Quellen immer wieder auf. Was folgt, ist das Ergebnis, wenn diese Namen auf echten Code treffen.
Harness: Was der Agent erreichen kann
Der Harness stellt die Funktionsgrenze dar. Er legt fest, welche Tools der Agent zur Verfügung hat, wie ihre Eingangs- und Ausgabeschemata aussehen, welche Berechtigungen er besitzt, welche Daten er abruft, welche URLs er aufrufen kann und welche Informationen er einsehen darf.
Betrachten Sie den Agenten als Auftragnehmer. Der Harness ist das Abzeichen an der Kordel: Es bestimmt, welche Türen sich öffnen, welche Systeme sichtbar sind und was im Inneren erlaubt ist. Er sagt nichts über Intelligenz aus. Er definiert die zugänglichen Bereiche – und auch die Sicherheit liegt dort. Ohne ihn ist ein Agent lediglich eine Funktionsaufruf mit einer API-Schlüssel, der frei darauf zugreifen, Daten lesen und zurückgeben kann. Mit ihm kann der Agent nur das tun, was der Harness erlaubt.
Die meisten Agentenprojekte erstellen diese Schicht niemals. Die Tutorials enden, sobald das Modell ein Tool aufgerufen hat und ein Ergebnis erhalten hat. Dabei werden drei Situationen übergangen: ein Tool, das einen 10MB großen JSON-Blob zurückgibt, der das Kontextfenster überläuft, ein Tool, das aufgrund von eingefügtem Text eine beliebige URL abruft, sowie ein wiederholt ausgeführter Nebeneffekt, weil keine Idempotenzschlüssel vorhanden sind. Alle drei sind Probleme des Harnesses, und ihre Behandlung ist es, was eine Demo in ein Produktivsystem verwandelt.
Das Musterkatalog legt das Prinzip für den Netzwerkaustritt fest. Die Domänen, mit denen Agenten-Tools in Kontakt treten dürfen, sollten durch Allow- und Deny-Listen begrenzt werden, die vom Harness durchgesetzt werden – dadurch wird das Modell außerhalb der Netzwerkvertrauensgrenze platziert. Wenn das Modell entscheidet, ob es sich verbinden soll, kann Prompt-Injection diese Kontrolle umgehen. Eine Überprüfung auf Harness-Ebene macht die Isolation zu einer strukturellen Eigenschaft und nicht zu einer probabilistischen.
Dieser Unterschied ist genau der Punkt. Modelle sind probabilistisch und können getäuscht, manipuliert oder verwirrt werden. Ein Harness hingegen ist strukturell und setzt Regeln im Code durch, nicht in den Anfragen. Dem Modell höflich zu bitten, keine willkürlichen URLs aufzurufen, ist eine Anfrage. Es unmöglich zu machen, ist eine Garantie.
Loop: Wenn der Agent aufhört
Der Loop ist das Herzstück. Der Agent beobachtet die Welt, wählt eine Aktion aus, führt sie aus, prüft das Ergebnis und entscheidet dann entweder, weiterzumachen oder anzuhalten.
Der Zyklus aus Beobachten, Handeln und Überprüfen ist der einfachere Teil. Der schwierige Teil ist die Stoppsbedingung, denn ohne diese verbraucht ein unbeaufsichtigter Agent endlos Token, ohne laut zu versagen.
Der Sourcing-Agent hatte keine. Er rief query_suppliers auf, überprüfte die Ergebnisse, entschied, dass er mehr benötigte, und rief erneut an – immer wieder. Das LLM behauptete ständig, es bräuchte mehr Daten, denn Sprachmodelle neigen dazu, weitere Tool-Aufrufe zu tätigen: Sie sind hilfsbereit, versuchen es weiter und wollen nicht aufgeben. Diese Eigenschaft ist bei kreativen Aufgaben ein Vorteil, bei produktiven Aufgaben hingegen ein Nachteil.
Das Musterkatalog bezeichnet dies als unbegrenzten Kreislauf. Die Umsetzung besteht darin, „so lange zu iterieren, bis das Modell mit unvollendeter Arbeit meldet“, wobei es außerhalb des Modells weder einen Zähler, Timer noch eine Ausgabengrenze gibt, die den Prozess stoppen könnte. Die stillschweigende Annahme ist, dass das Modell die Fertigstellung ankündigen wird. Bei schwierigen Aufgaben geschieht das selten: Es driftet zu verwandten Fragen ab, wiederholt fehlgeschlagene Aktionen und durchläuft Fehler immer wieder, ohne den Kreislauf zu erkennen.
Das ist keine Hypothese:
- Im Juli 2025 verbrauchte eine außer Kontrolle geratene Rekursion in Claude Code innerhalb von fünf Stunden zwischen 16.000 und 50.000 Dollar. Es kam weder zu einem Absturz noch zu einem Fehler. Die Agenten führten einfach Anweisungen ohne Endpunkt aus.
- In öffentlichen Fehlerberichten wird beschrieben, dass Subagenten Nachkommen auf mehr als 50 Ebenen tief erzeugten. Eine Sitzung verbrauchte innerhalb von fünf Minuten 4 Millionen Tokens. Ein anderer Nutzer zahlte 27,60 Dollar über dem Planlimit, da die Nutzungskredite mitten in der Sitzung aktiviert wurden und es keine Möglichkeit gab, den Ablauf zu stoppen. Im ersten Fall war die nützliche Forschung nach drei oder vier Agenten abgeschlossen, während die anderen 44 nichts Neues beitrugen. Im zweiten Fall interpretierten die Subagenten eine Anweisung zur Handhabung mehrerer unabhängiger Ziele als Erlaubnis, eigene Nachkommen zu erzeugen.
- Ein im Juli 2026 veröffentlichter Artikel bezeichnete das Problem als „Infinite Agentic Loops“. Sein Tool zur statischen Analyse, IAL-SCAN, untersuchte 6.549 Repositorien mit LLM-Agenten und stellte 68 Fehler in 47 Projekten fest. Achtundsechzig Agenten, die niemals enden, stellen weniger einen Fehlerzähler dar als eine Zeitbombe für die Abrechnung.
Ein Designleitfaden für begrenzte agenteische Schleifen stellt dieselbe Forderung: Die Begrenzung muss bereits beim Entwurf der Schleife berücksichtigt werden, nicht erst als Optimierung hinzugefügt werden, wenn etwas schiefgeht. Andernfalls ist die einzige Stopps Regel, dass das Modell entscheidet, dass es fertig ist – was eher eine Hoffnung als eine echte Kontrolle ist.
Daher muss die Stoppsbedingung mechanisch sein, nicht semantisch. „Das LLM denkt, die Arbeit ist abgeschlossen“ ist semantisch. „Drei Versuche wurden unternommen“ oder „50 Cent wurden ausgegeben“ ist mechanisch. Mechanische Bedingungen sind deterministisch, überprüfbar und stabil bei Wiederholung. Sie hängen nicht vom Urteil des Modells ab, das probabilistisch ist und bei erneutem Versuch unterschiedlich ausfallen kann.
Ein Agent eines Analyseunternehmens zeigt auf, was das kostet. Er sollte die Ursache für einen Timeout bei einer Zahlungsgateway in weniger als zwei Minuten aufspüren. Er durchsuchte die Protokolle, erhielt 4.200 Zeilen, komprimierte sie, kam zu dem Schluss, dass mehr Kontext benötigt wurde, und suchte erneut. Nach zwölf Durchgängen hatte das Ticket immer noch keinen Zusammenfassungstext, während der Vorgang 380.000 Tokens verbraucht und 47 Dollar an API-Kosten verursacht hatte. Nichts im Modell verweigerte die Fortsetzung, und nichts im Laufzeitumfeld ordnete eine Pause an. Nachdem das Unternehmen Iterationslimits, Stagnationserkennungsfunktionen (zur Erkennung wiederholter nahezu identischer Ergebnisse) sowie eine Kontrolle des Token-Budgets eingeführt hatte, sank der Anteil an aus dem Ruder gelaufenen Schleifen um 91 Prozent, und die mittlere Zeit zur Lösung eines Tickets sank von 4,1 auf 1,6 Minuten. Eine begrenzte Schleife erledigte die Arbeit – kein stärkeres Modell.
Was eine begrenzte Schleife enthält
Das Analysebeispiel weist auf die Bestandteile einer Schleife hin, die nicht außer Kontrolle geraten kann. Jeder dieser Bestandteile wird durch Code außerhalb des Modells überprüft:
- Iterationsbegrenzung. Eine feste Anzahl an Durchläufen – dadurch endet eine Aufgabe, die die Grenze erreicht, mit einem expliziten Fehler und nicht mit einem stillen Endloslauf.
- Stagnationsprüfung. Eine Analyse der kürzlichen Ergebnisse, sodass mehrere nahezu identische Ausgaben in Folge als kein Fortschritt gelten, selbst wenn die Anzahl der Schritte noch gering ist.
- Ausgabenobergrenze. Ein Budget in Tokens oder Geld, das die Ausführung nicht überschreiten darf; der Kontrollmechanismus befindet sich dabei während der Ausführung und nicht im Prompt.
- Zeitlimit. Eine Begrenzung der verstrichenen Zeit, die langsame Schleifen erfasst, die die anderen Kontrollmechanismen nicht auslösen.
Keine dieser Maßnahmen erfordert die Zusammenarbeit des Modells, und jede kann mit einem Fake-Modell getestet werden, das niemals angibt, die Aufgabe abgeschlossen zu haben. Ein fehlerhafter Versuch, der nach drei Anläufen laut endet, kostet wenige Cent. Derselbe Versuch, der dem Urteil des Modells überlassen wird, kostet so viel, wie das Messsystem zulässt.
Graph: wohin der Agent gehen kann
Der Graph stellt die Topologie dar: die vorhandenen Knoten, die Verbindungslinien zwischen ihnen, die Stellen, an denen der Pfad abzweigt, die Orte, an denen eine Person zustimmt, sowie der Zustand, der von Knoten zu Knoten weitergegeben wird. Wenn der Graph die Karte ist, dann ist der Loop das Fahrzeug, das darüber fährt, und die Beschränkungen bestimmen, was an jeder Haltestelle möglich ist.
Die zentrale Gestaltungsentscheidung ist die Explizitheit. Ein ReAct-Loop (denken, handeln, beobachten, wiederholen) ist ein impliziter Graph mit einem Knoten, der sich selbst versorgt, wobei das LLM als Kantenfunktion fungiert. Für einfache Aufgaben ist das in Ordnung. Sobald Struktur eine Rolle spielt, versagt dieses Konzept. Ein Vergleich von ReAct mit LangGraph fasst dies zusammen: Im ersten Fall wählt das Modell den nächsten Schritt aus dem Prompt und der Historie aus, im zweiten Fall wählt der Entwickler ihn über Code und explizite Routen aus.
Der Unterschied zeigt sich sofort, wenn ein menschlicher GenehmigungsSchritt erforderlich ist. In einem impliziten Graphen gibt es kein „zwischen“: Der Agent bewegt sich durch die Schritte aufgrund seines eigenen Schlussfolgerungsvermögens. Das Hinzufügen eines Kontrollpunkts bedeutet, den Kreislauf zu unterbrechen, ein Pausenmechanismus einzufügen, den Zustand zu externalisieren und bei einem Signal fortzusetzen. Das ist im Grunde das Anpassen eines Graphen an einen Kreislauf – und der Kreislauf wehrt sich dagegen, weil er dazu konzipiert ist, sich selbst zu steuern. In einem expliziten Graphen reicht ein neuer Knoten mit einem interrupt()-Aufruf aus. Der Graph ändert sich, während der Kreislauf und die zugehörige Struktur unverändert bleiben.
Ein Entwickler führte dieselbe Politik, dasselbe deterministische Modell sowie drei Szenarien über einen einfachen Tool-Aufruf-Loop sowie einen expliziten LangGraph-Zustandsgraphen durch. Das Verhalten stimmte genau überein, doch die graphbasierte Version benötigte 97 Zeilen gegenüber 50 – das sind etwa 94 % mehr Code. Somit gewinnt der einfache Loop bei drei Schritten in jeder Hinsicht. Der Entwickler wies anschließend darauf hin, was eine solche Vergleich nicht erfassen kann: Im Loop wird das Verhalten durch die Reihenfolge der Anweisungen impliziert und muss durch das Lesen des Codekörpers erfasst werden. Im Graphen hingegen werden Kanten als Daten deklariert, was es ermöglicht, die Topologie zu inspizieren, darzustellen und abzuspielen, ohne irgendeinen Knoten öffnen zu müssen. Der Wert beträgt bei drei Schritten „nil“, bei dreißig groß und noch größer, wenn eine andere Person die Kontrolle übernimmt.
Das ist der Produktionsargument. Demos bestehen aus drei Schritten, und die Person, die sie geschrieben hat, besitzt sie. Die Produktion umfasst dreißig Schritte und wird von der Person verwaltet, die um 3 Uhr morgens im Dienst ist. In diesem Umfang ist eine explizite Topologie eine Überlebensnotwendigkeit und kein zusätzlicher Aufwand.
Ein praktischer Test, um zwischen den beiden Formen zu entscheiden, besteht darin zu fragen, wer in sechs Monaten Eigentümer des Agenten sein wird. Ein kurzer, einzigartiger Agent, der von seinem Autor gewartet wird, kann weiterhin ein einfacher Schleifenalgorithmus bleiben. Ein Agent mit menschlicher Freigabe, mehreren Zweigen oder einem anderen Verantwortlichen benötigt eine explizite Graphstruktur, denn die Topologie wird zu Daten, die dargestellt, überprüft und nachgespielt werden können.
Wie das Verwirren der Schichten aussieht
Die unten aufgeführten Fehlermuster stammen aus einer Entwicklungsumgebung, die der sichere Ort ist, um Schichten zu vermischen. Es handelt sich dabei um dieselben Fehlermuster, die auch in der Produktion auftreten.
Der „Harness“ als Eigentümer der Schleife
In der verwirrenden Version befindet sich die Überprüfung „Ist das gut genug?“ innerhalb des Tools, sodass das Tool entscheidet, wann der Loop endet. Ein Tool sollte eine Fähigkeit darstellen, wie zum Beispiel „Kann Lieferanten abfragen“, und nicht ein Entscheider, der sagt „Genug, stopp“. Die Unterbringung der Stoppsbedingung dort macht den Loop für sich allein ungetestet, es wird unmöglich, das Tool auszutauschen, ohne die Stopplogik neu zu implementieren, und es bleibt kein Platz für eine Obergrenze der Anzahl der Schritte, da das Tool nichts über die Versuche weiß.
Die Korrektur ist strukturell. Man zieht die Stoppsbedingung aus dem Tool heraus und platziert sie im Graphenknoten: Das Tool gibt Daten zurück, der Knoten entscheidet, ob eine weitere Durchführung notwendig ist, das Harness stellt die Fähigkeit bereit, und der Loop befindet sich im Graphen. Die verwirrende Version sieht im Code so aus:
# CONFUSED LAYER — Harness owns Loop logic
async def query_suppliers(item: str, budget: float, quantity: int, attempts: int) -> dict:
"""Tool decides when the loop stops. Capability + control flow in one function."""
results = query_mock_marketplace(item, budget, quantity)
# The tool decides when to stop — couples execution logic with capability
if len(results) > 5 or attempts >= 3:
return {"status": "STOP", "suppliers": results}
return {"status": "CONTINUE", "suppliers": results}
async def research_node(state: AgentState) -> dict:
# The node just calls the tool and forwards whatever the tool decided.
# It has no idea why the loop stopped — that logic is hidden inside the tool.
result = await query_suppliers(
state["brief"].item, state["brief"].budget, state["brief"].quantity,
state.get("attempts", 0) + 1,
)
return {"suppliers": result["suppliers"], "status": result["status"].lower()}
# ✅ CLEAN SEPARATION — Harness is pure capability, Loop lives in the Node
MAX_RESEARCH_ATTEMPTS = 3
async def query_suppliers_impl(item: str, budget: float, quantity: int) -> dict:
"""Pure Harness capability: schema validation, circuit breaker, marketplace call.
The tool has no idea a loop exists. It returns data. It does not decide
when to stop. Circuit breaker + fault injection live here because they are
capability concerns (is the downstream reachable?), not control-flow concerns.
"""
breaker = get_circuit_breaker("query_suppliers")
if not breaker.allow_call():
raise CircuitBreakerOpenError("query_suppliers", breaker.state)
maybe_inject("query_suppliers") # fault injection for chaos tests
try:
results = query_mock_marketplace(item, budget, quantity)
breaker.record_success()
return {"suppliers": results}
except Exception as e:
breaker.record_failure()
raise
async def research_node(state: AgentState) -> dict:
"""Graph Node / Loop: mechanical stop conditions and routing decisions.
The node reads state, calls the tool through the registry, and decides
whether to stop — based on counters in state, NOT on the LLM's judgment
and NOT on the tool's opinion.
"""
brief = state["brief"]
attempts = state.get("attempts", 0) + 1
# Harness call — schema validation, timeout, output cap all happen inside registry.call
query_result = await registry.call("query_suppliers", {
"item": brief.item,
"budget": brief.budget,
"quantity": brief.quantity,
})
suppliers = query_result.get("suppliers", [])
# Mechanical stop condition — enforced OUTSIDE the tool, in code, on state.
# Deterministic. Survives replay. Testable without the tool being live.
if not suppliers and attempts >= MAX_RESEARCH_ATTEMPTS:
logger.info(
"STOP_CONDITION_FIRED path=no_matches item=%s attempts=%d max=%d",
brief.item, attempts, MAX_RESEARCH_ATTEMPTS,
)
return {"suppliers": [], "attempts": attempts, "status": "no_matches"}
if not suppliers:
return {"suppliers": [], "attempts": attempts, "status": "running"}
# Enrich, then mark completed
enriched = []
for s in suppliers:
quote = await registry.call("get_price_quote", {
"supplier_name": s["name"], "item": brief.item, "quantity": brief.quantity,
})
rating = await registry.call("check_seller_rating", {"supplier_name": s["name"]})
enriched.append({**s, "quote": quote, "rating_info": rating})
return {"suppliers": enriched, "attempts": attempts, "status": "completed"}
# The Graph owns WHERE the agent can go — conditional edges, not the node body.
def _should_retry_or_end(state: AgentState) -> str:
"""After Research: loop back if still running, else Score or END."""
if state["status"] == "running":
return "research" # self-loop — the retry
if state["status"] == "no_matches":
return END # mechanical stop → graph terminates
return "score" # success → next node
# Wiring (in build_sourcing_graph):
# graph.add_conditional_edges("research", _should_retry_or_end,
# {"research": "research", "score": "score", END: END})
Der Loop, der das Graph beherrscht
Viele Frameworks stellen einen ReAct-Loop zur Verfügung, bei dem die Topologie implizit ist: Der Agent entscheidet auf der Grundlage der Ausgabe des Modells über den nächsten Schritt, und es existiert keine explizite Karte. Das gilt solange, bis ein Schritt der menschlichen Freigabe zwischen Entscheidung und Bestätigung eingefügt werden muss; anschließend beginnt die Anpassung, wobei der Loop dagegen ankämpft.
Die Lösung besteht darin, das Graph von Anfang an explizit zu entwerfen – mit Knoten als Funktionen und Kanten als Übergängen, wobei einige davon bedingt sind und andere als Unterbrechungen fungieren. Der Loop führt den Agenten weiter, doch das Graph definiert die erreichbaren Orte, nicht der Loop. Die monolithische ReAct-Form, bei der der Loop das Graph und das LLM die Kantenfunktion ist, sieht so aus:
# ❌ CONFUSED LAYER — Implicit topology inside a monolithic ReAct loop
# The loop IS the graph. The LLM is the edge function. There is no "between."
while not agent.is_done():
action = llm.decide_next_step(history)
if action == "query":
data = query_suppliers()
elif action == "approve":
# Retrofitting human approval into an implicit loop requires messy pauses.
# You have to break the loop, externalize state, park the worker, and
# resume on a webhook — the loop fights you because it was designed to
# be self-driving.
pause_execution_and_wait_for_webhook()
elif action == "stop":
break
# ✅ CLEAN SEPARATION — Declarative graph with explicit nodes and human interrupt
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt
def approve_node(state: AgentState) -> dict:
"""Human checkpoint via interrupt() — a STRUCTURAL pause, not a prompt.
The graph genuinely suspends here. interrupt() pauses execution and waits
for a human to call Command(resume=approval_data). The worker is freed.
If the worker crashes while waiting, Temporal replays the activity, the
graph re-runs from the last checkpoint, and interrupt() fires again —
the human re-approves. No data loss.
"""
selected = state.get("selected_supplier", {})
brief = state.get("brief", {})
approval_request = {
"item": brief.item,
"quantity": brief.quantity,
"supplier": selected.get("name", ""),
"price": selected.get("price", 0),
"total_cost": selected.get("price", 0) * brief.quantity,
}
# interrupt() pauses the graph HERE. The human must call /approve
# with a resume value to continue.
approval = interrupt(approval_request)
approved = approval.get("approved", False)
comment = approval.get("comment", "")
return {
"approval_status": "approved" if approved else "rejected",
"approval_comment": comment,
"rejection_count": state.get("rejection_count", 0) + (0 if approved else 1),
}
def _after_approve(state: AgentState) -> str:
"""After Approve: go to Confirm if approved, retry Research if rejected."""
if state.get("approval_status") == "approved":
return "confirm"
if state.get("rejection_count", 0) + 1 >= 3:
return END
return "research"
def build_sourcing_graph(checkpointer=None):
"""Topology is DECLARED DATA, not statement order in a loop body.
START → Research → (retry) → Score → Decide → Approve → Confirm → END
↓ (rejected, < 3)
Research
↓ (rejected, = 3)
END
"""
graph = StateGraph(AgentState)
graph.add_node("research", research_node)
graph.add_node("score", score_node)
graph.add_node("decide", decide_node)
graph.add_node("approve", approve_node) # explicit checkpoint node
graph.add_node("confirm", confirm_node)
graph.add_edge(START, "research")
graph.add_conditional_edges("research", _should_retry_or_end,
{"research": "research", "score": "score", END: END})
graph.add_edge("score", "decide")
graph.add_edge("decide", "approve")
graph.add_conditional_edges("approve", _after_approve,
{"confirm": "confirm", "research": "research", END: END})
graph.add_edge("confirm", END)
return graph.compile(checkpointer=checkpointer)
Der Graph, der das Harness besitzt
Dieser Fall ist seltener, taucht aber in Code-Reviews auf. Ein Graphenknoten ruft direkt eine externe API auf, beispielsweise einen HTTP GET-Anfragen an einen Endpunkt eines Lieferanten innerhalb der Knotenfunktion, wodurch der Graph den Toolaufruf kontrolliert.
Das ist falsch, denn der Graph ist topologisch strukturiert und sollte nichts über HTTP, Authentifizierung, Wiederholungsversuche oder Circuit Breaker wissen. Wenn der Aufruf innerhalb des Knotens erfolgt, bedeutet das, dass ein Circuit Breaker durch Bearbeitung des Graphen hinzugefügt werden muss, ein Wechsel der Implementierung ebenfalls durch Bearbeitung des Graphen erfolgt, und das Testen des Graphen erfordert ein aktives Tool.
Die Lösung leitet den Aufruf über das Harness. Der Knoten bittet um call_tool("query_suppliers", {...}) und erhält ein validiertes, typisiertes Ergebnis. Das Harness kümmert sich um die HTTP-Anfrage, die Wiederholungsversuche, den Circuit Breaker sowie die Schemavalidierung. Der Graph weiß nicht, dass HTTP existiert, und das Harness weiß nicht, dass ein Graph existiert:
# ❌ CONFUSED LAYER — Graph node owns the tool call (Harness concern)
import httpx
def supplier_node(state: AgentState) -> dict:
"""The graph node reaches directly into HTTP, auth, retries, and parsing.
Why that's wrong: the graph is topology. It should not know about HTTP,
authentication, retries, circuit breakers, or schema validation. Those are
harness concerns. When the graph owns the tool call:
- You can't add a circuit breaker without modifying the graph.
- You can't swap the tool implementation without modifying the graph.
- You can't test the graph without the tool being live.
- You can't enforce an egress allowlist — the URL is hardcoded here.
"""
resp = httpx.get(
f"https://supplier-api.example.com/suppliers",
params={"item": state["brief"].item},
headers={"Authorization": f"Bearer {SUPPLIER_API_KEY}"},
timeout=10.0,
)
resp.raise_for_status()
suppliers = resp.json()["suppliers"] # no schema validation, no output cap
return {"suppliers": suppliers}
# ✅ CLEAN SEPARATION — Graph node calls through the harness; harness owns the call
async def supplier_node(state: AgentState) -> dict:
"""The graph node says 'call this tool with these args' and gets back a
validated, typed result. The graph doesn't know there's HTTP involved.
The harness doesn't know there's a graph involved.
"""
query_result = await registry.call("query_suppliers", {
"item": state["brief"].item,
"budget": state["brief"].budget,
"quantity": state["brief"].quantity,
})
suppliers = query_result.get("suppliers", [])
# Mechanical stop condition — the node's job, not the tool's
if state.get("attempts", 0) + 1 >= MAX_RESEARCH_ATTEMPTS and not suppliers:
return {"suppliers": [], "attempts": state.get("attempts", 0) + 1, "status": "no_matches"}
return {"suppliers": suppliers, "attempts": state.get("attempts", 0) + 1, "status": "completed"}
# What the harness does — the graph never sees this:
#
# registry.call("query_suppliers", {...})
# │
# ├─ 1. validate_input(QuerySuppliersInput, args) # Pydantic v2
# ├─ 2. EgressFilter(spec.allowed_egress) # URL allowlist
# ├─ 3. SandboxedHTTPClient(egress_filter) # blocked domains = structural
# ├─ 4. run_with_timeout(fn, kwargs, spec.timeout_seconds)
# ├─ 5. enforce_output_cap(result, spec.max_output_bytes) # 64KB cap
# └─ 6. validate_output(QuerySuppliersOutput, result) # garbage caught here
#
# The graph node gets back a validated dict. It never sees the HTTP call,
# the auth header, the retry, the circuit breaker, or the egress filter.
# Swap httpx for aiohttp, swap the supplier API for a new one, add a circuit
# breaker — the graph doesn't change. The harness doesn't know a graph exists.
Wer besitzt welche Schicht
Jede Schicht benötigt eine zugehörige Infrastrukturkomponente. Wenn die Zugehörigkeit unklar ist, entstehen Probleme zwischen den Schichten.
Temporal ist für die Absturzsicherheit des Loops verantwortlich
Wenn der Motor mitten in einem Zyklus ausfällt, übernimmt Temporal die Wiederherstellung. Es speichert jeden Schritt in einer Ereignishistorie. Nach einem Absturz eines Arbeiters wiedergibt es den Workflow ohne erneutes Ausführen der Aktivitäten ab, indem es stattdessen die aufgezeichneten Ergebnisse einsetzt, sodass der Loop von dem letzten abgeschlossenen Schritt aus neu gestartet wird und nicht von Null aus.
Deshalb muss eine Stoppsbedingung deterministisch sein. Eine Bedingung wie „das Modell beurteilt die erledigte Arbeit“ ist nicht deterministisch, und eine Wiedergabe könnte zu einem anderen Urteil führen. Ein Schrittzähler, eine Ausgabengrenze oder ein Qualitätslimit, das aus den Ausgaben der Tools abgeleitet wird, funktioniert bei einer Wiedergabe einwandfrei. Das Modell kann zwar mitbestimmen, aber die Prüfung if attempts >= 3: stop muss im Code enthalten sein.
LangGraph besitzt die Topologie des Graphen
In LangGraph sind Knoten einfache Funktionen, Kanten sind Übergänge, und bedingte Kanten sind Funktionen, die den Namen des nächsten Knotens zurückgeben. LangGraph kompiliert all das zu einem ausführbaren Graphen. Da der Graph explizit ist, kann er angezeigt werden, jeder Knoten kann einzeln getestet werden, und ein Knoten kann hinzugefügt werden, ohne den Rest zu stören. Ein menschlicher Freigabepunkt ist lediglich ein Knoten, der interrupt() aufruft, wodurch der Graph geändert wird, während Schleifen und Verbindungen unverändert bleiben.
LangGraph besitzt außerdem die Struktur des Zustands, einen TypedDict, der zwischen den Knoten übergeben wird. Jeder Knoten liest das, was er benötigt, und schreibt das, was er erzeugt, sodass der Zustand als Vertrag zwischen ihnen fungiert. Ein Knoten, der niemals suppliers schreibt, verhindert, dass der nächste Knoten darauf zugreifen kann, und der Typprüfer weist dies vor der Ausführung darauf hin.
Ein handgebautes Tool-Management-System übernimmt alles Weitere
Das ist eine Schicht, die weder Temporal noch LangGraph bieten, weshalb sie von Grund auf erstellt werden muss. Sie kümmert sich um:
- Registrierung. Jedes Tool gibt einen Namen, ein Pydantic-Eingabeschema, ein Ausgabeschema, eine Zeitbegrenzung sowie an, ob Idempotenz erforderlich ist, an.
- Validierung. Eingaben werden vor dem Ausführen eines Tools und Ausgaben danach überprüft, sodass fehlerhafte Antworten bereits vor dem Erreichen des Agents erkannt werden.
- Ausgangsfilterung. Tools, die HTTP-Aufrufe tätigen, gehen über einen in einer Sandbox befindlichen Client mit einer URL-Allowlist, sodass kein Tool eine beliebige Adresse aufrufen kann. Die Entscheidung trifft das Management-System, nicht das Modell.
- Ausgabebegrenzung. Ergebnisse werden vor dem Erreichen des Kontextfensters auf 64 KB abgeschnitten. Ein 10 MB großer Datensatz wird mit einem
_truncated: true-Marker gekürzt übermittelt. - Schaltkreisschützer. Jedes Tool verfügt über einen solchen, der in allen Agenten-Arbeitsabläufen gemeinsam genutzt wird. Fünf Fehler in Folge schließen den Schaltkreis, wodurch die Agenten ab diesem Zeitpunkt schnell versagen, anstatt weiterhin auf eine nicht mehr funktionierende API zuzugreifen.
- Concurrentitätsbegrenzungen pro Tool. Solche Limits verhindern, dass ein langsames Tool alle verfügbaren Ressourcen für Aktivitäten belegt.
- Idempotenzschlüssel. Bei Tools mit Nebeneffekten erstellt das System einen Schlüssel aus der Workflow-ID und dem Schrittnamen. Bei einer erneuten Ausführung wird das vorhandene Ergebnis wiederverwendet, sodass keine Duplikate entstehen.
Keines dieser Konzepte stammt von Temporal oder LangGraph. Es handelt sich um die Schicht, die es ermöglicht, Tools sicher aufrufen zu können – und genau diese wird in den meisten Agenten-Projekten weggelassen.
Überprüfung: „Der Agent sagt, es hat funktioniert“ versus „Es hat funktioniert“
Der dritte Schritt der Schleife ist die Überprüfung, und diese muss extern erfolgen. Damit ist nicht gemeint, dass der Agent seine eigene Ausgabe überprüft oder dass das LLM seine eigenen Aufgaben bewertet.
Im Musterkatalog wird diese Korrektur als Überprüfungsregister bezeichnet: Selbst gemeldete Angaben wie „Build erfolgreich“ weichen strukturierten Aufzeichnungen ab, bei denen jede Überprüfung eine INSERT-Operation in der Datenbank und jede Beweismittelgruppe eine SELECT-Operation darstellt. Ohne Kontrolle könnte ein Agent vorgeben, die Überprüfungen seien bestanden, Schritte überspringen, ohne es anzugeben, oder Ergebnisse von einem Tool behaupten, das er nie ausgeführt hat.
Das Honk-Team bei Spotify stieß genau auf dieses Problem. Ihre Agenten behaupteten manchmal „Tests erfolgreich, keine Probleme gefunden“, was falsch war. Sie integrierten deterministische Überprüfungstools für Format, Build und Tests in die Schleife und blockierten die Erstellung von Pull-Requests, sobald ein Überprüfungstool fehlschlug. Der Text des Agents war somit nicht mehr das Erfolgszeichen – stattdessen trat die strukturierte Ausgabe des Überprüfungstools an seine Stelle.
Andere Projekte gehen noch weiter. Ein Open-Source-Tool enthält entweder einen Beleg mit der Meldung „Erledigt, alle Tests bestanden“ oder kennzeichnet die Angabe als unüberprüft, und ein erfolgreiches Kommando allein führt niemals zu einer Aufwertung einer semantischen Angabe. Ein Forschungspaper formuliert die Regel wie folgt: Ableiten Sie die Angabe erneut und vertrauen Sie ihr nicht, da die Wortzählungen des Agents als Eingabe nichts wert sind.
In dem hier beschriebenen Stack ist die Überprüfung eine separate temporale Aktivität, wie zum Beispiel verify_po_exists oder verify_payment_initiated, die direkt in die Datenbank abfragt. Der Agent behauptet, er habe die Bestellbestätigung erstellt. Die Aktivität prüft die Bestelldatenbank, und falls die Bestellung fehlt, scheitert der Workflow unabhängig davon, was der Agent gemeldet hat.
Die Selbstberichterstattung ist das Signal, das am ehesten auf Fehler hinweist, die am schwierigsten zu erkennen sind, weshalb eine Überprüfung extern erfolgen muss. Der Agent namens create_po hat die Antwort analysiert und angegeben, dass alles funktioniert habe. Doch die Antwort hätte ein 200 OK mit einem Fehlerinhalt enthalten können, der Auftrag hätte erst erstellt und anschließend aufgrund einer Transaktionszeitüberschreitung rückgängig gemacht werden können, oder der Agent hätte ein falsches Feld gelesen und eine nie existierende ID gemeldet. Der Agent kennt nur, was das Tool zurückgegeben hat, nichts über den Zustand der Datenbank. Deshalb wird die Datenbank befragt.
Produktionsbelege bestätigen dies. Ein im Juli 2026 veröffentlichter Artikel untersuchte Leni, eine KI-Business-Analystin, deren Überprüfungszyklen mithilfe kleiner, auf bestimmte Aufgaben spezialisierter Modelle ausführen, beobachten, vergleichen und korrigieren. Das gesamte System übertraf sein Basismodell auf SpreadsheetBench um 11 Prozentpunkte – 91,25 Prozent gegenüber 80,25 Prozent. Allein die Überprüfung führte zu einem bescheidenen Zuwachs von 1,5 Punkten, doch diese Punkte lagen am oberen Ende des Score-Bereichs und ermöglichten die Bearbeitung von Aufgaben, die sonst fehlschlagen würden. Der größte Teil des Gesamtzuwachses stammte von Strukturmechanismen, Routenplanung und spezialisierten Modellen. Die Überprüfung rettet nicht jeden Fall – sie rettet lediglich die grenzwertigen Fälle, in denen der Agent eine Vollendung behauptet, während die Realität anders aussieht.
Das Trennungsprinzip
Die leitende Regel: Jede Schicht sollte testbar, ersetzbar und ohne die anderen verständlich sein.
- Der Graph kann durch emulierte Tool-Aufrufe überprüft werden, um zu bestätigen, dass die richtigen Knoten in der richtigen Reihenfolge ausgelöst werden.
- Das Harness kann durch direkten Aufruf der Tools sowie durch die Überprüfung der Schema-Validierung, Circuit Breakers und Idempotenz getestet werden.
- Der Loop kann mit einer einfachen Aktivität innerhalb eines Temporal-Workflows getestet werden, um zu überprüfen, ob er einen Absturz überlebt.
Das Ändern des Graphs beeinflusst das Harness nicht, das Austauschen der Tool-Implementierung beeinflusst den Graphen nicht, und die Anpassung der Wiederholungspolitik des Loops betrifft keines von beidem.
Der Nutzen ist praktisch. Das Graph des Sourcing-Agenten wurde dreimal geändert: ein Bewertungsknoten, eine auf Ablehnungen reagierende Wiederholungslogik sowie ein Überprüfungspunkt für menschliche Genehmigungen – ohne dass der Harness-Code angepasst werden musste. Der LLM-Anbieter wechselte von einer direkten Verbindung zu OpenAI auf eine Routing-Schicht, ohne den Graph-Code zu berühren. Die Wiederholungspolitik änderte sich von aggressiv zu strikt nicht wiederaufnahmefähig für auf Nebeneffekte beruhende Aktionen, ohne dass Graph oder Harness geändert wurden. Trennung bringt mehr Agilität als Reinheit: die Freiheit, etwas zu ändern, ohne drei andere Dinge zu stören.
Dieselbe Disziplin zahlt sich aus, wenn etwas schiefgeht. Ein fehlgeschlagener Ablauf kann durch das Stellen von drei präzisen Fragen auf eine bestimmte Ebene zurückgeführt werden. Hat ein Tool etwas zurückgegeben, was es niemals hätte zurückgeben dürfen, oder ist es an einen Ort gelangt, an den es niemals hätte gelangen dürfen? Das weist auf das Harness hin. Hat der Agent alle vernünftigen Grenzen überschritten? Das weist auf den Loop hin. Ist der Agent schließlich an einem Knoten angekommen, den er eigentlich nicht erreichen sollte, oder hat er einen Kontrollpunkt übersprungen? Das weist auf das Graph hin. Das Wechseln des Modells löst keines dieser Probleme.
Drei Fragen für jede Agent-Design
- Was kann es berühren? (Harness) Welche Tools existieren, mit welchen Schemata, wie viel Ausgangsverkehr ist erlaubt, wo liegen die Schwellenwerte des Circuit-Breakers und welche Idempotenzschlüssel gelten.
- Wann hört es auf? (Schleife) Die Schritt-Obergrenze, die Kostenobergrenze, die Zeitüberschreitung sowie die mechanische Stoppsbedingung, die nicht von der Beurteilung des LLM abhängt.
- Wohin kann es gehen? (Graph) Die Knoten, die Kanten, die bedingten Verzweigungen, die menschlichen Kontrollpunkte sowie die Zustandsformen, die zwischen den Knoten fließen.
Wenn die Antwort „das Modell entscheidet“ lautet, besteht ein Problem. Das LLM gibt Ratschläge, während der Code die Entscheidung trifft, das Framework sie durchsetzt, der Graph die Routen vorgibt und die Schleife die Grenzen setzt.
Kurz gesagt: Es gibt drei Ebenen, drei Verantwortliche und drei Fragen. Wenn man sie mischt, wirken die Fehler wie Probleme des Modells, obwohl das nicht der Fall ist. Hält man sie getrennt, bleibt die Unzuverlässigkeit des Modells in Schach, die Tools können sicher genutzt werden und der Agent stoppt, wenn nötig.
Verwandte Literatur
- Your Agent Graph Belongs in YAML: Compiling a 17-Agent LangGraph at Startup — Wie eine kleine Laufzeitumgebung eine YAML-Spezifikation in einen LangGraph StateGraph kompiliert: erzeugter Zustand, importlib-Plugins, Label-Routen, adaptive Aufrufe sowie Node-Austausche pro Sitzung.
- When the Dashboard Is Green and the Agent Is Wrong: Tracing State — Wie Agentfehler im zwischen gesunden Schritten übertragenen Zustand verborgen bleiben und welche Überwachungsmethoden, Laufzeitbeschränkungen, Routing-Einträge sowie Auswertungen erforderlich sind, um diese zu erfassen.