StateGraph.compile : compiler le blueprint en un Pregel exécutable
Responsabilités
StateGraph.compile est le « traducteur » entre le builder et le runtime. Il ingère un StateGraph (specs de nœuds, arêtes, branches conditionnelles, dict de canaux) et produit un CompiledStateGraph (CompiledStateGraph:1391) — qui hérite de Pregel et embarque toutes les méthodes runtime invoke / stream / astream / get_state. Avant compilation, votre graphe n'est que de la donnée de configuration ; après, il devient un exécutable.
Son travail central se résume à quatre tâches : 1) valider la structure du graphe (validate), 2) décider des canaux de sortie / stream, 3) injecter les dépendances runtime (checkpointer / store / cache / configuration d'interruption), 4) traduire chaque StateNodeSpec / edge / BranchSpec en PregelNode + abonnements de canaux Pregel (construction CompiledStateGraph:1333-1357). Cette traduction se fait en bouclant sur compiled.attach_node / compiled.attach_edge / compiled.attach_branch — ces méthodes convertissent les structures déclaratives du builder en champs runtime comme PregelNode.writers / PregelNode.triggers.
compile porte aussi deux responsabilités secondaires : 1) appliquer les valeurs par défaut de set_node_defaults (retry / cache / error_handler / timeout) à tous les nœuds qui ne les spécifient pas explicitement, 2) lorsque la sérialisation stricte msgpack est activée (_serde.STRICT_MSGPACK_ENABLED), construire une allowlist serde pour que le checkpointer ne sérialise que les champs présents dans le schéma (serde allowlist:1220-1241).
Motivation de conception
Pourquoi ne pas faire de StateGraph lui-même un Pregel, au lieu de scinder en deux étapes ?
- Séparation des préoccupations construction vs exécution : le builder se préoccupe uniquement de « à quoi ressemble le graphe », le runtime de « comment l'exécuter ». Cette séparation permet de compiler plusieurs fois le même builder en runtimes distincts — un même StateGraph avec différents checkpointer / store / nœuds d'interruption peut donner plusieurs instances de Pregel.
- Runtime immuable : après compilation, les champs du
CompiledStateGraphsont essentiellement figés dansPregel.__init__(Pregel.__init__:758-836), etauto_validate=Falselaisse à compile le contrôle du moment de la validation, évitant une validation prématurée par le constructeur. - Validation différée : à l'appel de
add_node, l'utilisateur ne fait qu'ajouter dans un conteneur, sans vérifier immédiatement « la source de l'arête existe-t-elle » — cela permet d'enregistrer nœuds et arêtes dans n'importe quel ordre ; la validation réelle a lieu en une fois à la compilation (appel validate:1247-1254). - Configuration d'interruption paramétrée :
interrupt_before/interrupt_aftersont des paramètres de compile, pas une propriété du graphe lui-même — un même graphe peut donc être compilé en une version « avec interruption » et une « sans interruption », un motif très courant en interaction humain-dans-la-boucle (paramètres interrupt:1170-1171). - Normalisation du type de checkpointer :
compile(checkpointer=...)accepteNone/True/False/BaseCheckpointSaver, etensure_valid_checkpointer(ensure_valid_checkpointer:107-117) valide de façon unifiée pour éviter une explosion runtime. - Les valeurs par défaut ne s'appliquent qu'à la compilation (
application defaults:1299-1325) — ce qui permet d'appelerset_node_defaultsaprèsadd_node, les nouvelles valeurs par défaut écrasant tous les nœuds qui ne les ont pas explicitement spécifiées.
Fichiers clés
signature compile:1164-1217— reçoitcheckpointer/store/cache/interrupt_before/interrupt_after/debug/name/transformers.ensure_valid_checkpointer:107-117— valide que le checkpointer est bien l'un deNone/True/False/BaseCheckpointSaver, sinonTypeError.appel ensure_valid_checkpointer:1218— la première chose que fait compile est de normaliser le checkpointer.serde allowlist:1220-1241— en mode msgpack strict, construit une allowlist et l'applique au checkpointer pour que le point de contrôle (checkpoint) ne stocke que les champs du schéma.fusion interrupt + validate:1243-1254—"*"signifie All (tous les nœuds), fusionneinterrupt_before/interrupt_afteren une seule liste passée àvalidate.canaux output / stream:1256-1273— un seul champ__root__utilise directement une chaîne, sinon une liste filtre les managed values.nœud error handler par défaut:1278-1297— l'error_handler global posé parset_node_defaultsest injecté comme un nœud spécial nommé__default_error_handler__.application defaults:1299-1325— applique retry / cache / error_handler / timeout deset_node_defaultsà chaque spec non explicitement renseignée ; cache et error_handler ne s'appliquent pas au nœud error-handler lui-même.node_error_handler_map:1327-1331— génère la correspondancenode_name -> handler_node_name, utilisée au runtime pour router l'exécution d'un nœud en échec vers son handler.construction CompiledStateGraph:1333-1357— fusionnechannels/manageddu builder, ajouteSTART: EphemeralValue(input_schema)comme canal d'entrée, et posestream_mode="updates",input_channels=START.trilogie attach:1360-1388— boucle surcompiled.attach_node(START, None)+ chaque nœud,attach_edgepar arête,attach_branchpar branche ; enfincompiled.validate().Pregel.__init__:758-836—CompiledStateGraph.__init__délègue à Pregel viasuper().__init__(**kwargs), qui déploie tous les champs runtime et, siauto_validate=True, appelleself.validate().
Flux de données
Voici le passage clé où compile construit réellement le runtime : tous les champs du builder sont injectés dans CompiledStateGraph, avec en plus un canal d'entrée 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)(construction + attach_node:1333-1362)
Ensuite, les arêtes et branches sont également traduites en abonnements de canaux Pregel, puis une dernière validation est déclenchée :
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__, une fois ces champs reçus, traduit les NodeBuilder de nodes en PregelNode, installe un Topic(Send, accumulate=False) sur le canal TASKS, puis appelle self.validate() si auto_validate=True (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)Limites et échecs
checkpointer=Trueinterdit sur un graphe racine (erreur True:2583-2584) —Truesignifie « hériter du graphe parent », réservé aux sous-graphes ; un graphe racine doit explicitement fournir un saver ouFalse.interrupt_before="*"etinterrupt_after="*"sont gérés en exclusion mutuelle (traitement *:1249-1253) — la liste deinterrupt_beforen'est fusionnée dans interrupt que siinterrupt_after != "*"; c'est la convention de priorité quand*désigne « tous les nœuds ».- Le nom de canal
TASKSest réservé (TASKS réservé:804-807) — définir dans le schéma un champ nommé__pregel_tasksdéclenche une erreur directe, car c'est le canal Topic interne au moteur pour fan-out desSend. - attach répété d'un nœud de même nom : le builder bloque déjà les doublons (
doublon:792-793) — mais les nœuds auto-générés comme__default_error_handler__doivent aussi éviter de collisionner avec un nœud utilisateur (conflit default handler:1280-1284). - cache et error_handler par défaut ne s'appliquent pas au nœud handler lui-même (
pas de cache au handler:1313-1321) — parce que « cacher le résultat d'un handler » n'est pas sûr (l'état du nœud en échec peut varier à chaque fois), et qu'« un handler qui s'attrape lui-même » bouclerait infiniment. - Compiler plusieurs fois le même builder est autorisé : chaque compile crée un nouveau
CompiledStateGraph; le champcompileddu builder est bien posé àTruelors devalidate()(compiled=True:1161), mais c'est juste un marqueur, qui n'empêche ni une nouvelle compile ni de continuer à appeleradd_node(un warning sera simplement émis).
Résumé
compile est la frontière entre builder et runtime — il traduit tous les specs accumulés via add_node / add_edge / add_conditional_edges dans StateGraph en structures runtime de Pregel, y attache la configuration checkpointer / store / interrupt, et renvoie enfin une sous-classe de Pregel directement invoke-able. Le comprendre, c'est comprendre toute la chaîne « déclaration de graphe -> exécutable ».
La suite possible : moteur Pregel pour voir comment invoke / stream exécutent ce produit compilé après Pregel.__init__, ou StateSnapshot pour voir comment l'état est lu depuis le produit compilé une fois le checkpointer attaché.
Voir la documentation officielle : LangGraph docs · README.