RemoteGraph und verschachtelte Untergraphen: Kompilierte Graphen als Knoten verwenden
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
RemoteGraphzu 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. Diethread_iddes Untergraphen erhält vom Elterngraphen automatisch einen Namespace-Präfix überCONFIG_KEY_CHECKPOINT_NS(parent_node:<task_id>|child_node:<task_id>), sodass die Checkpoints von Eltern- und Untergraph nicht kollidieren. - Einheitliche
PregelProtocol-Schnittstelle:Sowohl lokalePregelals auchRemoteGraphimplementierenPregelProtocol— die Methodensignaturen voninvoke/stream/astream/get_state/aget_state/get_subgraphssind identisch. Der Elterngraph muss sie nicht unterschiedlich behandeln. - stream-Durchreichung:Mit
subgraphs=Trueversieht der_outputdes 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 kannreturn 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_firstdes Elterngraphen verarbeitet die vom Untergraphen gemeldeten writes beim Abschluss.
Schlüsseldateien
RemoteGraph:118—RemoteGraph(PregelProtocol)-Client; hältassistant_id/client/sync_client/name.RemoteGraph.__init__:132— nimmturl/api_key/headers/client/sync_client; verwendet standardmäßigget_client/get_sync_clientzur Erstellung.RemoteGraph.stream:757— synchroner Streaming-Einstieg; reicht die Anfrage ansync_client.run_streamweiter.RemoteGraph.astream:912— asynchroner Streaming-Einstieg; reicht an den asynchronen Iteratorclient.run_streamweiter.RemoteGraph.invoke:1133— synchroner Konvergenz-Einstieg; verwendet internstreamund liefert den letzten Wert.find_subgraph_pregel:47— sucht rekursiv in derbound-Runnable des Knotens nach einerPregelProtocol-Instanz und identifiziert so den Untergraphen.PregelNode:97— KlassePregelNode; hält das Feldsubgraphs: Sequence[PregelProtocol].PregelNode subgraphs init:180—PregelNode.__init__ruftfind_subgraph_pregel(self.bound)auf und erkennt Untergraphen automatisch.get_subgraphs:1076— Elterngraph enumeriert direkte Untergraphen; beirecurse=Truewird rekursiv nach unten gegangen.attach_node:1431— wandelt zur Compile-Zeit die inadd_noderegistrierte action in einPregelNode(bound=node.runnable, ...)um; die Untergraphen-Erkennung geschieht in diesem Schritt.coerce_to_runnable:529— jedeRunnable-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):
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:
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 NoneEs 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 mitc.checkpointer is False(skip no-checkpointer:54) — solche «zustandslosen Untergraphen» werden nicht als Untergraph betrachtet; der Elterngraph sieht sie inget_subgraphsnicht, und Ereignisse tragen keinen Namespace. RemoteGraphbenötigt mindestensurloderclient:Wenn in__init__sowohlclientals auchsync_clientNonesind und keinurlübergeben wurde, werden beide Clients nicht erstellt; der spätere Aufruf von_validate_client()führt zuraise ValueError(validate client:181).RemoteGraphunterstützt nicht allestream_mode:_reject_v3_unsupportedlehnt 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 istparent_node:<task_id>+NS_SEP+child_node:<task_id>und rekursiv nach unten.add_nodelehnt Knotennamen ab, dieNS_SEPenthalten(reject NS_SEP:794), um Namespace-Mehrdeutigkeiten zu vermeiden. interrupt()des Untergraphen verbreitet sich entlang des Namespace:Wenn ein Knoten des Untergrapheninterrupt()auslöst undGraphInterruptwirft, erkennt dertickdes Elterngraphen den pending interrupt des Untergraphen, und der Elterngraph selbst geht in den interrupt-Zustand. Beim resume des Clients wirdCommand(resume=...)überCONFIG_KEY_RESUME_MAPnach Namespace an den entsprechenden Untergraphen geroutet(Command resume mapping:904).Command.PARENT-Grenze:Wenn ein Knoten des Untergraphenreturn Command(graph=Command.PARENT, ...)ausführt, wirftmap_commanddes Untergraphen direktraise InvalidUpdateError("There is no parent graph")(PARENT guard:58) — das ist eigentlich ein Fallback. Der normale Pfad ist: Der_get_updates-Mapper beiattach_nodedes Untergraphen überspringtCommand.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.