- 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
- 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
- 培歌行学LangGraph(3):编译、执行与可视化
- 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
- 培歌行学LangGraph(5):状态管理与
graph.invoke入参深度解析 - 培歌行学LangGraph(6):Multi Schema多状态管理详解
- 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
- 培歌行学LangGraph(8):控制流详解
- 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
- 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
- 培歌行学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 RunnableConfig | 25 | LangChain 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_limit和 langgraph_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 是业务循环,你需要:
- 在步数不足时返回"部分结果"给用户(比如"已查到天气,但热点还在查询中")
- 保存中间状态到检查点,方便后续恢复
- 让图正常结束,不抛异常
被动方法适合作为最后一道防线,但不是首选。
递归限制不是用来限制你的想象力,而是用来保护你的钱包。 一个无限循环的 Agent,跑的不是代码,是你的 API 账单。