最近不少朋友都在问Agent的搭建,尤其是想把大模型真正用起来,做一个能“边想边做”的智能体。市面上各种Agent框架和Demo满天飞,但你扒开底层会发现,大多数都绕不开一个核心范式——ReAct模式。这个词这几年在AI圈热度一直没降过,网上常有人把它跟前端框架React搞混,也有不少人照着帖子的代码复现成功,可真到了自己改工具、加逻辑的时候却一头雾水。今天我就把这套东西从头到尾捋一遍,概念、原理、代码、排坑一次讲透。
这篇内容适合谁?如果你是刚接触大模型应用开发、打算自己搭Agent的新手,或者已经在用LangChain/LangGraph但不太清楚背后运行逻辑的工程同学,都可以放心往下看。我会尽量把每一步都拆成能直接抄作业的代码和配置,也会把那些文档里不写的坑提前告诉你。
1. ReAct模式到底是什么:拆开"Re"和"Act"两层含义
1.1 先把名字说清楚:ReAct不是前端那套React
在往下聊之前,必须先把一个绕不开的混淆点说清楚。这里的ReAct是Reasoning + Acting的缩写,中文说得直白一点就是“推理与行动相结合”,和前端圈子天天用的那个React组件库没有任何关系。只是拼写恰好一样,走红的时间段也有重叠,所以很多搜索词里会同时出现“react 生命周期函数”、“react 图表”这种东西——那些讲的是前端框架,跟我们要聊的AI智能体完全是两个赛道。
如果你在GitHub上搜ReAct,大概率会看到一个比较有代表性的论文仓库,那篇工作发表于2022年底、后来被ICLR 2023接收,作者是Yao等人。它提出了一种让大模型交替执行“推理”和“调用外部工具”的策略。这个思路后来直接成了LangChain早期AgentExecutor的核心实现,也是现在各种Agent框架里最常见的循环骨架。
记住一点:ReAct里的大写“R”和“A”分别对应Reasoning和Acting,后面所有内容都围绕这两个动作展开。
1.2 核心循环:Thought、Action、Observation 三件套
ReAct的核心其实特别简单,就是让模型循环执行三个步骤:
第一步叫Thought,相当于模型把脑子里的思路“说出来”。比如“我需要先弄清楚北京今天的天气,再决定出门穿什么”。这一步不需要调用任何工具,纯粹是模型基于上下文做出的推理。
第二步叫Action,模型根据刚才的想法,决定调用哪把“工具”。工具的形态完全由你定义,可能是搜索引擎、计算器、数据库查询接口,也可能是某个业务API。模型只负责输出一个结构化指令,真正执行的是你的代码。
第三步叫Observation,也就是工具返回的结果。这一步很关键,它不靠模型编造,而是实实在在的系统反馈。模型看到这个观测结果后,会再次进入Thought阶段,继续推理下一步行动;信息够了,就输出最终答案。
整个过程形成一个循环:Thought引导Action,Action产生Observation,Observation再驱动下一个Thought。这跟你写代码时“想一步、做一步、看结果、再想下一步”的思路非常像,也是ReAct最本质的运作方式。
1.3 一个最小的例子:看一眼就懂
我举个最简单的场景,你就明白这个循环长什么样了。假设用户问:“帮我查一下Q3的营收,然后对比上季度增长了多少。”
模型第一轮输出可能是这样:
- Thought:用户需要两个数据,Q3营收和上季度营收。我先去知识库查Q3数字。
- Action:{"name": "search", "args": {"query": "2024年Q3营收"}}
- Observation:系统返回“2024年Q3营收为12.6亿”。
模型接着第二轮:
- Thought:拿到Q3数字了,现在需要上季度的营收做对比。
- Action:{"name": "search", "args": {"query": "2024年Q2营收"}}
- Observation:系统返回“2024年Q2营收为10.2亿”。
第三轮,模型手里已经有两个数字,不需要再查工具了:
- Thought:两个数字都有了,Q3是12.6亿,Q2是10.2亿,增速约23.5%,可以组织答案。
- Action:{"name": "finish", "args": {"answer": "Q3营收12.6亿,环比增长约23.5%"}}
看到没有,这就是一个完整的ReAct闭环。模型每次只走一小步,每一步都有真实工具结果兜底,不靠记忆硬编。这就是它比“让模型一口气直接回答”更可靠的原因。
2. 为什么ReAct能行:它精准捅破了三个痛点
2.1 纯推理的局限:模型再强也会一本正经地胡说
如果你只用Prompt让一个大模型直接回答复杂问题,它能答对一部分,但策略本质上是“猜”。模型是根据训练时见过的大量语料,生成一个概率上最合理的答案序列,它没有实时确认过这些信息的真假。你问它一个需要查询数据库、计算器或实时网页才能回答的问题,它很容易一本正经地给你编一个数字,而且语气非常自信,这就是行业内常说的“幻觉”。
很多人一开始不信邪,觉得“只要Prompt写得够好,模型就能想清楚”。但真实情况是,CoT(Chain-of-Thought,思维链)确实能让模型把复杂问题拆解成中间步骤,可拆出来的每一步依然是模型自己在脑内完成的推演,没有任何外部验证。只要中间一步的“记忆”是错的,后面所有推理都会跟着歪。
ReAct的做法不同,它把“内心独白”和“外部事实获取”强行分开。模型可以推理,但一旦涉及知识性、实时性或计算性问题,就必须调用工具拿真实结果,然后把结果作为下一轮推理的输入。这样幻觉空间就被压缩到了模型真正擅长的逻辑组合部分,而不是数据事实部分。
2.2 纯行动的局限:没有思考的智能体就是无头苍蝇
再往另一个极端看。有些人觉得,既然模型会编,那干脆别让它想太多,直接给它一堆工具,让它一个个试就完了。这种做法叫Act-only风格,比如早期的某些机器人流程自动化脚本,或者一些粗糙的Agent Demo。
你试过就会知道,纯行动模式最大的问题是“没有规划导致的盲目试错”。模型不知道为什么要调用某个工具,也不知道当前工具结果意味着什么,只是机械地执行一连串动作,经常在同一个失败路径上反复打转。比如查询了一个不存在的商品ID,它不会说“这个ID可能是笔误,我换一个关键词再试”,而是继续用同一个ID测三次,白白浪费时间和接口配额。
ReAct把Reasoning放回去,本质上就是给行动装了一个“方向盘”。每一次调用工具之前,模型都要明确说出“我为什么这么做”,而工具结果又会反过来修正模型的下一次判断。这种约束让整个行动过程变得可解释、可控制、可纠错,工程上你也更容易定位问题出在哪一步。
2.3 与CoT、Act-only、CoT-SC的横向对比
看完两个极端,再拉一张表对比主流方案,你会更清楚ReAct的位置。
| 方案模式 | 有推理 | 有外部信息获取 | 典型问题 |
|---|---|---|---|
| 直接Prompt | 否 | 否 | 错误率高、幻觉严重 |
| CoT(思维链) | 是 | 否 | 中间步骤无法验证 |
| CoT-SC(自洽性采样) | 是 | 否 | 采多轮投票,成本高且仍无事实校验 |
| Act-only | 否 | 是 | 盲目尝试、缺少规划 |
| ReAct | 是 | 是 | 需要设计工具、观测和解析逻辑 |
ReAct最核心的贡献不是发明了什么惊人算法,而是把“推理”和“行动”两个能力单元组合成了一套工程上可落地的协作协议。它同时拥有了CoT的解释性优势和工具调用的真实数据优势,代价则是你需要额外处理循环控制、输出解析和工具管理。
3. 手把手实现:搭建一个能思考与行动的ReAct智能体
3.1 环境准备与工具定义:先让智能体有手有脚
理解了原理,就该动手了。我以一个最常见的场景为例:搭建一个能联网搜索、能算数、最后综合回答问题的问答Agent。代码以Python + OpenAI接口为例,但你完全可以把模型换成任何兼容OpenAI格式的服务。
首先是环境准备,安装必要的库:
pip install openai然后定义工具函数。ReAct本身不限定工具数量,但每一步必须足够清晰。我这里规划三把工具:
def search_tool(query: str) -> str: """模拟搜索接口,实际项目中可替换为真实搜索API或知识库检索""" return f"关于“{query}”的搜索结果:根据内部知识库,相关内容为xxx。" def calculator_tool(expression: str) -> str: """模拟计算器,实际生产环境不要直接用eval""" try: return str(eval(expression)) except Exception as e: return f"计算错误: {e}"这里有个经验要提一下:我示例里的eval只是为了演示方便,生产环境绝对不能这么干,它会带来代码注入风险。你可以用ast.literal_eval处理字面量表达式,或者接入专门的表达式计算库,也可以让模型调用一个远程计算服务。工具执行的边界越严格,整个Agent就越不容易被恶意输入带偏。
3.2 提示词模板:把"想法-行动-观察"写进Prompt
工具定义好了,但模型怎么知道要按“Thought→Action→Observation”的格式走?答案全在系统提示词里。ReAct对提示词格式的依赖非常大,Prompt写得不清晰,模型就会自由发挥。
我建议一套经过多次调整的模板,风格偏工程化,用JSON表达Action,方便后面代码解析:
SYSTEM_PROMPT = """ 你是一个通过"思考-行动-观察"循环来解决问题的智能体。 每一步请严格输出: Thought: 你对当前问题的推理,以及下一步计划。 Action: 一个JSON对象,选择以下工具: {"name": "search", "args": {"query": "搜索关键词"}} {"name": "calculator", "args": {"expression": "算术表达式"}} {"name": "finish", "args": {"answer": "最终答案"}} 规则: 1. 不要伪造Observation,Observation由系统提供。 2. 信息不足时,继续输出Thought和Action。 3. 信息充分时,用Action: finish输出最终答案。 4. 每一步只能输出一个Thought和一个Action。 """这里有一个容易被忽略的要点:一定要在Prompt里写明“不要伪造Observation”。因为很多模型看到上面的历史对话中出现了“Observation: xxx”,就会无意识地模仿这种格式,自己去编造观测结果,进而污染整个推理链。一旦出现这种情况,你辛苦搭建的ReAct就退化成单纯靠模型编故事。最好的办法是,在解析阶段也兜底——当你发现模型自己输出了Observation,就把它截掉。
3.3 Agent循环代码:解析、执行、再喂回
接下来写核心的Agent循环。算法逻辑是这样的:把用户问题发给模型,拿到回复后解析Action,执行对应工具,把工具结果作为Observation追加进对话记录,然后再次请求模型,直到模型输出finish或触发最大步数。
import json import re from openai import OpenAI client = OpenAI() def parse_action(reply: str): """从模型回复中提取Action JSON""" pattern = re.compile( r'\{"name"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*\{[^{}]*\}\}', re.S ) matches = pattern.findall(reply) if not matches: return None return json.loads(matches[-1]) def run_agent(question: str, max_steps: int = 5): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": question}, ] for step in range(max_steps): response = client.chat.completions.create( model="gpt-4o", messages=messages, temperature=0.2, max_tokens=600, ) reply = response.choices[0].message.content print(f"--- Step {step + 1} ---") print(reply) action = parse_action(reply) if action is None: # 解析失败,向模型反馈并要求重新输出 messages.append({"role": "assistant", "content": reply}) messages.append({ "role": "user", "content": "你没有输出合法的Action JSON,请重新只输出Thought和Action。" }) continue if action["name"] == "finish": return action["args"].get("answer", "") tool_fn = { "search": search_tool, "calculator": calculator_tool, }.get(action["name"]) if tool_fn is None: observation = f"工具不存在: {action['name']}" else: try: observation = tool_fn(**action["args"]) except Exception as e: observation = f"工具调用异常: {e}" messages.append({"role": "assistant", "content": reply}) messages.append({"role": "user", "content": f"Observation: {observation}"}) return "达到最大步数,未能完成。"这段代码看起来不多,但已经包含了ReAct的最小完整实现。关键在于它的消息结构:模型的每次输出都作为assistant消息保留,工具结果则作为user消息里的一段Observation回填。这样模型下一轮生成时,能看到从问题到当前全部历史,从而保持连贯性。
3.4 关键参数设置与运行效果
跑这个Agent之前,有几个参数建议先调好。
max_steps是最重要的安全阀。再聪明的Agent也偶尔会陷入钻牛角尖的状态,如果不限制循环次数,它可能一直调用工具不结束。按我的经验,普通问答任务设5步够用,需要复杂多跳检索的任务可以放到8到10步,超过10步基本说明提示词或工具设计出了问题。
temperature建议调到0.2甚至0。ReAct不是创意写作,我们希望模型尽量稳定地遵循格式,而不是每次生成五花八门的Thought。温度越高,输出越飘,Action格式越容易崩。真到了需要多样化探索的时候再调高,比如让Agent生成多套候选方案时。
max_tokens也不要给太小。如果模型输出到一半被截断,很容易留下一个残缺的JSON,让正则解析失败。600到800是一个比较宽容的数值,既不会太慢,也能容纳完整的Thought与Action。
我实际跑过的效果是这样的:在一个内部知识库问答Demo上,直接问模型“A产品用户数和B产品用户数谁更多”,模型第一轮会先去查A产品数据,第二轮查B产品数据,第三轮就能正确对比总结。整个过程清晰可见,每一轮的Thought都像调试日志一样告诉你它为什么这么做。
4. 常见问题与排查技巧实录
4.1 输出格式不稳定,怎么办
我最早踩的坑就是模型时不时不按JSON格式输出。它可能输出一大段解释性文字,然后在末尾带一个Action;也可能把Action写成Action: Search[xxx]这种论文风格,导致我的解析代码处理不了。
应对思路有三个层次。第一,Prompt里加few-shot示例,给模型看一段完整的“Thought→Action→Observation→Thought→Action: finish”示例,模仿效果比单纯描述格式好得多。第二,解析时不要只找一个Action,用正则找所有Action,取最后一个,因为模型可能先举了个例子再输出真正的Action。第三,设计容错机制,解析失败时把错误反馈给模型,让它自己修正,这在绝大多数情况下都能挽回。
4.2 死循环与重复思考:让智能体及时停下来
死循环是ReAct最让人头疼的问题。模型可能在第4步已经拿到了所有信息,但还在傻傻地调用搜索工具,就是不输出finish。我见过最离谱的一次,同一个search工具被调用了7次,三次重复相同关键词。
排查思路主要看三点。一是检查提示词里是否明确给出了“终止条件”,如果没有,模型不知道什么时候该停。二是看是否缺少工具结果的多样性,如果连续几个Observation信息量很低,模型就容易原地打转。三是策略层面的兜底:在循环里加一个“重复动作检测”,如果发现连续两轮的Action完全一致,就直接中止本次调用,把已获得的信息交给模型做最终整理。
4.3 工具返回内容过长:截断与摘要
工具结果不一定都简短。真实场景里,搜索API可能返回几千字的网页正文,数据库接口可能丢过来一大坨字段。如果把这些原样塞进对话历史,很快就把上下文窗口撑爆,而且模型注意力会被无关信息淹没。
我的习惯是给每个工具输出加一层后处理:超过一定长度的内容先截断或者做摘要。比如搜索工具只保留标题、摘要和关键段落,数据库工具只保留字段名和数值。还可以把Observation限制在500个字符以内,让模型每次只聚焦最精炼的信息。这样既节省Token,也明显提升循环稳定性。
4.4 小模型跑ReAct的典型翻车点
不是所有模型都适合直接跑ReAct。我在实际测试中发现,参数量较小的模型对“JSON Action”这种强格式约束的理解能力有限,容易把Action写成半结构化文本,甚至直接输出中文描述。如果你只能使用小模型,建议把Action格式改成更简单的文本协议,比如Action: search[关键词],减少模型生成JSON的负担。
还要注意,小模型的推理链长度有限,给它们5步以上的循环往往会出现前后矛盾。这时候不要把任务拆得太碎,尽量让每一步工具调用解决一个较完整的信息获取需求,减少循环次数。
5. ReAct模式的进阶工程化建议
5.1 多工具场景:从一个搜索到一个工具箱
当工具数量从1个变成10个,问题就从“怎么调用工具”变成“怎么让模型选对工具”。最简单的做法是把每个工具的名称、描述、参数说明都写进系统提示词,让模型根据任务自行选择。
但如果工具太多、Prompt太长,模型反而容易迷失。我建议设计一个“工具注册表”,给每个工具打上标签和能力描述,并在每轮循环开始前只把相关的几个工具注入到上下文里。这就是一种朴素但有效的工具路由。更进一步,你可以让Agent先用一个轻量模型做意图识别,缩小工具候选范围,再让主模型进入ReAct循环,这样既快又稳。
5.2 引入长期记忆与状态管理
标准的ReAct循环不携带长期记忆,每一轮能看到的只是对话窗口里的历史。如果任务跨越多次会话,或者需要记住用户之前的偏好,就得额外引入记忆层。
常见做法是把关键信息抽出来存入向量数据库,下一轮开始时做相似度检索,把相关记忆作为System Prompt的一部分塞回去。这能把ReAct从“单次任务执行器”升级成“有记忆的持续工作者”。另一种做法是给Agent维护一个结构化的状态JSON,每一步工具执行结果都同步更新到这个状态里,使长期任务可追溯、可中断、可恢复。
5.3 在智能体框架中的位置:ReAct是骨架而不是全部
如果你看过LangChain、LangGraph这类框架的Agent框架图,会发现ReAct通常只是里面一个带循环的节点,而不是整个Agent的全部。框架图里一般会有几个输入输出端口:接收用户问题、调用大模型、执行工具、返回观测结果。这个ReAct节点负责“思考—行动—观察”的微循环,而框架本身还负责管理多节点编排、条件路由、人机交互、错误恢复等外层逻辑。
工程上需要分清主次。ReAct解决的是“单轮内如何决策与行动”,而一个完整的Agent产品还需要规划、记忆、多智能体协作、权限控制等多层能力。你可以把ReAct当作一块稳定的内核,其他功能都围绕它做扩展,但不要指望光靠一个ReAct循环就能支撑起复杂的生产系统。
5.4 我的几条实战建议
做Agent工程这么多年,我总结了几条非常朴素的建议。
第一,日志一定要全。ReAct循环每一步的Thought、Action、Observation、Token消耗都要留痕。出问题时,这些日志就是你排查的救命稻草。第二,工具结果要做缓存。同样的搜索词、同样的查询参数,短时间内很可能被模型反复调用,加一层缓存能省不少费用和延迟。第三,上下文窗口要留冗余。ReAct每一轮都会把历史追加进去,看起来步数不多,但Token消耗呈线性甚至超线性增长,最好在循环中做消息压缩或历史裁剪。第四,对finish条件做严格校验。模型说它完成了,不代表答案一定正确,可以让一个独立的评审模型对最终答案做二次校验,能明显提高输出质量。
我个人的体会是,ReAct之所以能成为当前Agent开发的主流范式,不是因为它的思路有多惊艳,而是因为它足够简单、足够可控。你不需要理解复杂的强化学习或规划算法,只要把握好Thought、Action、Observation这个三角循环,配上几个好用的工具,就能搭出一个真正能“边想边做”的智能体。每次看到模型在循环中自主修正方向,都觉得这套协议比单纯堆参数更接近“智能”的本质。如果你正在做Agent,不妨从今天这段最小实现开始,亲手跑通一遍,再一步步往里加工具、加记忆、加校验,你会比只看框架文档理解得深刻得多。