StateGraph.compile: Die Blaupause in ein Pregel-Laufzeit-Objekt übersetzen
Verantwortung
StateGraph.compile ist der «Übersetzer» zwischen Builder und Laufzeit. Es nimmt einen StateGraph (Knoten-Specs, Kanten, Verzweigungsbedingungen, Channel-Dict) und gibt ein CompiledStateGraph zurück (CompiledStateGraph:1391) – Letzteres erbt von Pregel und bringt alle Laufzeit-Methoden wie invoke / stream / astream / get_state mit sich. Vor dem compile ist der Graph nur Konfigurationsdaten; erst danach wird er zu einem ausführbaren Objekt.
Seine Kernaufgabe besteht aus vier Dingen: 1) Graph-Struktur validieren (validate), 2) Ausgabe- und Streaming-Channels festlegen, 3) Laufzeit-Abhängigkeiten injizieren (Checkpointer / Store / Cache / Interrupt-Konfiguration), 4) jeden StateNodeSpec / jede edge / jeden BranchSpec in PregelNode + Channel-Abonnements übersetzen (Konstruktion CompiledStateGraph:1333-1357). Die Übersetzung läuft als Schleife über compiled.attach_node / compiled.attach_edge / compiled.attach_branch – diese Methoden wandeln die deklarativen Datenstrukturen des Builders in Laufzeitfelder wie PregelNode.writers / PregelNode.triggers um.
compile übernimmt noch zwei Nebenpflichten: 1) Für alle Knoten ohne explizite Policy die Defaults aus set_node_defaults anwenden (retry / cache / error_handler / timeout), 2) Bei aktiviertem Strict-msgpack-Serialisierer (_serde.STRICT_MSGPACK_ENABLED) eine Serde-Allowlist aufbauen, damit der Checkpointer nur Felder serialisiert, die im Schema vorkommen (serde allowlist:1220-1241).
Entwurfsmotivation
Warum nicht direkt StateGraph selbst zu einem Pregel machen, sondern der Umweg in zwei Schritten?
- Trennung von Builder- und Laufzeit-Zuständigkeit: In der Builder-Phase geht es nur um «wie sieht der Graph aus», in der Laufzeit-Phase um «wie wird er ausgeführt». Getrennt kann der Builder mehrfach zu unterschiedlichen Laufzeit-Objekten kompiliert werden – derselbe StateGraph mit unterschiedlichen Checkpointern / Stores / Interrupt-Knoten liefert mehrere Pregel-Instanzen.
- Unveränderliches Laufzeit-Objekt: Die Felder des
CompiledStateGraphnach dem compile sind inPregel.__init__im Wesentlichen fest (Pregel.__init__:758-836);auto_validate=Falseerlaubt es dem compile, selbst zu steuern, wann validiert wird, und eine frühe Validierung im Konstruktor zu vermeiden. - Verzögerte Validierung: Beim
add_nodedes Nutzers wird nur in einen Container appended, ohne sofort zu prüfen, ob der Startpunkt einer Kante existiert – das erlaubt, Knoten und Kanten in beliebiger Reihenfolge zu registrieren. Die eigentliche Validierung findet erst beim compile in einem Rutsch statt (validate Aufruf:1247-1254). - Interrupt-Konfiguration als Parameter:
interrupt_before/interrupt_aftersind Parameter von compile, keine Eigenschaften des Graphen selbst – derselbe Graph lässt sich in eine Variante «mit Interrupt» und eine «ohne Interrupt» kompilieren, was in Mensch-Maschine-Szenarien häufig ist (interrupt Parameter:1170-1171). - Normalisierung des Checkpointer-Typs:
compile(checkpointer=...)akzeptiertNone/True/False/BaseCheckpointSaver;ensure_valid_checkpointer(ensure_valid_checkpointer:107-117) validiert einheitlich und vermeidet Fehler zur Laufzeit. - Defaults werden erst zur Compile-Zeit angewandt (
defaults Anwendung:1299-1325) – so kann der Nutzer nachadd_nodenochset_node_defaultsaufrufen, und die neuen Defaults überschreiben alle Knoten ohne explizite Angabe.
Schlüsseldateien
compile Signatur:1164-1217— Empfängtcheckpointer/store/cache/interrupt_before/interrupt_after/debug/name/transformers.ensure_valid_checkpointer:107-117— Validiert, dass der CheckpointerNone/True/False/BaseCheckpointSaverist, sonstTypeError.ensure_valid_checkpointer Aufruf:1218— Erste Aktion in compile ist die Normalisierung des Checkpointers.serde allowlist:1220-1241— Baut unter Strict-msgpack eine Allowlist und wendet sie auf den Checkpointer an, sodass der Checkpoint nur Schema-Felder speichert.interrupt Merge + validate:1243-1254—"*"bedeutet All (alle Knoten);interrupt_before/interrupt_afterwerden zu einer Liste zusammengeführt und anvalidateübergeben.output / stream channels:1256-1273— Bei einem einzelnen Feld, das__root__ist, wird direkt ein String verwendet, sonst eine Liste ohne Managed Values.Default error handler Knoten:1278-1297— Der globale error_handler ausset_node_defaultswird als spezieller Knoten namens__default_error_handler__injiziert.defaults Anwendung:1299-1325— Wendet retry / cache / error_handler / timeout ausset_node_defaultsauf jeden Spec ohne explizite Angabe an; cache und error_handler werden nicht auf den error-handler-Knoten selbst angewandt.node_error_handler_map:1327-1331— Erzeugt die Abbildungnode_name -> handler_node_name, anhand derer die Laufzeit den fehlgeschlagenen Knoten an den passenden Handler weiterleitet.Konstruktion CompiledStateGraph:1333-1357— Führtchannels/manageddes Builders zusammen, fügtSTART: EphemeralValue(input_schema)als Eingangschannel hinzu und setztstream_mode="updates",input_channels=START.attach Trilogie:1360-1388— Ruft in Schleifencompiled.attach_node(START, None)+ jeden Knoten,attach_edgefür jede Kante undattach_branchfür jede Verzweigung auf; abschließendcompiled.validate().Pregel.__init__:758-836—CompiledStateGraph.__init__delegiert übersuper().__init__(**kwargs)an Pregel; hier werden alle Laufzeitfelder entfaltet und beiauto_validate=Trueself.validate()aufgerufen.
Datenfluss
Nachstehend der zentrale Abschnitt, in dem compile das Laufzeit-Objekt konstruiert – alle Felder des Builders werden in CompiledStateGraph gesteckt, zusammen mit einem START-Eingangschannel:
compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
builder=self,
schema_to_mapper={},
context_schema=self.context_schema,
nodes={},
channels={
**self.channels,
**self.managed,
START: EphemeralValue(self.input_schema),
},
input_channels=START,
stream_mode="updates",
output_channels=output_channels,
stream_channels=stream_channels,
checkpointer=checkpointer,
interrupt_before_nodes=interrupt_before,
interrupt_after_nodes=interrupt_after,
auto_validate=False,
debug=debug,
store=store,
cache=cache,
node_error_handler_map=node_error_handler_map,
name=name or "LangGraph",
stream_transformers=transformers,
)
compiled._serde_allowlist = serde_allowlist
compiled.attach_node(START, None)
for key, node in self.nodes.items():
compiled.attach_node(key, node)(Konstruktion + attach_node:1333-1362)
Anschließend werden auch Kanten und Verzweigungen in Pregel-Channel-Abonnements übersetzt und einmal validiert:
for start, end in self.edges:
compiled.attach_edge(start, end)
for starts, end in self.waiting_edges:
compiled.attach_edge(starts, end)
for start, branches in self.branches.items():
for name, branch in branches.items():
compiled.attach_branch(start, name, branch)
return compiled.validate()(attach edge/branch:1378-1388)
Nachdem Pregel.__init__ diese Felder erhalten hat, wandelt es die NodeBuilder in nodes in PregelNode um, installiert für den TASKS-Channel ein Topic(Send, accumulate=False) und ruft bei auto_validate=True self.validate() auf (Pregel init:800-836):
self.nodes = {
k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items()
}
self.channels = channels or {}
if TASKS in self.channels and not isinstance(self.channels[TASKS], Topic):
raise ValueError(
f"Channel '{TASKS}' is reserved and cannot be used in the graph."
)
else:
self.channels[TASKS] = Topic(Send, accumulate=False)Grenzen und Fehler
checkpointer=Trueist für den Root-Graphen unzulässig (True Fehler:2583-2584) –Truebedeutet «vom Eltern-Graphen erben» und ist nur für Subgraphen erlaubt; der Root-Graph muss explizit einen Saver oderFalseangeben.interrupt_before="*"undinterrupt_after="*"werden asymmetrisch behandelt (* Behandlung:1249-1253) –interrupt_beforewird nur dann in die interrupt-Liste eingefügt, wenninterrupt_after != "*"; das ist eine Präzedenzregel für die Verwendung von*als «alle Knoten».TASKSist als Channel-Name reserviert (TASKS reserviert:804-807) – ein Feld namens__pregel_tasksim Schema führt zu einem direkten Fehler; es ist der interne Topic-Channel des Engines zum Auffächern vonSend.- Doppeltes attach desselben Knotennamens: Der Builder-Block hat bereits Gleiche Namen blockiert (
重名:792-793) – aber auch der automatisch generierte Name__default_error_handler__darf nicht mit einem Nutzernamen kollidieren (default handler Konflikt:1280-1284). - cache und error_handler-Defaults werden nicht auf den Handler-Knoten selbst angewandt (
cache nicht an handler:1313-1321) – «Handler-Ergebnisse cachen» ist unsicher (der State des fehlgeschlagenen Knotens kann jedes Mal anders sein), und «Handler greift sich selbst» führt zu Endlosschleifen. - Derselbe Builder darf mehrfach kompiliert werden: Jedes compile erzeugt ein neues
CompiledStateGraph; das Feldcompileddes Builders wird invalidate()zwar aufTruegesetzt (compiled=True:1161), ist aber nur ein Flag und hindert weder ein erneutes compile noch ein fortgesetztesadd_node(allerdings mit Warnung).
Zusammenfassung
compile ist die Grenze zwischen Builder und Laufzeit – es übersetzt die in StateGraph über add_node / add_edge / add_conditional_edges angesammelten Specs vollständig in Pregel-Laufzeit-Strukturen, bindet Checkpointer / Store / Interrupt-Konfiguration an und gibt schließlich eine Pregel-Subklasse zurück, die direkt invoke-fähig ist. Wer das verstanden hat, hat die gesamte Kette «Graphdeklaration → ausführbares Objekt» verstanden.
Als Nächstes lohnt sich ein Blick auf Pregel 引擎, wie nach Pregel.__init__ invoke / stream dieses Kompilat ausführen, oder auf StateSnapshot, wie man aus dem Kompilat samt Checkpointer den Zustand ausliest.
Siehe offizielle Dokumentation: LangGraph 文档 · README.