RemoteGraph y anidamiento de subgrafos: usar un grafo compilado como nodo
Responsabilidades
LangGraph permite usar un grafo compilado (una instancia local de Pregel o un RemoteGraph remoto) como nodo de otro grafo. RemoteGraph(RemoteGraph:118) es la implementación cliente de PregelProtocol, que invoca un grafo desplegado remotamente a través de la LangGraph Server API; el producto de StateGraph.compile() local es por sí mismo un Pregel (también PregelProtocol), por lo que se puede incrustar directamente con parent_graph.add_node(child_graph).
Esta capa se encarga de abstraer el «grafo anidado»: cuando el grafo padre programa este nodo, sin importar si detrás hay un Pregel local o una API HTTP remota, se usa la interfaz unificada invoke / stream; PregelNode llama a find_subgraph_pregel(find_subgraph_pregel:47)para encontrar recursivamente la instancia Pregel dentro del runnable del nodo y la asigna anode.subgraphs, de modo que get_subgraphsdel grafo padre pueda enumerar el subgrafo ystream(subgraphs=True)` reciba los eventos internos del subgrafo.
Motivación de diseño
- Despliegue distribuido: los grafos grandes suelen dividirse en varios servicios (gestión de diálogo, recuperación, llamada a herramientas), cada uno un despliegue LangGraph independiente; el grafo padre los encadena en un flujo de trabajo mediante
RemoteGraph. Cada servicio puede escalarse y actualizarse de forma independiente, sin necesidad de redesplegar el grafo padre. - Checkpoints anidados independientes: tras
add_node(compiled_graph), el subgrafo puede configurar su propio checkpointer (compile(checkpointer=...)) o heredarlo del padre. El thread_id del subgrafo recibe automáticamente un prefijo de namespace víaCONFIG_KEY_CHECKPOINT_NSpor parte del padre (parent_node:<task_id>|child_node:<task_id>), de modo que los checkpoints de padre e hijo no se mezclan. - Interfaz unificada con
PregelProtocol: tanto elPregellocal comoRemoteGraphimplementanPregelProtocol— las firmas deinvoke/stream/astream/get_state/aget_state/get_subgraphsson idénticas. El grafo padre no necesita tratarlos de forma diferente. - Transparencia del stream: con
subgraphs=Trueactivado,_outputdel padre añade un prefijo de namespace al evento al sacarlo de la cola(get_subgraphs:1076), y el llamador recibe una tupla(ns, mode, payload)que le permite saber de qué nodo y de qué nivel de anidamiento proviene el evento. Command.PARENTentre niveles: un nodo de subgrafo puede hacerreturn Command(graph=Command.PARENT, update=...)para escribir actualizaciones en el estado del grafo padre — se transmiten entre subgrafo y padre vía pending writes, y el_firstdel padre los procesa al cerrar.
Archivos clave
RemoteGraph:118— clienteRemoteGraph(PregelProtocol), contieneassistant_id/client/sync_client/name.RemoteGraph.__init__:132— recibeurl/api_key/headers/client/sync_client; por defecto se crean conget_client/get_sync_client.RemoteGraph.stream:757— entrada de streaming síncrono, delega la petición async_client.run_stream.RemoteGraph.astream:912— entrada de streaming asíncrono, delega al iterador asíncronoclient.run_stream.RemoteGraph.invoke:1133— entrada síncrona convergente, internamente obtiene el último valor a través destream.find_subgraph_pregel:47— busca recursivamente una instancia dePregelProtocoldentro del runnablebounddel nodo, para identificar el subgrafo.PregelNode:97— clasePregelNode, que contiene el camposubgraphs: Sequence[PregelProtocol].PregelNode subgraphs init:180—PregelNode.__init__llama afind_subgraph_pregel(self.bound)para identificar automáticamente el subgrafo.get_subgraphs:1076— el grafo padre enumera sus subgrafos directos; conrecurse=Truedesciende recursivamente.attach_node:1431— durante la compilación, convierte el action registrado poradd_nodeen unPregelNode(bound=node.runnable, ...), donde ocurre la identificación del subgrafo.coerce_to_runnable:529— cualquier instancia deRunnable(incluyendoPregel/RemoteGraph) se devuelve directamente; ésta es la entrada de «compiled graph as node».
Flujo de datos
add_node(compiled_child_graph) pasa por 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(...)Tanto Pregel como RemoteGraph heredan de Runnable, por lo que entran por la primera rama y se devuelven directamente. PregelNode.__init__ llama a find_subgraph_pregel(self.bound) durante la fase 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 NoneBusca recursivamente dentro de RunnableSequence / RunnableLambda / RunnableCallable — incluso si un nodo está envuelto en un RunnableLambda que invoca Pregel, también se identifica. El subgrafo identificado se asigna a self.subgraphs = [subgraph], de modo que get_subgraphs() del grafo padre lo puede enumerar(get_subgraphs:1076).
Durante la ejecución, PregelRunner.tick del grafo padre invoca stream / astream del subgrafo Pregel (o los métodos correspondientes de RemoteGraph); el subgrafo ejecuta internamente su propio bucle Pregel, y los eventos producidos se transparentan vía stream(subgraphs=True) del padre — el padre, al invocar al subgrafo, añade el prefijo de nodo a CONFIG_KEY_CHECKPOINT_NS, los eventos del subgrafo portan este namespace al escribirse de vuelta en la cola de stream del padre, y _output del padre los fusiona con los eventos locales al sacarlos de la cola.
Límites y fallos
- Subgrafos sin checkpointer no se identifican:
find_subgraph_pregelomite explícitamente losPregelconc.checkpointer is False(skip no-checkpointer:54) — estos «subgrafos sin estado» no se tratan como subgrafo; el grafo padre no los ve enget_subgraphsy sus eventos no portan namespace. RemoteGraphrequiere al menosurloclient: en__init__, si tantoclientcomosync_clientsonNoney no se pasóurl, no se crea ningún cliente y la siguiente llamada a_validate_client()lanzaraise ValueError(validate client:181).RemoteGraphno soporta todos losstream_mode:_reject_v3_unsupportedrechaza ciertas combinaciones de parámetros; el protocolo de stream de la API remota tiene más restricciones que elPregellocal(reject v3:195).- El namespace anidado usa
NS_SEP: el formato del namespace esparent_node:<task_id>+NS_SEP+child_node:<task_id>, descendiendo recursivamente.add_noderechaza nombres de nodo que contenganNS_SEP(reject NS_SEP:794)`, para evitar ambigüedades de namespace. - El
interrupt()del subgrafo se propaga por namespace: cuando un nodo del subgrafo lanzaGraphInterruptvíainterrupt(), eltickdel grafo padre detecta el pending interrupt del subgrafo y el padre también entra en estado de interrupt; al reanudar, el cliente enrutaCommand(resume=...)al subgrafo correspondiente por namespace a través deCONFIG_KEY_RESUME_MAP(Command resume mapping:904`). - Límites de
Command.PARENT: cuando un nodo del subgrafo hacereturn Command(graph=Command.PARENT, ...), elmap_commanddel subgrafo lanza directamenteraise InvalidUpdateError("There is no parent graph")(PARENT guard:58) — esto es en realidad una red de seguridad; la ruta normal es que, duranteattach_nodedel subgrafo, el mapper_get_updatesomitaCommand.PARENT, lo escriba tal cual en los pending writes del subgrafo, y el grafo padre lo identifica y enruta en_first.
Resumen
RemoteGraph permite a LangGraph soportar anidamiento de grafos entre servicios y procesos, mientras que el anidamiento con Pregel local se identifica automáticamente vía find_subgraph_pregel — ambos pasan por PregelProtocol. stream(subgraphs=True) del padre transmite los eventos del subgrafo mediante el namespace. Para detalles del flujo de eventos del subgrafo ver /stream/run-stream; para la programación de nodos del subgrafo ver /pregel/pregel. Véase la documentación oficial: Documentación de LangGraph · README