1. 从零理解 ReAct为什么它是智能体开发的分水岭ReAct 这个词如果你最近在关注 LLM 应用开发或者智能体方向大概率已经反复刷到过。它不是一个前端框架虽然热搜里“react 2026 前端面试”和“ReAct”经常混在一起出现但两者完全是两码事。ReAct 是 Reasoning Acting 的缩写是一种让大语言模型在推理和行动之间交替循环的提示工程范式也是当前绝大多数智能体框架的底层思想来源。我第一次接触 ReAct 是在做一个需要调用外部搜索接口来回答时效性问题的项目。当时最朴素的做法是把用户问题直接丢给模型模型答不上来就胡编。后来加了检索增强把搜索结果拼进提示词里再让模型回答效果好了一些但依然存在一个致命问题模型不知道该搜什么、搜几次、什么时候该停止。ReAct 解决的正是这个“该想的时候想、该做的时候做”的调度问题。它的核心逻辑用一句话概括就是让模型在每一步先输出一段思考再决定是否调用工具拿到工具返回结果后继续思考如此循环直到模型认为可以给出最终答案。这个循环看起来简单但它是目前所有主流智能体框架——无论是 LangChain 的 Agent、AutoGPT 的任务拆解还是各类垂直领域智能体——的共同祖先。这篇文章我会从 ReAct 的设计动机讲起拆解它的提示词结构、循环机制、工具调用协议然后手把手带你实现一个可运行的 ReAct 智能体最后分享我在实际项目中踩过的坑和排查技巧。适合已经了解 LLM 基本调用方式、想进一步做智能体开发的读者也适合刚入门提示工程、想搞清楚“智能体到底怎么运转”的朋友。2. ReAct 的整体设计与核心思路拆解2.1 为什么单纯的推理或单纯的行动都不够用要理解 ReAct 的价值得先看清楚它之前的两条路线各自的问题。第一条路线是纯推理也就是 Chain-of-Thought。你让模型一步步想它确实能把复杂问题拆开但它的知识完全来自训练数据。一旦问题涉及实时信息、私有数据或者需要精确计算它就只能靠“记忆”去编幻觉率非常高。我在早期项目里试过让模型直接回答“某公司最新财报的营收是多少”它给出的数字看起来有零有整实际上完全是虚构的。第二条路线是纯行动也就是让模型直接输出工具调用指令。这种方式的问题是模型没有“想清楚再动手”的环节它可能在还没理解问题的情况下就胡乱调用工具或者调用一次拿到结果后不知道怎么继续。比如用户问“帮我对比 A 和 B 两个城市今天的天气”纯行动模式可能只查了 A 就急着回答。ReAct 的思路是把两者交织起来。Thought 负责推理和规划Action 负责获取外部信息Observation 负责接收反馈然后进入下一轮 Thought。这个设计的精妙之处在于推理指导行动的方向行动的结果又反过来修正推理形成一个闭环。用生活化的类比来说这就像一个人做菜先想“我需要什么食材”Thought然后去冰箱拿Action看到冰箱里没有葱Observation再想“那我去楼下买”Thought再去买Action拿到葱Observation最后开始炒最终答案。2.2 ReAct 与普通提示工程的本质区别很多人会把 ReAct 和普通的提示词工程混为一谈觉得无非就是写一段更长的提示词。这个理解只对了一半。ReAct 确实是通过提示词来实现的但它和普通提示工程有一个本质区别普通提示工程是“一次性”的你给模型一段指令它输出一个结果结束。ReAct 是“循环式”的它的提示词里定义了模型可以反复使用的格式和工具模型会在多轮交互中不断产生新的输出直到满足终止条件。这个区别带来的工程影响非常大。普通提示工程你只需要关心单次输入输出的质量ReAct 你需要关心的是循环的稳定性、工具调用的正确率、终止条件的可靠性、以及多轮交互中的上下文管理。这也是为什么智能体开发比单纯的提示词编写要复杂得多。另一个区别是ReAct 对模型的指令遵循能力要求更高。模型不仅要理解任务还要严格按照 Thought/Action/Observation 的格式输出不能跑偏。实测下来指令遵循能力弱的模型在 ReAct 循环里很容易“忘记格式”输出一段自然语言就停了导致整个循环断裂。2.3 ReAct 循环的完整状态机把 ReAct 的循环拆开看它其实是一个状态机。初始状态是用户输入的问题然后进入 Thought 状态模型输出一段推理。接着进入 Action 状态模型决定调用哪个工具、传什么参数。然后系统执行工具进入 Observation 状态把结果拼回上下文。判断是否满足终止条件如果模型输出了 Final Answer循环结束否则回到 Thought 状态继续下一轮。这个状态机里有两个关键设计点。第一个是上下文的累积方式每一轮的 Thought、Action、Observation 都会追加到对话历史里模型在下一轮能看到之前所有的推理轨迹。这让模型能够基于历史信息做决策但也带来了上下文长度的问题。第二个是终止条件ReAct 原始论文里是靠模型自己输出“Final Answer:”来终止实际工程中通常还会加一个最大轮次限制防止模型陷入死循环。我在实际项目里会把最大轮次设成 5 到 8 轮。设太少复杂问题还没解决就被截断设太多一旦模型卡住会浪费大量 token。这个值需要根据你的任务复杂度来调没有万能数字。3. 核心细节解析与实操要点3.1 ReAct 提示词的结构拆解ReAct 的提示词通常由几个部分组成角色设定、工具描述、输出格式说明、示例Few-shot、以及用户问题。每一部分都有讲究。角色设定要明确告诉模型它是一个“可以使用工具来解决问题的助手”而不是一个纯聊天机器人。这个设定会影响模型的输出倾向。我试过不加角色设定模型经常直接给答案而不调用工具。工具描述是重中之重。每个工具需要说明名称、功能、参数格式。参数格式最好用结构化的方式写清楚比如“输入应该是一个搜索关键词字符串”。工具描述写得越清晰模型调用错误率越低。我见过很多 ReAct 实现失败根源就是工具描述太模糊模型不知道该传什么参数。输出格式说明要严格定义 Thought、Action、Observation、Final Answer 的写法。通常用“Thought:”“Action:”“Action Input:”“Observation:”“Final Answer:”这样的前缀。格式定义得越死模型越不容易跑偏。Few-shot 示例是提升稳定性的关键。给一到两个完整的循环示例让模型模仿。示例要覆盖“需要调用工具”和“直接回答”两种情况。实测下来加了示例之后模型的格式错误率能降低一半以上。3.2 工具调用的参数设计与边界处理工具调用是 ReAct 里最容易出问题的环节。模型输出的 Action Input 是自然语言生成的它可能格式不对、参数缺失、或者传了工具不支持的参数。所以工程上必须做参数校验和容错。我的做法是在工具执行层加一层解析和校验。首先尝试用 JSON 解析 Action Input如果失败就尝试用正则提取关键字段再失败就返回一个友好的错误信息给模型让它重新调用。这个错误信息会作为 Observation 拼回上下文模型看到之后通常会修正自己的调用。另一个要点是工具的幂等性。因为模型可能重复调用同一个工具工具本身最好设计成幂等的或者至少不会因为重复调用产生副作用。比如搜索工具重复调用没问题但“发送邮件”这种工具就要加去重逻辑。还有一个细节是 Observation 的长度控制。工具返回的结果可能很长比如搜索返回了十条结果每条几百字。如果全部拼进上下文几轮下来 token 就爆了。我的做法是对 Observation 做截断或摘要只保留最相关的部分。截断策略可以是按字符数截断也可以是让模型先对结果做一次摘要再拼回去。3.3 上下文管理与 token 预算控制ReAct 循环的上下文会随着轮次增加而膨胀。每一轮都会追加 Thought、Action、Observation如果工具返回结果很长上下文增长会非常快。我做过一个统计一个 5 轮的 ReAct 循环如果每轮 Observation 平均 500 token加上提示词本身总 token 消耗轻松超过 5000。控制 token 预算有几个手段。第一是限制工具返回结果的长度在工具层就做截断。第二是定期对历史做摘要把早期的推理轨迹压缩成一段简短总结。第三是设置最大轮次硬性截断。第四是选择上下文窗口更大的模型但这会增加成本。我个人的经验是对于大多数任务把工具返回结果控制在 300 到 500 字以内最大轮次设成 6基本能覆盖 80% 的场景。如果任务特别复杂再考虑加摘要机制。3.4 终止条件的可靠性设计终止条件是 ReAct 循环的出口设计不好会导致两种问题一是模型该停的时候不停陷入无限循环二是模型不该停的时候停了答案不完整。原始 ReAct 靠模型输出“Final Answer:”来终止。但实际中模型可能忘记输出这个前缀或者输出了但格式不对。所以工程上需要双重保险既检测模型输出里有没有“Final Answer:”也设置最大轮次作为兜底。另外我还会加一个“空 Action”检测。如果模型输出了 Action 但 Action Input 为空或者输出了无法解析的内容就判定这一轮无效让模型重试。连续无效超过两次就强制终止返回当前已有的信息。4. 实操过程与核心环节实现4.1 环境准备与依赖安装我们用一个最小化的实现来演示 ReAct 的完整流程。不依赖 LangChain 这类重型框架纯手写这样你能看清楚每一行代码在做什么。需要准备的东西很简单一个能调用 LLM 的 API这里用通用的 OpenAI 兼容接口举例、一个搜索工具用 SerpApi 举例你也可以换成任何搜索接口、以及 Python 环境。先装依赖pip install openai requests如果你用 SerpApi还需要去官网注册拿一个 API Key。不想注册的话可以先用一个 mock 的搜索函数代替返回固定结果先把循环跑通再换真实工具。环境变量里配置好 API Keyexport LLM_API_KEYyour_llm_api_key export SERP_API_KEYyour_serp_api_key注意API Key 不要硬编码在代码里也不要在公开仓库里提交。用环境变量或者配置文件管理这是基本的安全习惯。4.2 定义工具函数与工具描述先定义搜索工具。SerpApi 的调用很简单传一个查询词返回搜索结果。我们只取前三条的标题和摘要控制返回长度。import os import requests def search(query: str) - str: api_key os.getenv(SERP_API_KEY) url https://serpapi.com/search params { q: query, api_key: api_key, num: 3, hl: zh-cn } try: resp requests.get(url, paramsparams, timeout10) data resp.json() results data.get(organic_results, []) if not results: return 没有找到相关结果。 lines [] for i, r in enumerate(results[:3], 1): title r.get(title, ) snippet r.get(snippet, ) lines.append(f{i}. {title}: {snippet}) return \n.join(lines) except Exception as e: return f搜索出错: {str(e)}工具描述要写成模型能理解的格式。我通常用一个字典来管理工具键是工具名值是描述和函数引用。TOOLS { search: { description: 搜索工具输入一个搜索关键词字符串返回相关的网页摘要。当你需要获取实时信息或你不确定的知识时使用。, func: search } }工具描述里我特意强调了“当你需要获取实时信息或你不确定的知识时使用”这是给模型的行为指引。不加这句模型可能在任何情况下都调用搜索浪费轮次。4.3 构建 ReAct 提示词模板提示词模板是整个 ReAct 的核心。我把它拆成几个部分拼接方便维护。REACT_PROMPT 你是一个可以使用工具来解决问题的智能助手。 你可以使用以下工具 {tool_descriptions} 请严格按照以下格式进行推理和行动 Thought: 你的思考过程分析当前情况并决定下一步做什么。 Action: 要使用的工具名称必须是[{tool_names}]中的一个。 Action Input: 传给工具的输入参数。 Observation: 工具返回的结果。 ...Thought/Action/Action Input/Observation 可以重复多次 Thought: 我现在知道最终答案了。 Final Answer: 对用户问题的最终回答。 注意事项 1. 每次只能输出一个 Thought 和一个 Action等待 Observation 后再继续。 2. 如果不需要使用工具就能回答直接输出 Thought 和 Final Answer。 3. Action Input 必须是一个字符串不要输出 JSON 或其他格式。 4. 不要编造 ObservationObservation 由系统提供。 示例 用户问题今天北京的天气怎么样 Thought: 我需要查询北京今天的天气这需要实时信息应该使用搜索工具。 Action: search Action Input: 北京今天天气 Observation: 1. 北京天气预报: 今天晴气温 15-25 度微风。 Thought: 我已经获取到北京的天气信息可以回答了。 Final Answer: 北京今天晴天气温 15 到 25 度微风适合外出。 现在开始。 用户问题{question} 这个模板里有几个细节值得说。第一我明确写了“每次只能输出一个 Thought 和一个 Action”这是防止模型一次性输出多轮内容导致解析混乱。第二我强调了“不要编造 Observation”因为模型有时候会自己编一个 Observation 然后继续推理这会让整个循环失控。第三示例覆盖了完整的循环让模型有样学样。4.4 实现 ReAct 主循环主循环的逻辑是拼提示词、调模型、解析输出、执行工具、拼回上下文、判断终止。import re from openai import OpenAI client OpenAI(api_keyos.getenv(LLM_API_KEY), base_urlhttps://api.openai.com/v1) def parse_action(text: str): action_match re.search(rAction:\s*(.), text) input_match re.search(rAction Input:\s*(.), text) if action_match and input_match: return action_match.group(1).strip(), input_match.group(1).strip() return None, None def react_agent(question: str, max_turns: int 6) - str: tool_descriptions \n.join( f- {name}: {info[description]} for name, info in TOOLS.items() ) tool_names , .join(TOOLS.keys()) prompt REACT_PROMPT.format( tool_descriptionstool_descriptions, tool_namestool_names, questionquestion ) history prompt for turn in range(max_turns): response client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: history}], temperature0 ) output response.choices[0].message.content.strip() print(f--- 第 {turn 1} 轮 ---) print(output) if Final Answer: in output: return output.split(Final Answer:)[-1].strip() action, action_input parse_action(output) if not action or not action_input: history f\n{output}\nObservation: 格式错误请按照 Thought/Action/Action Input 的格式输出。\n continue if action not in TOOLS: history f\n{output}\nObservation: 工具 {action} 不存在可用工具为 [{tool_names}]。\n continue observation TOOLS[action][func](action_input) history f\n{output}\nObservation: {observation}\n return 达到最大轮次限制未能得出最终答案。这段代码里temperature0是为了让输出更稳定减少格式跑偏。max_turns6是兜底。每一轮把模型的输出和 Observation 拼回 history下一轮模型就能看到完整的推理轨迹。4.5 跑一个完整案例看效果用“对比北京和上海今天的天气”这个问题跑一下。第一轮模型输出 Thought 说要查北京天气Action 是 searchAction Input 是“北京今天天气”。系统执行搜索返回结果拼回上下文。第二轮模型看到北京的结果输出 Thought 说要查上海Action 是 searchAction Input 是“上海今天天气”。第三轮拿到上海结果模型输出 Final Answer 做对比。整个过程模型自主决定了搜两次、搜什么词、什么时候停。这就是 ReAct 的威力你不需要写死流程模型自己规划。实测下来这个最小实现能覆盖大部分信息查询类任务。如果任务涉及多步计算或者需要调用多个不同工具只需要在 TOOLS 里加工具在提示词里更新工具描述即可。5. 常见问题与排查技巧实录5.1 模型不按格式输出怎么办这是最常见的问题。模型可能输出一段自然语言没有 Thought 和 Action 前缀或者输出了 Thought 但忘了 Action或者把 Action Input 写成了 JSON。排查思路分三步。第一检查提示词里的格式说明是否足够明确示例是否覆盖了当前场景。第二检查 temperature 是否设得太高建议设成 0 或 0.1。第三检查模型本身的指令遵循能力有些小模型确实做不好格式遵循换一个更强的模型试试。如果格式问题依然频繁可以在解析层做容错。比如用正则同时匹配“Action:”和“动作:”或者匹配“Action Input:”和“输入:”。我还会在解析失败时把错误信息作为 Observation 拼回去让模型自己修正。通常模型看到“格式错误”的提示后下一轮会改过来。5.2 工具调用参数错误怎么处理模型可能传了工具不认识的参数或者参数格式不对。比如搜索工具期望一个字符串模型传了一个 JSON 对象。处理方式是在工具执行前做参数校验。如果参数不符合预期返回一个描述性的错误信息给模型。错误信息要具体比如“search 工具需要一个字符串参数你传的是 JSON 对象请重新调用”。模型看到具体错误后修正的概率很高。另外工具描述里要明确参数类型。我通常会在描述里写“输入应该是一个搜索关键词字符串不要包含引号或其他符号”。这种细节能显著降低参数错误率。5.3 循环停不下来怎么办模型可能反复调用同一个工具或者一直在 Thought 阶段打转不输出 Final Answer。这通常是因为模型没有从 Observation 里获取到有效信息或者任务本身超出了它的能力范围。兜底方案是设置最大轮次。超过轮次就强制终止返回当前已有的信息。同时可以在提示词里加一句“如果你已经获取到足够的信息请立即输出 Final Answer不要重复调用工具”。这句话能减少一部分无效循环。如果模型反复调用同一个工具且参数相同可以在工程层加一个去重检测。检测到重复调用时返回一个提示“你已经调用过这个工具并得到了结果请基于已有信息继续推理或给出最终答案”。5.4 常见问题速查表问题现象可能原因排查方向解决手段模型不输出 Thought/Action提示词格式说明不清检查提示词和示例强化格式说明加 Few-shot 示例Action Input 解析失败模型输出格式不规范检查模型输出原文加正则容错错误信息回传模型工具名不存在模型幻觉出工具名检查工具列表和描述错误信息里列出可用工具循环超过最大轮次模型未获取有效信息检查 Observation 质量优化工具返回加终止提示Observation 太长导致 token 爆工具返回未截断检查工具返回长度在工具层截断或摘要模型编造 Observation提示词未禁止检查提示词约束明确写“不要编造 Observation”5.5 我踩过的几个坑第一个坑是工具描述写得太简略。早期我只写了“search: 搜索”结果模型不知道该传什么参数经常传一个完整的句子或者带引号的词。后来把描述写详细包括参数格式和示例错误率明显下降。第二个坑是没控制 Observation 长度。有一次搜索工具返回了十条结果每条几百字拼进上下文后直接超了模型窗口报错。后来改成只取前三条每条截断到 200 字问题解决。第三个坑是终止条件只靠“Final Answer:”。有一次模型输出了“最终答案”而不是“Final Answer:”导致循环没终止多跑了两轮。后来在解析层同时匹配中英文的终止标记才彻底解决。第四个坑是没设最大轮次。有一次模型陷入了一个“搜索-没找到-再搜索”的死循环跑了十几轮token 消耗爆炸。加了 max_turns 之后最坏情况也可控了。6. ReAct 的扩展方向与工程化建议6.1 多工具协同与工具路由当工具数量增多时把所有工具描述都塞进提示词会导致提示词过长模型选择工具的准确率也会下降。这时候可以考虑工具路由先用一个轻量模型或者规则判断该用哪类工具再只把相关工具的描述拼进提示词。另一种做法是分层工具。把工具按领域分组第一层让模型选领域第二层在领域内选具体工具。这样每层的选择空间都变小准确率更高。6.2 与 RAG 的结合ReAct 和 RAG 是天然互补的。RAG 负责从知识库里检索相关内容ReAct 负责决定什么时候检索、检索什么、以及如何利用检索结果。可以把 RAG 的检索接口封装成一个工具让 ReAct 在需要知识库信息时调用。这种结合方式比传统的“先检索再生成”更灵活因为模型可以多轮检索逐步逼近答案。对于复杂问答场景效果提升明显。6.3 生产环境的稳定性保障生产环境里ReAct 智能体需要加监控和日志。每一轮的 Thought、Action、Observation 都要记录方便出问题时回溯。还要监控 token 消耗、轮次分布、工具调用成功率这些指标。另外建议加一个降级策略。如果 ReAct 循环失败可以降级到普通的 RAG 问答或者直接返回“暂时无法回答”。不要让用户看到一个报错页面。超时控制也很重要。每一轮 LLM 调用和工具调用都要设超时避免某个环节卡住导致整个请求挂起。我通常把单轮超时设成 30 秒总超时设成 90 秒。6.4 提示词的迭代与评测ReAct 的提示词不是写一次就完事的需要持续迭代。我的做法是建一个评测集包含几十个典型问题每次改完提示词就跑一遍看成功率、平均轮次、token 消耗的变化。评测集要覆盖不同类型的任务单工具调用、多工具调用、不需要工具直接回答、工具调用失败后恢复、复杂多步推理。只有覆盖全面才能发现提示词的短板。迭代时一次只改一个变量比如只改工具描述或者只改示例这样才能归因。同时改多个地方出了问题不知道是哪个改动导致的。6.5 关于模型选择的经验ReAct 对模型的指令遵循能力要求较高。实测下来同一个小模型在普通对话里表现不错但在 ReAct 循环里格式错误率明显偏高。如果预算允许建议用中等规模以上的模型跑 ReAct。另外不同模型对提示词的敏感度不同。有的模型对 Few-shot 示例依赖强有的模型对格式说明依赖强。换模型时提示词可能需要重新调优不能直接照搬。我在实际项目里的体会是ReAct 的上限取决于模型能力下限取决于工程兜底。模型再强没有格式校验、没有最大轮次、没有错误恢复循环照样会崩。反过来模型一般但工程做得扎实也能跑出可用的效果。所以别只盯着换模型先把工程层的稳定性做起来。最后分享一个小技巧在提示词里加一句“如果你不确定可以先搜索再回答”能显著降低模型在不确定时直接胡编的概率。这句话看起来简单但在实际使用中效果很好尤其是面对时效性强的查询时。