LangGraph Human-in-the-Loop: interrupt(), Zustandsänderungen und Zeitreisen
LangGraph – Mensch im Prozess: Freigabe von Tool-Aufrufen mithilfe von interrupt(), Hinzufügen von Feedback-Mechanismen, manuelle Bearbeitung des Zustands sowie Rückspulen mittels Zeitreise, inklusive funktionierender Beispiele.
Quellen: LangGraph Human-in-the-Loop: Pausing, Reviewing, and Rewinding Your Agent
Alles, was hier unten erklärt wird, setzt voraus, dass Sie mit der standardmäßigen siebenteiligen Struktur einer LangGraph-Datei (Imports, State, Tools, Nodes, Edges, Assembly, Entrypoint) vertraut sind und Checkpointer mit einem thread_id verwendet haben. Jedes der hier beschriebenen Muster basiert auf einem Checkpointer – ohne diesen funktioniert nichts davon.
Warum ein Mensch im Kontrollprozess bleiben muss
Sobald ein LLM in der Lage ist, zu handeln – sei es durch das Versenden einer E-Mail, das Löschen einer Datei, das Aufladen einer Karte oder das Hochladen von Code – kann es auch Fehler von erheblichem Ausmaß in Maschinengeschwindigkeit begehen, während niemand zuschaut. Human-in-the-Loop (HITL) ist keine zusätzliche Funktion, die einfach einem Agent hinzugefügt wird; es handelt sich dabei um ein Konzept, das sicherstellt, dass Agenten mit Werkzeugen aus der realen Welt sicher eingesetzt werden können. LangGraph wurde unter Berücksichtigung dieses Problems entwickelt. Sein Checkpointing-System dient hauptsächlich dazu, Pausen zu ermöglichen.
Vier Muster decken die meisten realen Systeme ab, sortiert nach ihrer Häufigkeit:
- Pause vor einer riskanten Aktion mit
interrupt(), dem aktuellen Standard. - Ein garantiertes Überprüfungspunkt in einem Workflow.
- Änderung des Zustands während der Pause, um den Agenten vor Fortsetzung zu korrigieren.
- Zeitreise: Wiedergabe und Abzweigung von jedem früheren Punkt in einem Gespräch.
Mehrere Elemente wurden absichtlich weggelassen, weil sie in echten Codebasen selten vorkommen: interne Token-Ebene-Streaming-Strukturen, das vollständige LangGraph Studio SDK sowie die ältere NodeInterrupt-Ausnahme, die durch interrupt() ersetzt wurde.
Unterbrechen und Fortsetzen
Jedes Muster lässt sich auf denselben zweistufigen Ablauf reduzieren:
1. interrupt() → Graph pauses. State is saved. Control returns to your code.
2. Command(resume=...) → Graph picks up exactly where it paused, with your input.
Alles andere sind Variationen: wo Sie interrupt() aufrufen, welche Daten Sie ihm übergeben und was Sie mit der Antwort machen.
Wortschatz
interrupt(value) wird aus langgraph.types importiert und innerhalb eines Knotens aufgerufen. Es stoppt den Graphen an dieser Stelle, speichert den gesamten Zustand über den Checkpointer und gibt value an die Person weiter, die den Ablauf gestartet hat. Die Pause ist real. Kein Thread arbeitet im Hintergrund und keine Ressourcen werden blockiert, sodass ein Thread monatelang in Pause bleiben kann.
Command(resume=value) wird in graph.invoke() oder graph.stream() eingesetzt, um einen pausierten Graphen wieder zum Laufen zu bringen. Der value wird zum Rückgabewert des interrupt()-Aufrufs, als wäre interrupt() eine synchrone input()-Funktion, die gerade eine Antwort erhalten hat.
__interrupt__ ist der Schlüssel, der in der Ausgabe erscheint, wenn die Ausführung pausiert wurde. Überprüfen Sie ihn, um herauszufinden, ob der Agent gestoppt hat und Eingabe benötigt.
Der Checkpointer (MemorySaver, SqliteSaver, PostgresSaver) ist zwingend erforderlich. Ohne ihn gibt es keinen gespeicherten Zustand, den man pausieren oder fortsetzen könnte. Der häufigste Grund dafür, dass der HITL-Code von Anfängern fehlschlägt, ist das Vergessen, das Diagramm mit einem solchen Checkpointer zu kompilieren.
Muster 1: Eine Toolaufruf genehmigen, bevor er ausgeführt wird
Das weitaus gebräuchlichste Szenario in der Praxis. Der Agent entscheidet sich, ein Tool aufzurufen, das tatsächliche Auswirkungen hat – wie das Versenden einer E-Mail, das Abfragen einer Datenbank oder das Platzieren einer Bestellung – und eine Person muss dies zunächst genehmigen.
Stellen Sie sich einen Manager vor, der einen Scheck mitunterschreibt. Der Mitarbeiter (der LLM) füllt den Scheck aus und übergibt ihn, und nichts geschieht, bis der Manager (der Mensch) unterschreibt. Wenn der Manager ablehnt, wird der Scheck ungültig.
Der Überprüfungsnode:
# ── IMPORTS needed ───────────────────────────────────────────
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver
# ── MODULE 4: NODES ───────────────────────────────────────────
def review_tool_call(state: AgentState) -> dict:
"""Pauses execution to let a human approve, edit, or reject a tool call
before it actually runs."""
last_message = state["messages"][-1]
tool_call = last_message.tool_calls[0] # Assume one tool call for simplicity
# interrupt() pauses HERE. The dict below is shown to whoever resumes the graph.
human_response = interrupt({
"question": "Approve this action?",
"tool_name": tool_call["name"],
"tool_args": tool_call["args"],
})
# Execution resumes here, with human_response being whatever
# was passed to Command(resume=...)
if human_response["type"] == "approve":
# Do nothing - let the tool call proceed as-is
return {}
elif human_response["type"] == "edit":
# Replace the tool call's arguments with the human's edits
last_message.tool_calls[0]["args"] = human_response["edited_args"]
return {"messages": [last_message]}
elif human_response["type"] == "reject":
# Short-circuit: skip the tool entirely, tell the LLM why
return {
"messages": [
ToolMessage(
content="User rejected this action.",
tool_call_id=tool_call["id"]
)
]
}
Dann schließen Sie es in das Diagramm ein:
graph_builder = StateGraph(AgentState)
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("review_tool_call", review_tool_call)
graph_builder.add_node("tools", tool_node)
graph_builder.set_entry_point("agent")
# Agent decides: call a tool (go through review) or finish
graph_builder.add_conditional_edges(
"agent",
should_continue,
{"tools": "review_tool_call", "__end__": END}
)
graph_builder.add_edge("review_tool_call", "tools")
graph_builder.add_edge("tools", "agent")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
Die Routierung unterscheidet sich leicht von der des Basis-Agenten: Eine Tool-Anfrage geht nun vor dem Tools-Node zu review_tool_call. Zwischen der Auswahl des Agenten und der tatsächlichen Ausführung steht der Prüfer, was dem Schritt der gemeinsamen Signierung vor der Auszahlung entspricht.
Ausführen und fortsetzen:
config = {"configurable": {"thread_id": "session-001"}}
# ── First call: runs until interrupt() pauses it ─────────────
result = graph.invoke(
{"messages": [HumanMessage(content="Send an email to the team about the delay")]},
config=config
)
# Check if the graph paused
if "__interrupt__" in result:
interrupt_data = result["__interrupt__"][0].value
print(f"Agent wants to: {interrupt_data['tool_name']}")
print(f"With arguments: {interrupt_data['tool_args']}")
# Show this to a human (in a real app: render a UI button, Slack message, etc.)
user_decision = input("Approve? (yes/no): ")
# ── Resume with the human's decision ──────────────────────
if user_decision.lower() == "yes":
final_result = graph.invoke(
Command(resume={"type": "approve"}),
config=config
)
else:
final_result = graph.invoke(
Command(resume={"type": "reject"}),
config=config
)
print(final_result["messages"][-1].content)
That's the whole pattern. What changes elsewhere is the content passed to interrupt() and what you do with the answer that comes back.
Pattern 2: a mandatory feedback gate
Sometimes you don't want a conditional pause depending on what the LLM chose. You want a pause at a fixed stage, every time. A content-generation agent that always stops for review before publishing is the usual example, regardless of what it wrote.
Think of a quality-control station on an assembly line. Each item stops there, no exceptions. The worker can pass it as is, send it back for rework or adjust it directly.
The feedback node:
def human_feedback(state: AgentState) -> dict:
"""A guaranteed pause point. Shows the human the latest draft
and lets them approve, request changes, or edit directly."""
draft = state["messages"][-1].content
feedback = interrupt({
"question": "Review this draft. Approve, or provide edit instructions.",
"draft": draft,
})
if feedback.get("approved"):
return {} # Pass through unchanged
# Feedback contains revision instructions — feed back to the agent
return {
"messages": [
HumanMessage(content=f"Please revise based on this feedback: {feedback['notes']}")
]
}
Und die Schaltung des Schleifensteuers:
graph_builder = StateGraph(AgentState)
graph_builder.add_node("writer", writer_node)
graph_builder.add_node("human_feedback", human_feedback)
graph_builder.set_entry_point("writer")
graph_builder.add_edge("writer", "human_feedback")
# After feedback, loop back to writer for revisions, OR end if approved
graph_builder.add_conditional_edges(
"human_feedback",
lambda state: "writer" if state["messages"][-1].content.startswith("Please revise") else "__end__",
{"writer": "writer", "__end__": END}
)
graph = graph_builder.compile(checkpointer=MemorySaver())
Das ist der Zyklus aus Überprüfung und Überarbeitung, der in nahezu jedem Inhalt-, Code-Review- oder Forschungsagenten vorkommt: Erstellen, eine Pause für einen Menschen einlegen, bei Bedarf überarbeiten, erneut pausieren und so lange wiederholen, bis die Zustimmung vorliegt.
Muster 3: Den Zustand manuell beheben
Manchmal möchte eine Person weder zustimmen noch ablehnen. Sie möchte etwas im Zustand des Agenten reparieren, bevor dieser weiterarbeitet, weil der Agent leicht fehlerhaft war und ein erneuter Versuch langsamer ist als die Korrektur.
Drei Aufrufe sind hier wichtig:
graph.get_state(config)gibt den aktuellenStateSnapshotzurück, inklusive.values(des Zustandsdictionaries) und.next(des Knotens, der bei Wiederaufnahme ausgeführt wird).graph.update_state(config, values)schreibt neue Werte am aktuellen Checkpoint ab. Dabei wird ein neuer Checkpoint erstellt, während die Historie unverändert bleibt – was später für Zeitreisen wichtig ist.as_node="node_name"ist ein optionales Argument fürupdate_state, das LangGraph mitteilt, dass die Aktualisierung von diesem Knoten ausgeht. LangGraph wählt die nächsten Kanten anhand des Quellknotens der letzten Aktualisierung aus, wodurch ein Fehler hierbei dazu führen kann, dass der Graph an einer unerwarteten Stelle fortgesetzt wird.
In der Praxis:
# ── Pause first (using interrupt_before at compile time) ────
graph = graph_builder.compile(
checkpointer=memory,
interrupt_before=["agent"] # Always pause before the agent node runs
)
config = {"configurable": {"thread_id": "session-002"}}
# Run until the breakpoint
result = graph.invoke({"messages": [HumanMessage(content="What's 5 + 5?")]}, config)
# ── Inspect what's about to happen ───────────────────────────
snapshot = graph.get_state(config)
print(f"About to run: {snapshot.next}") # e.g. ('agent',)
print(f"Current messages: {snapshot.values['messages']}")
# ── Directly correct the state ───────────────────────────────
# Suppose the human wants to inject a clarification before the agent responds
graph.update_state(
config,
{"messages": [HumanMessage(content="Actually, I meant 5 * 5, not 5 + 5")]},
)
# ── Resume - agent now sees the corrected message ────────────
final = graph.invoke(None, config) # None = "just resume, no new input"
print(final["messages"][-1].content)
Zwei Unterschiede führen oft zu Verwirrung. Erstens wird interrupt_before=[„node_name“] bei der Kompilierung gesetzt und veranlasst bei jedem Ausführungsvorgang immer zu einer Pause vor diesem Knoten, während interrupt() innerhalb eines Knotens liegt und nur dann pausiert, wenn es die Logik vorschreibt. Verwenden Sie interrupt_before für einfaches Debuggen oder garantierte Stopps, und interrupt(), wenn die Pause von Laufzeitbedingungen abhängt – was den Großteil der tatsächlichen Anwendungen abdeckt.
Zweitens bedeutet graph.invoke(None, config) „keine neuen Eingaben hinzufügen, von dem letzten Checkpoint fortsetzen“. Das unterscheidet sich von Command(resume=...), das auf einen ausstehenden interrupt() reagiert. Nach interrupt_before zusammen mit update_state sollte man mit None fortsetzen. Nach interrupt() hingegen mit Command(resume=...).
Muster 4: Zeitreise
Die Checkpoint-Geschichte funktioniert wie Commits in Git. Sie können jeden früheren Zustand ansehen und von dort aus einen neuen Zweig erstellen, um eine alternative Zeitlinie zu schaffen, ohne den ursprünglichen Zustand zu beeinträchtigen. Ein weiterer Vergleich sind die Speicherpunkte in Videospielen. get_state_history listet alle davon auf, und Sie können jeden beliebigen laden, nicht nur den neuesten.
graph.get_state_history(config)ist ein Generator, der jeden für den Thread gespeichertenStateSnapshotausgibt, wobei die neuesten zuerst kommen. Jeder Snapshot enthält seine eigeneconfigmit einer eindeutigencheckpoint_id.checkpoint_idbefindet sich beisnapshot.config[„configurable“][„checkpoint_id“]. Übergeben Sie einer Funktion wieinvokeoderupdate_stateeine Config-Datei mit einer bestimmtencheckpoint_id, dann wird diese spezifische Point angepeilt und nicht die neueste.
graph.invoke(None, config_with_old_checkpoint_id) aufzurufen. Dadurch wird der Ablauf ab diesem Punkt erneut ausgeführt. Für diesen Zweig bereits gespeicherte Checkpoints werden wiederverwendet anstelle neu berechnet, wobei die nach diesem Punkt folgende Knotenlogik erneut ablaufen muss.update_state anhand eines alten Checkpoints aufzurufen, wodurch ein neuer Checkpoint sowie eine neue Zeitlinie neben der ursprünglichen erstellt werden.config = {"configurable": {"thread_id": "session-003"}}
# Run a few turns normally...
graph.invoke({"messages": [HumanMessage(content="My name is Sam")]}, config)
graph.invoke({"messages": [HumanMessage(content="What's my name?")]}, config)
# ── Find a past checkpoint ────────────────────────────────────
history = list(graph.get_state_history(config))
for snapshot in history:
print(f"Step {snapshot.metadata['step']}: {len(snapshot.values['messages'])} messages")
# Pick an earlier checkpoint - say, right after "My name is Sam"
target_checkpoint = history[-2] # Adjust index based on what you printed above
target_config = target_checkpoint.config
# ── Branch: update state AT that old checkpoint ──────────────
# This creates a NEW checkpoint that forks from the old one.
# The original conversation history is untouched.
branch_config = graph.update_state(
target_config,
{"messages": [HumanMessage(content="Actually, my name is Alex")]},
)
# ── Continue from the new branch ──────────────────────────────
result = graph.invoke(None, branch_config)
print(result["messages"][-1].content)
# Agent now responds based on the BRANCHED history (name = Alex),
# while the original thread (name = Sam) still exists if you go back to it.
Die meisten Chatbots benötigen dies im Laufzeitbetrieb nicht. Sein Wert zeigt sich an drei Stellen: Bei der Fehlersuche – indem genau das Wiederholt wird, was bei einem fehlgeschlagenen Ablauf passiert ist, um den Fehlerort zu finden; bei der Bewertung – indem derselbe Dialog mit einer anderen Anfrage oder einem anderen Modell wiederholt und verglichen wird, was einer A/B-Testung ohne Wiederholung des gesamten Ablaufs entspricht; sowie bei der Fehlerbehebung – indem bei einem Ausfall mitten im Ablauf zurückgespult wird, bis kurz davor, die Ursache behoben und der Prozess fortgesetzt wird, anstatt den gesamten Dialog neu zu starten.
Ein vollständiger Agent mit Tool-Zulassung
Hier ist ein vollständiges Template, das die grundlegende Struktur, das Gedächtnis sowie die Zulassung für Tool-Aufrufe kombiniert. Es stellt die realistische Struktur eines Produktions-Agenten mit einem menschlichen Sicherheitsnetz dar.
# ============================================================
# LANGGRAPH AGENT WITH HUMAN-IN-THE-LOOP TOOL APPROVAL
# Extends: Part 1 (core structure) + Part 2 (memory)
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ── MODULE 2: STATE ──────────────────────────────────────────
class State(MessagesState):
pass # messages field is inherited; add custom fields if needed
# ── MODULE 3: TOOLS ──────────────────────────────────────────
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email. This is a SENSITIVE action requiring human approval."""
return f"Email sent to {to} with subject '{subject}'"
tools = [send_email]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: State) -> dict:
messages = [SystemMessage(content="You are a helpful assistant.")] + state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
def review_tool_call(state: State) -> dict:
"""Pauses for human approval before any tool executes."""
last_message = state["messages"][-1]
tool_call = last_message.tool_calls[0]
decision = interrupt({
"tool_name": tool_call["name"],
"tool_args": tool_call["args"],
})
if decision["type"] == "reject":
return {
"messages": [
ToolMessage(
content="Action rejected by user.",
tool_call_id=tool_call["id"]
)
]
}
if decision["type"] == "edit":
last_message.tool_calls[0]["args"] = decision["edited_args"]
return {"messages": [last_message]}
return {} # approved - pass through unchanged
# ── MODULE 5: ROUTING ─────────────────────────────────────────
def should_continue(state: State) -> Literal["review_tool_call", "__end__"]:
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "review_tool_call"
return "__end__"
def after_review(state: State) -> Literal["tools", "agent"]:
"""If the tool call was rejected, the last message is a ToolMessage -
skip the tools node and go straight back to the agent."""
last_message = state["messages"][-1]
if isinstance(last_message, ToolMessage):
return "agent"
return "tools"
# ── MODULE 6: GRAPH ASSEMBLY ──────────────────────────────────
graph_builder = StateGraph(State)
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("review_tool_call", review_tool_call)
graph_builder.add_node("tools", tool_node)
graph_builder.add_edge(START, "agent")
graph_builder.add_conditional_edges(
"agent", should_continue,
{"review_tool_call": "review_tool_call", "__end__": END}
)
graph_builder.add_conditional_edges(
"review_tool_call", after_review,
{"tools": "tools", "agent": "agent"}
)
graph_builder.add_edge("tools", "agent")
graph = graph_builder.compile(checkpointer=MemorySaver())
# ── MODULE 7: ENTRYPOINT ──────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "session-001"}}
result = graph.invoke(
{"messages": [HumanMessage(content="Email bob@example.com that the meeting moved to 3pm")]},
config
)
while "__interrupt__" in result:
interrupt_info = result["__interrupt__"][0].value
print(f"\n Approval needed:")
print(f" Tool: {interrupt_info['tool_name']}")
print(f" Args: {interrupt_info['tool_args']}")
choice = input("Approve (a) / Edit (e) / Reject (r)? ").strip().lower()
if choice == "a":
result = graph.invoke(Command(resume={"type": "approve"}), config)
elif choice == "e":
new_to = input("New 'to' address: ")
result = graph.invoke(
Command(resume={
"type": "edit",
"edited_args": {**interrupt_info["tool_args"], "to": new_to}
}),
config
)
else:
result = graph.invoke(Command(resume={"type": "reject"}), config)
print(f"\nAgent: {result['messages'][-1].content}")
Schnellreferenz
Pause und Wiederaufnahme. interrupt(value) pausiert innerhalb eines Knotens, speichert den Zustand und gibt value an den Aufrufer weiter, um später den Wert zur Wiederaufnahme zurückzugeben. Command(resume=value) beantwortet eine ausstehende Unterbrechung. „__interrupt__“ ist die Ausgabetaste, die eine Pause signalisiert, und result[„__interrupt__“][0].value zeigt an, was angezeigt wurde. interrupt_before=[„node_name“] pausiert immer vor dem benannten Knoten.
Bearbeitungszustand. graph.get_state(config) gibt einen StateSnapshot (.values, .next, .config) zurück. graph.update_state(config, values) fügt einen Checkpoint hinzu, ohne die Historie zu löschen. as_node bestimmt, welche Kanten beim Wiederaufnehmen berücksichtigt werden. graph.invoke(None, config) setzt die Ausführung ohne neue Eingaben fort.
Zeitreise. graph.get_state_history(config) listet die Snapshots vom Neuesten an. checkpoint_id identifiziert einen Speicherpunkt. Das Wiedergeben setzt die Zeitlinie an einem alten Checkpoint fort, während eine Abzweigung einen neuen erstellt.
Welches Muster passt
- Tools, die Geld ausgeben, Nachrichten senden oder externe Systeme ändern: Freigabe mit
interrupt(). Bei Produktionsagenten mit tatsächlichen Nebeneffekten sollte dies als erforderlich angesehen werden. - Ein Inhaltsschritt, der stets vor Weiterleitung des Workflows überprüft werden muss: der spezielle Feedback-Node mit einer Überarbeitungsschleife.
- Eine Person, die mitten im Gespräch Informationen korrigiert oder hinzufügt, ohne formelle Genehmigungsstufe:
interrupt_beforezusammen mitupdate_state. - Debugging eines fehlgeschlagenen Ablaufs, Testen von Prompt-Änderungen oder Wiederherstellen aus einem Fehler ohne Neustart: Zeitreise mithilfe von
get_state_history.
Sicherheit ist die Form des Graphen
Erinnerung, Zusammenfassung, Persistenz und menschliche Überwachung werden in LangGraph alle als Struktur dargestellt: ein Node hier, eine Kante dort, ein Feld im Zustand. Es gibt keinen speziellen Sicherheitsmodus, den man einschalten muss. Sicherheit ist einfach eine weitere Form, die der Graph annehmen kann.
Agenten versagen in der Praxis nur selten wegen einer falschen Entscheidung des LLM, was erwartet werden kann und behoben werden kann. Sie versagen, wenn eine falsche Entscheidung keinen Zwischenpunkt zwischen der Entscheidung und ihren Folgen aufweist. interrupt() ist dieser Zwischenpunkt. Er kostet einen zusätzlichen Knoten und eine zusätzliche Kante und kann der wertvollste Knoten im Graphen sein.
Mit einer Standardstruktur, einer Methode, um Gespräche kostengünstig und dauerhaft zu halten, sowie einem Sicherheitsnetz für alles Wichtige verfügt man über die Grundlage für nahezu jede ernsthafte LangGraph-Anwendung. Die natürlichen nächsten Themen sind die Mehr-Agenten-Orchestrierung mit Aufsichtsknoten und Untergraphen sowie Langzeitgedächtnis wie Benutzerprofile über verschiedene Sitzungen – beides basiert auf Zuständen, Knoten und Zwischenpunkten.
Verwandte Literatur
- MCP für LangGraph Agents, Schritt für Schritt: Server, Transportmechanismen und der Adapter-Brückenelement — Lernen Sie das Model Context Protocol aus der Sicht von Host, Client und Server sowie einen FastMCP-Server, den Unterschied zwischen stdio und HTTP, langchain-mcp-adapters und typische Probleme in der Produktion bei LangGraph.
- LangGraph erklärt: Graphen, Zustände, Routing und Produktionsmuster — Wie LangGraph Agenten als zyklische Graphen mit typisierten Zuständen, Reduzierfunktionen, Checkpoints, Routing-Strategien, Werkzeugen sowie ReAct-Schleifen modelliert, zusammen mit Mustern für den Einsatz in der Produktion.