State Management
Zustandsverwaltung in komplexen Workflows mit LangGraph
Inhaltsverzeichnis
- Kurzüberblick: Warum State Management?
- Grundkonzepte
- TypedDict vs. Pydantic
- Reducer-Funktionen
- State in LangGraph
- Praktische Beispiele
- Best Practices
- Häufige Fehler
- Zusammenfassung
- Abgrenzung zu verwandten Dokumenten
Kurzüberblick: Warum State Management?
Ein einfacher Chatbot benötigt keinen komplexen Zustand – die letzte Nachricht reicht. Doch sobald Workflows mehrere Schritte umfassen, Tools aufrufen oder Entscheidungen treffen, wird die zentrale Verwaltung von Zustandsdaten unverzichtbar.
Typische Herausforderungen ohne strukturiertes State Management:
| Problem | Auswirkung |
|---|---|
| Daten gehen zwischen Schritten verloren | Workflow bricht ab oder liefert falsche Ergebnisse |
| Unklare Datenstruktur | Fehler erst zur Laufzeit erkennbar |
| Parallele Änderungen | Überschreibungen und Inkonsistenzen |
| Debugging erschwert | Unklar, welcher Schritt welchen Zustand verändert hat |
State Management löst diese Probleme durch:
- Zentrale Datenstruktur – alle Komponenten arbeiten mit demselben State
- Typisierung – Fehler werden früh erkannt (IDE-Unterstützung, Autocomplete)
- Reducer-Funktionen – kontrollierte Aktualisierung (z.B. Nachrichten anhängen statt überschreiben)
- Nachvollziehbarkeit – jeder Schritt dokumentiert seine Änderungen
Grundkonzepte
Was ist “State”?
Der State ist ein zentrales Datenobjekt, das alle relevanten Informationen eines Workflows enthält. Er wird von Node zu Node weitergereicht und dabei transformiert.
[Node A] → State → [Node B] → State' → [Node C] → State'' → ...
Eigenschaften eines guten States
| Eigenschaft | Beschreibung |
|---|---|
| Minimal | Nur speichern, was tatsächlich benötigt wird |
| Typisiert | Klare Datentypen für jedes Feld |
| Immutable-freundlich | Änderungen erzeugen neue Versionen, kein Überschreiben |
| Serialisierbar | Für Checkpointing und Debugging speicherbar |
Beispiel: Einfacher Chat-State
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class ChatState(TypedDict):
messages: Annotated[list, add_messages] # Chat-Verlauf
user_id: str # Benutzeridentifikation
step_count: int # Zähler für Debugging
TypedDict vs. Pydantic
Für State-Definitionen stehen zwei Hauptansätze zur Verfügung. Die Wahl hängt vom Einsatzzweck ab.
TypedDict – Empfohlen für internen State
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class WorkflowState(TypedDict):
messages: Annotated[list, add_messages]
context: str
approved: bool
Vorteile:
- Teil der Python-Standardbibliothek
- Kein Runtime-Overhead (keine Validierung)
- Perfekt für State Machines
- Von LangGraph empfohlen
Pydantic BaseModel – Für Schnittstellen
from pydantic import BaseModel, Field
class UserInput(BaseModel):
query: str = Field(description="Die Benutzerfrage")
temperature: float = Field(default=0.7, ge=0, le=2)
Vorteile:
- Strikte Validierung zur Laufzeit
- Automatische Typkonvertierung
- Ideal für API-Eingaben und strukturierte LLM-Ausgaben
Entscheidungshilfe
| Kriterium | TypedDict | Pydantic |
|---|---|---|
| Performance | ⭐⭐⭐ Schnell | ⭐⭐ Langsamer |
| Validierung | Keine (nur Typen) | Strikt zur Laufzeit |
| LangGraph State | ✅ Empfohlen | ⚠️ Möglich, aber Overhead |
| LLM-Ausgaben | ⚠️ Keine Validierung | ✅ with_structured_output() |
| API-Eingaben | ⚠️ Unsicher | ✅ Validiert automatisch |
[!TIP] Faustregel
TypedDict für Graph-State, Pydantic für Ein-/Ausgaben.
Reducer-Funktionen
[!DANGER] Ohne Reducer werden State-Werte überschrieben
Jeder Node-Return ersetzt das gesamte Feld — beimessagesgehen so alle vorherigen Nachrichten verloren.Annotated[list, add_messages]ist kein optionaler Komfort, sondern notwendig für korrektes State-Management.
Reducer bestimmen, wie State-Felder aktualisiert werden. Ohne Reducer wird ein Feld bei jeder Änderung überschrieben. Mit Reducer können Werte intelligent kombiniert werden.
Das Problem ohne Reducer
# Ohne Reducer: Überschreiben
state = {"messages": ["Hallo"]}
# Node A gibt zurück:
{"messages": ["Wie geht's?"]}
# Ergebnis: messages = ["Wie geht's?"] ← "Hallo" ist weg!
Die Lösung mit add_messages
from typing import Annotated
from langgraph.graph.message import add_messages
class ChatState(TypedDict):
messages: Annotated[list, add_messages] # Reducer aktiviert
# Mit Reducer: Anhängen
state = {"messages": ["Hallo"]}
# Node A gibt zurück:
{"messages": ["Wie geht's?"]}
# Ergebnis: messages = ["Hallo", "Wie geht's?"] ← Beide erhalten!
Eingebaute Reducer
| Reducer | Verhalten | Anwendung |
|---|---|---|
add_messages | Fügt Nachrichten hinzu, dedupliziert nach ID | Chat-Verläufe |
operator.add | Addiert Werte (Listen, Zahlen) | Zähler, Log-Listen |
Beispiel: Eigener Reducer
from typing import Annotated
import operator
class AnalysisState(TypedDict):
messages: Annotated[list, add_messages]
findings: Annotated[list, operator.add] # Ergebnisse sammeln
total_tokens: Annotated[int, operator.add] # Token-Zähler addieren
Visualisierung: Reducer in Aktion
Initial State:
messages: []
findings: []
total_tokens: 0
Nach Node 1:
return {"messages": [msg1], "findings": ["Fund A"], "total_tokens": 100}
State nach Node 1:
messages: [msg1]
findings: ["Fund A"]
total_tokens: 100
Nach Node 2:
return {"messages": [msg2], "findings": ["Fund B", "Fund C"], "total_tokens": 150}
State nach Node 2:
messages: [msg1, msg2] ← add_messages
findings: ["Fund A", "Fund B", "Fund C"] ← operator.add
total_tokens: 250 ← operator.add (100 + 150)
State in LangGraph
LangGraph nutzt State als zentrales Element für Workflows. Jeder Node empfängt den aktuellen State und gibt Änderungen zurück.
Grundstruktur
from langgraph.graph import StateGraph, START, END
# State definieren
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
current_task: str
completed: bool
# Node-Funktion: Empfängt State, gibt Änderungen zurück
def process_node(state: AgentState) -> AgentState:
# Lese aus State
task = state["current_task"]
# Verarbeite...
result = do_something(task)
# Gib NUR die Änderungen zurück
return {
"messages": [result],
"completed": True
}
# Graph erstellen
graph = StateGraph(AgentState)
graph.add_node("process", process_node)
graph.add_edge(START, "process")
graph.add_edge("process", END)
app = graph.compile()
Wichtige Prinzipien
Nodes geben nur Änderungen zurück:
# ✅ Richtig: Nur geänderte Felder
def good_node(state: AgentState) -> AgentState:
return {"completed": True} # Nur was sich ändert
# ❌ Falsch: Gesamten State kopieren
def bad_node(state: AgentState) -> AgentState:
return {
"messages": state["messages"], # Unnötig
"current_task": state["current_task"], # Unnötig
"completed": True
}
State ist typsicher:
def typed_node(state: AgentState) -> AgentState:
# IDE zeigt Autocomplete für state["..."]
messages = state["messages"] # ✅ Typ: list
task = state["current_task"] # ✅ Typ: str
# Fehler werden früh erkannt
# state["invalid_field"] # ❌ IDE warnt
Praktische Beispiele
Beispiel: Mehrstufiger Analyse-Workflow
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain.chat_models import init_chat_model
# State mit mehreren Feldern
class AnalysisState(TypedDict):
messages: Annotated[list, add_messages]
document: str
summary: str
sentiment: str
keywords: list[str]
analysis_complete: bool
# LLM initialisieren
llm = init_chat_model("openai:gpt-4o-mini", temperature=0.0)
# Node 1: Zusammenfassung erstellen
def summarize_node(state: AnalysisState) -> AnalysisState:
doc = state["document"]
response = llm.invoke(f"Fasse zusammen: {doc}")
return {"summary": response.content}
# Node 2: Sentiment analysieren
def sentiment_node(state: AnalysisState) -> AnalysisState:
summary = state["summary"]
response = llm.invoke(f"Bestimme das Sentiment: {summary}")
return {"sentiment": response.content}
# Node 3: Keywords extrahieren
def keywords_node(state: AnalysisState) -> AnalysisState:
doc = state["document"]
response = llm.invoke(f"Extrahiere 5 Keywords: {doc}")
keywords = response.content.split(", ")
return {"keywords": keywords, "analysis_complete": True}
# Graph aufbauen
graph = StateGraph(AnalysisState)
graph.add_node("summarize", summarize_node)
graph.add_node("sentiment", sentiment_node)
graph.add_node("keywords", keywords_node)
graph.add_edge(START, "summarize")
graph.add_edge("summarize", "sentiment")
graph.add_edge("sentiment", "keywords")
graph.add_edge("keywords", END)
app = graph.compile()
# Ausführen
initial_state = {
"messages": [],
"document": "Ein langer Text...",
"summary": "",
"sentiment": "",
"keywords": [],
"analysis_complete": False
}
result = app.invoke(initial_state)
Beispiel: Bedingtes Routing basierend auf State
class RouterState(TypedDict):
messages: Annotated[list, add_messages]
query_type: str # "technical", "billing", "general"
response: str
def classify_node(state: RouterState) -> RouterState:
# Klassifiziere die Anfrage
query = state["messages"][-1].content
# ... Klassifizierungslogik ...
return {"query_type": "technical"}
def route_by_type(state: RouterState) -> str:
"""Routing-Funktion: Liest State und gibt Ziel-Node zurück."""
query_type = state["query_type"]
if query_type == "technical":
return "tech_agent"
elif query_type == "billing":
return "billing_agent"
else:
return "general_agent"
# Graph mit bedingtem Routing
graph = StateGraph(RouterState)
graph.add_node("classify", classify_node)
graph.add_node("tech_agent", tech_handler)
graph.add_node("billing_agent", billing_handler)
graph.add_node("general_agent", general_handler)
graph.add_edge(START, "classify")
graph.add_conditional_edges(
"classify",
route_by_type,
{
"tech_agent": "tech_agent",
"billing_agent": "billing_agent",
"general_agent": "general_agent"
}
)
Best Practices
State-Design
| Empfehlung | Begründung |
|---|---|
| Flache Strukturen bevorzugen | Einfacher zu debuggen und serialisieren |
| Aussagekräftige Feldnamen | user_query statt q |
| Optionale Felder vermeiden | Lieber Defaults setzen |
| Keine sensiblen Daten | PII gehört nicht in den State |
Reducer-Nutzung
# ✅ Empfohlen: Reducer für akkumulierende Felder
class GoodState(TypedDict):
messages: Annotated[list, add_messages]
logs: Annotated[list, operator.add]
# ⚠️ Vorsicht: Ohne Reducer werden Werte überschrieben
class RiskyState(TypedDict):
messages: list # Kann unbeabsichtigt überschrieben werden
Node-Design
# ✅ Node gibt nur Änderungen zurück
def good_node(state: MyState) -> MyState:
new_value = process(state["input"])
return {"output": new_value}
# ✅ Fehlerbehandlung im Node
def safe_node(state: MyState) -> MyState:
try:
result = risky_operation()
return {"result": result, "error": None}
except Exception as e:
return {"result": None, "error": str(e)}
Debugging
# State-Änderungen loggen
def debug_node(state: MyState) -> MyState:
print(f"[DEBUG] Eingehender State: {state}")
result = process(state)
print(f"[DEBUG] Rückgabe: {result}")
return result
Häufige Fehler
Fehler: Gesamten State zurückgeben
# ❌ Falsch
def bad_node(state: MyState) -> MyState:
state["new_field"] = "value"
return state # Gibt alles zurück, auch Ungeändertes
# ✅ Richtig
def good_node(state: MyState) -> MyState:
return {"new_field": "value"} # Nur die Änderung
Fehler: Reducer vergessen
# ❌ Problem: Nachrichten werden überschrieben
class BadState(TypedDict):
messages: list # Kein Reducer!
# ✅ Lösung: add_messages verwenden
class GoodState(TypedDict):
messages: Annotated[list, add_messages]
Fehler: State mutieren statt neue Werte zurückgeben
[!WARNING] State-Mutation erzeugt inkonsistente Checkpoints
Direkte Mutation des State-Objekts umgeht LangGraphs Reducer-Mechanismus und kann zu unerwartetem Verhalten beim Checkpointing führen.
# ❌ Falsch: In-Place-Mutation
def mutating_node(state: MyState) -> MyState:
state["items"].append("new") # Mutiert Original!
return {"items": state["items"]}
# ✅ Richtig: Neue Liste erstellen
def pure_node(state: MyState) -> MyState:
new_items = state["items"] + ["new"] # Neue Liste
return {"items": new_items}
Fehler: Untypisierter State
# ❌ Falsch: dict ohne Typen
graph = StateGraph(dict) # Keine IDE-Unterstützung
# ✅ Richtig: TypedDict mit Typen
class TypedState(TypedDict):
messages: Annotated[list, add_messages]
count: int
graph = StateGraph(TypedState) # Volle IDE-Unterstützung
Zusammenfassung
State Management bildet das Rückgrat komplexer KI-Workflows. Die wichtigsten Punkte:
| Konzept | Kernaussage |
|---|---|
| State | Zentrales Datenobjekt für alle Workflow-Informationen |
| TypedDict | Empfohlen für Graph-State (leichtgewichtig, typsicher) |
| Pydantic | Für Ein-/Ausgaben und LLM-Strukturierung |
| Reducer | Kontrollieren, wie Felder aktualisiert werden |
| add_messages | Standard-Reducer für Chat-Verläufe |
| Nodes | Geben nur Änderungen zurück, nicht den gesamten State |
Quick Reference
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# State definieren
class MyState(TypedDict):
messages: Annotated[list, add_messages]
data: str
# Node erstellen
def my_node(state: MyState) -> MyState:
return {"data": "processed"}
# Graph bauen
graph = StateGraph(MyState)
graph.add_node("process", my_node)
graph.add_edge(START, "process")
graph.add_edge("process", END)
app = graph.compile()
# Ausführen
result = app.invoke({"messages": [], "data": ""})
Abgrenzung zu verwandten Dokumenten
| Dokument | Inhalt |
|---|---|
| Checkpointing & Persistenz | Speicherung und Wiederherstellung des States über Sessions |
| Memory-Systeme | Langzeitgedächtnis als Ergänzung zum kurzlebigen Graph-State |
| Multi-Agent-Systeme | State-Übergabe zwischen mehreren Agenten im Graph |
Version: 1.0
Stand: November 2025
Kurs: KI-Agenten. Verstehen. Anwenden. Gestalten.