资讯动态

ReAct模式详解:Agent如何通过思考-行动-观察循环实现可靠推理

发布时间:2026/10/14 18:33:05 来源:尧图企业网站定制
最近好几个人拿着某个开源项目的源码来问我Agent 那个循环里一会儿Thought、一会儿Action、一会儿Observation到底在干嘛为什么它不直接把答案说出来非要绕这么大一圈其实这就是 ReAct 模式。老读者可能听过这个名字但真正用手写过一遍的人不多。我最初看论文的时候也觉得挺抽象后来自己把流程跑通之后才体会到所谓“想一步做一步”本质上是让模型一边推理、一边干活、一边看结果再把新信息拿回来继续推理。今天这篇就把这个机制从头到尾拆一遍顺便给一个可以直接跑起来的极简实现。1. 先搞清楚Agent 的“思考循环”到底在循环什么1.1 一句话先讲明白 ReActReAct 这个名字就是 Reason Act 的合成词。Reason 是推理Act 是行动二者交替进行。它不是说模型先想完所有的步骤再一口气执行而是每走一步都输出一段“我想干什么”然后调用一个工具看到工具返回的结果再决定下一步怎么走。标准的结构大致长这样Thought: 我需要查一下目标城市的当前天气才能判断该不该带伞。 Action: get_city_weather(city杭州) Observation: 晴25℃湿度40% Thought: 既然不下雨那出行方案可以选骑行。 Action: Finish(答案)看到没有模型每一次思考都只往前走一小步然后借助工具的返回结果作为新的输入继续想、继续做。整个过程就像你在导航里开车不是一上来把整条路线背下来而是开一段、看一次路牌、再开一段。1.2 为什么专门设计成一段一段的一开始我也是这个疑问让模型直接推理完再一次性输出答案不行吗这不是更节省 token 吗在某些简单场景确实可以比如“从三个句子里找错别字”这种任务模型内部推理链再长也不需要外部信息。但现实中的 Agent 任务往往是这样的用户问的是“帮我查一下这周末北京和上海哪个适合飞无人机”这个问题里包含实时天气、禁飞政策、场地评价多种信息模型靠参数记忆根本回答不了。ReAct 的价值就在于它可以调用外部工具、搜索引擎、数据库、代码解释器来补充信息并把每一步的结果都变成下一步推理的依据。这就是它和普通问答的本质区别。1.3 循环里的三个关键要素整个循环其实只有三个核心组件Thought、Action、Observation。要素含义示例Thought模型当前对任务的判断与决策我需要先查天气再决定出行方式Action要调用哪个工具传什么参数get_city_weather(city杭州)Observation工具执行完返回的结果晴25℃湿度40%这三个组件会循环出现。每轮 Action 都必须是可被外部环境执行的而 Observation 必须被反馈到下一轮 Thought 里。如果你把日志打出来会看到一条清晰的任务执行链而不是一团乱码一样的文本。这里有一个特别重要的工程细节Thought 不一定只能有一句但 Action 那部分一定要足够结构化。我在做 Agent 的时候见过很多次模型吐出一大段思考结果最后忘了写 Action或者把工具名写错循环直接断掉。这就是后面要说的输出约束问题。2. 为什么“想一步做一步”比“一口气想完”更可靠2.1 一口气想完的推理方式有什么坑先看一个真实案例。假设用户问“如果我把 2350 元分成 7 等份每份多少钱”模型如果直接走纯推理Chain-of-Thought也就是 CoT 模式会给出一个看似合理的分步计算然后再给一个答案。问题是计算过程如果出错外部没有任何机制能发现。模型在中间步骤算出 2350 ÷ 7 335.71但因为它没有真正执行计算器它不知道 335.71 × 7 2349.97和原数差了 0.03。这个误差在纯推理链路里不会被修正因为它缺少“观察自己行动结果”的环节。再比如用户问“我的服务器还剩多少磁盘空间”。模型根本不知道该服务器的实际状态如果硬推理唯一的办法就是从训练数据里找相似场景来“猜”。猜出来的数字看着很专业其实是编的。用过的人都知道这种幻觉在落地场景里非常致命。2.2 只做不思考也不行那反过来我完全不让模型思考只按固定动作执行行不行比如遇到问题就直接调用工具拿到结果就返回这不就避免幻觉了吗也不行。真实任务通常不是一次工具调用就能搞定的。就拿“对比两个城市的通勤成本”来说你需要先查两个城市的租房价格、公共交通价格、再来估算每月开销这些信息分散在不同地方中间还涉及单位换算如果模型没有 Thought 去做拆解它根本不知道该先调哪个工具、拿到结果之后又该做什么。没有思考的纯执行像一个没有方向感的实习生。你让他复印文件他就只复印完全不会发现文件里有两页纸是空的。这类 Agent 在简单任务上很稳但只要任务稍微复杂一点就立刻变成死板工具人。2.3 真正关键的是反馈闭环ReAct 最核心的价值不是“边想边做”这个形式而是它建立了一个反馈闭环Thought 指导 ActionAction 产生 ObservationObservation 修正下一步 Thought。你可以把它理解成炒菜。老手炒菜不会把盐一次性全放进去而是放一勺、尝一下、不够咸再加半勺。这一步得到的味道反馈直接影响下一步加多少盐。模型做多步任务也是这个道理它如果只“背菜谱”不知道锅里实际是什么状态翻车概率极高。而且这个闭环还有一个好处它能暴露问题。模型在中间某一步产生了错误假设Observation 会立刻告诉它“你查的这个城市名不存在”或者“这个日期没有数据”。有了外部的反馈错误就能被及时修正而不是在错误的路上越走越远。3. ReAct 的完整工作流程拆解3.1 一次典型的 ReAct 循环长什么样我们从用户提问开始走一遍完整流程。假设用户的请求是帮我算一下如果我要买 3 本单价 45 元的书和 2 支单价 12 元的笔总共要多少钱如果带 200 元够不够这是一个包含计算和判断的任务。一次性推理也可以但为了演示让模型走 ReAct 模式。模型首先生成Thought: 需要先分别计算书和笔的总价再做加法。 Action: calculator(3*45 2*12) Observation: 159 Thought: 159 小于 200所以钱足够。 Action: Finish(够总共需要159元带200元足够。)整个流程非常直观。模型不是一次性输出“够”而是先输出一个可执行动作工具返回结果后再做判断。这里面最容易被忽视的是 Action 后面的括号内容。比如 calculator(345 212)这里的字符串必须是工具接口能够解析的表达式。如果模型输出一个普通自然语言“帮我算一下”而不是结构化表达式工具就会被卡住。实际开发中我们通常会用正则或者 JSON Schema 来解析 Action让模型严格按照固定的动作格式输出。这也是为什么系统提示词里必须强调输出规范。3.2 工具从哪来、怎么注册ReAct 里的 Action 不是模型凭空创造的它只能调用开发者预先注册好的工具。每个工具通常包含三部分信息名称、描述、参数定义。模型在决策时先根据问题理解需要什么能力再从工具描述里选择最匹配的那个。这一步非常考验描述写得好不好。举个例子如果你只写get_weather: 查天气。模型在“杭州适不适合骑行”这种任务里可能会犹豫要不要调用它因为描述没有说它能查城市、日期、温度、风力这些信息。但如果你写get_weather(city, date): 查询某个城市在指定日期的天气情况返回温度、湿度、风力、降水概率用于判断出行是否需要带伞、是否适合骑行。模型一眼就知道这个工具能干什么、该传什么参数。所以工具描述本身就是给模型看的说明书写得越具体模型的工具选择准确率越高。3.3 记忆与上下文的处理方式ReAct 循环每走一步都会在对话里多出几条 Thought、Action、Observation。这些历史内容会被原封不动地塞进模型上下文里作为后续推理的参考。但上下文窗口是有限的。一个复杂任务可能会走上十几步每一步的 Thought 和 Observation 可能几百甚至上千字没几轮就接近 token 上限了。这时候有两个常见的处理策略只保留最近几轮把最早的那些长文本丢掉只保留最后三四轮。中间摘要让模型把前面的历史压缩成一条摘要比如“已完成天气查询已知杭州晴25℃接下来比较上海”。工程上比较稳的做法是两者结合。保留最近若干轮的完整内容同时把早期内容做摘要。这样既不会丢关键信息也不会爆上下文。3.4 终止条件怎么设计一个循环不能永远跑下去必须设计终止条件。我在代码里通常用三种方式模型输出Action: Finish(答案)表示任务已完成。步数达到上限强制结束。检测到重复调用同一个工具、且参数一样说明模型卡住了需要中断。这里面有个细节终止步数不能设得太小否则复杂任务还没做完就被掐断但也不能设得太大否则一旦模型死循环就会白白消耗大量 token。一般简单任务给 5 步中等任务给 8 步复杂任务给 15 步左右可以按实际情况微调。4. 从零实现一个极简 ReAct Agent4.1 准备环境与整体结构前面讲了这么多理论接下来用一个最小的 Python Demo 把整个流程跑通。我们不依赖任何 Agent 框架只用一个大模型接口调用函数加上一把循环逻辑核心代码大约几十行。需要的环境很简单python 3.10 # 任何可用的 LLM API 客户端库或者本地部署的模型 HTTP 接口为了不绑定任何特定厂商示例里用llm(messages)这个占位函数表示模型调用。你只需要把它替换成你自己接入的模型 API 即可。4.2 完整代码示例import json import re # 占位函数把它替换成你实际使用的模型 API def llm(messages): # messages 是 OpenAI 风格的消息列表 # 返回模型生成的文本字符串 return # 注册两个演示工具 TOOLS { calculator: { desc: 计算数学表达式例如 calculator(3*45 2*12), exec: lambda expr: str(eval(expr)), }, get_city_weather: { desc: 查询城市实时天气返回温度、湿度、风力例如 get_city_weather(杭州), exec: lambda city: f{city}今日晴25℃湿度40%, }, } SYSTEM_PROMPT 你是一个任务助手你通过交替执行 Thought、Action、Observation 来完成用户请求。 可用工具 - calculator(expression): 数学计算 - get_city_weather(city): 查询天气 输出格式要求 Thought: 你的思考 Action: 工具名(参数) 如果任务完成输出 Action: Finish(给用户的最终回答) def parse_action(text): 从模型输出中解析 Action 部分 match re.search(rAction:\s*(\w)\(([^)]*)\), text) if not match: return None return match.group(1), match.group(2).strip().strip(\) def run_agent(user_query, max_steps6): messages [{role: system, content: SYSTEM_PROMPT}] messages.append({role: user, content: user_query}) for step in range(max_steps): print(f\n Step {step 1} ) response llm(messages) print(模型输出:, response) # 如果模型表示任务完成直接提取答案 if Finish( in response: answer re.sub(rAction:\s*Finish\(([^)]*)\), r\1, response) print(最终答案:, answer) return answer parsed parse_action(response) if parsed is None: # 模型没输出合法 Action要求它重新输出 messages.append({role: assistant, content: response}) messages.append({ role: user, content: 你刚才没有输出合法的 Action请严格按照格式重新输出。 }) continue tool_name, args parsed if tool_name not in TOOLS: messages.append({role: assistant, content: response}) messages.append({ role: user, content: f工具 {tool_name} 不存在请使用可用工具之一。 }) continue tool_func TOOLS[tool_name][exec] try: observation tool_func(args) except Exception as e: observation f工具执行出错: {e} print(Observation:, observation) messages.append({role: assistant, content: response}) messages.append({ role: user, content: fObservation: {observation} }) # 超过最大步数强制返回 return 已达最大步数任务未完成。 # 运行测试 if __name__ __main__: run_agent(帮我算 3 本单价 45 元的书和 2 支单价 12 元的笔总共多少钱带 200 元够吗)这段代码没有做任何花哨的事就是把 ReAct 的三个要素串起来让模型生成文本解析其中的 Action执行工具再把 Observation 加入上下文继续跑。代码里的llm(messages)函数实际使用时需要传入消息列表然后返回模型输出。4.3 关键参数与设置说明这段代码里最容易被忽略的其实是SYSTEM_PROMPT。我一开始写 Agent 的时候以为模型天生就会按照 Thought/Action 格式输出结果发现完全不行。必须把格式写进提示词里并且给出示例模型才会规规矩矩地按格式来。另外几个参数值得留意参数建议值原因max_steps6-15太小完不成复杂任务太大容易死循环temperature0 或接近 0降低随机性让模型更稳定地输出工具调用上下文保留轮数最近 4-6 轮防止 token 膨胀工具描述长度50-100 字过短模型选不准过长浪费 token还要注意示例里的calculator直接用了eval生产环境千万别这么干会有严重风险。正确做法是用ast.literal_eval或者把表达式拆开解析也可以直接调用专门的表达式计算库。这里的目的是演示核心流程安全性同学们自己落地时一定补上。5. 实操中常见问题与排查实录5.1 模型走进死循环怎么办最常见的问题是模型反复调用同一个工具参数都不变日志里看到一模一样的 Action 出现三四次。比如它每次都输出get_city_weather(杭州)结果相同却始终不往下走。我排查过几次后发现原因通常是 Observation 没有进入下一轮思考。有些模型的上下文拼装方式不对Observation 被放在了一个比较靠后的位置模型没有真正“看到”它所以它以为自己还没查到天气就再查了一次。解决办法有三个检查消息拼接Observation 要作为 user 消息紧跟在对应 Action 之后。加重复检测如果发现当前 Action 和上一步完全一样就在提示词里强制要求模型换个方向。设置全局最大步数宁可提前结束也不能无限烧 token。5.2 工具返回格式五花八门模型解析崩溃工具不总是返回干净的字符串。比如数据库工具返回的是列表图片识别工具返回的是 JSON 结构如果原封不动塞到 Observation 里模型读到一大段未格式化的 JSON 往往不知所措。我踩过这个坑现在统一会在工具外面包一层结果处理器把返回值清洗成一段自然语言描述。比如数据库查询返回[{“city”: “杭州”, “temp”: 25}]我会转换成“杭州当前温度25℃”。这样模型读 Observation 的时候获得的是容易理解的信息而不是需要再解析的结构化数据。5.3 模型不调用工具直接编答案这个问题很经典。你在日志里看到 Thought 写得头头是道最后却没有 Action直接写了个虚构的答案。原因多半是模型被训练得太喜欢“直接回答”了它不习惯在做数学题之前调用计算器。一种思路是加 few-shot 示例。在系统提示词里放两三组示例让模型看到“遇到计算就调用 calculator”这个行为模式。另一种思路是降低 temperature并且把工具的可用性直接写成工具描述的一部分强调“你必须调用工具才能回答这些类型的问题”。5.4 上下文被观测结果塞爆当一个工具返回的内容很长比如查询日志返回了 2000 行即使任务要求只提取错误信息那些文本也会全部塞进上下文。多轮下来很快就把上下文窗口占满了。我的做法是工具返回不做全量透传而是先截断或摘要。比如日志工具只返回前 50 行加上“如需查看更多请使用翻页参数”。这既能保住上下文空间也逼着模型更精准地描述需求而不是把整个数据丢进来让模型慢吞吞地读。6. 扩展方向与我的实操体会6.1 从单 Agent 到多 AgentReAct 模式是一个基础模块。做复杂项目时不需要把一个 Agent 的能力做得无比巨大而是拆成多个小 Agent每个 Agent 负责一个领域然后由一个总控 Agent 协调它们。每一个小 Agent 内部仍然可以用 ReAct 循环但它的工具列表会更聚焦选择工具的准确率也更高。比如我有一个模拟项目把“查天气”和“算预算”分开成两个 Agent总控 Agent 根据任务类型路由。这样每个 Agent 的工具不超过三个系统提示词也简单模型几乎不会选错工具。这比把一个 Agent 做成万能要省心得多。6.2 规划器与执行器分离另一个常见的扩展是让模型先生成一个完整的行动规划再去逐步执行。这种思想和纯 ReAct 不同ReAct 是边想边做而 Plan and Execute 是先想后做。两者结合使用效果也不错先用规划器产出步骤列表然后每执行一步都根据 Observation 微调后续步骤。我在实际项目里一般会把这两种模式变成参数可配置的选项。简单任务用 ReAct复杂的长链路任务用 Plan and Execute具体效果要跑一批测试样例才能定不存在放之四海而皆准的方案。6.3 一点个人经验我自己在做 Agent 的时候有段时间特别迷“复杂架构”恨不得把记忆模块、反思模块、任务分解模块全堆上去。后来发现对大多数项目来说把 ReAct 循环本身的稳定性做好收益是最大的。所谓稳定无非就是三件事格式解析可靠、工具描述清晰、终止条件明确。另外一个非常见效的小技巧写正式代码之前先手工模拟四五轮对话把这些样例直接放进系统提示词里当 few-shot。你会发现模型立刻就像换了一个人工具选择准确率能上一个台阶。这招我用了很多次几乎每次都好使。好了ReAct 的核心思路和落地细节就讲到这里。如果你正在写自己的 Agent先从最简循环开始跑通之后再慢慢加复杂度这条路是最稳的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑