RunnableConfig:配置如何流遍整张图
职责
LangGraph 跑一张图,需要携带的「运行期上下文」远不止输入数据本身。recursion_limit、thread_id、checkpoint_id、回调、tags、metadata,以及给节点用的 store / stream_writer,都得在调用 graph.invoke(input, config) 的时候被传进去、被合并、被校验,最后被嵌进 configurable 这个字典透传到每个节点的 RunnableConfig。这套统一格式的配置对象就叫 RunnableConfig,它直接复用 LangChain Core 的同名类型 (from langchain_core.runnables import RunnableConfig),不是 LangGraph 自己重造的轮子。
它落在架构的最入口处:Pregel.invoke / Pregel.stream 一进来就调 ensure_config(ensure_config:322)把用户传的零散字段补齐默认值、把继承自父图的 configurable 跟调用时传入的合并,得到一份完整 config,再传给 PregelLoop。整个超步 (superstep) 循环里,这份 config 既是检查点的定位坐标 (thread_id + checkpoint_ns + checkpoint_id),也是节点拿运行时工具 (store / stream_writer / runtime) 的入口。
节点内部不直接接 config 字典取这些字段——LangGraph 暴露了 get_config(get_config:17)、get_store、get_stream_writer 三个快捷函数,内部从 contextvar 读出当前 context 里的 RunnableConfig,再从 configurable 字典里取出 Runtime 实例。所以从外到内,config 经历了「用户传 → ensure_config 补齐 → loop 透传 → contextvar 注入 → 节点取用」五个阶段。
设计动机
为什么要把一切塞进 RunnableConfig,而不是定义一套全新的 GraphConfig 类?
- 复用 LangChain 生态:Runnable 协议、tracing、callbacks 全都认
RunnableConfig,图节点天然兼容 LangChain 的工具链;ensure_config(empty = RunnableConfig(...):322-337)连tags/metadata/callbacks的合并规则都跟 LangChain 一致。 - 可继承可覆盖:子图跑在父图的某个节点内部时,父图的
configurable会自动透传;但只要子图显式给了自己的thread_id,就视作「重设检查点血统」,见ensure_config里的「explicit config」逻辑(explicit checkpoint coordinate:355-367)——否则子图会把检查点写到父命名空间下再也找不到。 - 字段边界清晰:标准字段 (
tags/metadata/callbacks/recursion_limit/configurable) 是CONFIG_KEYS,LangGraph 自己保留的运行时字段统一加__pregel_前缀塞进configurable,避免和用户自定义 key 撞车,见CONFIG_KEY_* 常量:33-77。 - 默认值可环境变量调:
recursion_limit默认走LANGGRAPH_DEFAULT_RECURSION_LIMIT环境变量(DEFAULT_RECURSION_LIMIT:32),不用改代码就能调高循环上限。
关键文件
get_config:17-29— 从var_child_runnable_config这个 contextvar 拿当前RunnableConfig;没在 runnable 上下文里就抛RuntimeError。get_store:32-123— 从config[CONF][CONFIG_KEY_RUNTIME].store取 store,文档里给了StateGraph和entrypoint两种用法。get_stream_writer:126-196— 拿runtime.stream_writer,节点用它发自定义流事件 (stream_mode="custom")。ensure_config:322-420— 合并多份 config、补默认recursion_limit=DEFAULT_RECURSION_LIMIT、处理 checkpoint 坐标重设。merge_configs:147-180— 多份 config 深度合并的底层实现,configurable字典走浅合并。patch_configurable:52-62— 只 patchconfigurable这一个 key,保留其它字段——loop 用来动态注入__pregel_checkpointer等。CONFIG_KEY_* 常量:33-77—__pregel_send/__pregel_read/__pregel_checkpointer/__pregel_runtime/__pregel_resuming等保留 key 全集中。CONF 常量:91-92—CONF = "configurable",所有保留 key 都挂在config[CONF]下。recursion_limit 校验:2563-2564—_setup_stream里直接读config["recursion_limit"],小于 1 就报错。loop.stop:1701—self.stop = self.step + self.config["recursion_limit"] + 1,把递归上限翻译成「最多跑到第几步」。GraphRecursionError:3005-3011— 跑到out_of_steps时抛GraphRecursionError,提示用户调高recursion_limit。
数据流
最关键的一段是 ensure_config:把 contextvar 里继承的 config 跟用户传入的 config 合并,遇到显式的 checkpoint 坐标就重置 ambient configurable,确保 recursion_limit / configurable / tags / metadata / callbacks 全部到位。
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)},
)合并完之后,Pregel._setup_stream(_setup_stream:2563)会从 config[CONF] 里读 CONFIG_KEY_CHECKPOINTER / CONFIG_KEY_RUNTIME / CONFIG_KEY_CACHE,决定本次运行用哪个 checkpointer / store / cache——优先级是「config 注入 > 构造时传入」。
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边界与失败
get_config在 runnable 上下文外调用直接抛RuntimeError(get_config 报错:29),所以单元测试里直接get_store()会炸——必须走graph.invoke或entrypoint装饰的函数才能取到。- Python < 3.11 异步下
get_store/get_stream_writer不可用(async 警告:53-57),因为 contextvar 传播依赖 3.11 才有的asyncio.create_task行为。 recursion_limit < 1直接报错(校验:2563-2564),不是静默回退到默认值。checkpointer=True不能用于根图(True 报错:2583-2584),True表示「从父图继承」,根图没父图,所以必须显式给一个BaseCheckpointSaver或False。- 开了 checkpointer 但没给
configurable里的thread_id等坐标会报错(checkpointer 要求坐标:2589-2593),提示需要thread_id/checkpoint_ns/checkpoint_id之一。 - 子图显式传
thread_id会重设 ambientconfigurable(explicit 坐标重设:362-367),这是为了避免子图把检查点写到父图命名空间下找不到——但反过来,如果你期望子图继承父的configurable自定义 key,这个重设会让你拿不到。
小结
RunnableConfig 是 LangGraph 跟 LangChain 生态对接的契约,也是检查点定位、运行时工具注入、回调与追踪的唯一载体。理解了 ensure_config 的合并规则和 __pregel_* 保留 key 的分工,基本就掌握了「配置怎么从用户传到节点」这条路。
继续看 StateSnapshot 怎么把这份 config 跟检查点绑在一起,以及 Pregel 引擎 里 loop.stop 怎么用 recursion_limit 卡死循环。
对照官方资料:LangGraph 文档 · README。