Skip to content

RunnableConfig : comment la configuration traverse tout le graphe

源码版本1.2.9

Responsabilités

L'exécution d'un graphe LangGraph nécessite un « contexte runtime » bien au-delà de la simple donnée d'entrée. recursion_limit, thread_id, checkpoint_id, callbacks, tags, metadata, ainsi que store / stream_writer utilisables par les nœuds, doivent être passés lors de l'appel graph.invoke(input, config), puis fusionnés, validés, et finalement intégrés au dictionnaire configurable pour être transmis au RunnableConfig de chaque nœud. Cet objet de configuration au format unifié s'appelle RunnableConfig ; il réutilise directement le type éponyme de LangChain Core (from langchain_core.runnables import RunnableConfig), ce n'est pas une roue reinventée par LangGraph.

Il se situe tout à l'entrée de l'architecture : dès qu'on entre dans Pregel.invoke / Pregel.stream, ensure_config est appelé (ensure_config:322) pour compléter avec des valeurs par défaut les champs épars fournis par l'utilisateur, fusionner le configurable hérité du graphe parent avec celui passé à l'appel, et produire une config complète transmise à PregelLoop. Tout au long de la boucle de superpas (superstep), cette config sert à la fois de coordonnées de point de contrôle (checkpoint) (thread_id + checkpoint_ns + checkpoint_id) et de porte d'accès aux outils runtime (store / stream_writer / runtime) pour les nœuds.

À l'intérieur d'un nœud, on n'accède pas directement au dict de config pour ces champs — LangGraph expose trois fonctions de raccourci : get_config (get_config:17), get_store, get_stream_writer, qui lisent en interne le RunnableConfig courant dans la contextvar, puis extraient l'instance de Runtime depuis le dict configurable. De l'extérieur vers l'intérieur, la config traverse donc cinq étapes : « passage utilisateur -> complétion par ensure_config -> transmission par le loop -> injection en contextvar -> consommation par le nœud ».

Motivation de conception

Pourquoi tout fourrer dans RunnableConfig plutôt que de définir une nouvelle classe GraphConfig ?

  • Réutilisation de l'écosystème LangChain : le protocole Runnable, le tracing, les callbacks reconnaissent tous RunnableConfig, donc les nœuds du graphe sont compatibles par construction avec la chaîne d'outils LangChain ; ensure_config (empty = RunnableConfig(...):322-337) suit même les règles de fusion de tags / metadata / callbacks de LangChain.
  • Héritable et surchargeable : quand un sous-graphe s'exécute à l'intérieur d'un nœud du graphe parent, le configurable du parent se transmet automatiquement ; mais si le sous-graphe fournit explicitement son propre thread_id, il est considéré comme « réinitialisant la lignée de points de contrôle », voir la logique « explicit config » dans ensure_config (coordonnées de checkpoint explicites:355-367) — sinon le sous-graphe écrirait ses checkpoints sous le namespace parent et ne les retrouverait plus.
  • Frontières de champs claires : les champs standards (tags / metadata / callbacks / recursion_limit / configurable) sont dans CONFIG_KEYS, et les champs runtime réservés par LangGraph sont systématiquement préfixés __pregel_ et rangés dans configurable, pour éviter les collisions avec les clés définies par l'utilisateur, voir constantes CONFIG_KEY_*:33-77.
  • Valeurs par défaut réglables par variable d'environnement : recursion_limit lit par défaut la variable LANGGRAPH_DEFAULT_RECURSION_LIMIT (DEFAULT_RECURSION_LIMIT:32), ce qui permet de relever la limite de boucle sans modifier le code.

Fichiers clés

  • get_config:17-29 — récupère le RunnableConfig courant depuis la contextvar var_child_runnable_config ; hors contexte runnable, lève un RuntimeError.
  • get_store:32-123 — récupère le store depuis config[CONF][CONFIG_KEY_RUNTIME].store ; la doc montre les deux usages StateGraph et entrypoint.
  • get_stream_writer:126-196 — récupère runtime.stream_writer, utilisé par les nœuds pour émettre des événements de stream personnalisés (stream_mode="custom").
  • ensure_config:322-420 — fusionne plusieurs configs, applique la valeur par défaut recursion_limit=DEFAULT_RECURSION_LIMIT, gère le reset des coordonnées de checkpoint.
  • merge_configs:147-180 — implémentation bas-niveau de la fusion profonde de plusieurs configs ; le dict configurable suit une fusion superficielle.
  • patch_configurable:52-62 — ne patche que la clé configurable, en préservant les autres champs — utilisé par le loop pour injecter dynamiquement __pregel_checkpointer et al.
  • constantes CONFIG_KEY_*:33-77 — inventaire complet des clés réservées __pregel_send / __pregel_read / __pregel_checkpointer / __pregel_runtime / __pregel_resuming.
  • constante CONF:91-92CONF = "configurable", toutes les clés réservées sont sous config[CONF].
  • validation recursion_limit:2563-2564_setup_stream lit directement config["recursion_limit"], erreur si inférieur à 1.
  • loop.stop:1701self.stop = self.step + self.config["recursion_limit"] + 1, traduit la limite de récursion en « step maximal à atteindre ».
  • GraphRecursionError:3005-3011 — quand out_of_steps est atteint, lève GraphRecursionError pour inviter à relever recursion_limit.

Flux de données

Le passage clé est ensure_config : il fusionne la config héritée de la contextvar avec celle passée par l'utilisateur, et en cas de coordonnées de checkpoint explicites, réinitialise le configurable ambient, pour garantir que recursion_limit / configurable / tags / metadata / callbacks sont tous présents.

python
empty = RunnableConfig(
    tags=[],
    metadata=ChainMap(),
    callbacks=None,
    recursion_limit=DEFAULT_RECURSION_LIMIT,
    configurable={},
)
if var_config := var_child_runnable_config.get():
    empty.update(
        {k: v.copy() if k in COPIABLE_KEYS else v
         for k, v in var_config.items() if _is_not_empty(v)},
    )

(ensure_config valeurs par défaut:331-345)

Une fois la fusion faite, Pregel._setup_stream (_setup_stream:2563) lit dans config[CONF] les valeurs CONFIG_KEY_CHECKPOINTER / CONFIG_KEY_RUNTIME / CONFIG_KEY_CACHE pour décider quel checkpointer / store / cache utiliser à l'exécution — la priorité est « injection par config > valeur passée à la construction ».

python
if self.checkpointer is False:
    checkpointer: BaseCheckpointSaver | None = None
elif CONFIG_KEY_CHECKPOINTER in config.get(CONF, {}):
    checkpointer = config[CONF][CONFIG_KEY_CHECKPOINTER]
elif self.checkpointer is True:
    raise RuntimeError("checkpointer=True cannot be used for root graphs.")
else:
    checkpointer = self.checkpointer

(résolution checkpointer:2579-2586)

Limites et échecs

  • get_config hors contexte runnable lève directement RuntimeError (erreur get_config:29), donc un test unitaire qui appelle get_store() directement explose — il faut passer par graph.invoke ou une fonction décorée par entrypoint pour y accéder.
  • En Python < 3.11, get_store / get_stream_writer ne sont pas utilisables en async (avertissement async:53-57), car la propagation de contextvar dépend du comportement d'asyncio.create_task apparu en 3.11.
  • recursion_limit < 1 déclenche une erreur directe (validation:2563-2564), pas de repli silencieux sur la valeur par défaut.
  • checkpointer=True interdit pour un graphe racine (erreur True:2583-2584) — True signifie « hériter du parent », et un graphe racine n'a pas de parent, donc il faut explicitement fournir un BaseCheckpointSaver ou False.
  • Si le checkpointer est activé mais sans coordonnées thread_id dans configurable, erreur (exigence de coordonnées:2589-2593) — il faut l'un de thread_id / checkpoint_ns / checkpoint_id.
  • Un sous-graphe qui passe explicitement un thread_id réinitialise le configurable ambient (reset coordonnées explicites:362-367) — pour éviter que les checkpoints du sous-graphe soient écrits sous le namespace parent et introuvables ; en revanche, si vous vous attendiez à ce que le sous-graphe hérite des clés personnalisées du parent, ce reset vous empêchera de les récupérer.

Résumé

RunnableConfig est le contrat entre LangGraph et l'écosystème LangChain, et le seul vecteur de localisation de points de contrôle, d'injection d'outils runtime, de callbacks et de tracing. Comprendre les règles de fusion de ensure_config et la répartition des clés réservées __pregel_*, c'est essentiellement maîtriser le chemin « comment la configuration va de l'utilisateur jusqu'au nœud ».

Continuer avec StateSnapshot pour voir comment cette config est liée au point de contrôle, et avec moteur Pregel pour voir comment loop.stop utilise recursion_limit pour borner la boucle.

Voir la documentation officielle : LangGraph docs · README.