第六章 ReAct:从对话到行动¶
导读¶
LLM 本身只能"说话",不能"做事"——它能写出"今天北京下雨"这句话,但不查天气 API 它就不知道今天到底下不下雨。ReAct(Reasoning + Acting)是把 LLM 从"对话生成器"变成"行动主体"的最基本范式:让模型在每一步显式输出一段 Thought(推理)、一段 Action(工具调用)、再观察 Observation(结果),循环直到任务完成。本章从 LLM 的固有限讲起,讲清 ReAct 的论文起源、prompt 模板、伪代码、优缺点与演进方向,是后续 Function Calling、规划、记忆、MCP 各章的概念基础。读完本章你能用 50 行 Python 写出一个最小可跑的 ReAct Agent,并理解为什么"生产环境最后大多改用 Function Calling"。
6.1 LLM 的局限¶
要理解为什么需要 ReAct,先要看清 LLM 本身的边界。LLM 是一个"下一个 token 预测器"——它只能基于上下文续写文本,没有任何外部接口。这导致四个根本局限,每条都对应一类 Agent 必须解决的场景。
知识截止¶
模型的权重在训练完成那一刻就冻结了,之后世界发生的事它一律不知道。GPT-4 的训练截止是某个具体日期,你问"GPT-5 发布了吗"、"今天股价多少"、"上个月发布的RFC 是什么"——模型基于训练数据答不上来,要么硬编一个看起来像但其实错的答案,要么坦白说"我不知道"。RAG(第 5 章)给了一种"开卷考试"的解法,但 RAG 只是让模型查到一段文档再续写答案,仍属于"说话"的范畴——ReAct 让模型主动决定"我要查搜索引擎"、"我要查日历",把知识获取变成行动而不是被动注入。
不能精确数学¶
LLM 是续写 token,不是符号计算。问它"1234 乘 5678 等于多少",它可能续写出"7006652"(正确是 7006652)——巧合对了;但也可能续写出"7016652"或"7006552"——错了。越大的数越容易错,多步运算(开方、矩阵乘、概率求和)几乎必错。原因是模型把数字拆成 token 后用神经网络"猜"下一个数字 token,没有真正的算术逻辑。解法是让模型调一个 calculator 工具——ReAct 让模型输出 Action: calculator("1234 * 5678"),工具返回精确结果 7006652,模型再基于这个结果继续推理。
不能访问实时数据¶
LLM 部署后是一个无状态推理服务,不连任何外部数据源。问"今天北京 PM2.5 是多少"、"现在美元兑人民币汇率"、"我的快递到哪了"——这些都需要调实时 API(天气、汇率、物流)。模型本身没有这种接口,闭门造车会输出训练时见过的旧数据或纯编造。ReAct 的 Action 步骤可以调用 HTTP API、查数据库、查消息队列,把实时数据拉进上下文。
不能执行业务操作¶
最关键的一类:业务操作(下单、退款、转账、发邮件、改数据库)必须真实执行,不是生成一段"已下单成功"的话就算数。LLM 本身只能生成文本,生成"我已为你下单 3 本书,订单号 ABC123"和真去电商系统下单是两回事——前者是文本续写,后者要调下单 API、写库存表、扣账户余额。ReAct 的 Action 步骤可以执行这些副作用操作,让 LLM 真正影响世界。这是 Agent 与聊天机器人的本质区别:聊天机器人只是"模拟"对话,Agent 是"做事"。
局限的根源¶
四条局限的根源都是同一个:LLM 是文本生成器,没有"行动接口"。ReAct 的核心思路就是给 LLM 接上工具——让模型在每一步输出"我想调哪个工具、用什么参数",外部框架执行工具把结果塞回上下文,模型基于结果继续推理。本质上 ReAct 把 LLM 变成了一个"边想边做"的循环:思考 → 行动 → 观察 → 再思考 → 再行动 → 再观察……
为什么不用 Fine-tune 解决¶
工程师可能想:"这些局限能不能靠 fine-tune 解决?训练一个会算术的模型、训练一个会查实时数据的模型。"答案是不能。第一,算术 fine-tune 不靠谱:训练数据上学会的算术在分布外必败——模型见过 4 位数乘 4 位数,没见过 10 位数乘 10 位数,没见过的就错。第二,实时数据 fine-tune 没意义:实时数据每天变化,今天训了明天就过期,fine-tune 成本几十万到几百万美元一次,不可能每天训。第三,业务操作 fine-tune 救不了:业务系统的 API 会变(参数、鉴权、URL),fine-tune 学的调用方式会失效,而 ReAct 的工具 schema 改一下就行。所以工具调用必须靠运行时机制(ReAct / Function Calling),不能靠 fine-tune。
ReAct 解决了什么¶
ReAct 给了 LLM 一个"行动接口"——让模型不是只输出最终答案,而是输出"我打算做什么"的过程。模型每一步输出三个东西:Thought(推理:为什么调这个工具)、Action(具体调用:工具名 + 参数)、模型把工具结果作为 Observation 接回来,进入下一轮推理。这个循环让 LLM 从"单轮续写器"升级为"多步行动主体"——这就是 Agent 的最基本形态。
6.2 ReAct 范式¶
ReAct 来自 Yao 等人 2022 年的论文 "ReAct: Synergizing Reasoning and Acting in Language Models"(arXiv:2210.03629)。核心思想是把"推理"(reasoning)与"行动"(acting)交织起来——传统 LLM 推理是单向的 Chain-of-Thought(思维链,CoT),只生成 thought 不生成 action;ReAct 在 thought 之间插入 action,让模型能调工具拿真实数据再继续推理。
Thought → Action → Observation 循环¶
ReAct 的核心是一个三步循环:
- Thought:模型显式输出一段推理——"我面对什么子问题"、"我打算调哪个工具"、"为什么这样调"。这是模型"想出声"的过程,对应 Chain-of-Thought 的"想"。
- Action:模型输出一段结构化文本——工具名 + 参数,如
Search["北京 今天 天气"]或Calculator["1234 * 5678"]。这是模型"做"的过程。 - Observation:外部框架执行 Action,把工具返回的结果作为 Observation 塞回 prompt,进入下一轮。
循环直到模型输出 Finish[最终答案] 或达到步数上限。
ReAct 论文核心实验¶
ReAct 论文(arXiv:2210.03629)在 HotpotQA(多跳问答)、FEVER(事实核查)、ALFWorld(家庭场景文本游戏)、WebShop(购物网页模拟)四个基准上验证了范式有效性。关键结论:在 HotpotQA 上,ReAct 比纯 CoT 提升 4-5 个百分点;在 ALFWorld 上,ReAct 成功率从 CoT 的不到 30% 拉到 60%+。论文还发现一个重要现象:纯推理(CoT)容易幻觉、纯行动(无 thought)容易乱调——把两者结合的 ReAct 在可解释性与准确率上双优。
与 Chain-of-Thought 的区别¶
Chain-of-Thought(CoT,Wei et al., 2022)让模型输出"先...再...所以..."的推理链,但 CoT 是封闭的——模型只能基于自己权重里的知识推理,错一步全错。ReAct 在 CoT 基础上插入 Action——模型推理时遇到"我不知道的事"就主动调工具补全,相当于把 CoT 从"闭卷推演"升级为"开卷推演"。CoT 是 ReAct 的特例(Action 步数为 0 时退化为 CoT)。
与 Reflexion / Plan-and-Execute 的区别¶
ReAct 是"边想边做",每一步都重新推理——适合工具调用密集、每步决策依赖上一步结果的场景。但它没有"全局规划"——每一步推理都是局部最优。Reflexion(第 8 章)在 ReAct 失败后让模型反思再重试。Plan-and-Execute(第 8 章)先一次性制定完整计划再分步执行——适合"任务结构清晰、子步骤解耦"的场景。ReAct 是这三者中最简单、最基础的一个,是 Agent 教学的基线。
ReAct 与 Toolformer 的关系¶
Toolformer(Schick et al., 2023, arXiv:2302.04761)是 Meta 提出的"自训练调用工具"路线——让模型在预训练阶段就学会"在哪里插入工具调用"。ReAct 是 prompt 阶段的范式(不动模型权重,靠 prompt 模板让现成 LLM 输出 ReAct 格式);Toolformer 是 fine-tune 阶段的范式(改模型权重让模型自然学会调工具)。两者是互补的:现代 Function Calling 模型(GPT-4 / Claude / Llama 3)都是经过类似 Toolformer 的工具调用训练的,所以它们在做 ReAct 任务时比纯 prompt 引导的早期 GPT-3 稳定得多。
ReAct 的工程定义¶
工程上 ReAct 可以这样定义:一个让 LLM 在文本输出中显式标注 Thought / Action / Observation 的 prompt 模板 + 解析循环。模型按模板续写,外部框架解析 Action 行调工具,把结果作为 Observation 行追加进 prompt,再让模型续写下一轮 Thought。整个过程没有任何模型权重的修改——纯靠 prompt 工程 + 解析循环实现。这种"靠 prompt 实现 Agent"的思路在 GPT-3 时代(2022 年中)是革命性的:不 fine-tune 就能让 LLM 学会调工具。
6.3 ReAct prompt 模板¶
ReAct 的关键工程实现就是 prompt 模板。下面给出一个完整的、可跑的 ReAct prompt 模板示例(带 few-shot)。
完整模板示例¶
你是一个 ReAct Agent,可以调用以下工具:
工具列表:
- Search[query]: 用搜索引擎查 query,返回 top-3 网页摘要
- Calculator[expr]: 用 Python eval 计算 expr,返回数值结果
- Finish[answer]: 给出最终答案,结束任务
格式要求:
Question: 用户问题
Thought 1: 推理第一步,决定调什么工具
Action 1: 工具调用(如 Search["..."])
Observation 1: 工具返回结果
Thought 2: 基于上一步结果继续推理
Action 2: ...
Observation 2: ...
...
Thought N: 我已经知道答案
Action N: Finish["最终答案"]
示例 1:
Question: 美国第 16 任总统是谁,他去世时多少岁?
Thought 1: 我需要先查美国第 16 任总统是谁
Action 1: Search["美国第 16 任总统"]
Observation 1: 亚伯拉罕·林肯(Abraham Lincoln),美国第 16 任总统,1809 年生,1865 年去世。
Thought 2: 林肯 1809 生 1865 卒,去世时 1865-1809=56 岁,但要确认日期差
Action 2: Calculator["1865 - 1809"]
Observation 2: 56
Thought 3: 我现在知道答案了
Action 3: Finish["林肯,去世时 56 岁"]
示例 2:
Question: 北京 2024 年奥运会金牌数比 2008 年多多少?
Thought 1: 我需要查 2008 与 2024 北京奥运会金牌数。但 2024 奥运会在巴黎,不在北京——可能是用户问错。我应该确认一下
Action 1: Search["2024 奥运会举办城市"]
Observation 1: 2024 年夏季奥运会在巴黎举办,2024 冬奥在米兰-科尔蒂纳。北京举办的是 2008 夏奥与 2022 冬奥。
Thought 2: 用户问题可能有歧义。我按"北京 2008 夏奥 vs 2022 冬奥"理解,并说明
Action 2: Search["北京 2008 奥运金牌数"]
Observation 2: 中国代表团 2008 北京夏奥得 48 枚金牌(原 51 枚,3 枚因兴奋剂被取消)。
Thought 3: 接着查 2022 北京冬奥
Action 3: Search["北京 2022 冬奥金牌数"]
Observation 3: 中国 2022 北京冬奥得 9 枚金牌。
Thought 4: 2008 是夏奥 48 枚,2022 是冬奥 9 枚——不同赛事不可直接比。我应该说明这点并给用户一个澄清
Action 4: Finish["2008 夏奥 48 枚 vs 2022 冬奥 9 枚;不同赛事不可直接比较,请确认问题"]
现在请处理用户问题:
Question: {用户问题}
Few-shot 示例怎么写¶
ReAct 靠 few-shot 示例教模型"格式"。写 few-shot 要注意几个要点。
第一,示例覆盖典型场景:上面示例 1 演示"调两个工具串起来用"——查事实 + 算算术;示例 2 演示"模型识别问题歧义并主动澄清"——这是高级能力,靠 few-shot 教。第二,示例风格统一:Thought 用"我需要..."句式,Action 用工具名 + 参数格式,Finish 用明确陈述句。第三,示例不要太多:2-4 个高质量示例足够,太多 prompt 太长 token 贵且注意力分散。第四,示例工具集要与真实工具集一致:示例里用 Search、Calculator,实际工具集也要包含这些;如果实际工具集是 QueryDatabase、SendEmail,示例要相应替换。
prompt 模板的几个陷阱¶
ReAct prompt 模板有几个工程陷阱。第一,Action 格式要严格:示例里 Action 1: Search["..."] 用了引号包裹参数,模型会模仿——如果示例没引号,模型可能输出不带引号的参数,解析时容易出错。第二,Thought 要短:每个 Thought 控制在 1-3 句,太长浪费 token 且模型容易"想飘"——指导模型只说"为什么这样调"。第三,Observation 不让模型生成:Observation 行是外部框架追加的,模型不应该自己写——prompt 模板要在 Observation 后截断,不让模型续写。
与 Function Calling 模板的对比¶
ReAct 的 prompt 模板靠"自然语言示例 + 严格格式"教模型输出 Thought/Action;Function Calling 靠"JSON Schema + 模型训练时学过工具调用格式"让模型直接输出结构化 tool_calls。前者要写很长的 few-shot,后者写一个 JSON Schema 即可——后者更稳定、更省 token、也更工程化。但 ReAct 的 prompt 模板在教学上有不可替代的价值——它让工程师看清"Agent 循环到底是怎么运作的"。Function Calling 是 ReAct 的工业化产物,第七章展开。
6.4 ReAct 伪代码¶
ReAct 的核心循环很短——本质上就是一个 while 循环,每轮让 LLM 续写一步,解析 Action 调工具,把 Observation 追加回 prompt。下面给出伪代码。
def react_loop(question, tools, max_steps=10, llm=llm_call):
"""ReAct 主循环:解析 Thought/Action/Observation 直到 Finish 或步数耗尽"""
prompt = build_react_prompt(question, tools) # 含 few-shot 模板
history = "" # 已生成的 Thought/Action/Observation 文本
for step in range(1, max_steps + 1):
# 让 LLM 续写一步(生成 Thought + Action)
completion = llm(prompt + history + f"Thought {step}:")
thought_action = parse_thought_action(completion)
history += f"Thought {step}: {thought_action.thought}\n"
history += f"Action {step}: {thought_action.action_raw}\n"
# 解析 Action 行:工具名 + 参数
tool_name, tool_args = parse_action(thought_action.action_raw)
if tool_name == "Finish":
return tool_args["answer"] # 直接给出最终答案
if tool_name not in tools:
observation = f"错误:未知工具 '{tool_name}'"
else:
try:
observation = tools[tool_name](**tool_args)
except Exception as e:
observation = f"工具执行错误:{type(e).__name__}: {e}"
history += f"Observation {step}: {observation}\n"
return "达到最大步数,未能完成任务"
几个关键的工程细节¶
伪代码看着简单,工程实现时有几个细节决定生死。
第一,Action 解析要稳:模型可能输出 Action 1: Search[北京天气](没引号)、Action 1: Search("北京天气")(圆括号)、Action 1: search[北京天气](小写工具名)——解析器要能容忍这些变体,统一规整成 (tool_name, args)。常用做法是用正则 + 容错:先匹配 (\w+)\[(.*)\],再解析参数。
第二,Observation 截断要长:工具返回的 Observation 可能很长(搜索返回 3 段网页摘要几千 token),直接拼进 prompt 容易爆 context window。工程上要对 Observation 做截断或摘要——如"只保留 top-3 摘要的前 200 token"。
第三,步数上限:max_steps 必设,5-10 步是常见值。不设的话模型陷入循环(反复调同一工具同一参数)会烧光 budget。max_steps 达到后返回"达到最大步数",让上层处理(如让用户重试或 fallback 到人工)。
第四,错误处理:工具调用失败时,把错误信息作为 Observation 喂回模型——让模型"看见"自己调错了,下一步可能改参数或换工具。这比直接抛异常退出更鲁棒——模型经常能从错误信息里学会正确调法。
解析器实现的几种思路¶
ReAct 的解析器有几种实现思路。第一,正则解析:用 re.match 匹配 Thought (\d+): (.*) 和 Action (\d+): (\w+)\[(.*)\] 模式,简单稳定,但要求模型严格按格式输出。第二,LLM 解析:让另一个 LLM 调用解析"提取 Thought 与 Action",能容忍格式漂移但慢且贵。第三,结构化输出:用 LLM 的 JSON mode / structured output 让模型直接输出 {"thought": "...", "action": "Search", "args": {...}},省去解析——这其实就是 Function Calling 的雏形。
LangChain ReAct 实现¶
LangChain 的 AgentExecutor + ReActAgent 是事实上的 ReAct 工业实现。它把上面伪代码包装成 agent.invoke({"input": "..."}),并加了内存、回调、错误恢复、并发等工程化能力。但教学时建议先用伪代码手写一遍 ReAct,理解每一步再上 LangChain——直接用 LangChain 的 ReAct 会让人对底层机制一团雾水,遇到 bug 不会调。
ReAct 与 Tool 调用频率限制¶
ReAct 的工具调用频率必须做限制——否则模型循环调用同一工具会拖垮下游服务。第一,单次任务上限:单次任务对单工具调用次数 ≤ N(如 5 次),超过主动终止——避免模型循环。第二,全局速率限制:单 Agent 全局对单工具调用 ≤ K 次/分钟(如 30 次),避免拖垮下游 API rate limit。第三,重复检测:连续 3 步 Action 完全相同主动终止——明显的循环行为。第四,预算监控:单任务累计 token 超 X 主动终止——防止烧钱。第五,人工干预 hook:高风险工具(写数据库 / 转账)触发人工审批 hook,Agent 等待人工确认再执行。这些限制是生产 ReAct 必备——没有限制的 ReAct 在边缘场景会失控。
6.5 ReAct 优缺点¶
ReAct 不是银弹,它有明显的优缺点。工程师选 ReAct 之前要看清它的边界。
优点:可解释、可调试、灵活¶
ReAct 最大的优点是**可解释**——Thought 是显式输出的,工程师看 trace 就知道模型每一步在想什么、为什么调这个工具。这对比 Function Calling 的"黑盒 tool_calls"是巨大优势:Function Calling 输出的是结构化 JSON,模型的"推理"被压缩在参数选择里,出错了不容易看清模型当时是怎么想的。ReAct 的 Thought 是完整文本,调试时一目了然。
第二,可调试:Thought/Action/Observation 都是文本行,可以直接打印、grep、搜索——出错了能定位是哪一步 Thought 不对(推理错)、还是 Action 不对(工具调错)、还是 Observation 不对(工具本身有问题)。这种"逐行可追溯"是 ReAct 在研究与教学场景的优势。
第三,灵活:ReAct 不依赖模型有没有经过工具调用训练——任何能续写文本的 LLM 都能跑 ReAct(GPT-2 / Llama 1 / 开源小模型都行)。Function Calling 必须用支持结构化输出的模型(GPT-4 / Claude / Llama 3+),但 ReAct 用 prompt 教任何模型都能跑。这是早期开源社区大量 ReAct 实现的原因。
缺点:慢、贵、易循环¶
ReAct 的缺点也很明显。第一,慢且贵:每一步都是一次完整 LLM 调用,5 步任务 = 5 次 LLM forward,延迟是单轮调用的 5 倍以上,成本也是 5 倍。Function Calling 可以一次调用并行调多个工具(如同时查天气 + 查日历 + 查股价),ReAct 默认是串行的——慢。
第二,Thought 占 token:ReAct 每步输出 Thought 文本("我需要先查..."),5 步任务的 Thought 加起来可能 500-1000 token,输入输出都计费——比 Function Calling 只输出 JSON tool_calls 贵 30-50%。
第三,易循环:ReAct 模型经常陷入"调同一工具同一参数"的死循环——尤其用未经过 ReAct 训练的开源小模型时。Function Calling 模型经过专门训练,循环概率低得多。
第四,解析脆弱:ReAct 靠自然语言格式约定,模型偶尔输出格式漂移(多一个换行、引号变全角、参数不加引号),解析器一崩整个 Agent 就停。Function Calling 是结构化输出,schema 校验严格,不会出这种问题。
ReAct vs 单轮 LLM 对比¶
下面用一张图直观对比单轮 LLM 与 ReAct 的差异。
单轮 LLM:user → LLM → answer,3 个节点,延迟低、成本低,但只能基于训练知识答——遇到实时/算术/业务操作场景就抓瞎。ReAct:user → LLM ↔ tool loop → answer,链路长,延迟 3-10 倍、成本 3-10 倍,但能调真实工具拿真实数据。工程上要按场景选——简单 QA 用单轮 LLM(如查模型知道的常识),复杂任务用 ReAct(如查天气 + 算算术 + 多步推理)。
何时仍该用 ReAct¶
虽然生产环境大多改用 Function Calling,但有几个场景仍该用 ReAct。
第一,教学与原型:ReAct 是 Agent 概念的最佳教学载体——它的 Thought/Action/Observation 显式可见,新手能看清 Agent 循环。原型阶段用 ReAct 验证业务逻辑再迁到 Function Calling 是常见路径。
第二,模型不支持 Function Calling:用早期开源模型(如 Llama 1、未微调的 7B 模型)时,它们没经过工具调用训练,Function Calling 效果差——这时候 ReAct prompt 引导比硬塞 JSON Schema 更有效。
第三,需要高可解释性场景:金融、医疗、法律场景对"模型为什么这样决策"有合规要求,ReAct 的 Thought 是完整推理文本,比 Function Calling 的 JSON 参数更容易审计。
第四,研究实验:研究新 Agent 范式(如反思、规划、自演进)时,ReAct 的开放文本格式更灵活——可以在 Thought 里加新字段(如"Confidence"、"AlternativePlan"),而 Function Calling 的 schema 改起来涉及模型重训。
何时绝对不该用 ReAct¶
生产高并发场景不该用 ReAct——延迟 3-10 倍意味着单台机器 QPS 降 3-10 倍,成本翻几倍。Function Calling 是工业标准。预算极敏感场景也不该用——Thought 的 token 浪费在百万 QPS 下是显著成本。需要严格结构化输出场景(如要直接把工具调用结果给下游业务系统)也不该用——ReAct 的自然语言解析总有不稳定风险。
6.6 经典论文与演进¶
ReAct 是 Agent 范式的起点,后续演进有清晰脉络。
ReAct (2022.10, arXiv:2210.03629)¶
Yao et al. 的 ReAct 论文是 Agent 范式的奠基之作。论文在 HotpotQA / FEVER / ALFWorld / WebShop 四个基准上验证了"思考 + 行动"交织优于纯思考(CoT)或纯行动(无 thought)。ReAct 论文的关键贡献是把"行动"引入 LLM 推理——之前的 CoT 只有 thought 没有 action,模型只能基于权重知识推理;ReAct 让模型在推理中插入工具调用,把推理从"闭卷"变"开卷"。
Toolformer (2023.2, arXiv:2302.04761)¶
Toolformer 是 Meta 提出的"自训练工具调用"路线——让模型在预训练阶段就学会"在哪里插入工具调用"。Toolformer 用一个标注流程:让 LLM 自己生成"这里该插入工具调用"的标注,过滤后做 fine-tune。结果是模型自然学会在合适的位置输出工具调用——不需要 ReAct 那种显式 prompt 模板。Toolformer 是现代 Function Calling 模型的技术前身:GPT-4 / Claude / Llama 3 的工具调用能力都是经过类似 Toolformer 的训练流程学到的。
HuggingGPT (2023.3, arXiv:2303.17580)¶
HuggingGPT(Shen et al.)把 ReAct 思路扩展到"调度专家模型"——让 LLM 作为控制器,根据任务选择 HuggingFace 上的专家模型(图像分类、目标检测、TTS、ASR 等)调度执行。用户问"描述这张图里的人在做什么"——HuggingGPT 让 LLM 输出 Action: 调用 BLIP 模型做图像描述,再调 ASR 模型转写视频音轨,多步串起来给最终答案。HuggingGPT 是 ReAct 的"多模态扩展"——Action 不只调简单工具,还能调整个 LLM/专家模型。它的思想影响了后续的多 Agent 协作(第 11 章)。
现代 Function Calling¶
2023 年起,OpenAI / Anthropic / Google / Meta 相继在自家模型里加入原生 Function Calling 支持——模型经过训练直接输出结构化 JSON tool_calls,无需 ReAct prompt 模板。Function Calling 是 ReAct 的工业化继承者:
- 保留:Thought + Action + Observation 循环的核心思想。
- 改造:Action 从自然语言文本变成结构化 JSON;Thought 不再显式输出(部分模型支持 reasoning 字段)。
- 优化:稳定性提升(schema 校验)、token 成本下降(不输出 Thought)、并行调用支持、流式输出支持。
第七章会详细展开 Function Calling。本章先建立"ReAct 是 Agent 循环的最基础形态"这一心智模型——后续章节所有更高级的范式(Function Calling、规划、反思、记忆、MCP)都是 ReAct 的变种或扩展。
ReAct 在生产中的现状¶
2024-2025 年的生产 Agent 大多用 Function Calling,纯 ReAct 在生产中已少见。但 ReAct 的思想没有过时——它仍然是 Agent 教学、原型、研究的最重要基线。LangChain、AutoGen、CrewAI 等框架的底层都支持 ReAct 模式(虽然默认推荐 Function Calling)。理解 ReAct 是理解整个 Agent 技术栈的前提。
一个常见误解:ReAct = Function Calling¶
工程师常把 ReAct 与 Function Calling 混淆。两者本质不同:
- ReAct 是范式:Thought + Action + Observation 循环,是一个"怎么组织 LLM 调用"的方法论。
- Function Calling 是接口:模型原生支持的 JSON Schema 工具调用,是一个"怎么让 LLM 输出结构化工具调用"的协议。
可以用 Function Calling 实现 ReAct 范式(让模型输出 thought + tool_calls + observation 循环),也可以用 ReAct prompt 模板教纯文本模型做 ReAct 范式。现代 Function Calling Agent 大多仍是 ReAct 范式(只是 Action 从文本变 JSON),但 ReAct 不等于 Function Calling——这点要在概念上分清。
ReAct 范式的现代表达¶
2024-2025 年 ReAct 在生产中已基本被 Function Calling 取代,但 ReAct 范式仍然在现代 Agent 框架里作为概念基石存在。LangGraph 把 Agent 状态机做成图——节点是 ReAct 步骤(Think 节点 / Act 节点 / Observe 节点),边是状态转换。AutoGen 把 ReAct 做成"对话式 Agent"——每个 ReAct 步骤是一次 assistant message。OpenAI Agents SDK 把 ReAct 做成"工具调用循环"——底层是 Function Calling 但顶层框架结构仍是 ReAct 思想。学习 ReAct 不只是历史知识——它是理解所有现代 Agent 框架内部机制的基础。看到 LangGraph 的图节点、AutoGen 的对话循环、OpenAI Swarm 的 handoff,背后都有 ReAct 范式的影子。
代码片段:最小可跑的 ReAct Agent¶
下面这段代码用 OpenAI Python SDK 实现一个最小可跑的 ReAct Agent(约 35 行),完整演示 Thought/Action/Observation 循环。
import ast
import re
from openai import OpenAI
client = OpenAI()
TOOLS = {
"Search": lambda q: f"模拟搜索结果:关于 '{q}' 的 top-3 网页摘要...",
"Calculator": lambda expr: str(ast.literal_eval(expr)),
"Finish": lambda ans: ans,
}
REACT_PROMPT = """你是一个 ReAct Agent。格式:
Thought N: 推理
Action N: ToolName[参数]
Observation N: 工具返回
可用工具:Search[query], Calculator[expr], Finish[answer]
示例:Thought 1: 我需要查 X
Action 1: Search["X"]
Observation 1: ...
现在处理:
Question: {question}
"""
def react_agent(question, max_steps=8):
history = ""
for step in range(1, max_steps + 1):
prompt = REACT_PROMPT.format(question=question) + history + f"Thought {step}:"
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
stop=[f"Observation {step}:"],
)
text = resp.choices[0].message.content
thought_match = re.search(r"Thought \d+:\s*(.+?)(?=\nAction)", text, re.S)
action_match = re.search(r"Action \d+:\s*(\w+)\[(.*)\]", text, re.S)
if not action_match:
return f"解析失败:{text}"
thought, tool, arg_raw = (thought_match.group(1).strip() if thought_match else "",
action_match.group(1), action_match.group(2))
arg = arg_raw.strip('"\'')
history += f"Thought {step}: {thought}\nAction {step}: {tool}[{arg_raw}]\n"
if tool == "Finish":
return arg
try:
obs = str(TOOLS.get(tool, lambda x: f"未知工具 {tool}")(arg))
except Exception as e:
obs = f"工具错误:{e}"
history += f"Observation {step}: {obs[:500]}\n" # 截断防 prompt 爆炸
return "达到最大步数"
print(react_agent("北京 2022 冬奥中国金牌数 vs 2008 夏奥金牌数"))
这段代码示范了 ReAct 的完整循环:prompt 模板 → LLM 调用 → 解析 Thought/Action → 调工具 → 拼接 Observation → 续写。生产系统还要加:并发、超时、回退、审计日志、人工 in the loop、工具调用频率限制。
工程实战要点¶
- ReAct 思考链 token 贵:每步 Thought 都要计费,5 步任务的 prompt 累加可能上万 token——生产环境用 Function Calling 省 30-50%。
- 工具 schema 要严:ReAct 靠自然语言解析,工具名/参数格式必须严格约定(如统一用
ToolName[quoted_arg]格式),解析器要能容错小写/全角引号/换行差异。 - 失败重试策略:工具调用失败时把错误信息作为 Observation 喂回模型——让模型"看见"自己调错了,下一步可能改参数或换工具;不要直接抛异常退出。
- thought 可观测:生产环境要把每步 Thought/Action/Observation 落审计日志,便于事后追溯与评测;这是 ReAct 相比 Function Calling 的核心优势。
- ReAct 是教学基线——生产用 Function Calling:ReAct 的稳定性、成本、并发能力都不如 Function Calling;新项目除非有强可解释性合规要求,否则直接上 Function Calling(第七章)。
小结¶
- LLM 有四条根本局限:知识截止、不能精确数学、不能访问实时数据、不能执行业务操作;ReAct 通过让模型调用工具解决后三条。
- ReAct = Thought(推理)+ Action(工具调用)+ Observation(结果观察)循环,来自 Yao et al. 2022 (arXiv:2210.03629)。
- ReAct prompt 模板靠 few-shot 示例教模型输出 Thought/Action/Observation 格式,外部框架解析 Action 调工具,把结果作为 Observation 追加。
- 伪代码核心是 while 循环 + 解析器 + 工具分发,max_steps 必设防死循环。
- 优点:可解释、可调试、灵活(任何 LLM 都能跑);缺点:慢、贵、Thought 占 token、易循环、解析脆弱。
- 演进:ReAct → Toolformer(自训练)→ HuggingGPT(多模态专家调度)→ 现代 Function Calling(结构化 + 训练)。
- ReAct 是 Agent 教学与研究的最重要基线;生产大多改用 Function Calling,但概念上 Function Calling 仍是 ReAct 范式。
练习题¶
- ReAct 的三个核心步骤是什么?
- ReAct 与 Function Calling 的根本差异?
- 为什么 ReAct 的 thought 既是优点也是缺点?
- ReAct 适合什么任务?不适合什么?
- ReAct 步数上限一般设多少?为什么?
- 列举 ReAct 的 3 个演进方向。