news 2026/10/17 11:56:37

ReAct模式:大模型从思维链到工具调用的Agent闭环实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ReAct模式:大模型从思维链到工具调用的Agent闭环实践

1. 只让模型"想"或只让它"做",都会翻车

1.1 纯思维链(CoT)能解决什么,解决不了什么

先从一个我实际遇到的问题说起。那时候我还在做一版智能客服的问答系统,前端用户问"今天深圳适合穿长袖还是短袖",我用思维链(Chain of Thought,CoT)的方式让模型先推理、再回答。模型倒是真的"认真思考"了:气温变化、昼夜温差、潮湿程度分析得头头是道。最后它跟我说,建议穿长袖。

但问题在于——它其实根本不知道今天深圳多少度。它所有的推理,都是基于训练数据里"深圳的气候特点"猜出来的。结果当天深圳温度到了30度,模型给出的建议完全不靠谱。

这不是Prompt写得不好,而是纯CoT模式的根本局限:它是"闭卷考试"。模型只能调动自己记忆里的知识,无法获取任何外部实时信息。你让它推理一道几何题、写一段代码、总结一份已有文本,它没问题;但一旦问题涉及到"今天""现在""某只股票实时价格""某个系统当前的状态",闭卷推理就是在沙滩上盖楼——步骤再精致,地基也是虚的。

还有一个不那么起眼但同样致命的短板:CoT把"推理过程"写在了答案里,但这个过程完全不可验证。模型说"因为气温偏低,所以建议穿长袖",你分不清它是真的查到了气温,还是编了一个看起来合理的理由。在需要追溯决策依据的场景里(比如金融风控、医疗建议),这种"可信感"是远远不够的。

1.2 只给工具不给策略:为什么模型会乱调

既然推理不够,那我们给它工具行不行?这就是很多人的第二反应:给模型接一个天气API、一个数据库查询接口,让它联网、让它查库。

结果很快你会发现另一个问题:模型像一个拿到了高级扳手却不会修车的人。它不知道该在什么时候用扳手、用哪一把、拧完怎么判断有没有修好。我在做另一个项目时,给模型注册了四个工具,其中有一个是"获取用户订单状态"。用户问"我的快递什么时候到",模型优先调用了订单查询——逻辑没错,但它完全没有先问用户要订单号,查了个空。

更常见的翻车形态是这样的:模型在同一个上下文里反复调用工具,但参数完全一样,工具返回一样的结果,它依然继续调。它不是在"用工具解决问题",而是在"为了调用而调用"。你盯着日志,能明显感到模型根本没有围绕最终目标去规划下一步动作。

问题出在哪里?行动没有推理来指引。没有思考环节,模型不知道"我现在获得了什么信息""距离回答用户还缺什么""下一步应该做什么"。它就像一个没有剧本的演员,在舞台上走来走去,但每一句台词、每一个动作都是孤立的。

1.3 ReAct给出的答案:把推理变成可观测的循环

ReAct的作用,恰恰是把这两者的优势拼成了一个闭环。这个名字是Reasoning和Acting的缩写,核心思想朴素到有点不像一篇顶会论文的标题:让模型在"思考"和"行动"之间交替进行,先想一句、再做一步、看到结果、再接着想。

你可以把它理解成下棋。棋手不可能一次算完所有着法,更不可能不看棋盘就落子。他每一步都会先观察局面(Observation),再判断形势(Thought),然后落子(Action),紧接着看对方的反应,再进入下一个思考。ReAct就是把这套"观察—思考—决策—行动—再观察"的循环,原样搬到了大模型身上。

我第一次跑通ReAct时最大的感受是:模型的推理过程终于"看得见"了。它每做一个动作之前都会先写一段Thought,说清楚自己为什么要做这一步;每次工具返回结果后,它会把新信息纳入判断,再决定下一步。你能清楚地看到它从"不知道"到"知道"的完整链路,而不是放弃追问。

这套思路可以拆成三个核心组件:Thought(推理)、Action(行动)、Observation(观测)。循环往复,直到模型给出Final Answer。后面的章节我会逐个展开,它们对应的工程问题也是整个Agent类应用最核心的骨架。

2. 把"想一步、做一步、看一步"翻译成大模型的语言

2.1 一个循环里到底发生了哪些事

我们先把ReAct一个完整循环拆开看。假设现在模型收到的任务是:"帮我查一下某市今天的天气,然后判断适不适合户外跑步。"

第一轮交互中,模型会输出三样东西:

  • Thought:用户需要实时天气数据,这不在我的知识范围内,我需要先调用天气查询工具。
  • Action:调用的工具名和参数,比如get_weather(city="某市")。
  • Observation(由系统返回,不是模型生成的):37度,晴,体感温度39度。

看到这条Observation之后,模型进入第二轮:新的Thought变成了"体感温度39度,明显不适合户外跑步,尤其要警惕中暑风险",于是它不再需要调用别的工具,直接给出Final Answer:"今天某市37度,非常热,不建议户外跑步,建议改为室内训练。"

整个过程里,模型输出的Thought和Action是"模型生成"的,Observation是"工具执行后返回"的。前三者组成一轮完整的模型输出,Observation作为系统反馈追加到对话历史里,然后开始下一轮。

这个结构里最容易被忽视的是:Thought和Observation是交替写入同一段对话历史的。这意味着模型每次生成时,都能看到自己之前的推理、自己调过哪些工具、工具返回了哪些结果。整个过程对模型自己是可见的,"失忆"问题从机制层面就被避免了。

2.2 为什么这样设计能行:上下文即记忆,观测即纠偏

很多人第一次看ReAct会问:这跟"把工具调用的结果拼到Prompt里再问一次"有什么区别?

区别在于设计意图。ReAct不是为了拼凑上下文,而是构造了一个**"行动引导思考、思考修正行动"的闭环**。当模型把工具的返回结果作为Observation纳入推理时,它必须基于这个新事实重新判断——这就不再是闭卷考试了,而是开卷考试:查到的内容是什么,就基于什么去推理。哪怕模型一开始猜错了方向,Observation也会把它拉回来。

举个例子。用户问"某市的GDP哪年突破一万亿",模型对某市并不熟悉,它的Thought可能会写"我记得某市应该在2019年左右突破",然后它选择调用数据查询工具,工具返回的是"2021年突破"。第二轮,模型看到这条Observation,立刻修正了之前的猜测,最终答案也是基于真实数据给出的。

这个"纠偏"能力就是ReAct相对CoT最具价值的部分。模型不再依赖自己的记忆盲区,而是依靠外部反馈持续校准。而把Thought和Action暴露在上下文中,还带来了一个工程上的额外好处——可审计性。模型每一步为什么这么做、调用了什么、拿到了什么,全部有日志可查。你不需要再对黑盒输出做事后解释,过程本身就是解释。

2.3 结构化输出:让行动从"写给人看"变成"写给程序执行"

不过这里有一个关键工程问题:模型生成的Action是一段自然语言,程序怎么知道要从里面解析出工具名和参数?

我在项目里见过很多种方案。最初级的有人用正则去匹配文本里的固定格式,稍微好一点的是让模型输出JSON,然后用Schema校验。但这些方案都有一个通病——模型的自然语言输出不稳定,今天能解析成功,明天换了一个说法就解析失败。

成熟的ReAct实现一般会采用两种方式之一:

方式一:约束输出格式,外加解析兜底。在Prompt里明确告诉模型,Action必须输出为一个JSON块,例如{"tool": "get_weather", "args": {"city": "某市"}}。程序侧用严格JSON解析器去加载,解析失败则让模型重新生成。这个方案不依赖特定平台,任何支持文本生成的大模型都能用,但我建议至少要加一层格式校验和重试逻辑,不要奢望模型百分百守规矩。

方式二:使用模型自带的结构化输出能力。现在很多大模型API原生支持工具调用(function calling):你可以预先提交一份工具清单,模型在需要时直接返回一个结构化的函数调用对象,而不是生成一段需要解析的文本。这种方式的解析成功率最高,也是目前Agent开发的主流做法。

选哪种,取决于你手里的模型能力和工程约束。如果你用的是原生支持function calling的模型服务,直接走方式二;如果你用的是普通文本模型,那就老老实实把格式约束写进Prompt,并做好重试机制。任何一个Robust的Agent系统,都不该把"模型一定输出合法JSON"当作前提。

3. 花40分钟搭一个能跑的ReAct循环

3.1 前置条件:选一个结构化输出稳定的模型服务

真正动手之前,我建议你先想清楚模型的选型。ReAct对模型的一个核心要求是:在输出Action时保持格式稳定。有些模型写作文能力很强,但一让它输出JSON就各种跑偏——这种模型用来做ReAct,会让你大半时间都在跟格式解析搏斗。

我的经验是,模型需要具备两个特征:一是长上下文能力不能太差,因为每多一轮循环,消息长度就会累积;二是Structured Output(结构化输出)的表现要稳定,至少要能在你的格式要求下可靠地输出正确格式。你可以先用一个简单的测试命令做验证:让模型连续输出20次标准格式的JSON,统计它的成功率。低于90%的模型,说实话不太适合直接用于ReAct生产链路。

另外,如果条件允许,优先选择原生支持工具调用接口的模型服务。它等于把"解析Action"这一步从你自己代码里移到了模型那一侧,工程复杂度会下降一个量级。

3.2 工具注册:把函数变成模型能读懂的"菜单"

模型没法直接调用你的Python函数,它需要一份"菜单"——也就是工具列表。每个工具,你要告诉模型三件事:工具的名字、这个工具是干什么的、调用时需要哪些参数以及参数的类型和含义。

我最初犯过一个很典型的错误:工具描述写得太简单。我当时注册了一个get_sales_data(date),描述就一句"获取销售数据",结果模型频繁在这个工具上乱传参数。后来我把描述改成了"获取指定日期的销售额明细,日期格式为YYYY-MM-DD,仅支持查询近90天数据,节假日数据可能延迟一天更新"。改完之后,模型选择工具和填参的准确率提升非常明显。

工具描述怎么写,直接决定了模型什么时候该用它、什么时候不该用。这比你在Prompt里反复强调"不要乱调用工具"要有效得多。我在实践中会加一条"负面信息":什么时候不要用。比如天气工具,我会写"仅当用户明确要求实时天气时才使用,否则不要调用"。模型对这类边界条件的理解,远超你想象。

下面是我在项目中常用的工具注册结构示例:

tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市当日的天气情况,包括气温、体感温度、风力、降水概率。仅当用户明确询问实时天气时才使用。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如:某市" } }, "required": ["city"] } } } ]

这个结构在不同的模型API里可能字段名略有不同,但核心信息一致:名字、描述、参数Schema。你完全可以把这套结构理解成给模型看的一份"工具说明书",说明书写得越清楚,模型用工具就越准。

3.3 核心循环代码:一个最小可用的Agent主循环

选好模型、注册好工具之后,剩下的就是主循环。一句话概括它的逻辑:把消息发给模型,判断模型是想要调用工具还是给出最终答案;如果要调用工具,就执行对应的函数、把结果拼回消息队列、再发给模型;如果是最终答案,就结束循环。

我把核心代码写成下面这样(这是高度简化的演示版本,但结构是完整的):

import json def run_agent(messages, tools, tool_map, max_iterations=6): for step in range(max_iterations): response = model_call(messages, tools=tools) # 判断模型是否要求调用工具 tool_calls = response.get("tool_calls") if not tool_calls: # 没有工具调用请求,说明模型给出了最终答案 return response["content"] # 逐个执行模型请求的工具调用 for call in tool_calls: func_name = call["function"]["name"] args = json.loads(call["function"]["arguments"]) func = tool_map[func_name] # 执行工具函数,拿到观测结果 observation = func(**args) # 把模型的工具调用请求和执行结果写回对话历史 messages.append({ "role": "assistant", "tool_calls": [call] }) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(observation, ensure_ascii=False) }) # 进入下一轮循环:让模型看到观测结果后继续推理 return {"error": "超过最大迭代次数"}

这个循环里有两个参数值得你特意关注一下。

第一是max_iterations。我建议设成6到8轮起步,不要一口气设成20。原因很简单:模型在某些复杂任务上确实可能需要多轮探索,但如果你的Prompt设计得足够好,大部分任务应该能在5轮以内收敛。设得太大,只会让模型在错误路径上越走越远,token成本也水涨船高。

第二是Temperature。做ReAct的时候,我建议把温度压到0到0.3之间。推理和行动链路需要的是确定性,不是创造性。温度太高,模型可能会在Action格式上产生随机错误,也可能在Thought里越编越离谱。当然,纯粹做头脑风暴类的任务另当别论,但那是另一个场景。

3.4 像下棋一样走一遍完整对话

光看代码可能还是有点抽象,我实际走一遍完整的过程给你看。任务设定是:"某市今天适合跑步吗?"

第一轮发给模型的用户消息只有这一句。模型返回如下内容:

  • Thought:用户询问今日是否适合跑步,需要实时天气信息,我应该查询某市今日天气。
  • Action:get_weather(city="某市")

系统执行工具,返回Observation:{"temperature": 33, "condition": "晴", "humidity": 0.8, "wind_level": 2}。

模型拿到这条Observation,进入第二轮。它的Thought变成了:"某市今日33度,湿度80%,风力2级。高湿高温条件不适合户外跑步,容易导致中暑和不适。"随后它没有发起新的工具调用,而是直接输出Final Answer:"某市今天天气炎热且湿度较高,体感闷热,不适合户外跑步。建议选择室内锻炼,并注意补水。"

整个过程只有两轮,模型一次思考、一次行动、一次观测,就完成了从"不知道"到"给出有依据的答案"的闭环。你可以把它想象成下棋:第一步落下(查询工具),看到对方回棋(工具返回),再到第二步判断,最终赢棋(给出建议)。这就是ReAct最典型的运行轨迹。

我做项目时,习惯把这种成功案例的完整对话保存下来,作为few-shot示例加入到Prompt里。实测下来,模型在模仿成功路径方面非常聪明,只要给一两组标准范例,后续任务的成功率会有明显提升。

4. 实测ReAct最容易踩的五个坑及排查思路

4.1 模型复读机:Thought在原地打转

这是我跑ReAct时遇到的第一个实质性bug。现象非常典型:模型在第一轮输出Thought "我需要查询A",调用工具,拿到Observation之后,第二轮又输出了几乎一样的Thought "我需要查询A",又调了同一个工具,循环往复直到撞上max_iterations上限。

排查的时候,我先看了消息历史,确认Observation确实已经写回去了,但模型似乎没有"吸收"它。后来我在日志里发现,问题出在工具返回的数据里:某个字段确实查到了数据,但返回结构太复杂,模型没能在短上下文里快速定位关键信息,干脆就按原计划重复执行。

我的解决方案有两个。第一是在工具返回内容里做一层"摘要预处理",把关键数字和结论直接放在最前面,比如改为"某市当前温度:33度,天气:晴",而不是抛一个大JSON给模型。第二是在系统Prompt里加一条硬性约束:"如果上一轮的工具已经返回了所需数据,请停止调用相同工具并基于已有结果作答。"这两招叠加之后,复读机现象基本消失了。

4.2 长文本观测把上下文撑爆

第二个坑是我在做一个文档问答Agent时候踩的。工具返回的不是小段数据,而是整份PDF解析结果,一次性塞进Observation里,连续几轮之后,上下文窗口直接触顶,模型开始"忘记"早期信息,输出质量断崖式下降。

这里的核心教训是:Observation并不需要把工具返回的原始数据原封不动地塞给模型。工具的输出是给程序看的,Observation才真正是给模型看的。你完全可以在工具执行和数据进入上下文之间插一个处理层:按长度截断、提取关键摘要、保留结构化字段。我后来给每个工具都配了一个"结果精简函数",统一把输出压缩到500字以内的要点形式。效果非常明显,模型反而更专注于关键数据了。

如果你用的是支持长上下文的模型,也不能掉以轻心。上下文越长,模型对中间信息的注意力就越分散,成本也越高。Agent工程里的观察结果,跟人看报告一样——信息要提炼,不是越长越有说服力。

4.3 工具返回格式与模型预期错位

第三个翻车现场,来自工具本身。我注册了一个查询用户信息的工具,它返回的是数据库的原始记录,里面有个字段是status=0。模型看到这个0,理解成了"已禁用",于是向用户反馈"账号无法使用"。但实际上在我们的业务表里,0代表"正常",1才是"禁用"。

这暴露了一个很普遍的问题:你懂工具返回值的含义,模型不懂。ReAct链条里,模型只能通过工具描述和Observation文本来理解世界。要解决这个问题,必须在注册工具时就提供准确的返回字段语义说明。我在工具描述里补充了:"status为0表示正常状态,为1表示禁用状态",之后的判断就再没出过错。

还有一个类似的常见问题是:工具返回空结果,模型不说是空的,反而基于幻觉继续编一个答案。这需要在Prompt里显式声明:"如果工具返回为空,请明确告知用户未查询到数据,不要推测或编造内容。"

4.4 幻觉式行动:模型"假装"调用工具

这个坑最隐蔽,也最坑人。有些情况下,模型没有真正发起工具调用请求,而是在生成的文本里模拟了一次调用——比如它直接输出了天气数据"某市今天33度",看起来像回事,但那根本不是真实查询的结果,是它自己想象的。

出现这种情况,往往是因为你的Prompt示例里包含了一些"看起来像Observation"的文本,或者历史消息里多次出现工具返回结果,模型误把模仿当成了执行。你可以理解成一个学生看了太多标准答案,以为自己也能直接写出答案。

排查方法很简单:在主循环里严格判断模型返回是否为结构化工具调用请求。如果模型只是在普通文本里"提到"了某个工具,那就说明它没有真的发起调用,不要把它当作Observation。我的处理办法是,在Prompt里明确区分两种输出通道:"如果你需要获取真实数据,必须生成工具调用请求;如果你在回答正文中提及数据,必须来自真实的Observation结果。"同时在代码侧,凡是没有对应工具调用ID的消息,都不允许以Observation身份进入上下文。

4.5 带副作用的工具必须有人工闸门

最后这个坑不属于格式问题,而是安全问题。我在一次内部试验中,给Agent接了一个"发送邮件"工具。模型在流程中判断"需要给用户发送一封确认邮件",于是真的准备调用了。问题是,它的判断依据并不充分——邮件的收件人、内容都没有经过任何人工确认。

ReAct赋予了模型行动能力,但这不意味着模型应该拥有所有行动的最终决定权。特别是有副作用或成本的工具,比如发送邮件、扣款下单、删除数据,建议在工程上做一个强制人工确认环节。模型可以生成调用请求,但请求进入"待确认队列",必须有真人点击确认才会真正执行。

这个闸门不需要多复杂,一个待确认消息列表、一个确认回调接口就够了。它不牺牲ReAct的自动化能力,但能从机制上拦住那些"看起来合理但实际不该执行"的危险动作。做Agent项目,安全边界这一层永远不应该省。

最后再说几句实在话

我在把项目从纯CoT迁移到ReAct的过程中,最大的感受不是"模型变强了",而是"模型的思考过程终于变得可检查、可干预了"。以前模型给一个错误答案,你只能猜它是哪里想错了;现在你在日志里能看到它每一步的依据,哪一步的判断失准一目了然。

如果你想在自己的项目里快速试起来,我建议不要一上来就追求复杂框架。先用手里的模型API,配两三个工具,写一个最简单的循环,跑通一个端到端任务。等你亲眼看到Thought—Action—Observation的循环在日志里滚动起来,再考虑加记忆、加多工具编排、加人工确认这些进阶能力。ReAct的优雅之处就在于它的核心逻辑足够简单,但越用越能感觉到这个简单的循环里藏着的巨大扩展空间。

最后分享一个我一直在用的小技巧:把项目里成功跑通的完整对话(Thought、Action、Observation、Final Answer四段齐全)整理成两份到三份few-shot样例,固化到系统Prompt里。每次迭代提示词之后,用同一批测试Case重新跑一遍,对比成功率。这个习惯,帮我省下的调试时间远比想象中多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 11:56:16

电器元件-加热管

简介电加热管是一种把电能转化为热能的电热元件,常用于加热水、油、空气或工业设备中的介质。应用场景家用电器:电热水器、电水壶、电饭锅、电烤箱、洗碗机等。工业加热:液体加热、油槽加热、管道保温、模具加热、烘干设备等。文字代号文字代…

作者头像 李华
网站建设 2026/10/17 11:56:07

RAG企业知识库搭建指南:从原理到工程实践

1. 从“模型只会背书”到“让模型学会查资料”先说个我在实际需求里反复见到的场景:管理层决定引入大模型能力,团队先是把公司文档一股脑扔给模型做微调,结果模型对着几千页制度文件“背”完以后,回答还是漏洞百出——要么张三的流…

作者头像 李华
网站建设 2026/10/17 11:55:58

反转链表一部分:递归与迭代的完整拆解

1. 题目拆解与整体思路先说清楚这是一道什么题:给定一个单链表,再给你两个位置 left 和 right,要求只把从 left 到 right 这一小段链表反转,其余部分保持原样。比如说1 -> 2 -> 3 -> 4 -> 5,left 2&#…

作者头像 李华
网站建设 2026/10/17 11:55:20

打造持久化Kali U盘:从裸盘到加密移动工作台全指南

玩了这么多年安全工具,我几乎每次都要劝身边人一句:别再用“裸奔”的 Live U 盘干活了。所谓“裸奔”,就是你辛辛苦苦把系统装进 U 盘、插到哪台机器都能启动,结果所有配置、工具、脚本、日志、取证数据,全在关机的一瞬…

作者头像 李华
网站建设 2026/10/17 11:55:02

Context Engine:如何将Agent的Token消耗降低71%?

做一个能把 agent 的 token 使用量压下去 71% 的中间层,听起来像是某种黑魔法,但真拆开看,我们只是老老实实回答了三个问题:每一轮调用 token 到底花在了哪、哪些 token 是必须花的、哪些是花了钱但根本没用的。把这三个问题搞清楚…

作者头像 李华
网站建设 2026/10/17 11:55:00

网络安全设计实战:从风险分析到落地验证的完整指南

"网安设计"这四个字拆开看,网安是网络安全,设计是方案设计。但把两个词拼在一起,很多刚入行的朋友反而懵了:这到底是要写一份文档,还是搭一套架构?是画拓扑图,还是填合规检查表&#…

作者头像 李华