算起来ReAct这个词从提出到现在已经不算新了但直到今天它依然是我给大模型应用做架构设计时最先考虑的思路。原因很简单它足够简单却又把大模型从会想推到了会做的位置上。这个范式真正解决的是纯语言模型只能纸上谈兵的问题——模型能写出一段滴水不漏的方案却没法查一眼实时天气也没法替你发一封邮件。很多刚接触智能体Agent概念的朋友一上来就去看复杂的规划算法、记忆机制结果被绕得云里雾里。其实把ReAct吃透就等于拿到了打开智能体大门的钥匙。这篇文章我会从一个实际动手者的角度把ReAct范式的核心机制、为什么它能成立、以及我自己在代码层面落地时的完整过程和踩坑记录一次讲清楚。无论你是刚入门大模型应用开发还是已经在做AI Agent相关的项目这篇内容都值得收藏备用。1. 为什么大模型需要一双手ReAct要解决的真实痛点1.1 纯语言模型的瓶颈推理不等于行动想要理解ReActReasoning Acting得先看大模型在没有手的时候是什么状态。我们平时用模型写文案、做总结、生成代码本质上是让它在一个封闭的语言空间里做概率预测。这个过程可以很强但它天然缺两个东西第一是实时信息。模型的知识有截止日期你问它今天某座城市的天气它只能靠训练数据里的统计规律去猜。第二是外部操作能力。模型无法真的去查询数据库、调用计算器、操作某个软件它只能告诉你应该怎么做但没法亲自动手。我用一个很直白的类比纯语言模型像一个没出过门的理论派顾问你知道很多道理但你拿不到现场数据也没法亲自去调整机器。遇到需要动手的活儿只能干瞪眼。这种能说不能做的局限在很多真实业务场景里是致命的。比如让模型做数据分析它可能把统计口径讲得头头是道但真要让它跑个脚本拉数据它做不到。1.2 CoT的启发与局限思维链Chain-of-ThoughtCoT的出现解决了一部分问题。它通过让模型在给出答案前先输出中间推理步骤显著提升复杂任务的准确率。这个思路的启示在于把思考过程显式化模型的表现会有质的提升。但CoT有个明显短板它的推理全程在脑内完成没有外部反馈。模型可以沿着一个错误方向越想越深最后一本正经地给出错误结论。就好比你让一个顾问闭着眼睛推演市场走势推演得再细腻方向错了就是错了。推理链条再长也没有纠错的锚点。所以单纯把推理说出来不够还需要让推理能够指导行动并且让行动的结果反过来修正推理。1.3 ReAct用一句话解题想一步做一步看结果再想下一步ReAct范式的核心逻辑用一句话就能概括让模型交替进行推理Reasoning和行动Acting在推理的指导下调用外部工具再把工具返回的结果作为新信息融入下一轮推理如此循环直到任务完成。这个推理-行动-观察-再推理的闭环就是ReAct的全部秘密。它本质上给大模型装上了一双可以触摸真实世界的手同时保留了一个可以不断纠错的大脑。我在实际开发里对这套逻辑感触很深。遇到需要事实核查、数学计算或多步骤调研的任务ReAct几乎是碾压纯Prompt的存在。比如让模型算一笔复利它直接写答案可能错得离谱但如果让它先写出计算公式调用计算器工具算出中间结果再基于结果继续推理每一步都有据可查正确率拉高一大截。2. ReAct的核心机制拆解Thought、Action、Observation三元组如何循环2.1 三元组的各自职责ReAct的每一轮循环里模型输出的内容被严格切分成三个字段Thought推理、Action行动、Observation观察。这三个词是整个范式的骨架。Thought是模型的思考过程它解释我为什么要这么做。比如模型可能会想用户问的是今天天气我需要先获取所在城市的实时天气数据因此我要调用天气查询工具。Action则是具体的行动指令告诉系统该调用哪个工具、传入什么参数。比如调用get_weather参数是北京。Observation是工具执行后返回的结果它被作为真实信息反馈给模型。注意Observation不是模型自己生成的而是系统从外部工具获取后拼接进上下文的。这个区别极其关键后面讲问题排查时我会展开说。从信息论的角度看Thought负责提出假设Action负责验证假设Observation负责带回验证结果。三者缺一不可少了Thought模型变成盲目调工具的机器人少了Observation模型又回到闭门造车的老路。2.2 为什么非要搞一个死板的格式你可能觉得让模型输出带固定字段的文本很别扭。但这份死板恰恰是ReAct能工程化落地的前提。原因有三点第一可解析性。程序需要稳定地从模型输出里提取行动指令。如果模型自由发挥五花八门的格式会让解析代码变成一场灾难。死板的格式让正则表达式或JSON解析变得极其可靠。第二可观测性。Thought字段记录了模型每一步的决策依据这对调试很有价值。模型为什么做了错误选择回溯Thought字段就能看清它的推理链路。第三上下文管理便捷。每一轮循环的Thought/Action/Observation都作为一个完整单元追加到对话历史里模型既能理解之前的操作又能基于最新的Observation调整下一步不会出现上下文错乱。你可以把这套格式想象成手术室里的器械盘——每把钳子、每把剪刀都有固定位置。主刀医生不需要思考钳子在哪只需要伸手就能拿到。格式的确定性换来的是系统的稳定和高效。2.3 工具注册表让模型知道自己有什么手模型必须提前知道有哪些工具可用、每个工具是干什么的、参数怎么填。这就是工具注册表Tool Registry的作用。我在实际项目里通常给每个工具准备四样东西工具名称、功能描述、参数说明、一个使用示例。比如一个时间查询工具它的描述可能是获取指定城市的当前时间参数为城市中文名。示例则展示完整的调用方式帮助模型理解该在什么场景下使用。工具描述的质量直接影响模型调用的准确度。描述太模糊模型会在不需要的时候乱调描述太啰嗦模型反而抓不住重点。我的经验是描述控制在两三句话以内把何时用、参数填什么说清楚就够了。3. 从零手写一个ReAct智能体代码级的完整实操3.1 先说整体架构LLM 工具集 循环调度器动手写代码之前先把架构想明白。一个最简的ReAct系统由三个部分组成大模型接口、工具集、循环调度器。大模型接口负责接收Prompt并返回模型输出工具集负责注册和实际执行外部操作循环调度器则是整个系统的心脏——它负责拼接Prompt、解析模型输出、调用工具、把Observation拼回上下文然后判断是继续循环还是终止。我选用Python来演示因为它的生态最适合快速原型开发。下面这段代码是一个不依赖任何重型框架的ReAct最小实现。你如果已经装了Python环境并且有一个可调用的大模型API接口就能直接跑起来。3.2 定义工具集以时间和计算器为例我先定义两个工具。一个用来获取当前时间一个用来做数学计算。选这两个工具是有讲究的时间查询覆盖了实时信息获取这个ReAct典型场景计算器则覆盖了外部工具辅助推理这个场景。import json from datetime import datetime def get_current_time(city: str 本地) - str: 获取当前时间。参数city仅用于兼容实际返回本机时间。 now datetime.now() return f{city}的当前时间是{now.strftime(%Y-%m-%d %H:%M:%S)} def calculator(expression: str) - str: 执行四则运算返回计算结果。 # 注意: eval有安全隐患, 生产环境请用ast.literal_eval或受限表达式解析器 return str(eval(expression)) TOOL_REGISTRY { get_current_time: { description: 获取当前本地时间无参数或参数为城市名。, function: get_current_time, }, calculator: { description: 计算数学表达式传入如 (35)*2 这样的字符串。, function: calculator, }, }这个工具注册表的结构很简单一个字典键是工具名值包含描述信息和可调用函数。实际项目里我会在这个基础上再增加参数schema的校验但最简版本里只要描述足够清晰模型基本能正确传参。3.3 编写Prompt模板把规矩写进系统提示词接下来是核心中的核心——Prompt模板。ReAct能不能跑对一半的功力都在Prompt里。模板的关键是把Thought、Action、Observation的格式规范、工具的用法、终止条件全讲清楚。Prompt模板里需要几家内容SYSTEM_PROMPT 你是一个严谨的AI助手可以通过调用工具来解决用户的问题。 你有以下工具可用严格按此格式调用 {tool_descriptions} 请严格遵循以下循环格式思考与行动 Thought: 你当前的思考过程。 Action: 要调用的工具名只能从上述工具中选择。 Action Input: 传给工具的参数必须是合法的JSON字符串。 Observation: 工具返回的结果由系统提供你不能自行编造。 ...(可重复多次 Thought/Action/Observation)... 当你认为信息足够时输出 Thought: 我现在可以回答用户的问题了。 Final Answer: 给用户的最终回答。 注意事项 1. 每一步都必须先输出Thought再决定是否调用Action。 2. 如果不需要调用工具直接输出Thought和Final Answer即可。 3. 工具的结果必须以Observation形式反馈给你你需要基于Observation继续推理。 tool_descriptions \n.join( f- {name}: {info[description]} for name, info in TOOL_REGISTRY.items() )这段Prompt的措辞我踩过好几版的坑。第一版写得太宽松模型经常不按格式来第二版又写得太复杂模型理解起来反而吃力。最终版本的关键是明确Action只能从工具中选择这句话它能大幅降低模型胡编工具名的概率。3.4 核心循环逻辑解析、执行、拼回、再推理循环调度器是整个系统里逻辑最重的部分。它的流程可以拆成四步import re def run_react(user_query: str, llm_func, max_iterations: int 5): messages [ {role: system, content: SYSTEM_PROMPT.format(tool_descriptionstool_descriptions)}, {role: user, content: user_query}, ] for step in range(max_iterations): # 第一步: 调用大模型 response llm_func(messages) messages.append({role: assistant, content: response}) # 第二步: 判断是否终止 if Final Answer: in response: return response.split(Final Answer:)[-1].strip() # 第三步: 解析Action和Action Input action_match re.search(rAction: (.*?)\n, response) action_input_match re.search(rAction Input: (.*?)\n, response) if action_match and action_input_match: action_name action_match.group(1).strip() action_input_str action_input_match.group(1).strip() if action_name not in TOOL_REGISTRY: observation f错误: 未知工具{action_name}请从工具列表中选择。 else: # 第四步: 执行工具并构造Observation try: if action_input_str: param json.loads(action_input_str) result TOOL_REGISTRY[action_name][function](**param) else: result TOOL_REGISTRY[action_name][function]() observation str(result) except Exception as e: observation f工具执行出错: {e} messages.append({role: user, content: fObservation: {observation}}) else: # 模型既没给Final Answer也没给出完整的Action指令需要引导 messages.append({role: user, content: 请按约定格式输出完整的Action和Action Input或直接给出Final Answer。}) return 已达到最大迭代次数未能得出最终答案。这段代码的循环逻辑很清晰。模型每次输出后系统先检查有没有Final Answer有就结束没有就尝试解析Action指令调用对应工具把执行结果包装成Observation追加回对话然后让模型基于Observation继续推理。3.5 实际运行演示一个混合任务走一遍全流程现在用一个任务来实际跑一遍看看这套循环如何工作。假设用户提问现在是北京时间几点然后帮我算一下此刻时间戳除以 7 的余数。这里有个相对复杂的推理过程模型并不知道此刻的时间戳它必须先调用工具获取时间再调用计算器做除法。如果没有ReAct循环模型只能瞎编一个答案。现在看看它如何一步步完成第一轮模型输出Thought: 用户需要先知道当前时间我可以调用get_current_time获取。 Action: get_current_time Action Input: {city: 北京}系统调用工具返回Observation北京的当前时间是2025-01-15 14:30:22。第二轮模型看到Observation后继续思考Thought: 当前时刻是2025年1月15日14:30:22。我需要先把这个时间转换为时间戳然后计算除以7的余数。我可以调用calculator工具。 Action: calculator Action Input: {expression: 1736922622 % 7}系统执行计算返回Observation2。第三轮模型拿到计算结果是2信心满满地输出Thought: 我已经得到时间戳除以7的余数为2。 Final Answer: 此刻北京时间的UNIX时间戳除以7的余数是2。这个过程最大的特点是每一步都有依据模型没有凭空捏造任何数据。时间从哪来、余数怎么算的全部可以通过回溯Thought和Action看到。3.6 关键参数配置与经验值跑通基础循环后有几个参数值得单独调max_iterations最大迭代轮数我一般设5到8。设太小复杂任务还没推理完就被截断设太大模型可能在某个错误方向上来回打转浪费大量token还拿不到答案。如果发现模型经常在最后一轮才给出Final Answer适当调大上限。temperature温度我倾向设低一些0.2到0.5之间。这个任务需要工具调用能力强太大的随机性会让模型输出格式不稳定。这里跟写文案不一样——创意任务可以高温但工具调用的任务稳定性优先。另一个容易被忽略的是Observation的截断。如果工具返回的内容非常长比如查询了一整张数据库表直接拼接进上下文会消耗大量token还可能让模型迷失重点。我的做法是对超过一定长度的Observation做摘要或截断只保留前若干字符和关键统计信息。4. 高频翻车场景实录ReAct落地最常见的5个坑4.1 模型自己伪造Observation这是ReAct系统最隐蔽、也最可怕的坑。模型在生成过程中有时会脑补一个工具结果写进来比如明明没有调用任何工具却输出了Observation: 北京的天气是晴天温度25度。如果不做约束模型会把幻觉信息当作真实结果继续推理整个系统就变成了带幻觉的百科全书。我处理这个问题的方式是双管齐下。第一在Prompt里明确写死Observation: 由系统提供你不能自行编造。第二在代码层面做防护——解析模型输出时直接丢弃其中可能冒充Observation的部分只从工具执行的真实结果中构造Observation。也就是说Observation永远以系统消息的身份注入而不是从模型输出里读取。4.2 模型绕来绕去就是不调用工具有时候模型面对一个明显需要工具的场景却一直在Thought里打转始终不输出Action。出现这个问题的原因通常是Prompt里没有强调工具的必要性或者模型不知道哪个工具能解决当前问题。我的对策是让Prompt里增加一句如果某个信息你不确定或没有实时数据请务必调用工具获取不要自行编造。另外工具描述的措辞也很关键——如果你的工具名字是内部代号比如某个服务的缩写模型很难猜到它该在什么时候用。用贴近自然语言的名字和描述会大大提升调用的准确率。4.3 工具参数解析失败模型传了个寂寞我遇到过很多次模型输出Action Input是乱写的JSON或者参数名跟工具定义对不上。比如计算器工具定义参数名为expression模型却传了个calc。这种问题的根源一是Prompt里没有给出参数示例二是工具说明不够明确。改进方法是工具描述里带上参数示例。比如我上面的计算器就写了传入如(35)*2这样的字符串这一句话能让模型少犯一半的错误。代码侧再兜底一层参数解析失败时返回一个固定的Observation比如工具执行出错: 参数无法解析请检查参数名和格式这样模型就知道该怎么修正。4.4 循环到最大轮数还是收不了尾这种情况我称之为鬼打墙模型一会儿调工具A一会儿调工具B来回折腾就是不给Final Answer。原因通常是任务本身过于开放或者模型在某个中间结果上犯了迷糊需要反复确认。除了调大max_iterations外我更推荐在Prompt里加一句如果你已经获取了足够信息请立即输出Final Answer不要重复调用工具。这句话不复杂但可以明显减少模型的无效循环。如果还是不行就检查是不是任务目标定义得不够清晰导致模型不知道什么时候算完成。4.5 性能与成本失控每一轮循环都在烧钱ReAct循环的本质是反复调用大模型每一轮都产生费用和延迟。一个3轮的循环成本就是单次调用的三倍还多。在项目初期我常常在月底看账单时肉疼。缓解思路有几个一是尽量精简工具描述和中间Observation的token长度二是对于简单的、一步能完成的任务可以在Prompt里约定不需要工具时直接给出Final Answer避免模型为了严谨而强行调工具三是尽量用小模型完成高频率的简单任务把流量集中在真正复杂的场景再让大模型上场。这一点在后面进阶部分细讲。5. 让ReAct更好用的进阶配置与调优心得5.1 给模型几个示范动作Few-shot示例的魔力最基础的ReAct实现用的是零样本Zero-shotPrompt模型完全靠系统提示词来理解任务。但如果你在Prompt末尾加上一两个具体的任务示范——展示完整的Thought/Action/Observation链路——模型的表现会立刻上一个台阶。我试着给系统Prompt加了一个用户问明天天气的完整示例模型后续调起工具的格式规范率几乎到了百分之百。这里的原理其实很简单大模型擅长模仿模式给一个高质量示范相当于告诉它这就是标准答案的样子比你说一百句请按格式输出都管用。5.2 让智能体能记住关键信息简单的记忆机制很多任务是有状态的比如用户先问帮我查一下A产品的价格接着又问那B产品呢这里的那指代的是前面提到的比价语境。如果没有记忆机制模型每次都是全新开始会丢掉前面对话里的关键信息。最简单的做法是把历史对话的摘要保存下来在每一轮循环开始前拼进Prompt。或者干脆保留完整的历史消息序列让模型在上下文里自己找线索。对于ReAct系统来说把每次Observation之后的推理结论压缩成一个短摘要是性价比很高的记忆方案。5.3 轻重搭配多模型协同调度在ReAct的每个循环里不是所有环节都需要最强的大模型。比如判断当前是否需要调用工具这种粗粒度的动作用一个轻量模型就够了真正需要深度推理和生成最终答复的时候再动用旗舰级模型。这个思路落地起来并不复杂循环调度器里加一个路由逻辑根据当前阶段选择不同模型。我在做某个内部工具时把频繁的工具结果理解环节切到轻量模型成本直接降了一半以上而用户体验几乎无损。当然这里的取舍要结合实际业务判断如果任务本身就极其复杂就不要为了省钱牺牲质量。5.4 安全与权限哪些工具不能随便让模型碰ReAct给了模型行动的能力也意味着给了它造成破坏的能力。我在项目里对工具做了分级只读类工具查询天气、搜索资料可以放心让模型随便调写操作类工具发邮件、改数据库必须加一层人工确认。这个人工确认可以是一个简单的回调函数在执行写操作前暂停循环把待执行的指令发给管理员审批。审批通过才继续执行不通过则把拒绝结果作为Observation返回给模型。这层保护在真实业务里不可或缺千万别因为追求自动化而省掉这个步骤。5.5 从工具调用到反思机制ReAct的下一个台阶ReAct跑顺之后我建议你尝试在这个闭环上加一层反思Reflection。思路是在模型给出Final Answer之前让它先审视自己以前几步的推理和行动找出可能的错误或遗漏然后决定是否需要补充行动。这个ReAct Reflection的组合能显著提升多步任务的完成质量。原理也不复杂模型在连续输出的过程中容易沿着惯性走偏反思步骤相当于强制让它停下来抽离一下重新审视整个路径的合理性。对过程复杂的任务比如制定一份可执行的调研计划这个增量收益尤其明显。写在最后的个人体会在把这些代码和方案跑到稳定之后我对ReAct的理解反而回归简单了它本质上模拟了人解决问题的本能——遇到不确定的事就动手查一下查到的结果用来修正下一步判断。大模型的推理能力被工具扩展之后才真正有机会成为能闭环工作的智能体。我个人在实际操作中的体会是ReAct最适合的场景是任务边界清晰、需要反复获取外部信息、且中间过程可被观察的任务而如果任务本身就是纯文本创作或者信息完全依赖模型内部知识硬套ReAct反而会拖慢速度、增加成本。最后再分享一个小技巧如果你在实验中发现模型频繁犯同一个错误不要急着改代码先把你给它的完整Prompt从头到尾读一遍。十次里有八次问题出在工具描述不清或者格式要求含混上。把这部分打磨清楚比调任何参数都见效快。ReAct的门槛其实不高迈过这一步之后后面的大厦能建多高就看你在实践里怎么组合和打磨了。
阅读完成 · 觉得有帮助?