1. 先搞明白:ReAct 为什么能"想一步走一步"
我第一次给客户演示 AI Agent 时,对方问了一句特别扎心的话:"你这 Agent 跟 ChatGPT 有什么区别?不都是聊天吗?"当时我做的 Demo 确实就是个聊天框,模型再聪明,它也只会"说"不会"做"。
后来我把 ReAct 模式接进去,同一个 Demo 立刻就不一样了:用户说"帮我查一下杭州明天天气,顺便看看有没有合适的航班",Agent 会自己先拆解问题,然后调天气 API、查航班接口,拿到结果再继续推理,最后把结论汇总成一段完整答复。整个过程不是一次生成答案,而是一轮一轮地"想一步、做一步、看一步"。
先提醒一句,这里的 ReAct 是 Reasoning + Acting 的缩写,跟前端圈那个 React.js 完全不是一回事,别搞混。ReAct 最早是 2022 年底由 Princeton 和 Google 的研究者在论文ReAct: Synergizing Reasoning and Acting in Language Models里提出的。它要解决的根本问题很简单:大语言模型本身只擅长"想",不擅长"做"。模型没有途径去查数据库、调接口、操作文件,而现实世界的任务恰恰需要这些外部动作。ReAct 的做法,是让模型在推理过程中主动声明"我现在需要做什么动作",然后由外部系统真正执行这个动作,把结果再喂回给模型,让它继续推理。
如果你是刚接触 AI Agent 的开发者,ReAct 是你绕不开的第一个执行范式。它同时也是现在大量 Agent 框架(LangChain、LangGraph、AutoGPT 等)底层默认采用的核心模式。它不依赖特定 SDK,也不是某个框架的专利,本质上就是一套"思考-行动-观察"交替进行的循环结构,用任何模型、任何编程语言都能实现。
1.1 从"只会聊"到"能干活",差的不是模型而是循环
做个思想实验。你让一个对话模型完成这个任务:"告诉我北京和上海今天的实时温差是多少。"模型如果没见过实时天气数据,它只能靠训练时的记忆瞎编,因为它根本没有途径获取今天的天气。传统的 prompt engineering 解决不了这个问题,因为瓶颈不在提示词,而在"获取外部信息"这个动作本身。
ReAct 的思路,是把"获取天气"这个动作显式地交给模型去决策。模型可以输出一个结构化指令,比如调用get_weather(city="北京"),然后外部代码真正去请求天气 API,把返回结果作为"观察"(Observation)再交给模型。模型看到北京气温 25 度、上海 28 度,就能自然算出温差 3 度。注意:这里的"计算温差"是模型推理能力的一部分,但"拿到两个城市的实时温度"这个前提,是靠行动循环补上的。
所以 ReAct 解决的问题可以概括成一句话:让模型借助外部工具,把"推理"和"行动"交替串联起来,最终收敛到一个可验证的答案。它给 LLM 补上了"可操作性"这层能力。模型不再只是回答问题,而是能调度工具、能感知结果、能根据结果调整下一步。这也是为什么同样一个模型,套上 ReAct 循环之后,能从一个"聊天机器人"变成真正"下地干活"的智能体。
1.2 "推理"和"行动"交替发生的底层逻辑
ReAct 的底层逻辑可以画成一个循环:模型根据当前已知信息,推理出下一步该做什么(Thought),然后声明一个具体动作(Action),外部系统执行动作后返回观察结果(Observation),模型再基于新的 Observation 重复这个过程,直到它认为信息足够、可以直接输出最终答案为止。
这个循环听起来简单,但它对应着人类解决复杂问题的基本方式。你接到"帮我规划周末出行"这个任务时,不会一次性把整套方案背出来,而是先想"出行得知道天气吧",然后去查天气(行动);看到下雨再想"那得带伞,再看看室内备选方案"(推理),接着查博物馆开放时间……每一步的"想"都建立在上一步"做"的结果之上。ReAct 就是把这个过程显式地建模成模型可以执行的循环。
这里有个关键区别:ReAct 不是让模型一次性输出"完整思考过程+所有动作",而是要求它每轮只输出当前这一步的推理和动作,执行完拿到结果后再继续。这种"逐步推进"设计有实际的工程意义——它让每一步都建立在真实、最新的观察结果上,而不是模型凭空的预测。在我实测过的多步工具调用任务里,ReAct 的准确率明显高于"让模型一次性规划完再执行"的做法,主要原因就是它每一步都有外部结果兜底。
2. 拆开 ReAct 的核心循环:Thought / Action / Observation 三段式
把 ReAct 循环的每一轮拆开看,其实就是三个角色在接力:模型负责 Thought 和 Action,外部系统负责执行并产生 Observation,然后 Observation 又成为下一轮模型输入的上下文。
2.1 一次循环里的三个关键角色
在 ReAct 的 Prompt 中,通常会要求模型按固定格式输出三段内容:
- Thought(思考):模型解释它为什么决定这么做,这一步的推理过程。
- Action(行动):模型声明要调用的工具名称和参数,比如
search(query="北京到上海机票")。 - Observation(观察):外部系统在模型输出 Action 后执行得到的结果,会作为下一轮输入的一部分再喂回给模型。
这里要特别强调:Thought 和 Action 是模型生成的,Observation 不是模型生成的,而是外部工具返回的真实结果。很多初学者第一次看 ReAct 执行日志时,会把 Observation 当成模型的输出,实际上它来自工具层。把这个角色搞混,调试 Agent 时会走很多弯路。
一次完整的循环大概长这样:
模型输出 -> Thought: 用户需要查询北京的天气,我需要先调用天气接口。 Action: get_weather(city="北京") 外部系统执行 -> get_weather 返回 {"temperature": 25, "condition": "晴"} 拼接下一轮 -> Observation: 北京今天 25 度,晴天模型看到 Observation 后,再决定是继续行动(比如还需要查上海天气),还是直接回答用户。这就是"想一步做一步"的最小闭环。整个 Agent 就是把这个循环重复若干次,直到收敛。
2.2 一个可以直接抄的 Prompt 模板
ReAct 的实现不依赖任何框架,核心是两件事:Prompt 设计 + 输出解析。我贴一个最简版本的 system prompt 模板,可以直接拿去改:
你是一个能够调用外部工具的智能助手。请严格按照以下格式输出: Thought: 你对当前任务的分析和你决定采取行动的原因。 Action: 你要调用的工具,格式为 工具名(参数1=值1, 参数2=值2)。 (当且仅当需要调用工具时输出 Action) Observation: 这是工具返回的结果,你需要基于它继续推理。 (Observation 不需要你生成,它由系统提供) 当你已经获得足够信息,不再需要调用工具时,请输出: Thought: 我已获得足够信息。 Answer: 你对用户问题的最终回答。这是最经典的 ReAct 格式。实际使用时,我强烈建议在 Prompt 里附上完整的工具清单和每个工具的详细说明(参数类型、返回值含义、使用示例),因为模型对"有哪些工具、参数怎么填"的全部感知都来自这里。在我的项目里,工具说明写得好不好,直接决定了模型调用工具的准确率能差出两到三成。别嫌 Prompt 长,这里多花的心思一定会在效果上赚回来。
3. 完整走一遍真实执行链路:让 Agent 查机票并给出建议
光看模板不够直观。我拿一个自己在项目里反复跑过的场景——"查机票价格并给建议"——把完整链路走一遍。假设 Agent 有两个工具:search_flights(origin, destination, date)和search_hotels(city, check_in, check_out)。
3.1 第一轮:模型生成 Thought 和 Action
用户提问:"帮我看看下周五从北京到上海的机票,顺便看看有没有便宜的酒店。"
模型收到问题后,第一轮输出可能是:
Thought: 用户需要北京到上海的机票和上海酒店信息。我需要先查询机票。 Action: search_flights(origin="北京", destination="上海", date="2026-05-15")这里有个非常容易被忽略的细节:用户说的"下周五"是个相对时间,模型需要自己推算具体日期。如果模型不知道当前日期,它在这一步就会卡住或者编造日期。所以实际工程中,通常要把当前日期作为系统信息注入到 Prompt 里,让模型知道"今天是什么日子"。我最早做 Agent 时没注意这个,结果模型一本正经地查了一个过去日期的航班,返回"无结果"——排查了半天才发现是日期推算的问题。
外部系统(你的代码)解析出 Action 后,调用真实的航班查询接口,返回结果:
Observation: 北京->上海 下周五共有 3 个航班: 国航 CA1501 08:00-10:15 950元 东航 MU5108 10:30-12:40 780元 吉祥 HO1252 19:00-21:10 680元3.2 中间轮:工具返回 Observation,模型继续推理
模型拿到 Observation,继续生成下一轮:
Thought: 有三趟航班可选,价格从 680 到 950 元。 用户还问了便宜的酒店,我需要继续查询上海酒店。 Action: search_hotels(city="上海", check_in="2026-05-15", check_out="2026-05-16")外部系统再次执行,返回:
Observation: 上海人民广场附近经济型酒店:如家 350元/晚、汉庭 320元/晚。 静安寺附近:亚朵 650元/晚。注意看,模型每一轮的 Thought 都依赖上一轮的 Observation。如果没有这个循环,模型在第一轮就一次性输出"我估计机票几百块、酒店五六百",那全是编的。ReAct 的强项恰恰在于:每一步结论都有工具结果作支撑,这也是它能用来做真实业务决策的根本原因。
3.3 终止轮:没有 Action,只有 Final Answer
模型看到酒店信息后,觉得自己掌握的信息足够回答用户问题了,于是输出:
Thought: 我已有航班和酒店信息,可以给出建议。 Answer: 从北京到上海下周五有三趟航班,最便宜的是吉祥 HO1252(680元), 但起飞时间是晚上 19:00。如果希望白天到达,东航 MU5108 是 780 元。 酒店方面,汉庭 320 元/晚性价比最高。到这里,一轮任务就完整收敛了。这个例子看着简单,但任务复杂度上去之后——比如需要对比多个渠道、需要根据中间结果动态改变查询条件、需要在多个结果之间做权衡——ReAct 逐步推理的优势会非常明显。我见过不少团队花大力气把 Agent 的"思考"写死在代码里,结果每次需求一变就要改代码,而 ReAct 的灵活性恰恰省掉了这部分维护成本。
4. ReAct 的边界:它和 Chain、Plan-and-Execute 到底差在哪
ReAct 不是唯一的 Agent 执行模式。实际工程里我经常被问到:"什么时候用 ReAct,什么时候用 Chain,什么时候用 Plan-and-Execute?"这三个模式我都在生产环境跑过,给你梳理一下它们的本质区别和取舍逻辑。
4.1 三类模式的运行方式对比
Chain(链式调用)最简单:把任务拆成固定几个步骤,按顺序执行,每步输入来自上一步输出。比如"先总结用户评论,再翻译成英文,再提取要点",就是一个固定链。它的特点是步骤是预设的、不可变的,模型没有"决定下一步做什么"的自由度。优点是可预测、好调试,缺点是任务一变化就失效。
ReAct 的特点是模型每轮自主决定下一步动作,没有预设步骤数,循环直到模型认为可以回答为止。它比 Chain 灵活得多,但也带来了不可预测性——你不知道它会调用多少次工具,也不知道它会不会跑偏、会不会陷入死循环。
Plan-and-Execute(规划后执行)是两段式:模型先一次性制定完整计划(Plan),然后按计划逐步执行。比如先规划"第一步查机票、第二步查酒店、第三步比较价格",然后一步步执行。它和 ReAct 最大的区别在于:ReAct 是"边想边做",Plan-and-Execute 是"先想后做"。
用表格对比更直观:
| 模式 | 决策方式 | 灵活度 | 适用场景 | 主要风险 |
|---|---|---|---|---|
| Chain | 预设固定步骤 | 低 | 流程稳定、不允许跳步的任务 | 步骤一变就失效 |
| ReAct | 每轮自主决策 | 高 | 多步工具调用、结果影响后续决策 | 可能循环过多或跑偏 |
| Plan-and-Execute | 先整体规划再逐步执行 | 中 | 任务结构清晰、步骤间相互独立 | 计划可能与实际情况脱节 |
4.2 什么时候别用 ReAct
ReAct 虽然灵活,但某些场景下它真不是最优解。比如你做的 Agent 必须严格遵循动作序列(先鉴权、再查库、再脱敏、再返回),这种时候固定 Chain 更可靠,因为你根本不想让模型"自由发挥"去跳过鉴权步骤——那会直接变成安全事故。
另外,当任务是一锤子买卖、不需要外部工具时(比如纯文本改写、翻译),ReAct 的循环完全是多余的,直接单次生成更快、更省 token。我见过不少团队把 ReAct 用在所有场景上,结果延迟高企、成本翻倍,其实很多简单任务用普通 prompt 就能搞定。
还有一种情况是"先规划"更合适:当任务步骤非常多、且单次工具调用成本很高(比如外部 API 收费、耗时很久),Plan-and-Execute 可以先让模型制定一份完整计划,执行时每一步都可以审视"计划是否仍然合理",必要时候再重新规划。它跟 ReAct 没有绝对的优劣之分,核心是看你更看重"灵活性"还是"可控性"。我的经验是:步骤少于 5 步、环境变化快的任务用 ReAct;步骤多、执行成本高、需要稳定流程的任务用 Plan-and-Execute 或者 Chain。
5. 工程落地时的四个关键决策点
理论讲完,说点工程落地一定会碰到的问题。我在好几个项目里从零实现过 ReAct 循环,下面这几个决策点几乎每个项目都会碰到,而且踩坑成本都不低。
5.1 工具怎么定义,模型才不容易出错
工具定义直接决定模型能不能正确调用。我建议每个工具的说明至少包含五件事:
- 工具名称:简短、语义明确,比如
get_weather,别用do_thing_1这种。 - 参数说明:每个参数的名称、类型、取值范围、是否必填。
- 返回值说明:返回的 JSON 结构或文本格式,模型需要知道结果长什么样才能正确解析。
- 使用示例:至少一个完整的调用例子,模型会模仿示例的格式。
- 使用场景提示:什么时候该用这个工具、什么时候不该用。
实测下来,工具说明里给示例的效果远好于只给字段说明。大模型的 few-shot 能力很强,你给它一两个正确调用示例,它就会照着例子输出格式。工具数量超过五六个以后,我建议把工具说明按业务域分组,或者给每个工具加一句"当用户提到 XXX 时使用本工具"的场景提示,能明显减少模型选错工具的概率。
5.2 最大循环轮数设置多少合适
ReAct 循环如果没有限制,模型可能陷入无限循环——不停调用工具、不停看观察结果、就是不出最终答案。所以工程上必须设置最大轮数(max_iterations)。我的经验值是:常规任务设 5 到 8 轮,复杂任务放宽到 10 到 15 轮,超过 15 轮还收敛不了,基本可以判定是 Prompt 或工具定义有问题,不是轮数不够。
轮数触顶之后的处理方式也很有讲究。最简单的做法是强制要求模型基于已有观察输出 Answer;更好的做法是给模型一个兜底提示:"你已用完所有尝试次数,请基于已有信息尽可能回答,如果信息不足以回答,请明确告诉用户缺什么。"这个兜底逻辑能显著降低 Agent 最后"硬编一个答案"的幻觉率。我见过很多 Agent 在轮数耗尽后为了完成任务开始瞎编数据,加一句话提示就能规避大半。
5.3 上下文窗口不够了怎么办
ReAct 的每次循环都会把 Thought、Action、Observation 追加到历史里,多轮之后 Prompt 会变得非常长。上下文窗口有限,Observation 太详细时(比如搜索接口返回 100 条结果),几轮下来窗口就炸了。
我的做法是两级策略。第一级:在把 Observation 喂给模型前先做裁剪,只保留关键字段、限制返回条数、超长内容截断。第二级:对历史对话做摘要压缩,把早期轮次的 Thought/Observation 压缩成一行摘要,只保留最近几轮的完整内容。这个"早期摘要 + 最近完整"的组合策略,在我实际项目里支撑过 20 轮以上的复杂任务,上下文窗口始终没爆过。记住一个原则:模型只需要看到做决策所需的最小信息集,而不是所有原始数据。
5.4 解析模型输出时,格式不稳定的处理
ReAct 依赖结构化输出(Thought/Action/Observation),但模型输出格式天然不稳定。你要求它输出Action: search("北京"),它可能给你加一段解释,或者把 Action 写进 JSON 里,或者用 Markdown 代码块包起来。解析失败是 ReAct 实现中最常见的 bug 来源,没有之一。
我的经验是三层防护。第一层:在 Prompt 里明确要求"只输出规定格式,不要输出任何解释性文字",并同时给正例和反例。第二层:用正则做宽松匹配,比如从整段输出里提取Action:标记之后的内容,修剪掉多余字符。第三层:解析失败时不要直接报错,而是把"你的输出格式不符合要求,请重新按格式输出"作为 Observation 喂回给模型,给它一次纠错机会。这个"格式纠错循环"在我的实测里能把解析成功率从 80% 拉到 95% 以上,是所有调优手段里性价比最高的一个。
6. 实测中踩过的坑和调优心得
最后分享几个我在真实项目里踩过、且有代表性的大坑。这些基本都是文档里不会写的东西,属于"不自己跑一遍根本不知道"的经验。
6.1 模型"幻觉式调用"工具
模型有时会在信息不足时硬编一个工具调用,比如虚构一个不存在的 ID、或者把查询参数乱填。我遇到最典型的一次:模型在 Action 里调用了一个根本不存在的工具名称,完全是根据我工具清单里的描述"编"出来的。排查下来发现,是因为那个工具说明写得太模糊,模型误以为还存在另一个变体工具。
解决方法是加一道"工具校验层":外部系统解析出 Action 后,先检查工具名是否在白名单里、参数类型是否合法,不合规就返回一个固定 Observation:"工具不存在,可用工具为:XXX",让模型自己纠正。千万不要让不合规的调用直接抛异常崩掉整个循环,那样整个 Agent 会话就废了。这个校验层成本很低,但对稳定性的提升非常明显。
6.2 Observation 内容过载
工具返回大段 JSON 时,如果把原始 JSON 直接作为 Observation 喂给模型,既浪费 token,又会干扰模型推理——模型在里头翻半天找不到关键信息,还容易找错重点。
我的做法是在工具层做一次"结果摘要":把原始返回 JSON 转成一句话或结构化要点,只保留模型做决策所需的信息。比如查询结果有 50 条,就取前 5 条,并注明"共 50 条结果"。模型不需要看全部原始数据,它只需要拿到能支撑下一步决策的最小信息集。养成这个习惯之后,整个 Agent 的稳定性和 token 成本都会有肉眼可见的改善。
6.3 冷启动、超时与并发场景的建议
如果你打算把 ReAct Agent 做成线上服务,有几个容易被忽略的工程点。一是冷启动:首次请求时模型要加载系统 Prompt 和工具清单,如果多轮循环串行执行,延迟会很高。建议把工具清单和系统 Prompt 缓存起来,或者用支持 prompt caching 的模型服务,首轮响应能快不少。二是并发控制:ReAct 循环通常会调用外部 API,多路并发时要对下游接口做限流。我遇到过不止一次 Agent 本身没挂、下游 API 先被打爆的情况。三是超时设置:每轮循环都要有独立的超时控制,避免某个工具调用卡死,导致整个 Agent 会话长时间挂起。
最后说一点模型选型的体会。ReAct 模式对模型的推理能力要求不低,小参数模型经常出现 Action 格式错误、Thought 逻辑跳脱的问题。如果你受限于部署条件只能用较小的模型,建议把任务拆得更细,或者改用 Plan-and-Execute 模式减少循环深度。如果项目预算允许,用推理能力强的模型跑 ReAct,整体体验会好一个档次——尤其是复杂任务,模型一弱,整个循环就变成在错误边缘反复试探,调试成本反而更高。