☰
Agent工具循环与recursionLimit:条件路由设计实战指南
2026/10/8 5:13:33 网站建设 项目流程

做Agent开发的兄弟应该都遇到过这么一幕:给AI配了两三个工具之后,它像脱缰的野马一样疯狂调用,搜索完又去执行代码,执行完又调API,一遍一遍就是不给你最终答案。最后救场的不是它自己想明白了,而是recursionLimit一脚刹车把它拦了下来。这个参数平时不起眼,关键时刻就是保命符,能把一个烧钱的死循环强制截断。这篇文章我把AI工具循环、条件路由和recursionLimit这三件事串起来讲清楚,重点聊聊怎么设计路由逻辑、怎么合理配置递归上限,以及我在实际项目里踩过的那些坑。不管你是刚入门Agent开发,还是已经跑过几个多轮工具调用场景,这篇都能给你一些能直接抄作业的经验。

1. AI工具循环:Agent体系的核心运转逻辑

1.1 为什么AI需要"循环"而不是一次到位

大部分人对AI能力的初始认知还停留在"输入Prompt直接输出答案"这个阶段,但在Agent场景里这个模式根本不够用。原因很简单:你问一个问题,可能连你自己都不知道需要哪些信息,AI也不可能仅凭一次推测就能拿到所有必要的数据。

举个例子,用户问"帮我分析一下今天某只股票为什么大跌"。这个任务里AI至少需要:先调用行情接口拿到当天分时数据,再搜索几条财报和新闻,甚至需要查一下大盘和板块的表现,最后才能组织起一段有依据的回答。以上每一步都对应一个工具调用,而每一次调用的输出都可能影响下一步到底调用什么工具、搜索什么关键词。

所以工具循环的本质就是:在AI完成最终回答之前,允许它反复进行"推理 → 选择工具 → 执行工具 → 观察结果 → 再推理"这个过程。这个循环不是bug,而是Agent能力的基础。真正的问题从来都不是"AI应该循环",而是"AI应该在什么时候停"。这一点认知如果不到位,后面所有参数配置都会变成玄学式调参。

1.2 工具循环的两大主流模式:ReAct与Function Calling

现在圈子里经常听到两个词:ReAct和Function Calling。很多人分不清,这里用直白的话解释一下。

ReAct(Reason + Act)是较早提出的一种Agent循环模式,核心思路是把推理过程和工具调用过程交替串联起来。模型每走一步,先输出一段推理文本(Thought),然后决定要调用哪个工具(Action),拿到工具返回的结果(Observation)以后再接着思考。整个过程像一套完整的工作流,适合需要长链推理、中间过程透明的场景。

而Function Calling是OpenAI、Claude等大模型API提供的结构化工具调用能力。模型不再自由生成"要调用什么工具"的文本,而是直接输出一个符合函数签名的JSON结构,程序拿到这个JSON之后去执行对应的函数,再把返回值塞回对话上下文。相比ReAct,它的输出更稳定,解析成本更低,是现在做Agent的主流选择。

这两种模式虽然底层机制不一样,但跑起来都是循环。无论你用LangGraph的StateGraph、AutoGen的conversable agents,还是自己手写一个While循环挂在大模型API外面,本质上都是在维护一个不断变化的上下文,让AI逐步逼近最终答案。

这里有一个关键点必须强调:工具循环的每一轮都会消耗Token,循环越深,成本越高。这不是线性增长,而是每一轮都要把前面所有对话历史重新发送给模型,所以越到后面单轮成本越贵。这也是为什么下面要专门聊条件路由和recursionLimit——它们直接决定了你的Agent是理性人还是失控的吞金兽。

2. 条件路由:让Agent知道什么时候该停

2.1 条件路由的本质:每步都做决策

条件路由(Conditional Routing)这个概念听起来有点玄,实际它就是一件事:在每个循环节点,根据当前状态决定下一步走哪条路。是从Agent节点跳到工具节点,还是回到Agent节点继续思考,还是直接进入END节点输出最终回答。

我把这个理解为十字路口的红绿灯。AI每执行完一步,都必须停下来看一眼红绿灯:当前条件满足了吗?信息足够了吗?还要不要再跑一轮?没有这层路由逻辑的Agent,就像没有交通规则的路口,所有车都想往前冲,最后一定堵死。

在实现层面,条件路由通常有两种形态。

一种是在LangGraph这类图编排框架里,通过添加条件边的方式实现。每个节点执行完毕后,会调用一个路由函数,根据返回值动态选择下一个节点。这种形态适合复杂的多节点图,你可以让Agent节点根据模型输出跳转到工具节点,也可以让工具节点根据执行结果跳回到Agent节点,甚至可以直接跳到某个中间处理节点。

另一种是自己在代码里写if-else判断。比如OpenAI Function Calling模式下,循环内部的逻辑通常是:模型返回的结果里有没有tool_calls字段,有就执行工具、再调一次模型,没有就跳出循环输出content。这个"有没有tool_calls"的判断,就是最基础的条件路由。

不管形态怎么变,路由决策的依据一般都逃不开这几类:工具返回内容有没有异常、Token预算还剩多少、当前迭代次数是不是已经接近上限、模型自己有没有输出终止信号。把这些依据想清楚,再去写代码,路由逻辑就不会乱。

2.2 路由策略设计:规则、Schema与混合模式

条件路由能不能管好Agent,核心看你怎么设计路由策略。我做过几个Agent落地的项目,总结下来常用的有三种策略。

第一种是纯规则路由。适合流程特别固定的场景,比如客服工单系统:用户输入地址触发地址解析工具,解析成功就走下一步,解析失败直接转人工。这种策略的好处是可控、稳定、好调试,坏处是死板,应对不了复杂多变的开放式任务。

第二种是Schema路由。让模型在输出结构化JSON时携带一个路由字段,比如route,取值是continue或者end。程序拿到这个字段再决定是否终止循环。这个方法适合大部分Agent场景,因为模型可以根据上下文动态判断自己还需要什么信息,比硬编码规则灵活得多。要注意的是,你必须强制模型给出这个字段,哪怕它觉得不需要也要给,否则解析会出问题。

第三种是混合模式。先用规则兜底,再用模型判断做增强。比如我先设一个硬性的最大步数5,就算模型一直说continue,到第5轮也必须强制end;同时每一轮都让模型输出一个confidence置信度,低于0.3就提前终止。这样既保留了模型灵活性,又给自己留了兜底,是我现在最推荐的做法。

2.3 路由与工具循环如何配合

有很多人把路由和工具循环当成两件独立的事来做,我觉得这是很大的误解。其实两者就是一体两面:工具循环的每一次迭代,都是由路由决策驱动的,而路由决策又依赖于工具执行的反馈。

举个实际的例子:我做过一个竞品信息收集Agent,它手上挂了网页搜索、内容抓取和数据库查询三个工具。用户问"对比一下A和B两个产品的定价策略"。第一轮,路由决策是"调用网页搜索",结果搜索返回了A的官网定价;第二轮路由一看缺了B的信息,决定继续调用搜索;第三轮拿到B的定价之后,数据还不够,路由又跳到数据库查询想找历史价格;第四轮所有信息齐了,路由终于给出end,模型把数据整理成表格输出。

这个例子里如果路由策略设计得不好,很容易出现A信息没拿到就急着回答,或者信息拿全了还不停继续搜索的情况。所以我的经验是:路由决策一定要有明确的终止信号来源,要么来自模型主动输出,要么来自程序侧的条件判断,绝不能稀里糊涂地放任循环自己跑。每一条路径的触发条件都必须能被日志记录,这样出了问题才能知道Agent是哪根神经搭错了。

3. recursionLimit:循环的保险丝与安全阀

3.1 recursionLimit到底是什么

先把这个参数说透。recursionLimit直译过来是"递归限制",但在Agent场景里它更准确的叫法应该是"最大执行步数限制"。

在LangGraph这类图执行引擎中,Agent的循环本质上是图节点之间的反复跳转。每跳转一次,执行引擎的递归深度就增加一级。recursionLimit就是设置这个递归深度的最大值。一旦达到限制,执行会停止,并抛出异常。

我见过很多人第一次看到RecursionLimitError就慌了,其实这就是你的保险丝断了。就像一个电路里如果电流过大、保险丝熔断,虽然说明后面有问题,但至少它保护了你的电线和设备。在Agent开发里,recursionLimit就是防止你因为循环失控而疯狂消耗Token和API额度的最后一道防线。

顺手统计一下常见的框架默认值:

框架参数名默认值含义
LangGraphrecursion_limit25图节点跳转最大次数
AutoGenmax_consecutive_auto_reply因版本而异两个Agent自动对话最大轮数
OpenAI Function Calling无内置无限需要在循环外层自己加上限
自定义实现max_steps通常自设工具调用最大次数

从表格里能看出来,并不是所有框架都会给你一个现成的安全网。OpenAI Function Calling如果没有自己实现限制,循环一旦失控就没有刹车。所以我在所有自定义Agent代码里,第一件事就是加一个计数器。

3.2 不同框架中的recursionLimit差异

这个参数在不同框架里长得不一样,写代码的时候最容易踩的坑就是:你在LangGraph里配了recursion_limit,跑到AutoGen里也去找同名参数,结果找不到;或者自定义框架里根本没人管这个参数,写了个死循环都不知道怎么打断。我实际用下来的经验分别说一下。

LangGraph中用config传入recursion_limit,比如app.invoke(initial_state, config={"recursion_limit": 50})。需要注意,这个参数是每次invoke都会重新计算的,不是全局持久化的。如果你在一个会话里多次调用graph,每次都得显式传一遍,不然它就会用默认值25。

AutoGen更关注的是两个Agent之间对话的轮次,要明确指定max_consecutive_auto_reply。这个参数主要限制的是"不经过用户输入、两个Agent自动连续对话"的轮数,能有效防止两个Agent互相尬聊停不下来。比如一个负责写代码、一个负责检查代码,如果你不设置上限,它们俩可能为了一个小bug来回对话几十轮。

如果是自己手写工具循环,通常就是一个while循环加一个计数器,比如max_steps = 10,每执行一次工具调用就让计数器加1,超过上限就强制break并返回当前结果或错误提示。这个方案最直白,也最好调,只要你记得在循环里加break条件就行。

我个人的建议是,无论用什么框架,都不要把recursionLimit当成可以依赖的默认配置,而是要当成一个必须显式配置的参数来看待。这样至少能让每个人在跑通代码之前就思考一遍:这个业务场景允许AI最多跑多少步。

3.3 合理设置recursionLimit的实战思路

很多人问我:"你一般把recursionLimit设多少?"其实这个问题没有标准答案,它不是拍脑袋随便填的,要根据任务复杂度、工具数量、上下文成本来综合判断。

主线任务是判断这个任务预期需要几个工具调用才能完成。比如只查一次天气,两步就够了,设置5步都嫌多;如果是"调研+写作+润色"的复合任务,可能需要的调用次数是8-15次,这时候设置25可能刚好;如果是涉及多次搜索、多次数据清洗的复杂工作流,30-40步也是有可能的。

另一个维度是成本。循环步数越多,Token消耗就呈线性甚至指数级增长。每一轮循环都会把之前所有的工具结果和对话历史重新塞给模型,所以第10步的消耗可能比第1步翻好几倍。我曾经跑过一个深度调研Agent,没设限制前跑到20多步,一次查询花了十几块钱。设了上限之后,虽然偶尔报错,但至少账单可控。

结合上面两点,我的做法是:先给一个宽松的初始值(比如30),跑几轮真实业务看平均步数,然后把这个平均步数乘以1.5-2倍作为生产环境的限制。如果经常撞上限,说明你的任务链路太长,应该从路由逻辑上精简,而不是单纯调大数值。

4. 实操:从零搭建一个带条件路由的工具循环

4.1 定义工具列表与输出Schema

这部分我用LangGraph举例,因为它的图式编排对条件路由和recursionLimit的体现最直观。先把基础工具准备好,我这里用一个天气查询工具和一个日期工具来做演示。

from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """查询指定城市的当前天气""" # 这里对接真实天气API return f"{city} 当前天气:多云,24度" @tool def get_today_date() -> str: """获取今天的日期""" return "今天是2026年5月18日"

定义好工具之后,最关键的是设计Agent节点的输出Schema。我强烈建议在Prompt里要求模型输出包含三个部分:reason(说明本次决策理由)、next_action(取值route_continue或route_end)以及final_answer(最终答案,仅最后一步有值)。

这里有个容易忽略的点:即便到了最后一步,也要让模型把next_action字段设置为route_end,不能让字段缺失。我之前遇到过模型在回答完毕之后直接把next_action给删了,结果程序认为它没做决策,又让它跑了一轮,白白浪费了Token。

4.2 实现条件路由节点

条件路由是这段代码的灵魂。我在graph里先定义两个节点,一个是agent(负责调用模型做决策),一个是tools(负责执行工具)。然后在agent和tools之间建立条件边,路由函数根据模型输出状态决定是继续走tools还是跳到END。

from langgraph.graph import StateGraph, END from typing import TypedDict, Literal class AgentState(TypedDict): messages: list route: Literal["route_continue", "route_end"] final_answer: str def agent_node(state: AgentState): # 调用模型,让它结合上下文输出决策 response = llm.invoke([ {"role": "system", "content": "你是一个会使用工具的助手,每次必须输出决策JSON"}, *state["messages"] ]) parsed = parse_decision(response.content) return { "messages": [{"role": "assistant", "content": response.content}], "route": parsed["next_action"], "final_answer": parsed.get("final_answer", "") } def tools_node(state: AgentState): # 执行上一步模型选中的工具 result = execute_tool(state["messages"][-1]) return {"messages": [{"role": "tool", "content": result}]} def router(state: AgentState): if state["route"] == "route_end": return END return "tools" graph = StateGraph(AgentState) graph.add_node("agent", agent_node) graph.add_node("tools", tools_node) graph.add_conditional_edges("agent", router, {"tools": "tools", END: END}) graph.set_entry_point("agent") app = graph.compile()

需要注意,工具执行结果至少要包含success和content两个字段,这样路由函数才能根据工具返回值做判断。如果工具本身没有明确返回是否成功,最好在包装函数里手动加一层状态封装,否则模型会把空结果当成"没查到",然后反复调用同一个工具。

4.3 配置recursionLimit并跑通全流程

在调用编译好的graph时,通过config参数传入recursion_limit。第一次调试时建议先设一个小一点的值,比如5,这样如果代码写错了可以快速暴露问题;后面再根据日志调整到合理范围。

result = app.invoke( {"messages": [{"role": "user", "content": "查询今天的天气并告诉我适合穿什么衣服"}]}, config={"recursion_limit": 5} )

跑起来之后要重点观察输出过程中的状态流转。LangGraph默认支持打印每个节点的执行顺序,能看到agent到tools再到agent再到tools这样的循环轨迹,这样你一眼就能看出路由判断是否正确。

正常流程应该是:第一轮agent调用get_today_date拿日期,第二轮agent调用get_weather拿天气,第三轮agent给出最终建议。3步以内就能完成,5的上限完全够用。如果发现模型在天气工具这一步反复调用,说明路由判断有问题,或者工具返回格式没有让模型满意。这时候不是调大recursionLimit就能解决的,要从Prompt和工具输出格式入手。

5. 常见问题与排查技巧实录

5.1 循环超限:Agent到底在干什么

遇到RecursionLimitError,第一步不是急着调大参数,而是先搞清楚Agent到底在哪一步卡住了。我的习惯是让图引擎输出详细的执行日志,或者自己把每一步的状态打印出来。

如果日志显示Agent在反复调用同一个工具、返回都一样的内容,基本可以断定是模型的"确认偏误":它总觉得刚才搜到的内容还不够,还想再搜一次同样的关键词确认。这时候解决方式通常是在路由函数里加一个去重判断,如果本轮调用的工具名和参数与上轮完全一致,就直接强制end,不让它原地打转。

还有一种情况是Agent在A和B两个工具之间反复横跳,日志看起来像:A说"我先去了解B的结果",B说"我需要先看A的结果"。这就是典型的循环依赖,要靠上下文重置或者冻结某个工具来打破,单纯调recursionLimit没用。我在一个数据清洗Agent里遇到过这种问题,最后解决办法是给每个工具加了一个冷却标记,短时间不允许连续调用同一个工具两次以上。

5.2 路由判断失效:为什么该停不停

路由判断失效最常见的原因,是模型没有严格输出你定义的schema。比如你要求它输出route字段来标记是否继续,结果模型生成的时候说了一堆话但JSON里没有route字段,解析直接失败。这时候你的程序要有一个容错分支:如果解析失败,按默认策略处理,通常是直接终止循环并返回当前结果,不要让异常把整个任务卡死。

另外,Prompt里对路由字段的语义描述一定要清晰。你光说"输出route字段"不够,要用具体的值给出示例:"如果信息已经充分,route设为route_end;如果还需要查更多,route设为route_continue"。模型对具体示例的理解能力远强于抽象描述。我在实际测试里发现,加了示例之后路由判断的准确率能提升将近三成。

我还遇到过一种很隐蔽的问题:模型每次都说route_continue,但拿回来的工具调用参数是空的,也就是说它想继续但不知道该调什么。这种问题出现在工具描述模糊的场景,模型不知道有哪些工具可用,只能乱猜。解决办法是在系统提示词里加上一份当前可用工具的清单和用途说明。

5.3 工具异常导致的死循环

工具本身出bug也会导致循环出不来。例如工具函数抛异常了,但包装层没有把异常转成可读的返回信息,模型不知道工具失败,只看到空结果,于是不断地重新尝试同一个调用请求。

在所有工具函数外面包一层try-except,把异常信息写到结果里返回给模型。这看起来很简单,但大部分人都没做到。工具调用失败是很常见也很正常的事,模型完全可以接受"查询失败、请重试"这样的输出,但如果输出是空字符串,模型就会一直猜。合理设计工具返回结构,比优化模型Prompt还重要。

我建议的工具返回结构统一长这样:

{ "success": true, "content": "查询到的有效数据", "error_message": "" }

失败时返回success为false,error_message写明原因。这样路由函数可以第一时间判断"这轮工具没成功,要么重试要么切换策略",不会让模型凭空猜测执行结果。

6. 我的几条实战心得

这里分享一点我个人的体会。我在实际项目里,recursionLimit从来不是单独出现的,它永远和条件路由、工具返回格式、Prompt设计一起出现。单独调大recursionLimit只会让Agent有更多犯错的空间,而不是有更多解决问题的能力。

我最推荐的组合拳是:合理的路由终止信号 + 明确的工具成功/失败标记 + 显式设置的recursionLimit。三者缺一不可。如果只依赖默认的边界值,某天线上Agent突然失控,API账单会直接教你做人。

最后再分享一个小技巧:在调试工具循环时,把每一步的工具调用参数和结果摘要都打印出来,不要偷懒。很多循环问题,光看最终报错是看不出原因的,必须看循环过程中的中间状态。我见过太多人只把异常堆栈贴到群里问为什么,其实日志里早就明明白白写了Agent在哪一步开始原地打转。学会读日志,比你调一百次参数都有用。

这个内容后续还可以这样扩展:给Agent加入记忆模块、多Agent协作、更复杂的图结构控制。但不管怎么扩展,控制循环的基本功都是这些。希望这篇能帮你少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询