ReAct模式深度解析:从“只会想”到“会行动”的Agent构建指南

📅 发布时间:2026/10/14 9:06:30
ReAct模式深度解析:从“只会想”到“会行动”的Agent构建指南
1. ReAct 到底在解决什么问题先想清楚“想”和“做”的关系先说个扎心的事实让大模型直接答题它写得头头是道但你要让它“帮我查一下明天三场航班的准点率然后订一个最不容易延误的”绝大多数模型当场就卡住了——不是不知道航班信息在哪而是它压根没有“去查一下”这个动作能力。这就是纯文本模型和真正的 Agent 之间那条看似很细、实则很宽的裂缝模型只会从上下文里推断答案不会主动去调用工具、观测结果、修正判断。ReAct 模式Reasoning Acting推理加行动要解决的正是这个“只会想不会做”的问题。它把模型的推理过程和执行过程拼成一条流水线模型先输出一段思考Thought说明这一步要做什么、为什么然后输出一个动作Action比如“调用天气查询工具”工具返回结果后模型把结果读进来再输出下一段思考……循环往复直到拿到最终答案。这就是标题里说的“想一步做一步”。我第一次在项目里真正把 ReAct 跑起来的时候第一反应不是“哇好智能”而是“这不就是把人类的办事流程模板化了吗”——你查资料、写结论、发现资料不够、再查、再修正本质上就是这么个循环。但难点在于怎么把这些步骤用提示词约束住怎么让模型输出的动作能被程序可靠地解析执行怎么让循环不失控。这篇文章会把设计思路、实操模板、避坑经验全部摊开来讲适合刚接触 Agent 开发、想从零搭建一个可运行的 ReAct 模型的开发者也适合已经写了一版 demo但总在试错的朋友对照排查。2. 整体设计思路拆解为什么是“推理–行动”交替而不是一步到位2.1 和两种极端方案的对比CoT 只动嘴Act-only 只动手在 ReAct 出现之前业内有两类主流做法。一类是思维链Chain-of-ThoughtCoT风格让模型把推理步骤写出来再接最终答案。它的优点是把复杂问题拆小了模型每一步的压力不大缺点也明显——模型算是一路“想”到了答案但没有验证手段。它说“明天第一班航班最稳”它并不知道第一班航班是不是真的延误因为全程没有接入查数据的动作。这就像一个人坐在屋里闭门写方案写得再漂亮和外面实际情况对不上也没用。另一类是纯动作式Act-only直接让模型输出工具调用拿到结果后继续调用。这种模式效率高但缺少中间的判断层。模型可能拿到一个不够准确的数据就直接进入了下一步或者工具报错了它也不知道换个思路再来。就像一个人只会机械地拨打电话但拨通以后该问什么、对方答的哪句话值得追问完全没有章法。ReAct 的设计恰恰是把这两者的优点缝到了一起推理负责“决定下一步干什么以及为什么”动作负责“真的去干”观测Observation负责“把结果拿回来验证”。每一步都留下文字痕迹模型能自我反思开发者也能排查问题。代价是每一步多花一些 token、多一两次请求但对大多数实际场景来说这个代价换来的准确率和可控性完全是值得的。2.2 核心循环机制Thought–Action–Observation 是怎么转起来的ReAct 的单步循环可以抽象成三个字段Thought思考当前这一步的判断。比如“用户需要知道明天三趟航班中哪一趟最可能准点那我需要先拿到三趟航班的实时状态”。Action动作决定调用哪个工具、传什么参数。比如“search_flight_status(flight_noCA1234, date2025-01-15)”。Observation观测工具执行后返回的结果。这一步不是模型生成的而是程序真实执行动作后把结果拼回给模型看的。模型每轮只生成 Thought 和 Action程序负责执行 Action 并构造 Observation再继续让模型生成下一轮 Thought。这个“生成–执行–回填”的循环一直持续直到模型认为信息够了不再输出 Action而是输出 Final Answer。拿生活场景类比这就是一个“先想一步再走一步用这一步看到的结果决定下一步怎么走”的过程。它不是让模型一开始就规划好全程而是每一步都基于最新观测做决策——这正是它应对不确定性的关键。2.3 为什么不用一次规划全部步骤动态修正才是 Agent 的护城河有人可能会问反正模型上下文能承载不少内容能不能让它先一次性生成完整的执行计划Plan然后按计划执行这其实是另一种思路——Planner-Executor 架构。但 ReAct 模式没有这样做的原因是真实环境充满变数。比如查询接口返回异常、某个航班的数据缺失、用户中途补充了新条件“价格别超过八百”如果计划是固定生成的遇到这些情况要么中断要么只能按错的计划硬跑。而“想一步做一步”的循环天然有韧性——每一步都是对上一步结果的回应。计划可能粗略但执行过程中模型的方向感是靠真实观测来校正的。这就像你出门前不会把“到哪个路口往哪拐、路上要打几次方向盘”全部想好而是开到路口看到路牌再决定往哪拐。开车靠的是路牌反馈Agent 靠的是工具返回的 Observation。3. 环境准备与模板搭建把 ReAct 的“循环骨架”先立起来3.1 整体技术选型模型、工具和编排框架怎么配在实际实现中ReAct 并不需要特别重的框架支撑。核心就三部分一个支持通用指令遵循的大模型聊天补全接口即可、一组可以被调用并返回文本结果的外部工具、一个负责循环调度和文本解析的编排层。我的建议是第一版不要一上来就上重量级 Agent 框架先用最简单的方式把循环逻辑写透跑通一条完整链路以后再考虑抽象和封装。具体来说模型端选择通用对话模型就好关键是上下文长度要够用至少把几轮 Thought/Action/Observation 装下并且输出格式可控。实测下来只要能稳定输出指定格式的 JSON都能跑 ReAct。工具端第一版挑 2-3 个纯函数式工具最好。比如“查询城市天气”“计算两个日期差”“从本地 CSV 读某列统计值”。工具返回结果统一转成纯文本字符串方便拼接进对话。编排层用普通代码写一个 while 循环加解析函数就够不需要引入复杂状态机。循环体做四件事把对话历史拼成 prompt 发给模型、解析模型输出、执行动作、把 Observation 拼回对话。这里有个容易被忽略的配置点模型温度。ReAct 对温度非常敏感温度太高模型容易在输出里加一些和格式无关的话导致解析失败温度太低又可能让思考模式僵化。我实际用的比较多的是 0 到 0.3 之间在可控性和一点随机性之间取平衡。3.2 Few-shot 示例怎么写先给模型做一个“三件套”示范ReAct 的提示词里最核心的部分是一组“案例演示”few-shot examples。目的是让模型明白 Thought、Action、Observation 三者怎么配合而不是靠一句“请你一步一步思考”空泛引导。一个标准的 few-shot 段落通常长这样这里用伪代码表达用户问题明天北京和上海的最高气温分别是多少哪个城市更适合户外跑步 Thought: 需要先获取两个城市明天的天气数据分别查询北京和上海。 Action: get_city_weather(city北京, date明天) Observation: 北京明天多云最高气温 26 摄氏度微风。 Thought: 北京气温 26 度条件不错。继续查上海。 Action: get_city_weather(city上海, date明天) Observation: 上海明天阵雨最高气温 31 摄氏度湿度较大。 Thought: 上海有雨且湿度大不适合户外跑步北京多云微风条件更好。 Action: finish(北京)看到关键点了吗每个 Thought 都是对上一条 Observation 的直接反应每个 Action 都只做一件事工具参数必须具体。这既是给模型做示范也等于给模型立了一条行为准则一次只走一步每步都有依据。3.3 动作空间约束不是所有工具都能随便调另一个关键设计是动作空间Action Space的明确约束。提示词里应当写清楚“你可以使用以下工具列表”并对每个工具附上简要说明和参数要求。比如get_city_weather(city, date)查询指定城市指定日期的天气情况city 为城市中文名date 为“明天”或具体日期。calculate_date_diff(date1, date2)计算两个日期的间隔天数。search_flight(flight_no, date)查询航班计划与实时状态。约束动作空间有两个作用一是防止模型脑补出根本不存在的工具二是让后续的动作解析逻辑简单可控。如果你不给工具列表模型偶尔会输出一些“像那么回事”但实际无法执行的函数名程序只能干瞪眼。注意每轮 prompt 中都要把这套 few-shot 和工具列表完整带上。ReAct 依赖上下文中的示例来约束格式删掉示例以后格式稳定性会立刻下降不要省这个 token。4. 实操演示用“想一步做一步”搭一个天气对比 Agent4.1 一个具体可跑的循环样例为了不空谈理论下面用一个简单但完整的例子演示整个 ReAct 循环如何一步步运行。假设工具库里有两个函数def get_city_weather(city: str, date: str) - str: # 实际项目里这里会调天气接口这里用模拟数据代替 data { (北京, 明天): 多云最高 26 度风力 2 级, (上海, 明天): 阵雨最高 31 度湿度 85%, (广州, 明天): 雷阵雨最高 33 度湿度 90%, } return data.get((city, date), 查无数据) def is_good_for_run(weather_info: str) - str: if 雨 in weather_info or 湿度 8 in weather_info: return 不适合户外跑步 return 适合户外跑步用户提问“明天北京、上海、广州三个城市里最适合户外跑步的是哪个”循环第一轮模型读入用户问题和 few-shot 示例后输出Thought: 我需要分别获取三个城市明天的天气信息先从北京开始。 Action: get_city_weather(city北京, date明天)程序解析出 Action 里的函数名和参数执行get_city_weather(北京, 明天)得到文本“多云最高 26 度风力 2 级”然后把它作为 Observation 拼回对话再发给模型。第二轮模型看到北京天气后会继续查上海第三轮查广州。三轮之后模型已经拿到三份天气观测进入收尾阶段Thought: 北京多云 26 度上海阵雨湿度高广州雷阵雨。综合看北京最适合户外跑步。 Action: finish(最终答案北京)这里我特意设计了一个finish动作代替常规 Final Answer 输出。原因是统一把“结束”也作为动作处理能让解析逻辑只面对一种模式——看见finish就停止循环否则继续。比同时兼容“输出 Final Answer 文字段”和“输出 Action 字段”两种状态要省心得多。4.2 解析与循环控制的四个关键细节解析这步看起来简单实际踩坑最多。我总结四个容易出问题的细节格式解析要宽容模型偶尔会在 Action 前后多输出解释性文字比如“我先查一下北京的天气Action: get_city_weather(...)”。用正则宽松提取Action:\s*(\w\(.*?\))通常比严格 JSON 解析更稳。但注意参数内部不要包含右括号否则正则会提前截断。循环要有上限必须设置max_iterations我一般设置在 6 到 8 轮。原因很直白模型可能一直在循环里绕不出来比如反复查同一个工具、参数始终不对。没有上限的循环是生产环境里最可怕的隐患——不仅浪费 token还可能在无人值守时把接口打爆。历史记录要完整拼接模型不具备自己的记忆每一轮请求都得把之前所有的 Thought/Action/Observation 按顺序拼接好再发回去。一个常见的低级错误是只发最近一轮导致模型失去前文信息行为立刻变得混乱。工具报错也是 Observation如果工具抛出异常不要把异常抛出中断程序而是把错误信息文本化后作为 Observation 返回给模型让模型自己决定怎么应对。比如天气接口返回“超时”模型可能会改一个时间范围重试或者换一个数据源。这一步是 Agent “看起来聪明”的关键它学过“收到错误可以不慌”。4.3 完整运行记录长什么样为了让你更直观地看到全貌这是我实际跑通一个类似 Demo 时的对话记录简化版用户: 明天北京、上海、广州哪个最适合户外跑步 Thought: 需要三个城市的天气数据先查北京。 Action: get_city_weather(北京, 明天) Observation: 多云最高 26 度风力 2 级 Thought: 北京天气不错继续查上海。 Action: get_city_weather(上海, 明天) Observation: 阵雨最高 31 度湿度 85% Thought: 上海有雨湿度高不适合跑步。继续查广州。 Action: get_city_weather(广州, 明天) Observation: 雷阵雨最高 33 度湿度 90% Thought: 广州雷阵雨更不适合。综合对比北京天气最好。 Action: finish(最终答案北京)整个过程清晰可见模型没有在一开始就猜“哪个城市最好”而是走一步、看一步用三次观测支撑最终判断。这也让使用者或调试者能一眼看出模型每一步的依据是否合理——这种透明性是直接端到端生成答案做不到的。5. 常见问题与排查技巧实录5.1 模型陷入重复循环反复调用同一个工具这是 ReAct 模式里我遇到过最多的一个问题。现象是模型每轮都输出几乎一样的 Action比如反复查同一个城市的天气哪怕 Observation 没变化。排查思路分三步先看是不是工具返回的 Observation 信息量太低。如果 Observation 永远只是“查询失败请重试”模型除了重试确实没有更好的选择。解决方式是改进工具返回让报错信息更具体“查询失败城市名或日期格式错误请传入标准城市中文名”。再看提示词里 few-shot 是否展示了“不同选择”。如果你的示例全都是顺序执行同一个工具模型学到的最优策略就是继续查下一个同样的工具。适当在示例中加入一次“上一步查询结果不理想所以更换策略”的示范能明显减少死循环。最后检查是否缺少finish引导。有些模型不知道什么时候该收手会默认“只要还能调用工具就继续调用”。在 few-shot 里专门放一个“信息已足够直接完成”的示例比在系统提示里强调一百遍“别循环了”都有效。5.2 模型输出格式不稳定Action 解析经常报错格式问题是新手最容易受挫的地方。明明 few-shot 里写的很清楚Action: func(arg)模型却输出成Action: 调用函数 func(arg)或者后面跟了一堆解释。我实践的解法是“双层兜底”。第一层提示词里明确说明“Action 必须严格遵循工具列表中的函数名和参数格式不要包含解释不要使用任何自然语言描述。”第二层解析代码里做容错处理先用正则识别出函数名(参数)片段找不到再尝试从文本里提取形如工具名 参数的片段。这里推荐一个技巧在工具列表描述里把参数类型也写清楚比如“date: string, 支持格式今天/明天/YYYY-MM-DD”。参数越明确模型就越不容易自由发挥。如果上述方式仍然不稳还有一个杀手锏要求模型把整轮输出放在 JSON 里例如{thought: ..., action: {name: ..., args: {...}}}但代价是偶尔会遇到括号嵌套或引号转义问题。对生产项目我还是倾向于“提示词约束 宽松正则解析”的组合整体稳定性更好。5.3 模型幻觉 Observation它根本没查工具直接编了个结果这个问题比格式问题更隐蔽。现象是模型没调用任何 Action直接输出了 Final Answer而且答案里带着貌似具体的数字或状态。深挖对话记录会发现这些“数据”其实是模型根据常识猜测的压根不是工具返回的。产生原因通常有两个一是 few-shot 示例让模型误以为“某些情况可以不调用工具直接回答”二是模型对用户问题判断失误觉得问题简单到不需要工具。解决办法是强化规则“所有回答都必须基于 Observation 中出现的信息任何没有在 Observation 中出现的数据都不允许出现在最终答案里。”这句话要在系统提示词里反复写。同时在 few-shot 中加入一个反例——模型一开始想直接猜答案然后自己否决了这个思路转而调用工具——这种“纠偏”示范能有效告诉模型宁可多走一步查询不要凭空编造。提示排查这类问题的时候把对话里所有Thought单独拉出来读一遍比盯着 Action 更容易找到问题根源。因为大多数“编数据”的行为会在 Thought 里留下蛛丝马迹比如“根据我的经验……”“我认为大概是……”看到这样的念头出现就要警惕最终答案是否可验证。5.4 工具参数太复杂模型总传不对当工具参数从一两个扩展到五六个甚至嵌套结构时模型传参出错的概率会明显上升。比如一个book_flight(from, to, date, seat_class, passenger_name)模型经常把机场代码和城市名搞混或者日期格式写错。我的应对策略是“拆工具”。不要试图让模型一步调用一个巨大函数而是把复杂动作拆成几个语义明确的小函数。比如先search_flights(from, to, date)拿到航班列表再get_flight_detail(flight_id)拿到具体信息最后book_flight(flight_id)下单。单步动作输入越简单模型出错概率越低。这本质上就是“想一步做一步”的精髓把大动作拆小每一步都有验证机会。如果实在不能拆就给每个复杂参数准备一个“归一化函数”。比如不管模型传的是“北京”“BJ”还是“PEK”归一化层统一转成标准代码。这套做法在生产环境里简直救命——与其要求模型永远不出错不如把容错放到代码侧。6. 从 Demo 到可用ReAct 实战进化的几条经验6.1 把工具返回变成“有观点”的文本而不是原始数据很多工程实现容易忽略一件事Observation 不只是给程序看的更是给模型“读”的。工具返回的原始数据往往不够友好。比如天气接口返回 JSON{code: 200, data: {temp: 26, cond: cloudy}}模型要自己翻译这串结构容易出错。我建议在工具函数内部就做一层“文本化包装”把结构化数据转成自然语言描述。上面的 JSON 可以包装成“北京明天多云最高气温 26 摄氏度”。模型生成下一步 Thought 时就不需要做额外解析准确率自然提升。这也引出一个设计原则工具的输出要以“服务于模型下一步决策”为目标而不是以“服务于程序接口规范”为目标。最开始我犯的错就是把原始 JSON 直接丢给模型结果模型总在一些不重要的字段上打转。6.2 使用记忆压缩循环过长时不能无限堆上下文ReAct 的循环轮数一旦超过十几步对话历史会变得非常长token 消耗上涨模型也可能被前面的干扰信息带偏。这时候就需要做记忆压缩把旧的 Thought 和 Observation 概括成一段摘要再接新的交互。我的做法是设置一个窗口大小比如最近 6 轮完整保留更早的部分每 2 轮合并成一句摘要。比如“此前已确认北京天气晴好、上海有雨广州数据待查”。这样既能保住关键信息链又能避免上下文过长引发理解漂移。这个压缩动作本身也可以看作一个 Agent 任务——有的框架会专门用一个轻量模型做摘要效果比简单截断更好。6.3 不同模型对 ReAct 的适配差异最后聊一个实战选型话题不是广告单纯是经验。不同模型对 ReAct 的指令遵循能力差异很大。有的模型写 few-shot 就能稳定跑循环有的模型必须借助系统提示强调“你是一个工具调度器只能输出规定字段”。我的建议是在选定模型前用一个统一的测试集把两三轮循环跑通观察格式稳定性、幻觉频率和循环收敛速度。选型时优先看重三点一是输出格式稳定性不是智商而是格式纪律二是长上下文下的记忆保持能力三是对“信息不足时应继续查询”的理解力。这三个点直接决定了 ReAct 循环能不能跑得顺。单轮问答的模型“聪明程度”和 Agent 场景下的表现并不完全正相关。有些模型单轮答题很惊艳但多轮循环里会逐渐偏题反而是些朴素但纪律强的模型在 ReAct 场景下表现更可靠。最后再分享一个体会ReAct 模式最迷人的地方不是它让模型“变聪明”而是它创造了一套可观测、可干预、可调试的“人机协作回路”。模型每想一步、每做一步都有明确依据可以追溯。这套回路里需要你仔细打磨的不是某个精密算法而是那些看似不起眼的提示词细节、工具返回设计和解析策略。照着前面的方法把第一版跑起来你会在调试中快速积累起自己的判断力——这个过程本身就是做 Agent 开发最值钱的经验。