培歌行学LangGraph(12):递归限制——别让你的图无限跑下去

0 阅读5分钟
  1. 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
  2. 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
  3. 培歌行学LangGraph(3):编译、执行与可视化
  4. 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
  5. 培歌行学LangGraph(5):状态管理与graph.invoke入参深度解析
  6. 培歌行学LangGraph(6):Multi Schema多状态管理详解
  7. 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
  8. 培歌行学LangGraph(8):控制流详解
  9. 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
  10. 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
  11. 培歌行学LangGraph(11):用循环结构实现 ReAct Agent

一、为什么需要递归限制?

循环结构是 LangGraph 的核心能力之一,但循环也有风险:

如果停止条件没写好,或者模型一直返回 tool_calls,你的图就会无限跑下去。

为了防止这种情况,LangGraph 提供了 递归限制(recursion_limit) ,限制单次运行中最多允许执行多少个 SuperStep(超步)。

当步数耗尽仍未停止时,LangGraph 会抛出 GraphRecursionError

你可以:

  • 主动处理:在图内部提前判断剩余步数,优雅退出
  • 被动处理:在图外面捕获异常,兜底处理

二、步骤计数器:认识 SuperStep

2.1 什么是 SuperStep?

SuperStep 是 LangGraph 的最小执行单位。

  • 顺序执行的节点,每个节点占一个 SuperStep
  • 并行执行的节点,多个节点共用一个 SuperStep

2.2 如何获取当前步数?

在节点函数里加一个 config参数即可:

from langchain_core.runnables import RunnableConfig

def my_node(state, config: RunnableConfig):
    current_step = config["metadata"]["langgraph_step"]
    print(f"当前在第 {current_step} 步")

来看一个顺序执行的例子:

def node_a(state, config: RunnableConfig):
    print(config["metadata"]["langgraph_step"])  # 1

def node_b(state, config: RunnableConfig):
    print(config["metadata"]["langgraph_step"])  # 2

def node_c(state, config: RunnableConfig):
    print(config["metadata"]["langgraph_step"])  # 3

三个节点依次执行,步数分别是 1、2、3。


三、配置递归限制

3.1 基本用法

invoke时传入 recursion_limit

graph.invoke(input_data, config={"recursion_limit": 10})

3.2 默认值是多少?小心踩坑

recursion_limit的默认值不是固定的,取决于你从哪里运行:

来源默认值说明
LangChain RunnableConfig25LangChain Agent 运行时
LangGraph 内部配置10000自定义 LangGraph 图
LangGraph API 服务端10011部署到 LangGraph Cloud

最佳实践:永远显式传 recursion_limit,别依赖默认值。


四、主动方法:优雅退出(推荐)

4.1 原理

LangGraph 提供了一个特殊的托管值 RemainingSteps,表示还剩多少步可用

你可以在 State 里声明它,然后在节点或路由函数里读取,提前判断是否该退出了。

4.2 代码示例

from langgraph.managed import RemainingSteps

class OverAllState(TypedDict):
    remaining_steps: RemainingSteps  # 自动注入,不用手动赋值

def loop_node(state: OverAllState, config: RunnableConfig):
    cur_step = config["metadata"]["langgraph_step"]
    remaining = state["remaining_steps"]
    print(f"第 {cur_step} 步,还剩 {remaining} 步")

def router(state: OverAllState) -> Literal["loop_node", END]:
    if state["remaining_steps"] < 3:
        print("步数不足,优雅退出")
        return END
    return "loop_node"

# 图结构:START → loop_node → router → (loop_node | END)
builder.add_edge(START, "loop_node")
builder.add_conditional_edges("loop_node", router)

graph = builder.compile()
graph.invoke({}, config={"recursion_limit": 10})

输出:

1 步,还剩 9 步
第 2 步,还剩 8 步
...
第 8 步,还剩 2 步
步数不足,优雅退出

4.3 优点

  • 图正常结束,不抛异常
  • 可以保存中间状态
  • 可以返回"部分结果"给用户
  • 适合业务循环(ReAct、重试、反思)

五、被动方法:异常捕获(兜底)

5.1 原理

如果循环没有停止条件,步数耗尽后 LangGraph 会抛出 GraphRecursionError

你在外面用 try/except抓住就行。

5.2 代码示例

from langgraph.errors import GraphRecursionError

class EmptyState(TypedDict):
    pass

def loop_node(state, config: RunnableConfig):
    print(f"第 {config['metadata']['langgraph_step']} 步")

builder = StateGraph(EmptyState)
builder.add_node("loop_node", loop_node)
builder.add_edge(START, "loop_node")
builder.add_edge("loop_node", "loop_node")  # 死循环,没有停止条件

graph = builder.compile()

try:
    graph.invoke({}, config={"recursion_limit": 10})
except GraphRecursionError as e:
    print(f"达到递归限制:{e}")

输出:

1 步
第 2 步
...
第 10 步
达到递归限制:Recursion limit of 10 reached without hitting a stop condition.

5.3 优点

  • 实现简单,不用改图结构
  • 适合作为"最后一道防线"

六、主动 vs 被动:怎么选?

对比项主动(RemainingSteps)被动(try/except)
检测时机达到限制之前超出限制之后
图是否正常结束✅ 是❌ 否,被异常中断
能否保存中间状态✅ 能❌ 不能
实现复杂度中等简单
推荐场景业务循环(ReAct、重试)兜底保护

经验法则

  • 业务逻辑里的循环 → 用主动方法,优雅降级
  • 纯粹防无限循环 → 被动方法兜底,两层保险更稳

七、面试题

面试题1:recursion_limitlanggraph_step是什么关系?

recursion_limit是最大允许的 SuperStep 数量,langgraph_step是当前所处的 SuperStep 序号。两者之差就是剩余可用步数。如果 langgraph_step达到 recursion_limit还没停止,就会抛 GraphRecursionError

面试题2:RemainingSteps的值是谁维护的?需要手动更新吗?

:不需要手动更新。RemainingSteps是 LangGraph 的托管值(Managed Value),运行时自动根据当前步数和 recursion_limit计算并注入到 State 里。你只管读,不管写。

面试题3:主动方法和被动方法可以一起用吗?

:可以。推荐的做法是:图内部用主动方法做优雅降级,图外部用被动方法兜底。这样即使主动方法漏判了,还有最后一层保障。

try:
    result = graph.invoke({}, config={"recursion_limit": 10})
except GraphRecursionError:
    # 兜底处理
    result = fallback_response()

面试题4:为什么 recursion_limit的默认值在不同地方不一样?

:因为 LangGraph 有多个运行入口:

  • 本地调试时走 langgraph._internal._config,默认 10000
  • 通过 LangChain Agent 调用时走 RunnableConfig,默认 25
  • 部署到 LangGraph Cloud 时走服务端配置,默认 10011

最佳实践:永远显式传值,不要依赖默认值。

面试题5:在 ReAct 循环里,推荐用哪种方式控制递归限制?

:推荐主动方法。因为 ReAct 是业务循环,你需要:

  1. 在步数不足时返回"部分结果"给用户(比如"已查到天气,但热点还在查询中")
  2. 保存中间状态到检查点,方便后续恢复
  3. 让图正常结束,不抛异常

被动方法适合作为最后一道防线,但不是首选。


递归限制不是用来限制你的想象力,而是用来保护你的钱包。 ​ 一个无限循环的 Agent,跑的不是代码,是你的 API 账单。