Skip to content

RemoteGraph y anidamiento de subgrafos: usar un grafo compilado como nodo

源码版本1.2.9

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ía CONFIG_KEY_CHECKPOINT_NS por 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 el Pregel local como RemoteGraph implementan PregelProtocol — las firmas de invoke / stream / astream / get_state / aget_state / get_subgraphs son idénticas. El grafo padre no necesita tratarlos de forma diferente.
  • Transparencia del stream: con subgraphs=True activado, _output del 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.PARENT entre niveles: un nodo de subgrafo puede hacer return 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 _first del padre los procesa al cerrar.

Archivos clave

  • RemoteGraph:118 — cliente RemoteGraph(PregelProtocol), contiene assistant_id / client / sync_client / name.
  • RemoteGraph.__init__:132 — recibe url / api_key / headers / client / sync_client; por defecto se crean con get_client / get_sync_client.
  • RemoteGraph.stream:757 — entrada de streaming síncrono, delega la petición a sync_client.run_stream.
  • RemoteGraph.astream:912 — entrada de streaming asíncrono, delega al iterador asíncrono client.run_stream.
  • RemoteGraph.invoke:1133 — entrada síncrona convergente, internamente obtiene el último valor a través de stream.
  • find_subgraph_pregel:47 — busca recursivamente una instancia de PregelProtocol dentro del runnable bound del nodo, para identificar el subgrafo.
  • PregelNode:97 — clase PregelNode, que contiene el campo subgraphs: Sequence[PregelProtocol].
  • PregelNode subgraphs init:180PregelNode.__init__ llama a find_subgraph_pregel(self.bound) para identificar automáticamente el subgrafo.
  • get_subgraphs:1076 — el grafo padre enumera sus subgrafos directos; con recurse=True desciende recursivamente.
  • attach_node:1431 — durante la compilación, convierte el action registrado por add_node en un PregelNode(bound=node.runnable, ...), donde ocurre la identificación del subgrafo.
  • coerce_to_runnable:529 — cualquier instancia de Runnable (incluyendo Pregel / 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`):

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(...)

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

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

Busca 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_pregel omite explícitamente los Pregel con c.checkpointer is False(skip no-checkpointer:54) — estos «subgrafos sin estado» no se tratan como subgrafo; el grafo padre no los ve en get_subgraphs y sus eventos no portan namespace.
  • RemoteGraph requiere al menos url o client: en __init__, si tanto client como sync_client son None y no se pasó url, no se crea ningún cliente y la siguiente llamada a _validate_client() lanza raise ValueError(validate client:181).
  • RemoteGraph no soporta todos los stream_mode: _reject_v3_unsupported rechaza ciertas combinaciones de parámetros; el protocolo de stream de la API remota tiene más restricciones que el Pregel local(reject v3:195).
  • El namespace anidado usa NS_SEP: el formato del namespace es parent_node:<task_id> + NS_SEP + child_node:<task_id>, descendiendo recursivamente. add_node rechaza nombres de nodo que contengan NS_SEP(reject NS_SEP:794)`, para evitar ambigüedades de namespace.
  • El interrupt() del subgrafo se propaga por namespace: cuando un nodo del subgrafo lanza GraphInterrupt vía interrupt(), el tick del grafo padre detecta el pending interrupt del subgrafo y el padre también entra en estado de interrupt; al reanudar, el cliente enruta Command(resume=...) al subgrafo correspondiente por namespace a través de CONFIG_KEY_RESUME_MAP(Command resume mapping:904`).
  • Límites de Command.PARENT: cuando un nodo del subgrafo hace return Command(graph=Command.PARENT, ...), el map_command del subgrafo lanza directamente raise InvalidUpdateError("There is no parent graph")(PARENT guard:58) — esto es en realidad una red de seguridad; la ruta normal es que, durante attach_node del subgrafo, el mapper _get_updates omita Command.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