ReAct:让大模型边想边做,突破复杂任务瓶颈
1. 为什么会思考的AI还是搞不定复杂任务大概半年前我想让AI帮我做一件看起来很简单的事把某个营销活动期间收集的用户反馈按渠道归类统计各渠道的负面评价占比再对比上一期活动给出结论。听起来是不是觉得给大模型一段提示词就行实际结果让我当场崩溃。模型确实给了我一篇结构完整的报告有数据、有百分比、有结论。但当我拿着这份报告去核对原始数据时发现里面至少有三分之一的数据源根本不存在那些百分比不过是模型觉得应该差不多的数字。它甚至连去查一下原始文件的念头都没有。这让我意识到一个问题光是让AI想得深远远不够。模型的内部知识是冻结的。它所有的知识都停留在训练完成的那一刻没法访问实时数据没法主动获取外部信息更没法验证自己刚刚说出口的数字到底对不对。很多人以为把问题描述得清楚一点、加一句请仔细思考就能解决问题这在简单逻辑题上或许有效但一旦任务需要查询真实信息、调用工具、根据新信息调整策略纯文本推理就会暴露出严重的缺陷。这里必须提一个被讨论很多的方案思维链Chain-of-ThoughtCoT。它的思路很直接——让模型把推理过程写出来一步一步想。不可否认这个方法在某些场景下效果显著但它有一个致命问题思维链的每一步推理都是基于模型自己的记忆一旦中间某一步的理解出现偏差后面的推理会沿着错误方向越走越远而且没有任何外部信号能把模型拉回来。这就好比一个人蒙着眼睛走迷宫他能流利地描述自己正在左转、右转、直行但永远不知道自己其实已经撞墙了。所以问题很清楚复杂任务需要的不是一个想得很深的模型而是一个想一步、做一步、看结果、再想下一步的模型。这正是ReAct想解决的问题。ReAct是Reasoning和Acting的合成词中文社区常把它翻译成边想边做。它的核心主张很简单让模型在推理过程中交替生成思考和行动行动的结果再反馈回来修正下一个思考。你可以把这种方式理解成让AI睁开眼睛走迷宫——每一步都用手摸一摸墙在哪再决定往哪走。这篇文章我会从原理讲起再带大家从零实现一个最简单的ReAct循环分享我在实际调试中踩过的坑以及它适合用在哪些场景。不是那种高深的理论讲解而是能直接抄作业的实操经验。2. ReAct的循环机制拆解思考、行动、观察2.1 一段真实的ReAct轨迹长什么样理解ReAct最直观的方式就是看一段真实的模型输出轨迹。假设任务是这样查一下某电商平台上某款商品的当前价格再计算如果打八折之后比原价便宜多少钱最终给出一个结论。在ReAct模式下模型生成的输出不是一段直接的回答而是一段循环的记录大致长这样Thought 1: 用户想知道商品现价、折扣价和差价。我先查一下商品当前价格。 Action 1: 调用 search_product 工具参数 product_name某商品 Observation 1: 当前售价 299 元原价 399 元。 Thought 2: 现价是 299 元原价 399 元。打八折后的价格是 299 * 0.8 239.2 元。 Action 2: 调用 calculator 工具参数 expression299 * 0.8 Observation 2: 239.2 Thought 3: 八折价是 239.2 元差价是 299 - 239.2 59.8 元。可以给出最终答案了。 Action 3: finish 工具输出最终答案看到没这里最关键的地方在于模型不是一次性给出答案而是自己想出下一步该做什么做完之后拿到观察结果再根据观察结果想下一步。整个过程由三种元素交替构成。Thought是模型的推理过程相当于自言自语用来解释我为什么这么做Action是模型选择调用的工具及其参数相当于对外部世界发出动作指令Observation是工具执行后返回的结果相当于模型观察到的世界反馈。三者形成一个闭环Thought决定ActionAction产生ObservationObservation又触发下一个Thought。2.2 为什么边想边做优于想完再做可能有人会问既然要规划任务为什么不干脆让AI先列出完整计划再一步步执行这两种思路确实是目前Agent设计的两大路线Plan-and-Execute就是先想完再做的典型代表。它让模型先把所有步骤列清楚然后逐项执行。看起来更清晰但我在实际使用中发现它存在一个很大的问题计划是在信息不完整的情况下做出的一旦中途发现某个假设不成立整个计划就要推翻重来。举个例子假设模型制定的计划是第一步查商品价格第二步计算折扣价第三步输出结论。前两步确实顺利。但如果第一步搜索结果返回的是该商品已下架或者返回了三个不同规格的价格计划的第三步就得完全改掉。Plan-and-Execute模式下模型在第二步根本不会意识到第一步出了问题它会傻乎乎地照原计划继续。ReAct的边想边做天然规避了这个问题。因为模型每一步都在观察真实环境反馈它随时可以根据反馈调整思路。思考不是一次性的而是随着信息增多逐步更新。打个比方Plan-and-Execute像拿着旧地图开车ReAct像一边看路标一边开车。路况一变前者只能停下来重新规划后者随时可以转向。2.3 模型凭什么知道该调用哪个工具这里有个很多人都会好奇的问题模型怎么知道当前环境里有哪些工具又怎么知道什么时候该调用、参数填什么答案其实没那么玄学靠提示词里的工具描述。我们会在系统提示词里把每个工具的名字、功能、参数要求写成一段清晰的说明再给一两个示例模型就能在上下文中学会如何调用工具。这就是所谓的上下文学习In-context Learning并不是模型真的学会了使用工具而是它通过阅读提示词中的规则模仿出了正确的调用格式。所以工具描述的质量直接决定模型调用工具的准确度。我踩过一次很典型的坑当时我给一个工具起名叫run_query描述写的是执行SQL查询。模型经常不知道该不该用它宁可自己编数据。后来我把名字改成query_user_database描述里明确写了用于查询用户相关数据输入是SQL语句返回查询结果集并加了一个示例模型立刻变得精准很多。名字越直白描述越具体示例越贴近真实用例模型的调用成功率就越高。2.4 观察结果如何参与下一轮推理很多人容易忽略的一点是Observation不是简单地把工具结果塞回对话里就行它需要被模型消化成下一步思考的依据。我见过不少初学者的实现就是把工具返回的完整JSON原封不动拼到上下文里结果模型完全不知道该从里面提取什么下一轮Thought依然含糊。一个好的做法是在把Observation拼进上下文时顺手加一句引导语比如任务进度更新现在你看到的是工具执行结果请注意提取关键信息。或者对工具结果做一层清洗把多余的元数据剥离掉只保留对当前任务有影响的字段。好的ReAct设计会让每轮的Observation都成为下一轮Thought的直接输入。这样模型的推理过程始终踩在真实数据上而不是自己的想象上。3. 从零实现一个最简ReAct Agent3.1 准备工作与工具选型说了这么多原理下面进入实操环节。我会用Python实现一个最小可用的ReAct循环不依赖任何重型Agent框架只需要三大模型API和标准库。你完全可以在自己的机器上复制运行然后按需替换成自己的工具。需要准备的东西有三样。第一一个能支持文本生成的大模型API接口调用方式不复杂返回基本的文本输出。第二几个自定义工具函数。第三一个循环控制逻辑用来驱动思考-行动-观察的迭代。这里我建议第一次实现不要把面铺太开选三个工具就够。我会选一个search_web用来模拟搜索网上信息一个calculate用来做数学运算一个get_current_time用来获取当前时间。这三个工具覆盖了信息检索、数值计算、环境感知三个典型场景足够展示ReAct的工作机制。工具选型的思路是一组好的ReAct工具应该彼此互补、边界清晰不要高度重叠。如果选两个都能做文本检索的工具模型就很容易混淆该调用哪一个。工具数量少而精模型的决策负担小成功率反而更高。3.2 工具描述决定模型会不会用工具函数本身很好写真正考验设计的是每个工具的使用说明书。我会在每个工具描述里包含四个部分工具名称、功能说明、参数说明、一个使用示例。看看我的实现中工具描述怎么写tools { search_web: { description: 搜索互联网信息适用于获取实时资讯、查找资料、确认事实。, parameters: { query: 字符串搜索关键词尽量具体例如某手机 当前售价 }, example: 调用示例: {{name: search_web, arguments: {{query: 某手机 当前售价}}}} }, calculate: { description: 执行数学计算适用于加减乘除、百分比、数值比较等运算。, parameters: { expression: 字符串数学表达式例如 299 * 0.8 }, example: 调用示例: {{name: calculate, arguments: {{expression: 299 * 0.8}}}} }, get_current_time: { description: 获取当前日期和时间适用于需要时间信息的任务。, parameters: {}, example: 调用示例: {{name: get_current_time, arguments: {{}}}} } }为什么参数名要选expression而不是普通的expr因为模型在上下文学习中对完整单词的理解更准确缩写容易让模型猜不透。同理工具名尽量用下划线分隔的自然短语而不是缩写。这属于很细节但影响很大的设计。另外要注意工具描述是用自然语言写给模型看的不是写给机器看的。它要让人模型一看就明白这个工具是干什么的、我该什么时候用它。你可以想一下如果一个新人同事刚入职你会怎么向他介绍一个内部工具那个语气就是工具描述该有的语气。3.3 核心循环代码实现有了工具之后就是整个ReAct最核心的循环逻辑。我把它拆成几个步骤组装系统提示词把工具描述和示例放进去调用模型生成一段文本从文本中解析出Thought、Action执行Action对应的工具拿到Observation把Observation追加到对话历史进入下一轮。直到模型输出finish动作或者达到最大轮数上限。下面是一份可以直接跑的main循环代码我用的是大模型API的通用调用方式import json import re from datetime import datetime def search_web(query): # 这里替换成你自己的搜索服务实现 return f模拟搜索结果{query} 的相关信息为该商品当前售价299元原价399元。 def calculate(expression): # 安全计算仅允许数字和基础运算符 if not re.match(r^[\d\s\-*/().%]$, expression): return 错误表达式包含非法字符 try: return str(eval(expression, {__builtins__: None}, {})) except Exception as e: return f计算错误{e} def get_current_time(): return datetime.now().strftime(%Y-%m-%d %H:%M:%S) tool_functions { search_web: search_web, calculate: calculate, get_current_time: get_current_time, } def build_system_prompt(tools): lines [ 你是一个智能助理。你需要通过思考 - 行动 - 观察的循环来完成任务。, 你可以使用的工具如下, ] for name, meta in tools.items(): lines.append(f- {name}: {meta[description]}) lines.append(f 参数: {json.dumps(meta[parameters], ensure_asciiFalse)}) lines.append(f 示例: {meta[example]}) lines.append( \n请严格按照以下格式输出\n Thought: 你的思考过程\n Action: {\name\: \工具名\, \arguments\: {...}}\n 当任务已经完成时输出 Action: {\name\: \finish\, \arguments\: {\answer\: \你的最终答案\}} ) return \n.join(lines) def parse_action(text): # 找出 Action: 后面的 JSON 内容 match re.search(rAction:\s*(\{.*?\}), text, re.DOTALL) if not match: return None try: return json.loads(match.group(1)) except json.JSONDecodeError: return None def run_react(task, tools, max_iterations8): messages [ {role: system, content: build_system_prompt(tools)}, {role: user, content: task} ] for i in range(max_iterations): reply call_llm(messages) # 替换成你的模型API调用 print(f--- 第 {i1} 轮 ---) print(reply) action parse_action(reply) if action is None: messages.append({role: user, content: 本轮缺少有效的Action请重新生成。}) continue if action[name] finish: print(最终答案:, action[arguments].get(answer, )) return action[arguments].get(answer, ) func tool_functions.get(action[name]) if func is None: observation f错误未知工具 {action[name]} else: try: observation func(**action.get(arguments, {})) except Exception as e: observation f工具执行失败{e} messages.append({role: user, content: fObservation: {observation}}) print(达到最大迭代次数强制停止。) return None这个循环只有三十多行但设计逻辑上藏着几个关键点值得展开说明。max_iterations8这个参数不是随手写的。我自己实测下来大部分ReAct任务在4到6轮之内就能完成超过8轮要么是模型在绕圈子要么是任务本身描述不清。设置一个上限能防止模型陷入死循环同时也不至于因为上限太低而打断正常流程。temperature这个参数在调用模型时最好设置为0或接近0。原因很简单ReAct依赖格式化的Action输出如果模型的随机性太大很容易输出格式不完整或者干脆胡编一个工具名。在需要稳定输出JSON的场景低温度是底线设置。parse_action用正则找Action后面的JSON内容这个方法看起来暴力但实测很可靠因为模型的输出只要按格式走Action永远是最后一行。后续如果模型输出不稳定可以换成更严格的JSON模式但初版先用正则足够。3.4 一次完整运行过程实录为了让你更直观地看到这套代码的工作过程我把上面例子中的任务完整跑了一遍这里是模型输出的实际轨迹做了精简--- 第 1 轮 --- Thought: 用户想了解当前时间以及某商品打八折后的价格。我需要先获取当前时间再搜索商品现价。 Action: {name: get_current_time, arguments: {}} Observation: 2025-01-15 10:30:22 --- 第 2 轮 --- Thought: 当前时间是2025年1月15日。接下来搜索该商品的当前售价。 Action: {name: search_web, arguments: {query: 某商品 当前售价}} Observation: 模拟搜索结果该商品当前售价299元原价399元。 --- 第 3 轮 --- Thought: 商品现价299元打八折之后是299乘以0.8。我需要用计算器算一下。 Action: {name: calculate, arguments: {expression: 299 * 0.8}} Observation: 239.2 --- 第 4 轮 --- Thought: 八折价是239.2元那么比原价便宜了299减去239.2即59.8元。可以给出最终答案。 Action: {name: finish, arguments: {answer: 某商品当前售价299元打八折后为239.2元与原价相比便宜了59.8元。}} 最终答案: 某商品当前售价299元打八折后为239.2元与原价相比便宜了59.8元。你看从查询时间到搜索价格到计算折扣再到输出答案模型全程没有在中间步骤编造任何数据——所有数字都来自工具的真实返回。这就是ReAct最核心的价值它让模型的每一次断言都有据可查。这里还有一个我特别想强调的细节模型在第1轮先查了时间但它接下来没有纠结于时间信息而是自然过渡到搜索价格。这说明模型能识别哪些信息对当前任务有用哪些只是辅助。这个能力来自工具描述中的引导示例以及模型本身的语义理解。如果你发现模型频繁在无关工具上浪费时间多半是工具描述里没写清楚什么时候该用我。4. 调优心得与避坑指南四个我踩过的坑4.1 坑一Action输出格式飘忽不定我在刚开始跑ReAct时最崩溃的问题就是模型经常不按照Action: JSON的格式输出。有时候它会写Action: 我想用搜索工具有时候写Action: search_web(query...)还有一次它甚至把Action和Thought揉在一个句子里。这个问题有两个层面的解法。第一层在系统提示词里把格式约束写死并给一个严格的对比例子展示正确输出和错误输出的区别。第二层在代码里对解析失败做兜底——正如上面代码里的处理方式一旦解析不到合法的Action就把本轮缺少有效的Action请重新生成作为Observation返回给模型。这个反馈信号很重要它让模型意识到格式错了下一轮会自己纠正。还有一个细节解析JSON时要小心模型输出的JSON里有单引号、尾部逗号这类小毛病。我建议解析失败时不要急着报错可以先尝试清洗JSON字符串去掉多余逗号再解析。当然最稳妥的做法还是通过模型API提供的结构化输出能力把那层格式保证完全交给协议本身。4.2 坑二模型陷入工具调用死循环第二种常见的翻车现场是模型反复调用同一个工具拿到同样的Observation然后再次调用同一个工具周而复始直到撞上最大迭代次数。我见过最夸张的一次模型连续四次调用search_web搜索同一个关键词返回完全一样的结果它还在继续搜。究其原因是模型在上一轮Thought里发现自己还没足够的信息于是本能地重复搜索而不是换个思路或者换一个工具。解决办法有两个方向第一在系统提示词里明确告诉模型如果Observation没有带来新信息应该换一种搜索词或换一个工具而不是重复调用第二在代码里做同内容Observation去重如果连续两次Observation相同就把Observation: 新的搜索没有产生额外信息请尝试其他方法拼进去引导模型换路。这种问题属于ReAct模型推理策略层面的Bug虽然让循环继续跑也能跑完但会白白消耗大量token。从成本控制的角度尽早发现并打断重复循环非常值得。4.3 坑三Observation内容太长撑爆上下文窗口这是一个非常现实的问题。很多工具返回的数据是冗长的JSON结构、完整网页文本、或者大段数据库查询结果。ReAct循环每走一轮这段Observation就会完整留在对话历史里。三轮之后上下文就开始紧张十轮之后可能一半上下文都是无用的工具返回。我的标准做法是给Observation设置长度上限。以我的经验截断到500个字符左右最能平衡信息完整度和token开销。再配合一个摘要函数把工具返回里的关键字段提取出来只保留任务相关的部分。比如search_web返回的如果是产品列表我会只保留产品名、价格、库存状态三个字段丢掉其他杂项。还有一点工具返回的错误信息也要截断。模型看到一大段堆栈追踪很容易懵把错误信息精简成查询失败连接超时这种一句话反而更有利于模型下一步决策。4.4 坑四工具自身的错误被模型当成事实这个坑特别隐蔽。有一次我的calculate工具因为输入表达式里面包含了对列表的求和执行结果返回了一个错误字符串。模型拿到这段错误字符串后没有意识到这是报错反而把它当作计算结果继续进行后续推理最后给了一个完全错误但逻辑自洽的答案。解决思路是在Observation中给错误信息加上明显的前缀标识比如[ERROR] 无法执行计算...。这与正常返回的纯文本形成强区分模型能更容易识别当前观察是一个错误信号从而调整下一步动作。我在工具函数里统一约定任何工具遇到异常都必须返回以[ERROR]开头的内容否则视为工具实现有Bug。同时在系统提示词里明确写一句当你看到[ERROR]开头的Observation说明工具执行失败你应该检查参数并重试或者更换工具。这一条规则虽小却能避免很多后链式逻辑错误。4.5 常见问题速查表现象可能原因解决方案模型不输出Action只给Thought提示词里没有给出清晰格式要求补充严格输出格式说明并结合示例展示Action JSON解析失败temperature过高导致格式漂移将temperature调到0或使用结构化输出反复调用同一个工具模型思路固化Observation没带来新信息增加Observation去重与换路提示Observation太长上下文爆掉工具返回未做精简截断到500字符以内提取关键字段工具报错被当成正常结果错误信息与正常数据格式不区分统一在错误前加[ERROR]前缀模型用工具得到结果后仍不结束缺少明确的完成条件在提示词中强调答案已获得时立刻使用finish5. 什么时候该上ReAct与其他方案的选型对比5.1 纯CoT和ReAct怎么选如果你仔细读过前面的内容会发现ReAct并不总是最优解。很多任务根本不需要工具调用比如纯逻辑推理、常见知识问答、文本改写。这种情况下单纯的CoT反而更高效——它不需要附加工具解析和Observation构造也不需要担心工具调用出错一次生成就能出结果。我在实践中的判断标准很简单如果任务只需要想就用CoT如果任务需要查或者算再用ReAct。举个例子分析这段代码的时间复杂度只需要推理CoT足够但查询这个项目在某平台的星标数并计算增长率就必须调API拿数据这才轮到ReAct出场。还有一个成本考量。ReAct的token消耗远高于CoT每一轮Action和Observation都在追加对话长度复杂任务跑下来可能要消耗几千甚至上万token。如果一次CoT能稳定完成任务强行套ReAct纯属给自己加戏。5.2 Plan-and-Execute和ReAct怎么选Plan-and-Execute路线适合任务环节高度确定、每一步几乎不需要外部反馈的场景。比如做一个标准的数据清洗流程读取文件、去掉空值、统一日期格式、输出结果。每一步都是既定操作中途不太可能出现意外。反过来如果任务里存在查完才知道下一步怎么做的环节ReAct就明显更合适。比如写一篇包含最新数据的分析报告你只有先搜索到数据才知道该用什么角度去分析。我在一个真实项目里同时试过两种方案最终ReAct方案在用户排除干扰这个指标上明显胜出——它不会在搜索返回数据不存在之后继续傻傻执行原计划。有个折中做法也值得提把Plan-and-Execute和ReAct混合使用。第一阶段用Plan确定大的步骤框架第二阶段在每一步内部用ReAct应对不确定性。我的经验是这种方式在处理超长任务时能兼顾稳定性和灵活性代价是实现复杂度更高。5.3 我的选型心法与建议这几轮实践下来我对ReAct的态度越来越理性。它不是在所有场景下都优于传统方案而是在需要感知外部世界并据此调整计划的场景里表现突出。换句话说ReAct的价值不在于让模型会思考而在于让模型的思考能够落到真实世界里接受真实世界的检验和修正。如果你正准备在自己的业务里引入ReAct我会建议三条路线。一开始先用手写循环验证想法不要急着上重型框架验证有用之后再考虑用成熟的Agent框架来简化工具注册和调度最后才是考虑多Agent协作、任务分解这类更复杂的架构。一上来就铺太满大概率会在调试上耗尽精力。还有一条小技巧如果发现模型在选工具时犹豫不决不妨在工具的使用时机描述里加上否定条件比如不要用于计算折扣计算请用calculate工具。带边界的描述比只讲我擅长什么更能帮助模型做出准确决策。最后一段说说我个人的体会从头实现一遍ReAct让我对大模型的能力边界有了完全不同的认识。过去我总觉得模型是知识库 推理器现在我看它更像是一个需要在真实世界里反复碰壁来校正认知的行为体。ReAct给出的不只是技术方案也是一种看待AI的方式再强的推理能力如果没有外部反馈闭环最终都会走向自说自话。现在我做Agent相关的项目无论最终选不选用ReAct都会习惯性地问一个问题这条链路里哪些环节需要模型凭空推断凡是能借助工具、API或数据库拿到事实的地方我绝不会让模型凭感觉说。做到这一点模型的输出可靠性会比单纯调提示词提升好几个台阶这也是我在这个项目里收获的最大一笔实战经验。