Skip to content

StateGraph : le blueprint déclaratif d'une machine à états

源码版本1.2.9

Responsabilités

StateGraph est la classe avec laquelle les utilisateurs de LangGraph interagissent le plus directement — c'est un builder, pas un runtime. Vous y enregistrez le schéma d'état, les fonctions de nœuds, les arêtes et les arêtes conditionnelles, puis vous appelez .compile() pour le compiler en un CompiledStateGraph (qui hérite de Pregel) exécutable. Il n'a pas lui-même de invoke / stream : pour exécuter le graphe, il faut d'abord le compiler (avertissement documentation:139-144).

Dans l'architecture, il se situe entre l'API utilisateur et le moteur Pregel : l'utilisateur écrit StateGraph(State).add_node(...).add_edge(...).compile(), et StateGraph traduit chaque champ d'état en instance de canal (channel), enveloppe chaque nœud dans un StateNodeSpec, et range chaque arête dans les trois collections edges / waiting_edges / branches, en attendant la compilation pour confier le tout à CompiledStateGraph.attach_node / attach_edge / attach_branch qui les traduit en abonnements de PregelNode (champs de StateGraph:201-213).

La convention centrale de StateGraph est que la signature d'un nœud est State -> Partial<State>, et que chaque champ d'état peut optionnellement être annoté avec Annotated[type, reducer] pour déclarer une fonction de réduction ; lorsque plusieurs nœuds écrivent simultanément le même champ, ils sont fusionnés selon le reducer plutôt qu'écrasés (docstring de classe:131-138). Ce mécanisme de reducer est l'origine des deux types de canaux LastValue (écrasement par défaut) et BinaryOperatorAggregate (par exemple operator.add).

Motivation de conception

Pourquoi ne pas écrire directement une chaîne d'appels de fonctions, et construire tout cet édifice « machine à états + canaux » ?

  • Découplage entre nœuds : un nœud ne connaît que l'état, pas les autres nœuds ; l'ordre d'exécution est décidé par les arêtes, donc modifier le flux ne touche pas le code des nœuds. C'est le bénéfice central d'une machine à états (state machine).
  • Le reducer donne une sémantique définie aux écritures parallèles : si plusieurs nœuds écrivent le champ messages dans le même superpas (superstep) avec Annotated[list, operator.add], le moteur sait qu'il doit faire + au lieu d'« écraser » (doc reducer:135-137) ; un champ sans reducer utilise un canal LastValue dont le comportement est « erreur si plusieurs écritures dans le même superpas » ou « écrasement », selon le type de canal.
  • Validation à la construction : à la compilation, validate() (validate:1116-1162) vérifie que « source et cible de chaque arête sont bien dans nodes », que « START est bien la source d'une arête », que « les nœuds d'interruption existent », etc., pour bloquer les erreurs avant l'exécution.
  • Séparation des schémas input/output/state : un même graphe peut avoir input_schema ≠ state_schema ≠ output_schema (schémas en entrée:217-221), avec une interface externe étroite et un état interne large — un motif courant pour écrire des agents où le schéma d'entrée orienté LLM diffère de l'état accumulé en interne.
  • Method chaining : add_node / add_edge / add_conditional_edges renvoient tous Self (Returns Self:749) — ils supportent le chaînage, et construire un graphe ressemble à un fichier de configuration plutôt qu'à une séquence impérative.

Fichiers clés

  • définition de la classe StateGraph:130-144Generic[StateT, ContextT, InputT, OutputT], documenté explicitement comme « builder, ne peut pas être invoqué directement ».
  • champs de classe:201-213 — les sept conteneurs centraux edges / nodes / branches / channels / managed / schemas / waiting_edges.
  • __init__:215-269 — reçoit state_schema / context_schema / input_schema / output_schema, convertit les anciens noms config_schema / input / output en nouveaux noms avec avertissement, puis appelle _add_schema pour enregistrer les trois schémas state/input/output dans channels / managed.
  • _add_schema:342-372 — utilise _get_channels pour dériver les canaux et managed values du schéma ; en cas de conflit, tout sauf LastValue déclenche une erreur.
  • validate:1116-1162 — validation pré-compilation : parcourt toutes les sources/cibles d'arêtes, vérifie la présence de START et des nœuds d'interruption, puis pose self.compiled = True.
  • set_node_defaults:271-334 — fixe des stratégies retry / cache / error_handler / timeout par défaut pour tout le graphe, priorité plus basse que les paramètres explicites de add_node.
  • signature compile:1164-1217 — reçoit checkpointer / store / cache / interrupt_before / interrupt_after / name, etc., renvoie un CompiledStateGraph.
  • CompiledStateGraph:1391-1409 — hérite de Pregel, ajoute simplement les champs builder / schema_to_mapper et les méthodes de JSON schema d'input/output.
  • attach_node:1431-1470 — à la compilation, traduit un StateNodeSpec en PregelNode et définit _get_updates pour décider quels canaux écrire depuis la valeur de retour du nœud.
  • BranchSpec.from_path:83-120 — traduit le path (Runnable) + path_map de add_conditional_edges en un dict « condition -> nœud cible » ; lorsque path_map n'est pas fourni, tente de le déduire du type de retour Literal[...].

Flux de données

Voici le cœur de StateGraph.__init__ qui enregistre les schémas comme canaux — c'est ici qu'a lieu la traduction « champ d'état -> instance de canal » :

python
self.nodes = {}
self.edges = set()
self.branches = defaultdict(dict)
self.schemas = {}
self.channels = {}
self.managed = {}
self.compiled = False
self.waiting_edges = set()

self.state_schema = state_schema
self.input_schema = cast(type[InputT], input_schema or state_schema)
self.output_schema = cast(type[OutputT], output_schema or state_schema)
self.context_schema = context_schema

self._node_defaults: _NodeDefaults = _NodeDefaults()

self._add_schema(self.state_schema)
self._add_schema(self.input_schema, allow_managed=False)
self._add_schema(self.output_schema, allow_managed=False)

(initialisation des champs:251-269)

Chaque schéma est éclaté par _add_schema en un triplet (channels, managed, type_hints) ; les canaux vont dans le dict self.channels, les managed dans self.managed, et lorsqu'une même clé est enregistrée deux fois, seuls les LastValue sont acceptés en compatibilité :

python
self.schemas[schema] = {**channels, **managed}
for key, channel in channels.items():
    if key in self.channels:
        if self.channels[key] != channel:
            if isinstance(channel, LastValue):
                pass
            else:
                raise ValueError(
                    f"Channel '{key}' already exists with a different type"
                )
    else:
        self.channels[key] = channel

(logique d'enregistrement _add_schema:353-364)

Limites et échecs

  • StateGraph ne peut pas être invoke directement (warning:139-144), il faut passer par .compile() ; utilisé comme Runnable tel quel, il lui manque les champs runtime comme nodes.
  • Conflit de noms de champs d'état = erreur (conflit de canal:360-362), sauf si les deux sont des LastValue — pour éviter une incohérence implicite lorsqu'une même clé a des reducers différents dans deux schémas.
  • input_schema / output_schema n'acceptent pas de managed channels (vérification managed:346-352), les canaux managés étant réservés à state : ce ne sont pas de simples canaux de valeur, mais des objets à cycle de vie comme ContextManager.
  • L'ancien nom config_schema est obsolète (config_schema deprecated:224-231), depuis v1.0 il faut utiliser context_schema, et il sera retiré en 2.0.
  • compile() suivi d'un ajout de nœud ne fait qu'un warning, pas d'erreur (compiled warning:932-936), add_node / add_edge affichent Adding ... to a graph that has already been compiled, mais la modification n'est pas répercutée sur l'instance compilée — piège classique d'ordre de chaînage.
  • validate exige au moins une arête partant de START (vérification entrypoint:1129-1132), Graph must have an entrypoint, sinon l'exécution ne sait pas où démarrer.

Résumé

StateGraph est le blueprint déclaratif d'une machine à états : vous lui fournissez le schéma et des nœuds + arêtes, et il les traduit en PregelNode + abonnements de canaux exécutables par Pregel. Son existence épargne à l'utilisateur le modèle d'actors de Pregel pour ne se concentrer que sur trois choses : champs d'état, signature de nœud et arêtes.

Continuer avec add_node / add_edge / add_conditional_edges pour remplir ce blueprint, et compile pour voir comment le compiler en Pregel. Le modèle d'exécution global est dans moteur Pregel.

Voir la documentation officielle : LangGraph docs · README.