Skip to content

StateGraph.compile: compilar el plano en un ejecutable Pregel

源码版本1.2.9

Responsabilidades

StateGraph.compile es el «traductor» entre el constructor y el runtime. Recibe un StateGraph (spec de nodos, bordes, condiciones de rama, dict de canales) y devuelve un CompiledStateGraph(CompiledStateGraph:1391) — este último hereda de Pregel y trae invoke / stream / astream / get_state y todos los métodos de runtime. Antes de compile tu grafo es solo datos de configuración; después de compile se convierte en un ejecutable.

Su trabajo central es cuatro: 1) validar la estructura del grafo (validate), 2) decidir los canales de salida/streaming, 3) inyectar dependencias de runtime (checkpointer / store / cache / configuración de interrupt), 4) traducir cada StateNodeSpec / edge / BranchSpec a PregelNode + suscripciones de canal de Pregel(构造 CompiledStateGraph:1333-1357). Esa traducción se hace iterando compiled.attach_node / compiled.attach_edge / compiled.attach_branch, que convierten las estructuras de datos declarativas del builder en campos de runtime como PregelNode.writers / PregelNode.triggers.

compile también asume dos responsabilidades secundarias: 1) aplicar los defaults fijados con set_node_defaults (retry / cache / error_handler / timeout) a los nodos sin policy explícita, y 2) cuando está activo el msgpack estricto (_serde.STRICT_MSGPACK_ENABLED), construir una allowlist de serde para que el checkpointer solo serialice los campos que aparecen en el schema(serde allowlist:1220-1241).

Motivación de diseño

¿Por qué no hacer que StateGraph sea ya Pregel, en lugar de partirlo en dos pasos?

  • Separación de preocupaciones build vs runtime: la fase builder solo se preocupa por «cómo es el grafo»; la fase runtime se preocupa por «cómo correrlo». Separadas, un builder se puede compilar varias veces en ejecutables distintos — el mismo StateGraph con distintos checkpointer / store / interrupt da lugar a varias instancias Pregel.
  • Ejecutable inmutable: los campos del CompiledStateGraph tras compile quedan básicamente fijados en Pregel.__init__(Pregel.__init__:758-836); auto_validate=False deja que compile controle cuándo validar y evite una validación prematura en el constructor.
  • Validación perezosa: cuando el usuario llama add_node solo se hace append al contenedor, sin comprobar al instante «si el origen del borde existe» — permite al usuario registrar nodos y bordes en cualquier orden; la validación real ocurre una sola vez en compile(validate 调用:1247-1254).
  • Configuración de interrupt parametrizada: interrupt_before / interrupt_after son parámetros de compile, no propiedades del grafo — un mismo grafo se puede compilar en versiones «con interrupt» y «sin interrupt», muy común en escenarios de colaboración humano-máquina(interrupt 参数:1170-1171).
  • Normalización del tipo de checkpointer: compile(checkpointer=...) admite None / True / False / BaseCheckpointSaver; ensure_valid_checkpointer(ensure_valid_checkpointer:107-117) valida de forma uniforme y evita explosiones en runtime.
  • Los defaults se aplican en compilación(defaults 应用:1299-1325) — permite al usuario llamar set_node_defaults después de add_node; los nuevos defaults sobreescribirán a cualquier nodo sin policy explícita.

Archivos clave

  • compile 签名:1164-1217 — recibe checkpointer / store / cache / interrupt_before / interrupt_after / debug / name / transformers.
  • ensure_valid_checkpointer:107-117 — valida que el checkpointer sea None / True / False / BaseCheckpointSaver; si no, TypeError.
  • ensure_valid_checkpointer 调用:1218 — lo primero que hace compile es normalizar el checkpointer.
  • serde allowlist:1220-1241 — con msgpack estricto, construye la allowlist y la aplica al checkpointer para que el checkpoint solo guarde los campos del schema.
  • interrupt 合并 + validate:1243-1254"*" significa All (todos los nodos); fusiona interrupt_before / interrupt_after en una sola lista que pasa a validate.
  • output / stream channels:1256-1273 — si es un único campo y es __root__, se usa la cadena directamente; si no, una lista filtrando los managed value.
  • 默认 error handler 节点:1278-1297 — el error_handler global de set_node_defaults se inyecta como un nodo especial llamado __default_error_handler__.
  • defaults 应用:1299-1325 — aplica el retry / cache / error_handler / timeout de set_node_defaults a cada spec sin policy explícita; cache y error_handler no se aplican al propio nodo error-handler.
  • node_error_handler_map:1327-1331 — genera el mapping node_name -> handler_node_name que en runtime enruta la ejecución del nodo fallido al handler correspondiente.
  • 构造 CompiledStateGraph:1333-1357 — fusiona los channels / managed del builder, añade START: EphemeralValue(input_schema) como canal de entrada, fija stream_mode="updates" e input_channels=START.
  • attach 三件套:1360-1388 — itera compiled.attach_node(START, None) + cada nodo, attach_edge por cada borde, attach_branch por cada rama; al final compiled.validate().
  • Pregel.__init__:758-836CompiledStateGraph.__init__ delega a Pregel vía super().__init__(**kwargs); aquí se despliegan todos los campos de runtime y, con auto_validate=True, se llama self.validate().

Flujo de datos

Este es el tramo clave donde compile construye de verdad el ejecutable — vuelca todos los campos del builder en CompiledStateGraph y añade un canal de entrada START:

python
compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
    builder=self,
    schema_to_mapper={},
    context_schema=self.context_schema,
    nodes={},
    channels={
        **self.channels,
        **self.managed,
        START: EphemeralValue(self.input_schema),
    },
    input_channels=START,
    stream_mode="updates",
    output_channels=output_channels,
    stream_channels=stream_channels,
    checkpointer=checkpointer,
    interrupt_before_nodes=interrupt_before,
    interrupt_after_nodes=interrupt_after,
    auto_validate=False,
    debug=debug,
    store=store,
    cache=cache,
    node_error_handler_map=node_error_handler_map,
    name=name or "LangGraph",
    stream_transformers=transformers,
)
compiled._serde_allowlist = serde_allowlist

compiled.attach_node(START, None)
for key, node in self.nodes.items():
    compiled.attach_node(key, node)

(构造 + attach_node:1333-1362)

Finalmente, los bordes y las ramas se traducen también a suscripciones de canal de Pregel y se dispara validate una vez más:

python
for start, end in self.edges:
    compiled.attach_edge(start, end)

for starts, end in self.waiting_edges:
    compiled.attach_edge(starts, end)

for start, branches in self.branches.items():
    for name, branch in branches.items():
        compiled.attach_branch(start, name, branch)

return compiled.validate()

(attach edge/branch:1378-1388)

Pregel.__init__, al recibir estos campos, traduce los NodeBuilder de nodes a PregelNode, instala un Topic(Send, accumulate=False) en el canal TASKS y, con auto_validate=True, llama a self.validate()(Pregel init:800-836):

python
self.nodes = {
    k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items()
}
self.channels = channels or {}
if TASKS in self.channels and not isinstance(self.channels[TASKS], Topic):
    raise ValueError(
        f"Channel '{TASKS}' is reserved and cannot be used in the graph."
    )
else:
    self.channels[TASKS] = Topic(Send, accumulate=False)

(Pregel init nodes:800-809)

Límites y fallos

  • checkpointer=True no se puede usar en un grafo raíz(True 报错:2583-2584) — True significa «heredar del grafo padre», solo los subgrafos pueden usarlo; un grafo raíz debe recibir un saver explícito o False.
  • interrupt_before="*" y interrupt_after="*" se procesan de forma mutuamente excluyente(* 处理:1249-1253) — solo cuando interrupt_after != "*" se fusiona la lista interrupt_before en interrupt; es la convención de prioridad cuando se usa * para significar «todos los nodos».
  • El nombre del canal TASKS está reservado(TASKS 保留:804-807); si el usuario define en el schema un campo llamado __pregel_tasks, da error directo — es el canal Topic interno del motor para fan-out con Send.
  • Re-attach de nodo con mismo nombre: la fase builder ya frenó los duplicados(重名:792-793), pero los nombres autogenerados como __default_error_handler__ también deben evitar chocar con un nodo de usuario(default handler 冲突:1280-1284).
  • cache y error_handler por defecto no se aplican al propio nodo handler(cache 不给 handler:1313-1321) — porque «cachear el resultado del handler» no es seguro (el state del nodo fallido puede ser distinto cada vez) y «un handler que se atrapa a sí mismo» entraría en bucle infinito.
  • Compilar el mismo builder varias veces está permitido: cada compile crea un CompiledStateGraph nuevo; el campo compiled del builder se pone a True en validate()(compiled=True:1161), pero es solo un flag — no impide seguir compilando ni seguir haciendo add_node (solo lanzará warning).

Resumen

compile es la frontera entre el constructor y el runtime — traduce todos los spec acumulados en StateGraph con add_node / add_edge / add_conditional_edges a la estructura de runtime de Pregel, ata la configuración checkpointer / store / interrupt y, por último, devuelve una subclase de Pregel invocable directamente con invoke. Entenderlo es entender la cadena completa «declaración del grafo → ejecutable».

A partir de aquí se puede ver en Motor Pregel cómo, tras Pregel.__init__, invoke / stream ejecutan ese producto compilado, o en StateSnapshot cómo se obtiene el estado del producto compilado una vez atado el checkpointer.

Véase la documentación oficial: documentación de LangGraph · README