Skip to content

RemoteGraph und verschachtelte Untergraphen: Kompilierte Graphen als Knoten verwenden

源码版本1.2.9

Verantwortung

LangGraph unterstützt, einen kompilierten Graphen (lokale Pregel-Instanz oder entfernten RemoteGraph) als Knoten eines anderen Graphen zu verwenden. RemoteGraph(RemoteGraph:118) ist die Client-Implementierung von PregelProtocol und ruft über die LangGraph Server API einen remote deployten Graphen auf; das Produkt von lokalem StateGraph.compile() ist selbst eine Pregel (ebenfalls PregelProtocol) und kann direkt via parent_graph.add_node(child_graph) eingebettet werden.

Diese Schicht ist dafür verantwortlich, die Abstraktion des «verschachtelten Graphen» zu verbergen: Wenn der Elterngraph diesen Knoten schedult, wird — egal ob lokal Pregel oder remote HTTP-API — die einheitliche invoke- / stream-Schnittstelle verwendet; PregelNode nutzt find_subgraph_pregel(find_subgraph_pregel:47), um in der Runnable des Knotens rekursiv die Pregel-Instanz zu finden und an node.subgraphs zu hängen. So kann der Elterngraph über get_subgraphs die Untergraphen aufzählen, und stream(subgraphs=True) liefert die internen Ereignisse des Untergraphen.

Entwurfsmotivation

  • Verteilte Deployment:Große Graphen werden oft in mehrere Dienste aufgeteilt (Dialogmanagement, Retrieval, Werkzeugaufrufe) — jeder Dienst ein LangGraph-Deployment, der Elterngraph fügt sie über RemoteGraph zu einem Workflow zusammen. Jeder Dienst kann unabhängig skaliert und aktualisiert werden, ohne dass der Elterngraph neu deployt werden muss.
  • Unabhängige verschachtelte Checkpoints:Nach add_node(compiled_graph) kann der Untergraph seinen eigenen checkpointer konfigurieren (compile(checkpointer=...)) oder vom Elterngraphen erben. Die thread_id des Untergraphen erhält vom Elterngraphen automatisch einen Namespace-Präfix über CONFIG_KEY_CHECKPOINT_NS (parent_node:<task_id>|child_node:<task_id>), sodass die Checkpoints von Eltern- und Untergraph nicht kollidieren.
  • Einheitliche PregelProtocol-Schnittstelle:Sowohl lokale Pregel als auch RemoteGraph implementieren PregelProtocol — die Methodensignaturen von invoke / stream / astream / get_state / aget_state / get_subgraphs sind identisch. Der Elterngraph muss sie nicht unterschiedlich behandeln.
  • stream-Durchreichung:Mit subgraphs=True versieht der _output des Elterngraphen das Ereignis beim Dequeue mit einem Namespace-Präfix(get_subgraphs:1076); die aufrufende Seite erhält ein Tripel (ns, mode, payload) und weiß, aus welcher Verschachtelungsebene und von welchem Knoten das Ereignis stammt.
  • Command.PARENT über Hierarchiegrenzen:Ein Knoten in einem Untergraph kann return Command(graph=Command.PARENT, update=...) ausführen und Updates in den Zustand des Elterngraphen schreiben — die Übertragung erfolgt über pending writes zwischen Eltern- und Untergraph, und das _first des Elterngraphen verarbeitet die vom Untergraphen gemeldeten writes beim Abschluss.

Schlüsseldateien

  • RemoteGraph:118RemoteGraph(PregelProtocol)-Client; hält assistant_id / client / sync_client / name.
  • RemoteGraph.__init__:132 — nimmt url / api_key / headers / client / sync_client; verwendet standardmäßig get_client / get_sync_client zur Erstellung.
  • RemoteGraph.stream:757 — synchroner Streaming-Einstieg; reicht die Anfrage an sync_client.run_stream weiter.
  • RemoteGraph.astream:912 — asynchroner Streaming-Einstieg; reicht an den asynchronen Iterator client.run_stream weiter.
  • RemoteGraph.invoke:1133 — synchroner Konvergenz-Einstieg; verwendet intern stream und liefert den letzten Wert.
  • find_subgraph_pregel:47 — sucht rekursiv in der bound-Runnable des Knotens nach einer PregelProtocol-Instanz und identifiziert so den Untergraphen.
  • PregelNode:97 — Klasse PregelNode; hält das Feld subgraphs: Sequence[PregelProtocol].
  • PregelNode subgraphs init:180PregelNode.__init__ ruft find_subgraph_pregel(self.bound) auf und erkennt Untergraphen automatisch.
  • get_subgraphs:1076 — Elterngraph enumeriert direkte Untergraphen; bei recurse=True wird rekursiv nach unten gegangen.
  • attach_node:1431 — wandelt zur Compile-Zeit die in add_node registrierte action in ein PregelNode(bound=node.runnable, ...) um; die Untergraphen-Erkennung geschieht in diesem Schritt.
  • coerce_to_runnable:529 — jede Runnable-Instanz (inkl. Pregel / RemoteGraph) wird direkt zurückgegeben; das ist der Einstieg für «compiled graph as node».

Datenfluss

add_node(compiled_child_graph) geht über coerce_to_runnable(coerce_to_runnable:529):

python
def coerce_to_runnable(
    thing: RunnableLike, *, name: str | None, trace: bool
) -> Runnable:
    """Coerce a runnable-like object into a Runnable."""
    if isinstance(thing, Runnable):
        return thing
    elif is_async_generator(thing) or inspect.isgeneratorfunction(thing):
        return RunnableLambda(thing, name=name)
    elif callable(thing):
        # ...
    elif isinstance(thing, dict):
        return RunnableParallel(thing)
    else:
        raise TypeError(...)

Sowohl Pregel als auch RemoteGraph erben von Runnable und nehmen daher den ersten Branch, der sie direkt zurückgibt. PregelNode.__init__ ruft in der attach_node-Phase find_subgraph_pregel(self.bound)(find_subgraph_pregel:47) auf:

python
def find_subgraph_pregel(candidate: Runnable) -> PregelProtocol | None:
    from langgraph.pregel import Pregel

    candidates: list[Runnable] = [candidate]

    for c in candidates:
        if (
            isinstance(c, PregelProtocol)
            # subgraphs that disabled checkpointing are not considered
            and (not isinstance(c, Pregel) or c.checkpointer is not False)
        ):
            return c
        elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
            candidates.extend(c.steps)
        elif isinstance(c, RunnableLambda):
            candidates.extend(c.deps)
        elif isinstance(c, RunnableCallable):
            if c.func is not None:
                candidates.extend(
                    nl.__self__ if hasattr(nl, "__self__") else nl
                    for nl in get_function_nonlocals(c.func)
                )
            elif c.afunc is not None:
                candidates.extend(
                    nl.__self__ if hasattr(nl, "__self__") else nl
                    for nl in get_function_nonlocals(c.afunc)
                )

    return None

Es sucht rekursiv in RunnableSequence / RunnableLambda / RunnableCallable — selbst wenn ein Knoten in eine RunnableLambda eingepackt ist, die Pregel aufruft, wird es erkannt. Der erkannte Untergraph wird an self.subgraphs = [subgraph] gehängt, sodass der Elterngraph über get_subgraphs()(get_subgraphs:1076) enumerieren kann.

Zur Ausführungszeit ruft der PregelRunner.tick des Elterngraphen stream / astream des Untergraphen Pregel auf (oder die entsprechenden Methoden von RemoteGraph); der Untergraph läuft intern seinen eigenen Pregel-Loop, und die erzeugten Ereignisse werden über stream(subgraphs=True) des Elterngraphen durchgereicht — beim Aufruf des Untergraphen fügt der Elterngraph CONFIG_KEY_CHECKPOINT_NS einen Knotennamen-Präfix hinzu, und die Ereignisse des Untergraphen tragen diesen Namespace zurück in die Stream-Queue des Elterngraphen; beim Dequeue mischt _output des Elterngraphen sie mit den lokalen Ereignissen zusammen.

Grenzen und Fehler

  • Untergraphen mit deaktiviertem checkpointer werden nicht erkannt:find_subgraph_pregel überspringt explizit Pregel mit c.checkpointer is False(skip no-checkpointer:54) — solche «zustandslosen Untergraphen» werden nicht als Untergraph betrachtet; der Elterngraph sieht sie in get_subgraphs nicht, und Ereignisse tragen keinen Namespace.
  • RemoteGraph benötigt mindestens url oder client:Wenn in __init__ sowohl client als auch sync_client None sind und kein url übergeben wurde, werden beide Clients nicht erstellt; der spätere Aufruf von _validate_client() führt zu raise ValueError(validate client:181).
  • RemoteGraph unterstützt nicht alle stream_mode:_reject_v3_unsupported lehnt bestimmte Parameterkombinationen ab; das Stream-Protokoll der Remote-API hat mehr Einschränkungen als lokaler Pregel(reject v3:195).
  • Verschachtelter Namespace verwendet NS_SEP:Das Format des Namespace ist parent_node:<task_id> + NS_SEP + child_node:<task_id> und rekursiv nach unten. add_node lehnt Knotennamen ab, die NS_SEP enthalten(reject NS_SEP:794), um Namespace-Mehrdeutigkeiten zu vermeiden.
  • interrupt() des Untergraphen verbreitet sich entlang des Namespace:Wenn ein Knoten des Untergraphen interrupt() auslöst und GraphInterrupt wirft, erkennt der tick des Elterngraphen den pending interrupt des Untergraphen, und der Elterngraph selbst geht in den interrupt-Zustand. Beim resume des Clients wird Command(resume=...) über CONFIG_KEY_RESUME_MAP nach Namespace an den entsprechenden Untergraphen geroutet(Command resume mapping:904).
  • Command.PARENT-Grenze:Wenn ein Knoten des Untergraphen return Command(graph=Command.PARENT, ...) ausführt, wirft map_command des Untergraphen direkt raise InvalidUpdateError("There is no parent graph")(PARENT guard:58) — das ist eigentlich ein Fallback. Der normale Pfad ist: Der _get_updates-Mapper bei attach_node des Untergraphen überspringt Command.PARENT, schreibt es unverändert in die pending writes des Untergraphen, und der Elterngraph erkennt und routet es in _first.

Zusammenfassung

RemoteGraph erlaubt LangGraph die graph- und prozessübergreifende Verschachtelung; lokale Pregel-Verschachtelung wird über find_subgraph_pregel automatisch erkannt — beide gehen über PregelProtocol. stream(subgraphs=True) des Elterngraphen reicht Untergraphen-Ereignisse über Namespaces durch. Details zum Ereignisstrom des Untergraphen siehe /stream/run-stream; das Scheduling von Untergraphen-Knoten siehe /pregel/pregel. Siehe offizielle Dokumentation: LangGraph 文档 · README.