LangGraph GraphRecursionError(25) 根因分析

日期:2026-05-19   入口:endpoint_test.pyagui.py

1. 现象

langgraph.errors.GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition.

尽管 endpoint.py 中���配置 "recursion_limit": 1000,运行时仍以默认值 25 触发限制。

2. 调用链路

#位置说明
1endpoint.py:36Agent config 设置 recursion_limit: 1000
2agui.py:225 (retry)super().run(input) 进入 ag_ui_langgraph
3ag_ui_langgraph/agent.py:142ensure_config(self.config.copy()) → config 含 recursion_limit=1000
4ag_ui_langgraph/agent.py:311regenerate 路径prepare_regenerate_stream 被触发
5ag_ui_langgraph/agent.py:392fork = await self.graph.aupdate_state(...) 返回仅含 configurable 的 config
6ag_ui_langgraph/agent.py:401get_stream_kwargs(config=fork) — fork 中无 recursion_limit
7langchain_core/.../event_stream.py:1027ensure_config(fork) 填充默认值 → recursion_limit=25
8langgraph/pregel/main.py:2804ensure_config(self.config, config) — 后者(25)覆盖前者(1000)

3. 根因

regenerate (time-travel) 路径中,aupdate_state 返回的 fork config 仅包含 checkpoint 元数据(thread_id, checkpoint_id),不含 recursion_limit

经过 langchain_core.ensure_config 后被填充为默认值 25。随后在 Pregel.astream 中,langgraph 的 ensure_config 按顺序赋值(无 "skip if default" 保护),最终 25 覆盖了 graph 自身的 1000。

关键差异:langgraph ensure_config vs merge_configs

# langgraph ensure_config — 简单赋值,后者覆盖前者
for config in configs:
    for k, v in config.items():
        if _is_not_empty(v) and k in CONFIG_KEYS:
            empty[k] = v          # ← 25 覆盖 1000

# langgraph merge_configs — 有保护逻辑
elif key == "recursion_limit":
    if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT:
        base["recursion_limit"] = config["recursion_limit"]  # ← 25 == default,跳过

4. 触发条件

  1. Thread 有历史消息(agent_state.messages > client_messages
  2. 进入 regenerate 路径(time-travel checkpoint 存在)
  3. fork config 缺少 recursion_limit

首次调用和 retry 均可能触发(retry 更容易,因为首次失败后 state 中已有新消息)。

5. 修复方案

BaseLangGraphAGUIAgent.get_stream_kwargs 中,将 config 传入 kwargs 前,确保 recursion_limit 始终从 self.config 继承:
# agui.py — get_stream_kwargs
if config:
    if self.config and "recursion_limit" in self.config and "recursion_limit" not in config:
        config = {**config, "recursion_limit": self.config["recursion_limit"]}
    kwargs['config'] = config

此修复覆盖所有代码路径(normal / regenerate / resume),不修改原始 config 对象。

6. 涉及文件

文件角色
src/agents/copilot/agui.py修复位置 — get_stream_kwargs
src/agents/copilot/endpoint.pyAgent 配置入口
ag_ui_langgraph/agent.pyprepare_regenerate_stream(问题源头)
langgraph/_internal/_config.pyensure_config 覆盖行为
langchain_core/runnables/config.pyDEFAULT_RECURSION_LIMIT = 25