RemoteGraph et imbrication de sous-graphes : utiliser un graphe compilé comme nœud
Responsabilités
LangGraph permet de traiter un graphe compilé (instance locale de Pregel ou RemoteGraph distant) comme un nœud d'un autre graphe. RemoteGraph(RemoteGraph:118) est l'implémentation cliente de PregelProtocol, qui appelle un graphe déployé à distance via l'API LangGraph Server ; le produit de StateGraph.compile() local est lui-même un Pregel (donc un PregelProtocol), qui peut être directement inclus via parent_graph.add_node(child_graph).
Cette couisseur est chargée d'abstraire le « graphe imbriqué » : quand le graphe père ordonnance ce nœud, peu importe qu'il y ait derrière un Pregel local ou une API HTTP distante, l'interface unifiée invoke / stream est utilisée ; PregelNode via find_subgraph_pregel(find_subgraph_pregel:47)recherche récursivement l'instance Pregel dans le runnable du nœud et l'attache ànode.subgraphs, de sorte que get_subgraphsdu graphe père puisse énumérer le sous-graphe et questream(subgraphs=True)` récupère les événements internes du sous-graphe.
Motivation de conception
- Déploiement distribué : les grands graphes sont souvent découpés en plusieurs services (gestion de conversation, retrieval, appel d'outils), chacun étant un déploiement LangGraph ; le graphe père les enchaîne en workflow via
RemoteGraph. Chaque service peut être mis à l'échelle et mis à jour indépendamment, sans redéploiement du graphe père. - Checkpoint indépendant pour l'imbrication : après
add_node(compiled_graph), le sous-graphe peut configurer son propre checkpointer (compile(checkpointer=...)) ou hériter de celui du père. Le thread_id du sous-graphe reçoit automatiquement un préfixe de namespace viaCONFIG_KEY_CHECKPOINT_NScôté père (parent_node:<task_id>|child_node:<task_id>), donc les checkpoints des graphes père et fils ne se mélangent pas. - Interface unifiée
PregelProtocol: que ce soit unPregellocal ou unRemoteGraph, tous deux implémententPregelProtocol—invoke/stream/astream/get_state/aget_state/get_subgraphsont des signatures identiques. Le graphe père n'a pas à les traiter différemment. - Transmission du stream : avec
subgraphs=True, le_outputdu graphe père ajoute un préfixe de namespace à l'événement à la sortie(get_subgraphs:1076), et l'appelant reçoit un triplet(ns, mode, payload)lui permettant de savoir de quel nœud et de quel niveau d'imbrication cet événement provient. Command.PARENTfranchit les niveaux : un nœud de sous-graphe peutreturn Command(graph=Command.PARENT, update=...)pour écrire dans l'état du graphe père — les writes passent entre graphes père et fils via les pending writes, et le_firstdu père traite les writes remontés par le sous-graphe à la fin.
Fichiers clés
RemoteGraph:118—RemoteGraph(PregelProtocol)client, détientassistant_id/client/sync_client/name.RemoteGraph.__init__:132— accepteurl/api_key/headers/client/sync_client, créé par défaut viaget_client/get_sync_client.RemoteGraph.stream:757— entrée stream synchrone, délègue àsync_client.run_stream.RemoteGraph.astream:912— entrée stream asynchrone, délègue à l'itérateur asynchroneclient.run_stream.RemoteGraph.invoke:1133— entrée synchrone convergente, passe parstreamen interne pour récupérer la dernière valeur.find_subgraph_pregel:47— recherche récursive d'une instancePregelProtocoldans le runnableboundd'un nœud, pour identifier le sous-graphe.PregelNode:97— classePregelNode, détient le champsubgraphs: Sequence[PregelProtocol].PregelNode subgraphs init:180—PregelNode.__init__appellefind_subgraph_pregel(self.bound)pour identifier automatiquement le sous-graphe.get_subgraphs:1076— le graphe père énumère les sous-graphes directs,recurse=Truedescend récursivement.attach_node:1431— à la compilation, convertit l'action enregistrée paradd_nodeenPregelNode(bound=node.runnable, ...), c'est ici que l'identification du sous-graphe se produit.coerce_to_runnable:529— toute instanceRunnable(y comprisPregel/RemoteGraph) est renvoyée telle quelle ; c'est l'entrée « compiled graph as node ».
Flux de données
add_node(compiled_child_graph) passe par 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(...)Pregel et RemoteGraph héritent tous deux de Runnable, donc la première branche s'applique et les renvoie directement. PregelNode.__init__ appelle find_subgraph_pregel(self.bound) lors de la phase attach_node(find_subgraph_pregel:47):
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 NoneIl recherche récursivement dans RunnableSequence / RunnableLambda / RunnableCallable — même si un nœud emballe un Pregel appelé via une couche RunnableLambda, il peut être identifié. Le sous-graphe identifié est attaché à self.subgraphs = [subgraph], de sorte que get_subgraphs() du graphe père puisse l'énumérer(get_subgraphs:1076`).
À l'exécution, le PregelRunner.tick du graphe père appelle stream / astream du sous-graphe Pregel (ou la méthode correspondante de RemoteGraph) ; le sous-graphe exécute sa propre boucle Pregel en interne, et les événements produits sont transmis via stream(subgraphs=True) du graphe père — le graphe père ajoute au moment de l'appel le préfixe du nom du nœud à CONFIG_KEY_CHECKPOINT_NS, les événements du sous-graphe portent ce namespace en étant écrits dans la file de stream du graphe père, et le _output du graphe père les fusionne avec les événements locaux à la sortie.
Limites et échecs
- Sous-graphe sans checkpointer non identifié :
find_subgraph_pregelsaute explicitement lesPregeloùc.checkpointer is False(skip no-checkpointer:54) — ces « sous-graphes sans état » ne sont pas traités comme sous-graphes ;get_subgraphsdu graphe père ne les voit pas, et leurs événements ne portent pas de namespace. RemoteGraphrequiert au moinsurlouclient: dans__init__, siclientetsync_clientsont tous deuxNoneet qu'aucunurln'est passé, aucun client n'est créé, et_validate_client()lèveraraise ValueError(validate client:181).RemoteGraphne supporte pas tous lesstream_mode:_reject_v3_unsupportedrejette certaines combinaisons de paramètres ; le protocole stream de l'API distante a plus de restrictions que lePregellocal(reject v3:195).- Le namespace imbriqué utilise
NS_SEP: le format du namespace estparent_node:<task_id>+NS_SEP+child_node:<task_id>, récursivement vers le bas.add_noderejette les noms de nœuds contenantNS_SEP(reject NS_SEP:794)`, pour éviter l'ambiguïté de namespace. - L'
interrupt()du sous-graphe se propage le long du namespace : quand un nœud de sous-graphe lèveGraphInterruptviainterrupt(), letickdu graphe père détecte le pending interrupt du sous-graphe, et le graphe père entre aussi dans l'état interrupt ; à la reprise par le client,Command(resume=...)route viaCONFIG_KEY_RESUME_MAPselon le namespace vers le sous-graphe correspondant(Command resume mapping:904`). - Limite de
Command.PARENT: quand un nœud de sous-graphe faitreturn Command(graph=Command.PARENT, ...), lemap_commanddu sous-graphe lève directementraise InvalidUpdateError("There is no parent graph")(PARENT guard:58) — c'est en réalité un filet de sécurité ; le chemin normal est que le mapper_get_updatesduattach_nodedu sous-graphe sauteCommand.PARENT, l'écrit tel quel dans les pending writes du sous-graphe, et le graphe père l'identifie et le route dans_first.
Résumé
RemoteGraph permet à LangGraph de supporter l'imbrication de graphes inter-services et inter-processus ; l'imbrication locale de Pregel est identifiée automatiquement via find_subgraph_pregel — les deux empruntent PregelProtocol. Le stream(subgraphs=True) du graphe père transmet les événements du sous-graphe via le namespace. Pour les détails du flux d'événements du sous-graphe voir /stream/run-stream, pour l'ordonnancement des nœuds de sous-graphe voir /pregel/pregel. Voir la documentation officielle : LangGraph 文档 · README