LangGraph.js 循环停不下来?正确设置 recursionLimit 与退出条件

发布时间:2026/10/29 2:08:17
LangGraph.js 循环停不下来?正确设置 recursionLimit 与退出条件

LangGraph.js 循环停不下来?正确设置 recursionLimit 与退出条件

研究型 Agent 常见的失败方式,是模型不断要求“再查一个来源”,工具也不断返回部分信息,最后整次运行抛出GraphRecursionError。单纯把限制调高,通常只是让失败来得更晚、成本更高。

要解决它,需要同时设置图执行上限和业务停止条件,并在触顶前收束已有结果。

1. recursionLimit 统计什么

LangGraph 的超级步(super-step)是图执行的一轮。并行执行的一组节点可以处于同一个超级步;顺序执行的节点通常占用不同超级步。它不是 Token 数、工具调用次数,也不是 Python 或 JavaScript 的调用栈深度。

一个简单的agent -> tools -> agent回路通常要经过两个节点步骤才能完成一轮工具往返。如果工具节点内调用多个工具,工具调用数量与超级步数更不能直接等同。

核对时,LangGraph.js 的官方文档给出的默认限制为 25,并明确要求把recursionLimit放在调用配置的顶层。版本可能变化,项目应以锁定依赖和运行行为为准。LangGraph.js 图 API 文档

awaitgraph.invoke(input,{recursionLimit:30,configurable:{thread_id:"ticket-42"},});

不要把它写成configurable.recursionLimit。业务线程标识与执行预算是不同的配置项。

2. 图必须有正常退出边

有三类情况应走向结束:任务完成;证据不足且继续检索价值有限;业务预算耗尽。图框架的限制是最后一道保险,不能替代这些业务判断。

下面给出新增的离线示例,只模拟工具返回,不调用模型或网络。业务上限按“本次调用中的检索轮数”计数,比试图把所有图内部步骤精确折算成工具次数更直观。示例使用兼容的AnnotationAPI;实际项目应锁定依赖并验证升级后的行为。

import{Annotation,StateGraph,START,END}from"@langchain/langgraph";constState=Annotation.Root({question:Annotation<string>,rounds:Annotation<number>,maxRounds:Annotation<number>,notes:Annotation<string[]>,answer:Annotation<string>,});constapp=newStateGraph(State).addNode("check",()=>({})).addNode("search",(s)=>({rounds:s.rounds+1,notes:[...s.notes,`第${s.rounds+1}轮:模拟检索结果`],})).addNode("summarize",(s)=>({answer:`达到${s.rounds}轮检索预算。已有线索:${s.notes.join(";")}`,})).addEdge(START,"check").addConditionalEdges("check",(s)=>s.rounds<s.maxRounds?"search":"summarize",["search","summarize"],).addEdge("search","check").addEdge("summarize",END).compile();constresult=awaitapp.invoke({question:"为什么结账慢?",rounds:0,maxRounds:3,notes:[]},{recursionLimit:20},);console.log(result.answer);

示例已在@langchain/langgraph@1.5.2、@langchain/core@1.3.0、tsx@4.23.15上离线运行验证。安装这些固定版本后,可用npx tsx demo.ts执行;项目使用 ESM 模块配置,配套示例与运行说明见本包的 examples 目录。

真实 Agent 还要增加“模型不再请求工具”的完成路径,并对maxRounds进行范围校验。示例中的 20 只是覆盖这条固定路径的保护值,不是通用推荐值。

3. 业务计数与框架计数要分开

原文用状态中的stepsUsed计算当前执行消耗,但复杂图含有中间件、子图和并行节点时,自定义计数不一定等于框架超级步数。更稳妥的做法是分别记录检索轮数、工具调用、总成本和墙钟时间,并为框架限制保留收束余量。

状态中的计数还要明确作用域:本次请求清零,还是跨整个会话累积。若启用检查点,新一轮输入应显式初始化本轮计数,避免沿用上次已耗尽的预算。

绝对的历史步骤编号也不应直接与当前调用预算比较。尤其在继续执行、恢复线程或迁移版本时,需要验证框架实际提供的计数语义。

4. 异常处理不能制造无限重试

捕获GraphRecursionError可以避免服务直接退出,但异常本身不等于拿到了完整结果。若已配置检查点,可按线程读取已保存的状态,返回可确认的线索,并注明尚未完成。

不要在捕获异常后无条件再次调用相同工作流。若没有跨调用总预算,每次重试都可能获得新的执行额度,最终把有限循环变成无限循环。

同样,MemorySaver适合本地示例,不提供进程退出后的持久性。生产恢复需要持久化检查点存储、正确的线程标识以及对副作用的幂等处理。

5. 发布前验证哪些边界

用任务能完成、预算耗尽、工具持续失败和同线程第二次调用四种场景检查行为。预期应分别是正常完成、返回部分结果、明确失败或降级,以及本轮计数按设计初始化。

还应记录停止原因。用户看到的“证据不足”和“达到检索上限”含义不同;运维侧也需要区分正常收束与触发框架保护,才能判断下一次该改提示词、工具还是预算。