RunnableConfig : comment la configuration traverse tout le graphe
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 detags/metadata/callbacksde LangChain. - Héritable et surchargeable : quand un sous-graphe s'exécute à l'intérieur d'un nœud du graphe parent, le
configurabledu parent se transmet automatiquement ; mais si le sous-graphe fournit explicitement son proprethread_id, il est considéré comme « réinitialisant la lignée de points de contrôle », voir la logique « explicit config » dansensure_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 dansCONFIG_KEYS, et les champs runtime réservés par LangGraph sont systématiquement préfixés__pregel_et rangés dansconfigurable, pour éviter les collisions avec les clés définies par l'utilisateur, voirconstantes CONFIG_KEY_*:33-77. - Valeurs par défaut réglables par variable d'environnement :
recursion_limitlit par défaut la variableLANGGRAPH_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 leRunnableConfigcourant depuis la contextvarvar_child_runnable_config; hors contexte runnable, lève unRuntimeError.get_store:32-123— récupère le store depuisconfig[CONF][CONFIG_KEY_RUNTIME].store; la doc montre les deux usagesStateGraphetentrypoint.get_stream_writer:126-196— récupèreruntime.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éfautrecursion_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 dictconfigurablesuit 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_checkpointeret 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-92—CONF = "configurable", toutes les clés réservées sont sousconfig[CONF].validation recursion_limit:2563-2564—_setup_streamlit directementconfig["recursion_limit"], erreur si inférieur à 1.loop.stop:1701—self.stop = self.step + self.config["recursion_limit"] + 1, traduit la limite de récursion en « step maximal à atteindre ».GraphRecursionError:3005-3011— quandout_of_stepsest atteint, lèveGraphRecursionErrorpour inviter à releverrecursion_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.
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 ».
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_confighors contexte runnable lève directementRuntimeError(erreur get_config:29), donc un test unitaire qui appelleget_store()directement explose — il faut passer pargraph.invokeou une fonction décorée parentrypointpour y accéder.- En Python < 3.11,
get_store/get_stream_writerne sont pas utilisables en async (avertissement async:53-57), car la propagation de contextvar dépend du comportement d'asyncio.create_taskapparu en 3.11. recursion_limit < 1déclenche une erreur directe (validation:2563-2564), pas de repli silencieux sur la valeur par défaut.checkpointer=Trueinterdit pour un graphe racine (erreur True:2583-2584) —Truesignifie « hériter du parent », et un graphe racine n'a pas de parent, donc il faut explicitement fournir unBaseCheckpointSaverouFalse.- Si le checkpointer est activé mais sans coordonnées
thread_iddansconfigurable, erreur (exigence de coordonnées:2589-2593) — il faut l'un dethread_id/checkpoint_ns/checkpoint_id. - Un sous-graphe qui passe explicitement un
thread_idréinitialise leconfigurableambient (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.