
做Agent开发一年多被问得最多的问题就是“模型怎么知道什么时候该调用工具工具返回之后它又怎么判断下一步”市面上的Agent框架看起来一个比一个复杂函数调用、多轮记忆、规划器、代码解释器一大堆概念但把包装拆掉之后大多数框架的内核还是同一个东西——ReAct。这个词来自“Reasoning Acting”核心就是让大模型在每一步交替输出推理和行动再把环境返回的观察结果接住形成“思考-行动-观察”的闭环。这篇文章我会把ReAct这套思维范式彻底拆开它解决什么问题、为什么能解决、运行机制长什么样、怎么从零手写一个最小实现以及跑起来之后最容易踩的坑。适合正在做LLM应用、想搞懂Agent底座的读者也适合准备从零搭一个带工具调用机器人的朋友。看完可以直接照着搭不用先学任何重型框架。1. 为什么需要“推理加行动”而不是只推理或只行动1.1 只推理的思维链卡在“闭卷考试”ChatGPT发布没多久大家就发现一个技巧让模型把思考过程一步步写出来复杂问题的正确率高不少。这就是思维链CoTChain of Thought。原理不复杂——大模型在连续生成的文本里每一步都能“看到”自己之前的推理相当于把工作记忆外置了算长一点的逻辑链不容易串味。但CoT有个硬伤它是彻底的“闭卷考试”。模型只能依赖训练时记住的参数知识对外部世界一无所知。你问一个时效性问题比如某个刚发生的事件、某份内部报表里的数字它只能凭印象瞎编。我在一次内部工具演示时问模型“本季度还剩多少预算”它一本正经输出了一串小数仔细一查根本对不上——因为它压根没有数据来源。这不是小事。真实业务里Agent要解决的大多数问题都需要实时数据查库存、算运费、看天气、读数据库。推理写得再漂亮如果“事实底座”是错的结果只会错得越来越自信。模型的一次幻觉可以通过校验发现但CoT是一长串推理链前面一步歪了后面每一步都在错误前提上继续推查错成本非常高。1.2 只行动不推理像没有导航的司机和CoT相对的另一种做法是Act让模型直接生成动作比如去调用搜索、调用API。Act的好处是能和真实世界交互但坏处也很明显——缺了中间推理模型根本不知道该调哪个工具、调完拿到结果怎么用。做个类比一个司机只知道踩油门和打方向盘但没有导航也没有路牌该在哪个路口转弯完全靠运气。你可能见过这种Agent的表现它反复查询同一个关键词搜出来的页面根本不用或者上一个搜索结果都已经证明当前方向不对了它还在原地打转。问题出在哪行为与目标之间没有建立联系。模型需要判断“我为什么要调这个工具”“这个结果说明什么”“距离最终答案还差什么”这些恰恰是推理步骤该干的事。纯Act等于把决策过程全部省略只保留动作本身在任务链短、工具简单的情况下勉强能用一旦涉及多步任务失败率直线上升。1.3 ReAct的核心洞察把推理写成看得见的决策中枢ReAct的解决思路很朴素把CoT的“思考链”和Act的“行动调用”拼在一起让模型交替输出Thought和Action系统把工具的返回结果作为Observation注入对话循环往复直到模型给出Final Answer。这套范式最早来自2022年的一篇论文作者在多跳问答和交互式决策任务上对比了标准提示、CoT、Act、ReAct四种方案结论很明确ReAct在需要外部知识的问答上能显著降低幻觉在需要多步决策的任务上成功率明显高于纯推理或纯行动。这个“交替出现”的设计不是拍脑袋它有认知科学的影子。人类面对复杂任务时不会凭空把所有决策都在脑子里做完而是边想边做、边做边看反馈再调整。把“推理”显式写到对话里相当于给模型搭了一个“认知脚手架”每一步的想法都落成文本模型可以随时回看、修正外部观察结果又不断给推理提供新材料。推理给行动提供方向和理由行动给推理提供事实依据二者互为补充这就是它比单独用任何一种都稳的根本原因。2. ReAct的循环机制Thought / Action / Observation 怎么配合2.1 三要素的职责边界ReAct的Prompt结构非常简单只有三要素Thought、Action、Observation。很多初学者把这三种文本混在一起结果解析全乱。先分清楚Thought模型的“内心独白”包括对当前问题的分析、上一步观察的解读、决定下一步做什么的理由。这个字段完全由模型生成是决策过程的文字化。Action模型选择要调用的工具以及传给它的参数。一般形式是工具名加参数比如search(关键词)。这个字段也由模型生成但格式必须严格受控是解析和执行的依据。Observation工具执行后返回的结果由系统注入不是模型写的。这是决定一个Agent是“真Agent”还是“套壳对话机器人”的分水岭。我常用的Prompt模板是下面这个结构模型被要求严格遵守格式每步先输出Thought再输出Action然后等待Observation返回不跨步输出。用户输入的部分我通常会加上一句话你是一个能调用外部工具的AI助手请严格按以下格式输出——思考过程写在Thought:后面需要执行的动作写在Action:后面每次只写一个动作动作返回结果会作为Observation:提供给你。当你已经可以回答问题在Final Answer:后面直接输出答案。这个“一次只写一个动作”的约束很重要。我见过不少模型在一步里连续输出两个Action结果解析器要么丢弃第二个要么把两个动作拼成一个字符串直接把工具调用搞挂。宁可多跑几轮也不要贪一步多动作。还有一点容易被忽略Thought和Action必须允许跨行。模型有时候会在Thought里写一两百字的分析然后换行输出Action。如果你用简单的正则按行匹配很容易把Action漏掉。我建议先把整段文本抓下来再分别解析而不是按行切分。2.2 一条完整轨迹长什么样举个例子用户问“某某公司最新财报里的毛利率是多少”。假设当前工具集只有两个search网页搜索和fetch打开并读取页面内容。一次完整的ReAct执行轨迹大概是这样的用户某某公司最新财报里的毛利率是多少Thought用户需要最新财报的毛利率数据。我需要先找到该公司财报所在页面。 Actionsearch(某某公司 最新财报) Observation返回三个结果第一个是财报发布页第二个是投资者关系页第三个是新闻稿。Thought第一个链接看起来最相关我需要打开它读取具体数据。 Actionfetch(第一个结果的链接) Observation页面内容包含“毛利率 36.2%”“营收同比增长 7%”等字段。Thought页面里的毛利率是36.2%且页面上写明这是最近一期财报符合用户对“最新”的要求。可以回答。 Final Answer某某公司最新财报毛利率为36.2%。这看起来像流水账但恰恰是这套范式最强的点每一步都可追溯。用户能看见模型为什么选择这个工具、为什么判断结果够用出错了也能指出是哪一步的问题。在实际产品里中间Thought和Action完全可以直接展示为“Agent运行日志”用户对结果的信任度比黑盒问答高很多。这里要强调一个实操细节Observation截断。工具返回结果经常是几千字的页面正文全塞进去会撑爆上下文。我的做法是只保留前800到2000个字符并在后面加一句提示“页面内容过长已截断”让模型知道信息不完整必要时可以再次调用fetch获取另一段内容。很多新手忽略这个结果一次fetch之后上下文直接爆了后面的推理质量断崖式下降。2.3 终止条件让Agent知道什么时候该停ReAct循环要能终止否则就是死循环。终止条件一般有两个维度模型主动认为信息足够输出Final Answer系统设置最大轮数比如最多10轮Thought-Action超过就强制停止并返回当前进展。模型主动终止靠Prompt里的一句约束“当你有足够信息回答问题直接输出Final Answer并停止不要再产生Action。”但模型对“足够信息”的判断可能不靠谱它可能搜完两个页面就自认为够了其实还漏了关键数据。所以实际部署时两个条件必须同时用Prompt约束告诉模型“可以停”最大轮数作为保险兜底。另外还有第三种兜底重复检测。模型连续几轮输出完全相同的Action大概率是卡死了。这时候直接截断循环把已有Observation汇总一下然后在下一轮提示“你已经重复该动作请换一个策略或者直接给出当前最有把握的答案”。最大轮数怎么定我的建议是根据任务步骤数反推。预期需要3步工具调用的任务最大轮数至少给到6到8轮留出修正的余量。太小会让模型在还没完成时被粗暴打断太大又会让失败任务白白消耗token。多步问答类任务我常用的起始值是8轮超过这个数先排查Prompt有没有歧义而不是盲目调高。3. 从零实现搭建自己的ReAct Agent3.1 工具集设计给Agent一张好用的“工具箱清单”工具是Agent的“手”工具描述是Agent“知道自己有这双手”的唯一途径。工具描述写得差后面所有环节都是白搭。有几个原则我一直在用名称要动词开头、含义明确。search、fetch、calculate_date比func1、do_thing这种强一万倍。模型对语义理解依赖名称和描述的文本向量名称越直白选错概率越低。描述要写清楚“什么时候用、输入格式是什么、返回什么”。一句话不够两三句为佳。例如“计算两个日期之间相差几天。输入为两个ISO格式日期用逗号分隔。返回一个整数。”参数要尽量枚举。能枚举的就枚举不要给模型自由发挥的空间。比如一个天气查询工具只接受城市名和日期你可以在描述里把支持的城市直接列举出来模型就不会编造一个不存在的城市名。我常用Python字典来定义工具结构大概是TOOLS { search: { description: 搜索互联网输入关键词返回相关结果列表, parameters: [keyword], handler: search_func }, fetch: { description: 读取指定URL的正文内容输入URL, parameters: [url], handler: fetch_func } }把工具定义和工具实现分离好处是后续做白名单校验、参数校验、权限控制都很方便。解析到Action之后先查TOOLS字典工具不在字典里直接拦截不要交给执行器。工具数量控制也很关键。ReAct的Prompt里如果塞20个工具每个工具几百字描述光工具说明就占了上下文一大块模型在长上下文里注意力容易分散选错工具的概率会上升。我的习惯是能用5个工具解决的场景绝不放10个。如果确实需要很多工具就把它们分组或者让模型先选工具组再选具体工具。少而精的工具表加上一条高质量示例比塞一堆工具定义的效果好得多。3.2 循环控制主逻辑管住Agent的每一步ReAct的核心理念不是某个框架而是循环控制。把下面的逻辑跑通就是一个可用的Agent骨架。我用一段最简Python代码来演示def run_react(task, tools, max_steps8): messages [ {role: system, content: build_system_prompt(tools)}, {role: user, content: task} ] for step in range(max_steps): response chat_model(messages, temperature0.2) text response.choices[0].message.content messages.append({role: assistant, content: text}) # 终止条件一模型主动给出最终答案 if Final Answer: in text: return extract_final(text) # 解析动作 action parse_action(text) if action is None: # 格式错误把错误信息作为观察返回让模型自己修正 messages.append({ role: user, content: 格式错误请重新输出格式为Action: 工具名(参数) }) continue # 执行工具把结果作为观察注入 result execute_tool(action, tools) messages.append({ role: user, content: fObservation: {truncate(result, 2000)} }) # 超过最大轮数返回最后一句里的答案或提示信息 return extract_final(messages[-1][content])这段循环看起来简单但它是一切复杂Agent的地基。后续加记忆、加工具拦截、加失败重试都是围绕这个循环做扩展。有两个设计细节值得留意第一Observation要以user角色注入而不是assistant。一是因为user/assistant角色在很多模型里对应不同的生成策略Observation作为user更接近“任务输入”的语义二是方便日志审计出问题时能快速区分哪句是模型自己编的、哪句是系统给的。第二工具执行要设置超时。工具调用是外部请求不是本地计算可能几秒也可能几十秒、甚至超时。超时后返回“工具超时请尝试其他方案”同样作为Observation注入。别让模型一直等一个可能永远不返回的请求。3.3 少样本示例模型上限的一半取决于示例质量大模型对格式的遵循能力主要靠示例学习而不是规则约束。即使把Prompt格式写得很清楚不给示例模型还是容易在几轮之后“自由发挥”。给1到3条完整的ReAct轨迹作为少样本示例格式遵循度立刻提升一个档次。这些示例必须满足几个条件与真实工具集完全一致工具名一个字母都不能差覆盖关键场景至少有一条包含“信息不足需要继续搜索”的情况长度适中每条控制在4到6轮以内太长的示例会让模型模仿过度每步都输出一大段废话。示例的作用不只是教格式还在教策略。你可以通过示例传递“何时用哪个工具”“拿到Observation后怎么判断是否足够”这类高阶逻辑。我在一个问答Agent里刻意放了一条“搜索结果有多个备选链接需要先打开最相关的那个”的示例模型在真实使用时就学会了这个策略。这比在Prompt里写“不要浪费时间打开不相关的链接”有效得多因为模型是通过具体轨迹学会的不是通过抽象规则。写少样本示例的时候还有一个坑示例里的Observation要和真实工具返回风格一致。比如你的search工具返回的是“标题|链接|摘要”的格式示例里也要用同样的格式。模型会把Observation的格式当成模板来学习如果你示例里Observation是纯摘要文本真实返回却是带管道符的列表模型反而会困惑。3.4 参数设置稳定性的关键旋钮大模型生成有随机性Agent对这种随机性很敏感。一个Action错了整个流程就断了。所以ReAct场景下我的参数偏好比较固定temperature设为0或0.2。太高会让Action格式漂移、Thought发散。Agent执行任务需要的是“确定性决策”不是“创意写作”。0.2是我试过很多次后觉得最稳的既保留少量多样性防止模型在同一个错误结果上反复纠结又不会让格式失控。max_tokens要够。每轮的Thought和Action加起来一般不会太长但模型可能在Action前写一大段分析如果max_tokens太小Action被截断解析直接失败。宁可给足比如500以上也不要卡着边界。系统角色和用户角色分工要清晰。工具清单放在system里Observation放在user里模型对“哪些是事实”的判断会清晰很多。把Observation混在system里会让模型分不清系统指令和事实反馈。还有一点是并发与限流。工具调用是外部请求如果Agent同时跑多轮、每轮又去调HTTP接口很容易触发外部限流。我通常在观察层做并发控制同一个Agent最多同时执行N个Action超出就排队。这虽然是个工程细节但真跑服务的时候能救命比调Prompt更立竿见影。4. 跑起来之后最容易踩的坑与排查经验4.1 模型不按格式输出解析直接崩表现模型输出的Thought没有Thought:前缀Action里的工具名和定义对不上或者一会儿用中文冒号一会儿用英文冒号。原因往往不是模型笨而是Prompt里的格式定义和少样本示例不一致或者示例太少。模型在模糊约束下会自己发明一种“相似但不同”的格式。排查顺序先看解析器报错日志把模型的原始输出完整打出来对比Prompt定义哪里不一致一目了然。很多初学者一看到解析失败就去改Prompt其实先把日志看清楚问题往往很简单。对策有几个层次解析容错要加比如同时匹配中英文冒号检测到解析失败时不直接报错而是把“格式错误请重新输出”作为Observation回传给模型很多模型发现自己格式错了能自我纠正如果平台支持结构化输出直接把Action限定为JSON格式解析成功率会高很多。4.2 无限循环、原地打转表现同一个Action反复执行Observation完全一样模型像复读机。原因大概率是模型陷入了局部决策错误它以为下一次调用会拿到不同结果或者它对Observation的文本分析不足没意识到“答案已经在这个页面里了”。排查经验把重复次数记录进日志连续重复超过3次就强制打断然后提示模型“总结已有信息如果答案已足够直接输出Final Answer否则换一个之前没试过的Action”。这个“换一个Action”的提示很关键它会强制模型跳出思维定式。如果问题是Prompt里没有告诉模型“不要重复”就在system prompt里补一句当你发现Observation与之前某轮完全相同时说明该换策略了。另一个很隐蔽的原因是Observation文本太长模型根本没有注意到关键差异。这种情况要把Observation截断改成结构化摘要或者提取关键词后再给模型。长文本里埋着答案但模型注意力有限被前面的大段无关内容带偏了。4.3 上下文越跑越长后半程失忆长任务最头疼的就是这个问题每轮把Observation全文塞回去上下文迅速膨胀模型开始“失忆”。我自己踩过这个坑早期版本不设Observation截断第6轮时上下文已经几乎塞满后面几轮完全跑偏排查了半天以为是Prompt问题最后发现是上下文过载。把截断加上问题立刻缓解。解决思路有几种按成本从低到高排序截断Observation保留前后各200字中间省略并提示“内容过长已省略中段”。成本最低但不治本。摘要每一步把Observation喂给模型生成一段摘要后再存回历史。多消耗一点生成token但能大幅压短历史。重构超过阈值时把早期轮次压缩成一条“已完成步骤列表”只保留最近几轮完整内容。这样信息不会完全丢但上下文长度可控。结构性拆任务如果任务本身就是多步骤的优先考虑用Plan-and-Execute而不是用ReAct硬扛长任务这点后面我会详细展开。4.4 幻觉依然存在模型会编造工具和结果ReAct能降低幻觉但不能消灭幻觉。高发场景有三种模型编造了一个不在工具列表里的工具名模型在Action之前伪造了Observation内容模型在Final Answer里写了Observation中不存在的数字。原因往往是Observation太长模型“偷懒”了没有从里面提取信息就直接给自己脑补了一个结论。对策分为系统和Prompt两层。系统层面工具调用前做白名单校验工具名不在列表里就拦截返回“工具不存在可用工具包括...”Observation永远由系统注入模型输出里出现Observation字段一律忽略或报警。Prompt层面要求模型在Final Answer里引用数字时必须说明该数字来自哪一次Observation。更进一步可以加一个独立的“审查Agent”让另一个模型实例判断Final Answer与Observation是否一致。这确实会增加成本但金融、客服这类正确性要求极高的场景很值得。5. ReAct的边界与后续演化方向5.1 什么场景适合ReAct什么场景别硬套ReAct很适合三类场景多跳问答需要综合多个来源的事实性问答逐轮搜索、提取、判断相当于量身定做需要调用外部API的自动化任务只要决策链长度适中逐步验证的稳定性很高有可解释性需求的任务每一步都能拿出日志方便审计和复盘。不适用的情况也很明显目标模糊、需要大量探索的研究型任务每一步都在瞎猜因为没有明确的正确方向超长任务几十步的场景容易被上下文和误差累积双重压垮需要严格过程保证的业务比如必须按固定顺序执行特定步骤这时候ReAct引入的灵活性反而有害直接写流程编排更稳。这告诉我们ReAct适合做“Agent中枢决策”但不要所有任务都往里面硬塞。架构选型永远是从需求反推的不是为了用范式而用范式。5.2 Reflexion给Agent加一面“复盘镜”ReAct的局限很明显它没有记忆。同一个任务失败后它不会从失败中学习下次照样犯错。Reflexion在ReAct循环外增加了一个“评估-反省”机制任务失败后让模型自己生成一段经验总结存到独立记忆里下一轮尝试时把这段总结作为上下文的一部分注入。打个比方ReAct是盲试Reflexion是带错题本的盲试。第二次做同样的事会先翻翻错题本再动手。实际落地非常简单在ReAct外层包一个循环每次执行完用一句“根据你的经验这次尝试哪里出了问题下次应该怎么做”生成反思文本再存到messages里下一轮尝试开始时注入。形式虽然粗糙但实测效果很好尤其适合代码生成、问题修复这类需要反复试错的任务。Reflexion不是独立架构更像是对ReAct的增强装饰器。理解了这一点你可以在任何ReAct实现上叠加反思逻辑。5.3 Plan-and-Execute把规划从执行中分离出来ReAct的另一个问题是所有决策都在循环里即时产生遇到多步任务时每一步都对但整体路径可能绕远路。Plan-and-Execute换了个思路先让模型一次性生成完整的多阶段计划然后逐阶段执行、校验。类比一下ReAct是走一步看一步的贪心算法Plan-and-Execute是先画全图再动工。这两种范式不是二选一而是可以组合。外层用Plan-and-Execute负责任务拆解和顺序控制内层每个子任务用一个ReAct循环来处理需要外部工具的部分。我在搭建复杂Agent时常用“Planner Executor”的双层结构Planner用一次LLM调用产出计划Executor对每个阶段调用一个ReAct循环完成该阶段后返回状态全部阶段完成后再把各阶段结果汇总成Final Answer。这套结构的核心收益是可控性和并行性阶段的依赖关系能提前看到某些独立阶段可以并行执行。代价是规划本身会出错一旦Planner拆分不合理后面所有阶段全错。所以最好让Executor在子任务完成后把结果回传给PlannerPlanner根据实际结果动态调整剩余计划。这种“计划-执行-再计划”的循环在真实业务里比纯静态计划可靠得多。关于这套范式我个人最深的体会是不要一上来就上复杂的框架和花哨的多Agent编排先把一个最朴素的ReAct循环写透日志打全再多跑几个真实任务你会发现大部分问题其实都出在格式约束和上下文管理上。这两个问题解决之后模型的“智能”才能真正发挥出来。ReAct是一个很好的起点但别停在起点。跑通它、理解它然后再去打破它这才是做Agent的正确打开方式。