StateGraph.compile: compilar el plano en un ejecutable Pregel
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
CompiledStateGraphtras compile quedan básicamente fijados enPregel.__init__(Pregel.__init__:758-836);auto_validate=Falsedeja que compile controle cuándo validar y evite una validación prematura en el constructor. - Validación perezosa: cuando el usuario llama
add_nodesolo 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_afterson 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=...)admiteNone/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 llamarset_node_defaultsdespués deadd_node; los nuevos defaults sobreescribirán a cualquier nodo sin policy explícita.
Archivos clave
compile 签名:1164-1217— recibecheckpointer/store/cache/interrupt_before/interrupt_after/debug/name/transformers.ensure_valid_checkpointer:107-117— valida que el checkpointer seaNone/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); fusionainterrupt_before/interrupt_afteren una sola lista que pasa avalidate.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 deset_node_defaultsse inyecta como un nodo especial llamado__default_error_handler__.defaults 应用:1299-1325— aplica el retry / cache / error_handler / timeout deset_node_defaultsa 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 mappingnode_name -> handler_node_nameque en runtime enruta la ejecución del nodo fallido al handler correspondiente.构造 CompiledStateGraph:1333-1357— fusiona loschannels/manageddel builder, añadeSTART: EphemeralValue(input_schema)como canal de entrada, fijastream_mode="updates"einput_channels=START.attach 三件套:1360-1388— iteracompiled.attach_node(START, None)+ cada nodo,attach_edgepor cada borde,attach_branchpor cada rama; al finalcompiled.validate().Pregel.__init__:758-836—CompiledStateGraph.__init__delega a Pregel víasuper().__init__(**kwargs); aquí se despliegan todos los campos de runtime y, conauto_validate=True, se llamaself.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:
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)Finalmente, los bordes y las ramas se traducen también a suscripciones de canal de Pregel y se dispara validate una vez más:
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):
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)Límites y fallos
checkpointer=Trueno se puede usar en un grafo raíz(True 报错:2583-2584) —Truesignifica «heredar del grafo padre», solo los subgrafos pueden usarlo; un grafo raíz debe recibir un saver explícito oFalse.interrupt_before="*"yinterrupt_after="*"se procesan de forma mutuamente excluyente(* 处理:1249-1253) — solo cuandointerrupt_after != "*"se fusiona la listainterrupt_beforeen interrupt; es la convención de prioridad cuando se usa*para significar «todos los nodos».- El nombre del canal
TASKSestá 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 conSend. - 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
CompiledStateGraphnuevo; el campocompileddel builder se pone aTrueenvalidate()(compiled=True:1161), pero es solo un flag — no impide seguir compilando ni seguir haciendoadd_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