先别急着写代码。做 AI Agent尤其是基于大语言模型做那种能“自己拿主意”的智能体你绕不开一个最基础也最核心的套路ReAct 模式。我最早接触这个概念的时候也觉得不就是一个“推理再行动”的循环循环吗但真正动手去搭、去调、去把 Agent 扔到真实场景里跑我才发现这个模式里藏着很多日常文档没写透的细节。今天这篇就把我的实际经验掰开揉碎讲清楚ReAct 是怎么让模型“想一步做一步”的以及如果你准备动手实现每一步应该怎么做、容易在哪儿翻车。1. 为什么是 ReAct从“会聊天”到“能办事”的跨越1.1 ReAct 到底解决什么问题先说个生活化的类比。假设你让一个刚入职的实习生去帮你办一件事“去楼下便利店买一瓶酱油顺便看看有没有蒜。”这个实习生有两种干法第一种他站在原地想很久——他要先回忆便利店在哪、怎么走、一瓶酱油大概多少钱、蒜是买一头还是一袋……想完一套流程之后他走出门结果到了店里发现今天超市装修关门了。第二种他出门之后先往便利店方向走到了发现关门了然后他立刻掏出手机搜一下附近还有没有别的超市搜完导航过去买完酱油再顺手看一眼蒜买了回来了。第一种叫“先想后做”第二种叫“边想边做”。ReAct 模式就是让 AI 用第二种方式干活。核心就八个字推理与行动交替进行。模型在每一步都会先观察当前状态再基于推理结论调用一个工具拿到工具返回的结果之后再做下一轮推理。整套流程绕着一个循环运转——没有预设的步数上限也没有固定的行动顺序全靠模型每一步的上下文自己决定“下一步干什么”。在技术上这个机制解决了大语言模型两个极其要命的短板第一模型本身的推理依赖真实世界信息。你训练数据里没有“楼下便利店今天是否装修”这个信息但通过调用工具去查询模型就能拿到实时数据然后基于新数据继续推理而不是闭着眼睛瞎猜。第二模型的输出需要验证不能只靠生成的“感觉”。纯让模型生成答案你无法判断它到底是对是错。但 ReAct 让模型每一步产出都能和真实结果对照调了工具就有返回值查询了数据库就有记录操作反馈就是验证信号。每一步都对最后的结果大概率就靠谱。1.2 ReAct、Plan-and-Execute 和普通 Prompt 的差异很多人把 Agent 实现方案理解成“给模型写个好 Prompt 就行”。实际上“好 Prompt”和 ReAct 的差距非常大。如果只是写 Prompt你是在一次调用中期待模型能完成所有步骤模型没有机会“中止计划”或者“根据意外情况调整策略”。而 ReAct 是循环式的每一步都是独立的模型调用所以每一步都能看到最新的观察结果来做新的推理。市面上还有一个常见的 Agent 架构叫 Plan-and-Execute就是“先做计划再执行计划”。这种方式的优点在于全局规划能力强适合目标任务很长、需要拆解为多个子步骤的场景。但它有个弱点执行过程中出现的意外事件往往不在最初的计划里模型需要重新规划时响应不够敏捷。ReAct 走的路线恰恰是反过来的不搞长期规划只专注于“当前这一步该干什么”。这一步需要看到什么信息就看什么信息、需要调什么工具就调什么工具绝不提前假设未来。所以 ReAct 的反应速度快、对突发事件适应能力强适合做那些需要即时反馈的任务比如问答、网页操作、客服对话。当然它的短板也明显遇到需要全局规划的多步任务容易陷入一种“走一步看一步”的状态可能会绕远路但胜在每一步都走得扎实。我自己在实际项目里喜欢这么取舍简单的工具调用场景直接用 ReAct如果需要做长周期多任务的复杂流水线就在 ReAct 的外层再套一个 Plan-and-Execute 的总控。很多 Agent 框架的源码里也是这种混合模式。1.3 Token 消耗一个绕不开的代价聊 ReAct 就绕不开 Token 消耗这个话题。每一步推理过程中框架都要给模型传一遍当前场景的任务描述、历史观测结果、还有工具定义列表。如果循环跑了 10 步最后一轮的上下文窗口里可能已经积累了大量重复的描述文字和中间结果。你算一笔账就很清楚了假设工具定义列表有 3000 个 Token每轮循环都要带着这 3000 Token 让模型作用10 轮循环就是 30000 Token 的了解释性基础消耗这个开销还没算模型自己生成的推理逻辑。如果是大项目里调了几十个工具这个开销更是很大。有些框架想了个取巧的办法比如把历史消息利用压缩机制做摘要、把工具定义折叠进一个独立系统提示词模块里避免重复加载。但实际用下来系统提示词部分影响了模型性能的情况还是存在的最稳妥的办法还是控制 Agent 循环步数和精简工具列表。我自己的经验是单个 Agent 的工具数量尽量控制在 10 个以内每一步的工具定义能简短就简短这比任何花哨的压缩策略都有效。2. 核心代码实现手写一个最小可用的 ReAct Agent概念聊清楚了拿代码来说话。为了保证轮子够“白”我直接用纯 Python 加 OpenAI 的接口来写不套 LangChain 之类的高层框架。底层逻辑搞明白了你后续去看 LangChain 源码也就不虚了。2.1 基础框架搭建循环、推理、工具调用的骨架ReAct 主循环其实就是三个动作的反复轮回推理Thought根据当前可用的上下文信息推理出当前状态、判断下一步做什么。行动Action调用某个工具带着参数。观察Observation拿到工具返回值作为新的上下文信息喂回给模型。我用最简洁的方式实现这个循环import json import openai SYSTEM_PROMPT You are a helpful AI assistant. You have access to the following tools: {tools} Answer the users question by following exactly this format: Thought: ... Action: tool_name Action Input: {{param: value}} (must be valid JSON) .strip() class ReActAgent: def __init__(self, tools: dict, modelgpt-4o-mini, max_steps5): self.tools tools # {tool_name: function} self.model model self.max_steps max_steps def run(self, user_input: str) - str: messages [{role: system, content: SYSTEM_PROMPT.format(toolslist(self.tools.keys()))}] messages.append({role: user, content: user_input}) for step in range(self.max_steps): response openai.chat.completions.create( modelself.model, messagesmessages, temperature0 ) output response.choices[0].message.content print(f--- Step {step1} ---\n{output}\n) if Action: in output and Action Input: in output: # 解析模型输出的 Action 行 action_line [l for l in output.split(\n) if l.startswith(Action:)][0] action_name action_line.replace(Action:, ).strip() input_line [l for l in output.split(\n) if l.startswith(Action Input:)][0] action_params json.loads(input_line.replace(Action Input:, ).strip()) # 执行工具调用 if action_name in self.tools: result self.tools[action_name](**action_params) else: result fError: unknown tool {action_name} # 把本轮推理和观察结果追加到消息 messages.append({role: assistant, content: output}) messages.append({role: user, content: fObservation: {result}}) else: # 没有 Action 说明模型已经给出最终答案 return output return Error: max steps reached without final answer.这段骨架就是整个 ReAct 的灵魂。你细看可以发现模型每一轮产出之后我把它自己的输出原文作为 assistant 消息追加到上下文再把工具返回结果作为一条 Observation 消息追加进去。下一轮模型调用时就能同时看到历史推理过程和最新的观察结果从而继续推理下一步。2.2 写一个带工具的 Cloud 场景示例计算器和信息查询为了演示我来定义两个常用的工具一个是简单的四则运算计算器另一个是模拟的天气查询接口。这两个工具在真实 Agent 项目里非常有代表性——一个代表“确定性计算”一个代表“外部数据获取”。import datetime import random def calculator(expression: str) - str: Evaluate a mathematical expression safely. # 生产环境建议用 ast.literal_eval 或 eval 时加白名单 try: return str(eval(expression)) except Exception as e: return fError: {e} def weather(city: str) - str: Get current weather for a city. Mock data for demo. base {city: city, temperature: random.randint(15, 30), condition: sunny} return f{base[city]}: {base[temperature]}°C, {base[condition]} (mock) agent ReActAgent( tools{calculator: calculator, weather: weather}, modelgpt-4o-mini ) result agent.run(昨天北京的天气是多少度比上海高还是低帮我算一下温差。) print(\nFinal answer:, result)你把这个跑起来就能看到模型会先调用 weather 函数拿北京和上海的天气然后再调用 calculator 做减法。它不是一次直接回答你的问题而是分了好几个步骤每步都依赖上一步的观察结果。整个过程有点像个查资料的实习生——查数据、记下来、再查下一个、再对比、最后总结。为什么我在代码里专门写了temperature0这是为了保证推理过程尽量稳定。Agent 场景最怕模型“灵光一闪”输出个格式不规范的 Action一旦解析不了就直接卡壳。把随机性压到最低输出格式更可控。实用主义一点说Agent 不需要创造力需要的是稳定性。2.3 核心机制拆解Thought-Action-Observation 循环的运作逻辑你可能会好奇模型输出的格式为什么要用“Thought: ... Action: ... Action Input: ...”直接让模型输出 JSON 不好吗其实这是个工程上的权衡。Thought 行是可选的Action 和 Action Input 才是硬性要求。但在实际调试中Thought 行有巨大的价值它把模型的推理过程暴露出来方便你判断模型是否跑偏。比如你看到 Thought 写的是“让我先猜一个答案”你就知道模型在偷懒。加上 Thought 行的另一个好处是给模型一个“中间思考缓冲区”——写完思考之后再去写行动客观上减少了模型直接胡说的概率。至于为什么要用“Action: 工具名”加“Action Input: JSON正文”这种结构化格式而不直接让模型输出 JSON —— 因为对模型来说在自然语言里夹一句指定格式的指令比让它输出一大段结构严谨的 JSON 容易得多。很多大厂的 Agent 框架用的是 JSON function call 原生的方式但那是因为人家对输出解析有整套兜底机制。自研的话用文本格式中间表示更稳。这段循环里我特意没有加退出条件靠的是max_steps兜底。真实项目里还有个常见做法如果模型回复没有 Action 就直接当作最终答案但如果模型既没给 Action 又没有明确答案呢也必须兜底返回错误不能让循环空转。3. 推理框架与优化让 Agent 走得更稳更远3.1 思维链Chain-of-Thought与 ReAct 的配合很多人问ReAct 和思维链CoT是什么关系可以理解成CoT 是 ReAct 的一个子集——ReAct 中的 Thought 步骤就是一次思维链推理只是这个推理的结论不是拿来直接回答用户而是用来引导下一步动作。这个配合在实操中特别重要。模型在某个领域知识不足时你直接让它调工具不如先引导它经过一番思考让它意识到“我需要数据”。比如用户问“请帮我算一下某种外币汇率换算成人民币是多少”如果模型直接编一个汇率就算那就是典型的幻觉。但有了 Thought 机制模型会倾向于先想汇率是动态的我需要实时查询汇率数据——然后它就会去调汇率接口。以前我看到有的框架会把两种 Prompt 揉在一起让模型在“给出最终答案之前先展示推理过程”。效果确实提升了但也带来一个麻烦模型常常为了“展示推理”而强行推理明明一步能完成的偏绕一个大圈子。这其实是 Prompt 设计时过于强调 CoT 导致的。我的处理方式是把推理步骤拆开但克制。系统提示词里明确告诉模型“只在需要更多外部信息时使用工具”这样把 CoT 的“强制感”降下来让模型的思考服务于真实行动需求。3.2 自我反思Agent 出错后怎么自我纠正这是 ReAct 模式里容易被忽略却极其好用的扩展点。核心思路是当工具执行返回了异常结果模型不应该直接把这个异常结果当成最终指标而是应该重新推理想想是不是参数传错了、或者工具换一个传参方式。例如场景用户问某城市当前温度气象接口限流返回 429 错误。模型不应把“request failed: 429”直接念给用户听而是应该再想想——是不是可以查询备用接口、或者重试一次。这个反思能力其实不需要额外加什么复杂“反思步骤”只要在 Observation 里明确提醒“如果发生错误请考虑重新调用或换参数”模型自然就会倾向于自救。当然更工程化的做法是在观察结果字符串里加个前缀比如Observation (error):给模型一个语义暗示——这一步出错了请推理如何恢复。3.3 结构化输出的约定与脏数据防御我在最开始做 ReAct Agent 的时候踩过最大的坑就是模型的 Action Input 经常解析不出来。有的是把 JSON 写成了多行有的是把布尔值写成了字符串 True 而不是布尔真值还有的是把 Action 和 Thought 写在了一行里。这里分享我经过无数次失败后总结的容错代码模板import re import json def parse_action(output: str): # 用正则提取 Action 和 Action Input 块 action_match re.search(rAction:\s*(.), output) input_match re.search(rAction Input:\s*(\{.*\}), output, re.DOTALL) if not action_match or not input_match: return None, None action_name action_match.group(1).strip() raw_input input_match.group(1).strip() # 容错处理把单引号替换为双引号把 True/False/None 规范化 raw_input raw_input.replace(, ) raw_input re.sub(r\bTrue\b, true, raw_input) raw_input re.sub(r\bFalse\b, false, raw_input) raw_input re.sub(r\bNone\b, null, raw_input) try: params json.loads(raw_input) except json.JSONDecodeError: return action_name, None return action_name, params这个看起来很简单但实际帮我把 Agent 的成功率提升了不少。因为模型很多时候生成的 JSON 不是严格的 JSON——它可能用单引号、尾部多了一个逗号。正则加规范化替换本质上就是在面对大模型的不完美输出时做一层“防弹衣”。另一个实用建议工具的参数最好设计成“少而简单”。如果工具的入参很多模型传错参数的概率会成倍上升。这和写代码时的“函数参数越少调用方越不容易传错”是一个道理。3.4 步数限制和死循环的防御ReAct 即便思路再清晰模型也偶发陷入死循环。最常见的情况模型反复调用同一个工具且每次的参数都相同但期望得到不同结果——比如反复查天气接口期望传给 calculator 之前“多等一会儿温度自己变”。这种死循环会白白浪费大量 Token还会拖慢整体响应时间。防御手段有这么几个步数上限这个必设而且建议设置得不要太大具体任务一般 5-8 步足够。重复动作检测维护一个历史动作列表如果模型连续 3 次调用同一个工具且参数相同就直接打断返回默认兜底。提示词明确“不要重复调用”把我这句原样放进系统提示词里——“如果某工具已调用过且结果未改变不要再重复调用。考虑改变策略或直接回答。”这三板斧在实践里比我想象中有效。很多时候模型陷入循环不是因为它蠢而是因为它的上下文窗口里没有“我已经试过这个参数”的显式意识。只要我在 Observation 里补一句“你之前已经调用过此工具并得到相同结果”它通常立刻就会换个策略。4. 常见问题与深度调优实录那些文档没告诉你的坑4.1 模型“不会推理”怎么办——Prompt 微调的核心技巧不少入门者会问我“老周我的 Agent 总是乱来不按我的指令走是不是模型不行”先别急着换模型。九成的情况下是系统提示词写得不够清楚。ReAct 对 Prompt 的依赖远高于普通对话场景。你写提示词时最核心的就是把“你是一个能调用工具的 Agent”这个身份讲透同时把输出格式的约束讲明白。我的标准做法是在系统提示词里加一段“自带示例输出”的三轮演示。Example 1: User: 北京天气 Assistant: Thought: 用户需要我获取北京的天气数据我需要调用天气工具。 Action: weather Action Input: {city: 北京} Observation: 北京: 25°C, sunny Assistant: 北京当前25°C晴朗。这段示例起到了“对齐格式”的作用模型会照猫画虎。我之前测试过没有示例输出的 Agent 成功率大约在六成左右加了示例之后能拉到九成以上。另外有个容易被忽略的点不要把“工具描述”和“任务指令”混在一块写。工具的 description 字段是给模型了解工具适合干什么的任务指令是告诉模型怎么干活。两者作用不同写混了模型就会混乱。4.2 工具调用的并发与失败重试策略真实场景里 Agent 往往要同时跑多轮工具调用。比如一个电商客服 Agent它可能同时查询订单、查库存、查物流。如果串行调用光等待时间就累积到用户不可接受。比较好的方式是模型在一次 Action 里允许同时输出多个工具调用请求但这对 ReAct 的基础循环是个改造。标准 ReAct 一次只产生一个 Action没有并发概念。我的经验是除非你的场景必须极速响应否则最好不要动这个设计——并发多工具调用会让模型的观察信息爆炸上下文变得混乱反而降低任务完成率。如果你确实需要可以按“串行为主局部小并发”的思路实践比如在模型决定要查询两个城市天气后把这两个查询放到同一个工具函数里内部并发执行对外仍公开成一个调用接口。这样既保住了 ReAct 的简洁性又降低了整体延迟。失败重试方面我给工具封装时都设置了一个“轻量重试”机制内置次数为 1——即首次失败后等几百毫秒再试一次。二次失败时把错误原样返回给模型告诉模型“错误信息在这儿请你判断换一种方式”。这个策略兼顾了偶尔超时的临时问题和必错的死局。4.3 案例复盘一个数据查询 Agent 的完整调优过程前阵子给一个做运维数据分析的朋友改造内部工具查询 Agent场景是用户说话查询服务器指标Agent 判断要调哪些监控工具去取数再计算最终结果。第一版效果惨不忍睹模型总是忘记调用工具直接凭印象编指标数据。这应该是我遇到的最典型的 ReAct 没生效的案例了。逐层排查后发现问题出在工具声明上。我写的describe_tool()只写了工具名称、参数格式但没有告诉模型“不调工具你根本不可能知道这些数据”。模型的倾向性是尽量少调用工具能猜就猜。于是我在系统提示词里加了这么一句用户的问题涉及实时数据你无法从训练数据知道。必须调用监控查询工具才能获取。禁止编造数据。加上这一句问题立刻改善。这个经历给我的启发是ReAct Agent 中的提示词不光是交代任务更重要的是定义“什么情况下必须打破 NLP 直觉”。模型默认倾向是“让我用常识回答”你得断掉这个默认倾向。另外一个坑是工具返回的数据太啰嗦。监控工具返回的 JSON 动辄几百个字段把这些数据全都塞进 Observation模型的注意力就被稀释了后续推理质量直线下降。后来我在工具层就把输出清洗为关键字段的摘要同时保留关键数字Observation 变得更短更精炼Agent 的表现明显上一个台阶。4.4 遗留风险与经典回答质量问题速查我用表格整理一份我在实际项目中碰到频率最高的问题和解决方案方便你直接查现象可能原因解决方式模型不调用工具直接编数据提示词没强调“实时数据必须调工具”在系统提示词中加“禁止编造必须调工具”的强制条款输出无法解析模型输出格式不规范用正则JSON 规范化容错并在示例中明确输出格式调用死循环模型重复同一个动作限制步数上限加“重复驳回”机制中间结果太挤导致模型漂移Observation 信息超载工具层将返回结果清洗为最短的关键信息知道了多个数据但不会算总结模型缺乏操作数据的工具认知增加一个计算/汇总工具让模型调用而不是自己心算多工具协作总出错历史 Observation 太多太长对历史长 Observation 做摘要或限制上下文保留数量这六类问题我翻来覆去在每个项目里至少都遇到一次。你把这些解决方案直接内化到自己的 Agent 模板里可以少踩很多坑。5. 实操心得与未来扩展建议5.1 我的一些实操心得笔记拿我个人的项目管理经验来说ReAct Agent 最舒服的使用场景是“中等复杂度、高变动性任务”。比如问答客服、运维查询、信息聚合、文档整理。这些任务的特点是需要调用外部工具的次数不多但每一步都有极强的动态性。反过来如果一个任务的结构非常固定——比如“每天定时生成报表”你完全没必要用 ReAct直接写脚本就完了。Agent 的价值不在于“显得智能”而在于“灵活应对变化”。这个原则说起来简单但很多项目硬把结构化流程往 ReAct 上套结果反而引入了不必要的失败点。还有一点心得是关于英文模型的调用策略。通常我给 Agent 的 Prompt 是英文还是中文实测下来用英文写系统提示词模型的输出格式会更稳定一些因为大模型的训练语料英文占主导它对外文格式的遵循能力更强。但工具返回结果和用户输入保持中文无妨——模型在处理输入输出的语言切换上没什么障碍。这个经验不一定放之四海而皆准但值得一试。5.2 后续可以怎么玩ReAct 只是 Agent 最底层的“内功心法”它的升级方向其实挺多的。业界现在已经有很多种变体带记忆的 ReAct在循环里加入一个“长期记忆模块”记下前几轮 Task 的结论新任务来了先查记忆再决定行动。多 Agent 互相 ReAct多个 ReAct Agent 之间互相传递观察结果各司其职再汇总比如一个 Agent 负责查询数据一个 Agent 负责生成总结通过“观察-反馈”驱动协作。ReAct 反思循环每次生成最终答案之前加一个反思步骤尝试用不同方案解决同一问题选出最优结果。这对复杂推理题的准确率提升很大。我自己最近在尝试的方向是把 ReAct 循环中模型产出的 Json 结构化输出先交给一个校验器校验不通过就自动重新生成。等于在模型外又加了一层“优秀约束”效果挺惊喜的。但这套方案对校验器的设计能力要求很高不是三两天能搞定的先列为下一步的研究计划。无论从哪个方向扩展只要底层理解了“观察-推理-行动”的闭环运转方式你搭任何 Agent 架构都只是“脚手架”的问题。学会了内功招式怎么耍都有影子。