Skip to content

StateGraph.compile: Die Blaupause in ein Pregel-Laufzeit-Objekt übersetzen

源码版本1.2.9

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 CompiledStateGraph nach dem compile sind in Pregel.__init__ im Wesentlichen fest (Pregel.__init__:758-836); auto_validate=False erlaubt es dem compile, selbst zu steuern, wann validiert wird, und eine frühe Validierung im Konstruktor zu vermeiden.
  • Verzögerte Validierung: Beim add_node des 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_after sind 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=...) akzeptiert None / 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 nach add_node noch set_node_defaults aufrufen, und die neuen Defaults überschreiben alle Knoten ohne explizite Angabe.

Schlüsseldateien

  • compile Signatur:1164-1217 — Empfängt checkpointer / store / cache / interrupt_before / interrupt_after / debug / name / transformers.
  • ensure_valid_checkpointer:107-117 — Validiert, dass der Checkpointer None / True / False / BaseCheckpointSaver ist, sonst TypeError.
  • 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_after werden zu einer Liste zusammengeführt und an validate ü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 aus set_node_defaults wird als spezieller Knoten namens __default_error_handler__ injiziert.
  • defaults Anwendung:1299-1325 — Wendet retry / cache / error_handler / timeout aus set_node_defaults auf 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 Abbildung node_name -> handler_node_name, anhand derer die Laufzeit den fehlgeschlagenen Knoten an den passenden Handler weiterleitet.
  • Konstruktion CompiledStateGraph:1333-1357 — Führt channels / managed des Builders zusammen, fügt START: EphemeralValue(input_schema) als Eingangschannel hinzu und setzt stream_mode="updates", input_channels=START.
  • attach Trilogie:1360-1388 — Ruft in Schleifen compiled.attach_node(START, None) + jeden Knoten, attach_edge für jede Kante und attach_branch für jede Verzweigung auf; abschließend compiled.validate().
  • Pregel.__init__:758-836CompiledStateGraph.__init__ delegiert über super().__init__(**kwargs) an Pregel; hier werden alle Laufzeitfelder entfaltet und bei auto_validate=True self.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:

python
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:

python
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):

python
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)

(Pregel init nodes:800-809)

Grenzen und Fehler

  • checkpointer=True ist für den Root-Graphen unzulässig (True Fehler:2583-2584) – True bedeutet «vom Eltern-Graphen erben» und ist nur für Subgraphen erlaubt; der Root-Graph muss explizit einen Saver oder False angeben.
  • interrupt_before="*" und interrupt_after="*" werden asymmetrisch behandelt (* Behandlung:1249-1253) – interrupt_before wird nur dann in die interrupt-Liste eingefügt, wenn interrupt_after != "*"; das ist eine Präzedenzregel für die Verwendung von * als «alle Knoten».
  • TASKS ist als Channel-Name reserviert (TASKS reserviert:804-807) – ein Feld namens __pregel_tasks im Schema führt zu einem direkten Fehler; es ist der interne Topic-Channel des Engines zum Auffächern von Send.
  • 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 Feld compiled des Builders wird in validate() zwar auf True gesetzt (compiled=True:1161), ist aber nur ein Flag und hindert weder ein erneutes compile noch ein fortgesetztes add_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.