Skip to content

StateGraph.compile : compiler le blueprint en un Pregel exécutable

源码版本1.2.9

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 CompiledStateGraph sont essentiellement figés dans Pregel.__init__ (Pregel.__init__:758-836), et auto_validate=False laisse à 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_after sont 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=...) accepte None / True / False / BaseCheckpointSaver, et ensure_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'appeler set_node_defaults après add_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çoit checkpointer / store / cache / interrupt_before / interrupt_after / debug / name / transformers.
  • ensure_valid_checkpointer:107-117 — valide que le checkpointer est bien l'un de None / True / False / BaseCheckpointSaver, sinon TypeError.
  • 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), fusionne interrupt_before / interrupt_after en 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é par set_node_defaults est injecté comme un nœud spécial nommé __default_error_handler__.
  • application defaults:1299-1325 — applique retry / cache / error_handler / timeout de set_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 correspondance node_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 — fusionne channels / managed du builder, ajoute START: EphemeralValue(input_schema) comme canal d'entrée, et pose stream_mode="updates", input_channels=START.
  • trilogie attach:1360-1388 — boucle sur compiled.attach_node(START, None) + chaque nœud, attach_edge par arête, attach_branch par branche ; enfin compiled.validate().
  • Pregel.__init__:758-836CompiledStateGraph.__init__ délègue à Pregel via super().__init__(**kwargs), qui déploie tous les champs runtime et, si auto_validate=True, appelle self.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 :

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)

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

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

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)

Limites et échecs

  • checkpointer=True interdit sur un graphe racine (erreur True:2583-2584) — True signifie « hériter du graphe parent », réservé aux sous-graphes ; un graphe racine doit explicitement fournir un saver ou False.
  • interrupt_before="*" et interrupt_after="*" sont gérés en exclusion mutuelle (traitement *:1249-1253) — la liste de interrupt_before n'est fusionnée dans interrupt que si interrupt_after != "*" ; c'est la convention de priorité quand * désigne « tous les nœuds ».
  • Le nom de canal TASKS est réservé (TASKS réservé:804-807) — définir dans le schéma un champ nommé __pregel_tasks déclenche une erreur directe, car c'est le canal Topic interne au moteur pour fan-out des Send.
  • 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 champ compiled du builder est bien posé à True lors de validate() (compiled=True:1161), mais c'est juste un marqueur, qui n'empêche ni une nouvelle compile ni de continuer à appeler add_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.