1. ReAct是什么:让大模型从“答题”切换到“解题”
先聊个现象。早期我们做大模型应用,最常吐槽的一点是:模型看起来很聪明,一测就露馅。让它做数学题,它能一本正经地写出错误的推导过程;让它回答时效性问题,它能自信地告诉你一个三年前的数据。问题出在哪?出在“闭卷考试”式的架构上——模型只能依靠参数里固化的知识去作答,既不能翻书,也不能验算。
ReAct这个名字,拆开就是Reasoning + Acting,翻译成大白话就是“边想边做”。它不是某个模型的名称,而是一种提示词工程与推理范式:让大模型在回答问题的过程中,把“推理轨迹”和“行动调用”交替执行——先想一下当前需要什么信息,然后调用一个工具(比如搜索引擎、计算器、数据库)去获取结果,观察工具返回的信息,再接着思考下一步怎么做。整个流程像一个侦探在办案:分析线索、出门取证、回来继续分析,而不是坐在办公室里凭记忆写结案报告。
我第一次接触ReAct是在做一个问答类项目的时候。当时项目里需要回答大量需要实时数据支撑的问题,比如“某产品目前的最新版本是什么”“某项政策的最新动向如何”,纯靠模型内部知识根本答不准。后来改用ReAct模式,给模型挂上搜索工具,准确率立刻上了一个台阶。那次之后我就意识到,ReAct不是什么高深莫测的理论,它更像是给大模型配了一套“手脚”,让它从“背答案的考生”变成了“会查资料的实习生”。
这篇文章适合谁看?如果你在做大模型应用开发、智能体(Agent)相关项目,或者正在研究怎么让模型在复杂任务上更可靠,那这篇内容应该能给你一些可以直接上手的参考。我会从原理拆解、循环机制、代码实现到问题排查,完整走一遍ReAct的落地流程。
2. 为什么需要ReAct:从CoT到“知行合一”的进化
2.1 CoT的局限:想得多,但不代表想得对
在ReAct出现之前,业内最常用的推理增强手段是CoT(Chain-of-Thought,思维链)。CoT做的事情很简单:在提示词里加一句“请一步步思考”,或者给出几个带推理过程的示例,引导模型把思考过程写出来再作答。这个方法的有效性已经被大量实验验证过,尤其对数学题、逻辑题这类需要多步推理的任务,效果提升非常明显。
但CoT有一个致命短板:它的思考完全发生在模型内部,是一个“封闭系统”。模型推理过程中如果缺乏某个关键事实,它只会凭直觉瞎猜,而且因为推理链条看起来逻辑自洽,结果往往更具迷惑性。说白了,CoT让模型学会了“自言自语”,但没学会“出门求证”。
我举个具体例子。问模型“某个产品在过去一年内更新了多少个大版本”,模型即使按照CoT的方式推理出了“先确认初始版本、再统计每个版本、最后求和”的步骤,它依然不知道这些版本信息,最后只能编一个数字出来。这个场景下思维链再长也没用,问题出在“信息来源”上,而不是“推理路径”上。
2.2 ReAct的解法:让推理和行动互相校准
ReAct的思路就很直接:既然模型缺信息,那就让它自己去找;既然推理可能出错,那就让外部反馈来纠正它。ReAct把整个过程拆成一个循环,每个循环包含三个动作:
- Thought(思考):模型分析当前状态,总结已知信息,决定下一步需要什么。
- Action(行动):模型选择一个工具并调用它,比如搜索、计算、查询数据库。
- Observation(观察):模型读取工具返回的结果,把它作为新的上下文继续推理。
关键点在于,这三个动作会循环往复,直到模型认为已经收集到足够信息,才输出最终答案(Final Answer)。整个过程中,模型的每一轮思考都服务于一个目的:决定下一步采取什么行动;而每一个行动的结果,又会反过来修正模型的思考方向。这是一个典型的“感知-决策-行动”闭环。
打个比方,CoT像是让一个人蒙着眼睛走迷宫,靠手感一步步摸;ReAct则是让他每走几步就睁开眼看看路标,走错了就回头重试。前者在简单迷宫里可能走得通,但路线稍微复杂一点就必然迷路;后者虽然每次睁眼都要花点时间,但胜在路径可靠,走错也能及时发现。
2.3 三种范式的对比:什么时候用哪种
我把常见的几种推理范式放在一起对比过,这里直接列个表格,方便你根据场景选型:
| 范式 | 核心机制 | 适用场景 | 主要短板 |
|---|---|---|---|
| 直接回答 | 模型一次生成答案 | 简单常识、无需外部信息 | 无法处理复杂推理、容易幻觉 |
| CoT(思维链) | 推理后再回答,但不调用外部工具 | 数学题、逻辑推理题 | 封闭推理,缺事实时照样编造 |
| ReAct | 推理与工具调用交替循环 | 需要实时信息、多步求证、状态动态变化的任务 | 循环次数多、延迟较高、依赖工具质量 |
| Plan-and-Solve(先规划再执行) | 先生成完整计划,再逐步执行 | 流程明确、步骤稳定的任务 | 计划一旦出错,后续全部跑偏 |
从这个表格能看出,ReAct并不是要全面取代CoT,它更像是在CoT的推理能力之上增加了“行动能力”。实际项目中,两者也经常混合使用——先用CoT做整体推理,再用ReAct解决需要外部信息的环节。核心原则就一条:模型不能确定的事情,别让它猜,让它查。
3. ReAct循环机制详解:思考、行动、观察如何协同工作
3.1 一个完整的ReAct循环长什么样
理解ReAct的最好方式,是看一个具体的循环轨迹。下面是一个典型场景:让模型回答“某城市目前的人口大约是多少”,并且给模型挂了一个搜索工具。
Thought: 我需要知道某城市的最新人口数据,但我训练数据里的信息可能过时了,所以应该先搜索一下。 Action: Search("某城市当前人口") Observation: 搜索结果显示,某城市2024年公布的人口为X百万。 Thought: 搜索结果给出了2024年的数据,这应该是最新的官方统计,可以作为答案依据。 Action: Finish("某城市目前人口约为X百万")看到没,整个过程非常像一个人查资料的流程:先意识到自己不知道,然后去查,查到之后判断这个信息是否靠谱、是否足够,够了就给出结论。每一步都透明可解释,哪一步出了问题,回头就能定位——这是ReAct一个很大的附加价值:可追溯性。
3.2 思考(Thought)环节的设计要点
Thought是循环的起点,也是整个ReAct中最考验提示词设计能力的部分。很多初学者以为Thought就是让模型随便写两句“我需要做什么”,实际上Thought的质量直接决定后续行动的质量。
好的Thought应该包含两个要素:一是对当前状态的摘要(已知什么、还缺什么),二是对下一步行动的明确理由(为什么选择这个行动)。比如:
- 差的Thought:我需要搜索。
- 好的Thought:我已经知道该产品的基本信息,但它最近的更新日志不在我的知识范围内,因此需要搜索其官方发布记录来获取准确的版本时间。
后者之所以更好,是因为它给模型一个清晰的决策依据,模型不容易在循环中“跑偏”。我在实践里的做法是,在提示词模板里明确写出Thought应该遵循的结构:“总结已知信息 + 说明信息缺口 + 提出下一步行动理由”,这样模型输出的思考会更可控。
3.3 行动(Action)环节的工具调用设计
Action环节是整个ReAct循环“动手”的部分。模型会根据Thought中提出的需求,从预先定义好的工具列表里选择一个工具,并传入相应的参数。这个工具列表是开发者预先配置好的,比如:
- 搜索引擎
- 计算器
- 代码解释器
- 数据库查询接口
- 文件读取函数
在具体实现中,工具通常以函数的形式提供给模型。模型需要按照约定的格式调用,比如输出一段特定的JSON或者一行动作指令,然后由外层代码解析并实际执行。这里有一个非常现实的问题:模型输出的工具调用格式经常不稳定,有的模型喜欢用JSON,有的喜欢用函数的自然语言描述,有的会漏掉参数。这个我在后面“常见问题”章节会详细展开,这里先提一句:工具调用格式设计得越简单、越严格,模型越不容易犯错。
3.4 观察(Observation)环节的反馈处理
Observation环节容易被忽略,但它恰恰是ReAct和其他推理范式拉开差距的关键。工具执行后返回的结果会被拼接到对话上下文中,作为模型下一轮思考的依据。这部分内容的质量对最终答案有决定性影响。
我这里提几个实操中的处理技巧:
- 截断过长的返回结果:搜索结果动辄几千字,全部塞进上下文既浪费token,又容易让模型迷失在信息海洋里。我的做法是只保留前N个字符或前几条结构化结果,并且加上来源标识。
- 格式化工具返回:如果工具返回JSON数据,最好在拼接前转换成简洁的自然语言描述,比如“搜索到3条结果,其中第一条是:……”。模型处理自然语言比处理原始JSON稳定得多。
- 给失效结果留退路:如果搜索没找到结果,别直接返回空字符串,而是返回“未查询到相关信息,请尝试换一个关键词重新搜索”,这样模型会知道要重试,而不是硬着头皮编答案。
观察结果的反馈质量,很大程度上取决于开发者对工具的包装能力。同样一个搜索工具,有人能让模型高效定位信息,有人则让模型越搜越糊涂,差别就在这些细节上。
4. 动手实现一个最小ReAct智能体:完整代码拆解
4.1 工具选型与运行环境
理论讲完,直接进入代码环节。我这边用Python来实现一个最小可行的ReAct循环,核心思路是:定义两个工具(搜索、计算器),写一个提示词模板,然后用大模型接口驱动循环。这里我选用了当前比较主流的大语言模型接口,但逻辑可以适配任何支持函数调用或提示词驱动的大模型。
运行环境方面,需要准备以下东西:
- Python 3.10及以上
- 一个大模型API的访问密钥(我这里用Anthropic风格的接口,你也可以换成其他主流模型的接口)
- 一个简单的网页搜索API(用于演示搜索工具)
- 少量第三方库:requests、json,基本够用
注意:在这个Demo里,我不依赖LangChain之类的框架,纯手写循环逻辑。好处是你能看清ReAct的每一步内部发生了什么,不会被框架的抽象层掩盖细节。跑通了之后,再决定要不要上框架也不迟。
4.2 首先定义工具函数
工具函数是ReAct循环的“手”,它们负责真正执行行动。我定义了两个:一个用于搜索,一个用于计算。
import json import re import requests def search(query: str) -> str: """ 模拟一个搜索工具。 真实项目中可以替换为搜索引擎API、知识库检索等。 """ # 这里是示意代码,实际项目替换为真实的搜索API if "人口" in query: return "根据公开统计数据,某城市2024年人口约为2180万。" if "版本" in query.lower(): return "该产品于2024年11月发布了v3.2版本,2025年3月发布了v3.3版本。" return "未找到与查询相关的信息,请尝试换一个关键词。" def calculate(expression: str) -> str: """ 安全版本的简单计算器,仅支持加减乘除和括号。 实际项目中应该用更严谨的表达式解析器。 """ allowed_pattern = re.compile(r'^[0-9+\-*/(). ]+$') if not allowed_pattern.match(expression): return "错误:表达式中包含不支持的字符。" try: result = eval(expression, {"__builtins__": {}}, {}) return f"计算结果为:{result}" except Exception as e: return f"计算错误:{str(e)}" TOOLS = { "搜索": search, "计算器": calculate, }这里有个细节值得说一下:工具函数的返回信息里,我特意加了一句“未找到与查询相关的信息,请尝试换一个关键词”。这不是废话,而是为了让模型在观察环节能收到有效反馈——知道这次没查到,并且知道下一步可以怎么做。如果你直接返回空字符串,模型很可能误以为搜索成功但没有结果,然后陷入逻辑混乱。
4.3 定义ReAct提示词模板
提示词模板是ReAct的“大脑”,它规定模型如何生成思考、行动和最终回答。我整理了一个在实践中反复打磨过的模板框架:
REACT_SYSTEM_PROMPT = """ 你是一个智能助手,你可以使用工具来获取信息或执行计算来回答问题。 可用工具: {工具列表} 你的回答必须严格遵循以下格式,每一轮只能输出一个部分: Thought: 你当前的想法,总结已知信息和需要什么信息。 Action: 你要调用的工具名称,必须从工具列表中选择。 Action Input: 传给工具的参数,必须是一个JSON字符串。 当收集到足够信息时,使用以下格式回复: Thought: 我已经获取到足够的信息。 Final Answer: 你对问题的最终回答。 注意: - 每次行动后,你会收到Observation作为工具的执行结果,然后继续思考。 - 不要在一个回复中同时生成多个Action。 - 如果工具调用失败,尝试换一种方式重新搜索。 """.strip() def build_prompt(tool_descriptions: str, question: str) -> list: messages = [ {"role": "system", "content": REACT_SYSTEM_PROMPT.replace("{工具列表}", tool_descriptions)}, {"role": "user", "content": f"请回答这个问题:{question}"}, ] return messages为什么Action Input要规定成JSON字符串?因为JSON是机器解析最稳定的格式之一。你可能会看到有些实现允许模型自由文本描述参数,那样解析的时候很容易出错。强制JSON格式,配合正则表达式提取,出错率会显著下降。
4.4 主循环:让模型“边想边做”直到收敛
主循环是整个ReAct实现的核心。流程如下:
- 把当前对话上下文发给模型。
- 模型返回一段文本,解析其中的Thought和Action。
- 如果包含Final Answer,结束循环。
- 否则,根据Action调用对应工具,拿到Observation。
- 把Observation拼接到上下文里,回到步骤1。
import re def extract_action(text: str) -> tuple: """ 从模型输出中提取动作和参数。 返回 (动作类型, 是否结束, 最终回答) """ if "Final Answer:" in text: final = text.split("Final Answer:")[-1].strip() return ("final", True, final) action_match = re.search(r"Action:\s*(.+)", text) input_match = re.search(r"Action Input:\s*(\{.+?\})", text, re.DOTALL) if not action_match or not input_match: return ("error", False, None) action = action_match.group(1).strip() action_input = json.loads(input_match.group(1)) return (action, False, action_input) def run_react(question: str, max_iterations: int = 5): tool_descriptions = "\n".join([f"- {name}: {func.__doc__.strip()}" for name, func in TOOLS.items()]) messages = build_prompt(tool_descriptions, question) for step in range(max_iterations): print(f"\n===== 第 {step+1} 轮 =====") response = get_model_response(messages) # 调用API返回文本 print(f"模型输出:\n{response}") action, finished, final_answer = extract_action(response) if finished: return final_answer if action == "error": print("解析失败,将重新让模型尝试") messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": "你的输出格式不正确,请严格按要求输出Thought和Action。"}) continue # 执行工具调用 if action in TOOLS: try: observation = TOOLS[action](**action_input) except Exception as e: observation = f"工具调用出错:{str(e)}" else: observation = f"错误:未知工具 {action},请从工具列表中选择。" print(f"工具执行结果: {observation}") # 将本轮输出和观察结果加入上下文,进入下一轮循环 messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": f"Observation: {observation}"}) return "达到最大迭代次数,未能得出答案。"这段代码里,get_model_response是调用大模型接口的函数,你可以直接用任何模型库的API替换。我需要特别提醒的是max_iterations这个参数——它在实际项目中极其重要。如果没有这个上限,模型在遇到复杂问题时可能陷入无限循环,既浪费token又拉长响应时间。我一开始做项目时忘了设置这个参数,结果模型因为搜索结果一直不理想,连续循环了二十多轮,调用成本直接爆表。经验值:简单问答设置3轮,复杂任务设置5-8轮比较合理。
4.5 实际运行效果与分析
我分别用两个问题测试了上面这个Demo,观察它的行为。
第一个问题:“某城市目前人口大约是多少?”
===== 第 1 轮 ===== 模型输出: Thought: 用户询问某城市的最新人口数据,这个信息具有时效性,我需要搜索最新数据。 Action: 搜索 Action Input: {"query": "某城市当前人口"} 工具执行结果: 根据公开统计数据,某城市2024年人口约为2180万。 ===== 第 2 轮 ===== 模型输出: Thought: 我已经获取到该城市的最新人口数据,可以回答用户的问题了。 Final Answer: 根据公开统计数据,某城市2024年人口约为2180万。这个案例展示了ReAct最典型的路径:检索-判断-作答。模型准确识别出“人口数据”这种信息需要外部验证,调用搜索工具获取答案,并在确认信息足够后直接终止循环。两轮解决,效率很高。
第二个问题:“计算 (123 + 456) × 78 的结果”。
===== 第 1 轮 ===== 模型输出: Thought: 这是一个数学计算问题,使用计算器工具可以更准确。 Action: 计算器 Action Input: {"expression": "(123+456)*78"} 工具执行结果: 计算结果为:45162 ===== 第 2 轮 ===== 模型输出: Thought: 我已经使用计算器计算出正确结果。 Final Answer: 计算结果为 45162。这个案例展示了工具对“可靠性”的价值。虽然这种两层运算大模型也可能口算出来,但准确率随手势、输入方式波动很大。让模型调用计算器,等于把“可能出错”的部分外包给确定性组件,这是ReAct的一大优势:把不可靠的推理与可靠的工具进行分工。
5. 进阶优化:让ReAct更稳定、更高效的三个关键技巧
第四节的基础版本已经能跑通整个流程,但离“生产可用”还有距离。我这里分享几个在真实项目中摸索出来的进阶技巧。
5.1 缓存与记忆:避免反复搜索同一信息
ReAct循环里有个很常见的毛病:模型在多步推理时,会在后续轮次忘记前面已经获取过的信息,导致重复搜索同一内容。比如先搜索了“某产品v3.2发布时间”,后来又搜索“某产品最近版本”,两次调用查的是同一个事件的不同表述。
解决方案很简单:在上下文中维护一个“已获取信息摘要”,每一轮循环开始前,先把之前的观察结果做一个紧凑的汇总,提示模型“你已经知道以下内容……”。这样做既能让模型快速定位已有信息,又能节省token开销。我用这个方法之后,平均循环轮数降低了大约三分之一。
5.2 多工具协同:设计信息依赖关系的策略
复杂任务往往需要多个工具协同。比如回答“某公司在过去一年推出过几个新产品”,可能需要先搜索公司主页获取新闻列表,再逐一搜索每个新产品的详情,最后用计算器统计数量。这种任务如果让模型自由发挥,它很容易迷失在多个工具之间。
我的对策是:在提示词里增加一个“任务拆解建议”区域,允许模型先写一个简短的行动计划,再进入ReAct循环执行。这相当于把Plan-and-Solve和ReAct做了融合——先规划大方向,再小步快跑地执行。两套范式并不冲突,实际效果反而更好。
5.3 温度参数的调优:让模型“理性”而不是“发散”
大模型的temperature参数对ReAct的稳定性影响很大。ReAct循环中的每个Action调用本质上是一个决策行为,决策需要的是稳定性和可预测性,而不是创造性。我在实践中,把temperature设置在0到0.3之间效果最好。如果调得太高,模型会在Thought里“脑补”很多不存在的假设,导致行动偏离实际需求;适度降低温度,模型会更专注于利用已有信息做判断。
这里有个小技巧:你可以给Thought和Action分别设置不同的temperature。Thought环节稍微高一点(比如0.7)让思考更灵活,Action环节用低温(比如0.1)确保调用格式稳定。如果用的API支持分步生成参数,这种混搭方式实测很有效。
6. 常见问题与排查技巧实录
6.1 模型陷入死循环,反复调用同一个工具
这个是最常见的问题。症状:模型连续好几轮调用同一个搜索关键词,得到的Observation完全相同,但它就是不给出Final Answer,或者换一个搜索词之后又回到原来那个词。
排查思路:
- 先检查是不是工具返回信息不足。如果Observation只是简单重复,没有增加新信息,模型就缺少“信息增量”来推动决策。解决办法是让工具返回更丰富的结果,比如追加相关标题列表、来源URL等。
- 再检查提示词里是否明确告诉模型“当Observation没有新信息时,应停止搜索并基于已有信息作答”。很多时候模型不是故意死循环,而是它的训练模式里缺少“中止”的决策指令。
- 最后,务必设置max_iterations上限,这是工业级应用的兜底防线。
6.2 工具调用格式不稳定,偶尔解析失败
这个问题在换用不同大模型时特别常见。有的模型输出Action Input时喜欢用单引号而不是双引号,有的会在JSON前后加说明文字,有的直接不按模板生成多行内容。
我的解决方案有两层:
- 第一层是提示词加固:在模板里给出明确的例子(few-shot),通常能大幅提升格式遵守率。
- 第二层是解析兜底:写一个容错解析函数,先用正则提取Action和Action Input,如果提取失败,就把整段文本发给模型,告诉它“你刚才的输出格式不合法,请重试”,并附加格式要求。这个“重试机制”看似笨拙,但实测能解决大部分格式问题。
6.3 模型选错工具或传入错误参数
比如应该用计算器,结果模型调用了搜索;或者应该传数字给计算器,结果传了文字描述。这类问题的根源通常是工具描述不够清晰,模型根本不理解每个工具的适用场景和参数要求。
改进方法是把工具描述写得像“使用说明书”:说明这个工具是干什么的、适合什么场景、参数是什么格式、返回值是什么形式。我举一个对比:
- 差的描述:搜索(query): 百度搜索。
- 好的描述:搜索(query: str):用于查询最新的实时信息和事件,适合查找新闻、数据、百科资料等,返回匹配结果列表。当问题涉及事实性信息且你的知识可能过时的时候,优先使用该工具。
清晰、有场景、有偏好的工具描述,能让模型的选择准确率提升一大截。这个优化成本极低,但收益非常直观。
6.4 快速排查表
我把上述问题和对应的排查方向整理成一张表,方便你遇到问题的时候对照处理:
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 死循环、重复调用同工具 | 工具返回信息增量不足、缺少终止指令、温度过高 | 丰富工具返回值、在提示词中加入停止条件、降低温度、设置迭代上限 |
| Action解析失败 | 模型输出格式不稳定、提示词示例不足 | 增加few-shot示例、加入容错重试机制、强制JSON格式 |
| 选错工具 | 工具描述不清晰、工具列表过杂 | 重写工具描述(明确场景与偏好)、精简工具数量 |
| 最终答案仍然错误 | 观察结果被模型误读、最终判断缺乏依据 | 格式化工具返回内容、增加“判断信息是否充分”的提示 |
| 响应太慢、token消耗高 | 循环轮数过多、上下文过长 | 收敛max_iterations、增加信息摘要机制、压缩观察结果 |
7. 从ReAct到完整Agent:你能走得更远
ReAct解决了“模型如何在单次任务中边想边做”的问题,但真实的业务场景往往更复杂:任务可能有多个子任务,每个子任务需要不同的工具组合,中间还可能要停下来问用户澄清需求。这时候单纯的ReAct循环就不够用了,你需要把它封装成Agent框架里的一环。
我在实践中推荐的演进路径是:
- 第一步:先用ReAct解决单轮问题,确保模型能稳定调用工具。
- 第二步:在ReAct外围加一个任务分解层,让模型先规划子任务清单,每个子任务内部用ReAct执行。
- 第三步:引入记忆系统(短期对话记忆+长期向量记忆),让模型跨会话保留经验。
- 第四步:增加人工介入机制,当模型的工具调用置信度低或执行次数过多时,主动询问用户确认。
这条路走完之后,你就拥有了一个具备基本智能体能力的系统。ReAct是整个体系的地基,地基打牢了,上层建筑才能盖得高。
我在实际项目中最深的体会是:ReAct最了不起的地方,不是让模型变得更聪明,而是让模型的“不确定性”变得可见、可控制。传统模式下模型答错你只能认栽,但ReAct模式下,你能观察它在哪个环节产生了错误认知,然后针对性地修正工具描述或提示词。这种“可调试性”,才是ReAct真正带来的工业价值。
如果你打算在项目里引入ReAct,我的建议是别急着上完整框架,先手写一个小循环跑通主流程,感受一下思考、行动、观察三个环节是怎么协作的。跑通了,再决定要不要引入更重的抽象。这一步体验过的人,后面做任何Agent相关设计都会心里有底。