简介:本文从原理、架构与代码实现三个层面系统拆解 ReAct 模式。读者将理解“思考—行动—观察”循环的运作机制、上下文管理策略与工具调用设计,并获得可落地的通用实现思路,帮助构建更可靠、可交互的智能体应用。
传统大语言模型在处理复杂任务时,本质上是一个被动的文本生成器。它根据输入提示输出一段文本,既不主动获取外部信息,也不对自身推理过程进行显式校验。这种工作方式带来几个典型问题:
第一,知识静态性。模型训练完成后,其内部知识被固化在参数中。当用户询问实时天气、最新股价或当前系统状态时,模型无法自行获取更新后的信息,只能基于训练数据中的旧知识作答。
第二,幻觉倾向。面对知识盲区,模型往往不会主动承认“不知道”,而是以流畅的语句编造看似合理的事实。例如描述一个不存在的产品型号,或给出一个逻辑通顺但完全错误的计算过程。
第三,多步推理脆弱。在需要连续推理几步甚至几十步的任务中,任何一步的偏差都可能导致最终结论错误。传统模式下,模型一次性生成所有步骤,缺乏中途校验和修正的机制。
第四,缺乏环境交互能力。模型与外部工具、数据源、计算引擎之间没有连接通道。它无法查询数据库、调用计算器、读取文件或请求 API,只能依赖自身的参数化知识。
ReAct 模式的出现,正是针对上述结构性缺陷的系统性修正。它的核心主张可以概括为一句话:让模型在推理过程中主动执行动作,并根据动作返回的结果修正后续推理。这一思想来自 2022 年一项具有里程碑意义的研究工作——Google 团队发表的论文《ReAct: Synergizing Reasoning and Acting in Language Models》,该论文首次系统性地将“推理”和“行动”统一到一个循环框架中,并证明这种协同能够显著提升模型在知识密集型任务、决策任务和交互式任务中的表现。
理解 ReAct 的关键在于:它不是一个具体的模型,也不是一套固定的参数设置,而是一种提示组织与流程控制模式。它规定了模型在每个决策节点上应该输出什么结构、系统如何解析这些输出、以及如何将外部反馈重新注入上下文。这使得任何具备基本指令跟随能力的大模型都可以在 ReAct 框架下工作,无需对模型本身做任何修改。
观察人类解决复杂问题的过程,可以发现一个高度一致的行为模式:
思考 → 行动 → 观察 → 再思考 → 再行动 → 再观察……
比如规划一次周末出行:你先在心里推理——“得先看天气怎么样”;然后执行动作——打开天气应用查询;接着观察结果——“周六有大雨”;基于这个观察调整计划——“户外活动不行,改去看电影”;随后又产生新的问题——“看哪部电影、哪个场次有空位”;于是继续行动查询,如此循环。
这个过程的本质在于:思考指导行动,行动的结果反过来修正思考。没有行动的思考是空转,没有思考的行动是盲目。人类之所以能处理高度不确定、信息不完全的任务,正是因为掌握了这种“边想边做、边做边改”的能力。
ReAct 将上述人类认知模式形式化为一个三元素循环结构:
| 元素 | 含义 | 产出 |
|---|---|---|
| Thought(思考) | 模型对当前状态的推理与分析 | 一段自然语言推理文本 |
| Action(行动) | 模型决定调用的外部工具及参数 | 一个结构化动作指令 |
| Observation(观察) | 工具执行后的返回结果 | 一段描述执行结果的文本 |
一个完整的 ReAct 循环包含以下步骤:
第一步:任务输入。 用户向系统提交一个复杂任务,例如“查一下北京未来三天的天气,并判断是否需要带伞”。
第二步:模型生成思考。 模型接收当前上下文,输出一段推理过程。例如:“要判断是否需要带伞,需要先知道未来三天北京是否有降水,因此我需要查询天气预报。”
第三步:模型生成行动。 紧接着思考,模型输出一个结构化动作指令,指明要调用哪个工具以及传什么参数。例如调用天气查询工具,参数为城市“北京”和天数“3”。
第四步:系统执行行动。 框架层解析模型输出的动作指令,调用实际的外部工具或 API,并拿到返回结果。
第五步:模型接收观察。 工具返回的结果以“Observation”的形式追加到上下文中,例如:“查询结果:周五晴,周六小雨,周日阴。”
第六步:基于观察继续推理。 模型读取新增的观察信息,进行下一轮思考。如果信息已足够回答问题,模型输出最终答案;如果不够,则生成新的动作继续查询。
第七步:循环直到终止。 整个循环持续进行,直到模型输出最终答案或达到预设的最大迭代次数。
ReAct 之所以有效,核心在于推理与行动之间产生了一种双向信息增益:
从工程实现角度,一个 ReAct 智能体系统通常包含以下核心组件:
上下文管理器负责维护完整的交互轨迹。它保存的不仅仅是用户输入和模型回复,还包括每一轮的 Thought、Action 和 Observation。在每一轮循环开始时,上下文管理器将累积的轨迹拼接成模型可接受的输入格式。
典型的消息组织方式如下:
User: 请帮我查一下北京未来三天的天气,判断是否需要带伞。Thought: 我需要先获取北京未来三天的天气信息。Action: weather_query(city="北京", days=3)Observation: 周五晴,周六小雨,周日阴。Thought: 周六有小雨,需要带伞。Final Answer: 建议带伞,周六北京有小雨。
这种设计的关键在于全量轨迹入模。模型在每一轮都“看到”之前的全部思考和观察,从而保持决策的连续性和一致性。
模型输出的 Action 通常以结构化格式呈现,例如 JSON 或特定标记包裹的文本。动作解析器的职责是将模型的自然语言输出转换为可执行的工具调用指令。
一个常见的动作输出格式设计如下:
{"tool": "weather_query","params": {"city": "北京","days": 3}}
解析器需要处理几个实际问题:模型输出不完全是合法 JSON 时如何容错;工具名称不在注册表中时如何处理;参数缺失或类型错误时如何反馈给模型进行修正。健壮的解析器通常会捕获解析异常,将错误信息包装为 Observation 反馈给模型,让模型自行纠正,而不是直接中断流程。
工具注册表维护所有可用工具的名称、描述、参数模式和执行函数。工具描述至关重要,因为模型需要依据描述来判断“该调用哪个工具”以及“怎么传参”。好的工具描述应包含:
执行器则负责实际执行工具调用。它可以是本地函数(如计算器、字符串处理)、外部 API 调用(如天气服务)、数据库查询(如 SQL 执行)或任意封装好的业务逻辑。执行器需要在超时、异常、权限控制等方面做好防护。
终止控制器判断循环是否应该结束。常见的终止条件包括:
Final Answer 标记。终止控制器的作用是防止模型陷入无限循环或持续产出无效输出,保证系统的资源消耗可控。
┌─────────────────────────────────┐│ 用户任务输入 │└──────────────┬──────────────────┘▼┌─────────────────────────────────┐│ 上下文管理器 ││ (累积轨迹:T → A → O → ...) │└──────────────┬──────────────────┘▼┌─────────────────────────────────┐│ 大语言模型 ││ (生成 Thought + Action) │└──────────────┬──────────────────┘▼┌─────────────────────────────────┐│ 动作解析器 ││ (提取 tool 名称与参数) │└──────────────┬──────────────────┘▼┌─────────────────────────────────┐│ 工具注册与执行器 ││ (调用外部工具,获取返回结果) │└──────────────┬──────────────────┘▼┌─────────────────────────────────┐│ Observation 构造 ││ (将结果格式化后写入上下文) │└──────────────┬──────────────────┘│▼┌─────────────────────────────────┐│ 终止控制器 ││ (判断是否输出最终答案) │└──────────────┬──────────────────┘│ 是▼最终答案输出
以下给出一个通用的 ReAct 循环实现骨架,帮助开发者理解各组件之间的协作关系。该代码仅为结构示意,不依赖任何特定框架或 SDK。
class ReActAgent:def __init__(self, model, tools, max_iterations=10):self.model = modelself.tools = tools # 工具注册表:{name: tool_object}self.max_iterations = max_iterationsself.context = [] # 累积轨迹def run(self, user_task):self.context.append({"role": "user", "content": user_task})for i in range(self.max_iterations):# 1. 调用模型,获取下一段输出response = self.model.generate(self.context)# 2. 解析模型输出,区分 Thought / Action / Final Answerparsed = self.parse(response)if parsed["type"] == "final_answer":return parsed["content"]if parsed["type"] == "action":# 3. 执行工具调用tool_name = parsed["tool"]tool_params = parsed["params"]if tool_name not in self.tools:observation = f"错误:工具 {tool_name} 不存在。可用工具:{list(self.tools.keys())}"else:try:result = self.tools[tool_name].execute(**tool_params)observation = f"工具执行成功,返回结果:{result}"except Exception as e:observation = f"工具执行失败,错误信息:{str(e)}"# 4. 将行动与观察写入上下文self.context.append({"role": "assistant","content": f"Thought: {parsed['thought']}\nAction: {parsed['raw']}"})self.context.append({"role": "system","content": f"Observation: {observation}"})continuereturn "达到最大迭代次数,任务未完成。"
提示模板设计。为了让模型稳定输出 Thought-Action-Observation 结构,提示词需要明确给出输出格式约束。一个典型的系统提示如下:
你是一个具备工具调用能力的智能体。请按照以下格式输出:Thought: 你对当前状态的分析和下一步计划。Action: 一个 JSON 格式的工具调用,包含 tool 和 params 字段。在获得 Observation 后,继续输出新的 Thought 和 Action。当你已经获得足够信息回答用户问题时,输出:Final Answer: 最终答案。可用工具列表:- weather_query: 查询指定城市指定天数的天气预报。参数:city(字符串), days(整数)- calculator: 执行数学计算。参数:expression(字符串)
上下文长度管理。随着循环轮次增加,累积轨迹会持续增长。当上下文接近模型窗口上限时,需要采取压缩策略。常见做法包括:对早期轮次做摘要压缩、只保留最近 N 轮完整轨迹、对 Observation 内容做截断等。
容错与自纠正。实际运行中,模型输出格式偏离预期的概率并不低。解决思路是“把错误当作 Observation 喂回去”。当解析失败时,系统不直接终止,而是将格式错误描述作为 Observation 追加到上下文,让模型自行修正并重新尝试。
实时信息获取类任务。当任务依赖最新数据时(如天气、汇率、股票、新闻),ReAct 通过工具调用补足模型知识的时效性短板。
精确计算与结构化查询类任务。对于数学计算、数据库查询、代码执行等要求精确性的操作,ReAct 将计算职责“外包”给确定性工具,从根本上规避模型在数值运算上的不可靠性。
多步决策与规划类任务。在需要逐步收集信息、动态调整策略的场景中(如旅行规划、故障排查、研究调研),ReAct 的迭代循环结构天然适合处理这种渐进式的不确定性。
知识密集型的开放问答。当问题涉及多来源、多层次的信息整合时,ReAct 可以驱动模型依次查询不同信息源,逐步拼凑出完整答案,同时保留完整的推理链条供审查。
| 维度 | 传统 LLM 直接生成 | ReAct 模式 |
|---|---|---|
| 结果事实性 | 依赖模型内部知识,易幻觉 | 基于工具返回的真实数据,事实性更强 |
| 推理可追踪性 | 推理过程不可见或不可靠 | 完整 Thought-Action 轨迹可审查 |
| 处理未知问题的能力 | 知识不足时只能编造 | 可主动调用工具补足信息 |
| 多步任务可靠性 | 一步错步步错,无中间校验 | 每步可基于 Observation 修正 |
| 系统集成能力 | 无法触达外部资源 | 标准化工具接口支持任意集成 |
粒度适中。工具不宜过于细碎(如一个工具只做一件事的极小步骤),也不宜过于宽泛(如一个工具包揽所有逻辑)。适中的粒度能让模型有效规划调用序列,同时减少调用次数。
描述面向模型而非面向人。工具描述的主要读者是大模型。描述应该包含模型判断“何时该用这个工具”和“如何正确传参”所需的全部信息,措辞要明确,避免歧义和过于笼统的表述。
错误信息要可理解。当工具执行失败时,返回给模型的错误信息应清晰说明失败原因,最好附带修正建议。模糊的错误信息(如只返回“失败”)会让模型无法有效调整。
工具调用过度。模型可能陷入“为调用而调用”的循环,不断执行无信息增益的动作。可通过设置连续无效调用的阈值来限制。
观察信息过载。工具返回的结果可能非常冗长(如网页全文抓取)。需要设计结果截断、摘要预处理或字段筛选机制,只把关键信息作为 Observation 送回模型。
模型偏离格式。随着上下文变长,模型遵循输出格式的能力可能下降。合理的做法是加强提示词约束、缩短每轮上下文、并在格式解析层面做好容错和重试机制。
ReAct 架构的价值不在于它引入了某种全新的模型能力,而在于它通过“思考—行动—观察”的循环结构,将大模型从被动的知识应答器升级为主动的任务执行体。这一升级的核心逻辑非常朴素:给模型一个手,让它能摸到真实世界;再给它一个日志,让它能反思自己的路径。
对于开发者而言,落地 ReAct 的关键不在于引入多么复杂的框架,而在于设计好三件事:清晰的角色提示与输出契约、合理的工具注册与描述体系、健壮的轨迹管理与终止策略。这三件事做到位,ReAct 就能在实时查询、精确计算、多步规划等场景中产生实质性价值,显著降低幻觉,增强结果的可信度与可解释性。
随着大模型能力的持续演进,ReAct 所代表的“推理-行动协同”范式已经成为构建实用智能体系统的核心基础之一。理解其架构本质,掌握其实现要点,对于每一位希望将大模型从“聊天工具”转化为“生产力工具”的开发者而言,都是一项不可或缺的基本功。