Agent运行模式对比:ReAct、Plan-and-Execute与状态机选型指南
发布时间:2026/10/17 16:05:11 锦皓数字建站

简介这份源码资源面向AI智能体设计与开发的学习者聚焦ReAct推理-行动与Plan-and-Execute规划-执行两种主流运行模式的对比分析。内容涵盖两种模式的核心思想、工作流程、优劣势及适用场景并给出根据项目需求选择模式的建议同时提出可结合使用两种模式以实现更灵活策略的思路适合希望深入理解智能体架构、提升任务编排能力的中级开发者参考。资源包共3个文件包含1个inscode工程配置、1个html页面和1个gitignore忽略规则文件压缩包约10KB结构精简便于快速导入查看与二次修改。目前已有58人学习下载。通过这份源码读者可直观对照两种模式的实现差异理解动态任务环境下ReAct的即时调整优势以及目标明确场景下Plan-and-Execute的全局规划价值为智能体项目选型与代码实践提供参考。1. Agent运行模式对比为什么同样一个任务换个循环方式就翻车很多人第一次写 Agent都是从一个while True开始的把用户输入丢给模型模型返回一段文本解析出工具调用执行把结果塞回上下文再循环。跑通 demo 那一刻很爽但只要任务稍微长一点你就会遇到三种典型症状上下文越滚越大直到超限、同一个工具被反复调用、模型开始忘记最初的目标。这时候你才会意识到Agent 的运行模式不是语法问题而是架构问题。所谓 Agent 运行模式指的是模型思考—工具执行—结果回灌这个循环由谁驱动、状态存在哪里、什么时候终止。常见的有 ReAct 式单轮循环、Plan-and-Execute 两段式、以及带显式状态机的编排模式。源码层面它们的差别其实很小无非是循环体里多几个分支但行为差异巨大。这篇笔记面向已经能跑通单轮工具调用、想搞清楚该选哪种循环的开发者我会把三种模式的源码骨架、参数取舍和踩坑点摊开讲让你能照着改自己的项目。2. 三种运行模式的源码骨架与选型依据在动手改代码之前先把三种模式的循环体长什么样讲清楚。选型不是看哪个高级而是看你的任务有没有中途需要重新规划的需求。下面每一段都是可以直接抄进项目的最小骨架我刻意去掉了框架依赖只用最朴素的 Python 表达控制流这样你能看清每种模式真正多出来的是什么。2.1 ReAct 单轮循环最小可用但最容易失控ReAct 的核心是想一步、做一步模型每轮输出一段包含 Thought 和 Action 的文本执行完把 Observation 拼回上下文进入下一轮。它的源码骨架就是一个带最大轮数保护的 whiledef react_loop(task, tools, max_steps8): # 上下文只维护一条消息列表所有历史都往里塞 messages [{role: user, content: task}] for step in range(max_steps): # 每轮都把完整历史发给模型让它决定下一步 reply call_llm(messages, tool_specstools) action parse_action(reply) # 解析出工具名和参数 if action is None: # 模型直接给最终答案 return reply # 执行工具把结果作为 observation 回灌 observation run_tool(action.name, action.args) messages.append({role: assistant, content: reply}) messages.append({role: tool, content: observation}) return 达到最大步数仍未完成 # 兜底防止死循环这段代码里最关键的两个参数是max_steps和上下文裁剪策略。max_steps一般设 6 到 10设太小复杂任务做不完设太大一旦模型陷入循环就是烧钱。上下文裁剪我一般用保留首条任务 最近 N 轮的方式因为中间那些失败的尝试对后续决策价值很低留着只会干扰模型。ReAct 适合工具数量少、任务步骤在 5 步以内的场景比如查资料、简单数据查询。它的软肋是一旦某一步走错模型很难跳出来重新规划因为它没有全局计划这个概念只能顺着错误往下滚。2.2 Plan-and-Execute 两段式先出计划再逐步执行Plan-and-Execute 把一次循环拆成两个阶段先让模型产出一个步骤列表然后逐个执行每执行完一步可以选择性地重新规划。源码上它比 ReAct 多了一个 plan 数据结构和一层外层循环def plan_execute(task, tools, max_replan3): plan call_llm(build_plan_prompt(task, tools)) # 阶段一产出步骤列表 plan parse_plan(plan) # 解析成 [步骤1, 步骤2, ...] results [] for i, step in enumerate(plan): # 阶段二把当前步骤和已有结果一起发给模型执行 ctx build_step_context(task, plan, results, step) output call_llm(ctx, tool_specstools) results.append(run_step(output, tools)) # 每步之后判断是否需要重新规划 if need_replan(results) and max_replan 0: plan call_llm(build_replan_prompt(task, plan, results)) plan parse_plan(plan) max_replan - 1 return summarize(task, results)这里有两个必须调的参数max_replan和need_replan的判定逻辑。max_replan我一般设 2 到 3因为重新规划本身也要花一次模型调用太频繁反而拖慢整体。need_replan的判定我常用一个轻量规则如果当前步骤执行结果里包含失败未找到错误这类信号就触发重规划而不是每步都问模型一次。Plan-and-Execute 适合步骤明确、可以提前拆解的任务比如整理一份报告这种有固定流程的活。它的代价是首轮规划如果错了后面全歪所以规划提示词里一定要把可用工具和约束写清楚。2.3 状态机编排模式把控制权从模型手里拿回来前两种模式的控制流都由模型输出驱动状态机模式则相反开发者预先定义好状态和转移条件模型只在特定节点被调用。源码上它表现为一个显式的状态字典加转移表# 状态定义每个状态对应一个处理函数 STATES { parse: handle_parse, # 解析用户意图 retrieve: handle_retrieve, # 检索资料 act: handle_act, # 执行工具 verify: handle_verify, # 校验结果 done: handle_done, } def state_machine_run(task, max_transitions12): state parse ctx {task: task, history: []} for _ in range(max_transitions): handler STATES[state] ctx, next_state handler(ctx) # 处理函数返回下一个状态 ctx[history].append((state, next_state)) if next_state done: return ctx[result] state next_state return 状态转移超限状态机模式的关键参数是max_transitions和每个状态的退出条件。它的好处是行为可预测、可测试每个状态都能单独写单元测试出问题能精确定位到哪个转移。代价是灵活性差遇到没预设过的分支就卡住。我一般把它用在生产环境里那些流程固定但需要模型做局部判断的场景比如客服工单处理。三种模式的选型可以对照下面这张表模式控制权适合任务主要风险典型步数上限ReAct模型短链路、探索型循环、上下文膨胀6-10Plan-and-Execute模型计划可拆解、流程型首轮规划错误计划步数×2状态机开发者固定流程、生产分支覆盖不全8-123. 把三种模式跑起来最小可复现工程与参数调优光看骨架还不够得让它们真的跑起来才能对比。这一章我用一个统一的工具集和统一的测试任务把三种模式各跑一遍重点讲清楚每个参数改了之后行为怎么变。所有代码都基于同一个call_llm和run_tool抽象这样对比才公平。3.1 统一工具层与模型调用封装先搭一个最小工具层三个工具足够暴露问题查天气、算数、查数据库。模型调用我用一个带重试和超时的封装因为 Agent 循环里任何一次调用失败都会让整个流程崩掉import time, json def call_llm(messages, tool_specsNone, retries2, timeout30): # 统一入口所有模式都走这里方便对比 for attempt in range(retries 1): try: resp llm_client.chat( messagesmessages, toolstool_specs, timeouttimeout, ) return resp.content except TimeoutError: if attempt retries: raise time.sleep(1.5 ** attempt) # 指数退避避免雪崩 return def run_tool(name, args): # 工具执行统一加异常兜底返回字符串而非抛异常 try: if name get_weather: return json.dumps(fetch_weather(args[city])) if name calc: return str(eval(args[expr])) # 仅演示生产禁用 eval if name query_db: return json.dumps(query_db(args[sql])) except Exception as e: return f工具执行失败: {type(e).__name__} return 未知工具这里有两个容易被忽略的点。第一run_tool一定要把异常转成字符串返回而不是往上抛因为模型需要看到失败信息才能决定下一步抛异常会直接中断循环。第二call_llm的重试要带退避Agent 循环里模型调用密集不加退避在限流时会把整个任务拖死。工具描述tool_specs里的参数说明要写得像给新人看的文档模型解析参数的准确率跟描述质量强相关这一点在三种模式里都一样。3.2 用同一任务对比三种模式的实际行为测试任务我选一个能体现差异的查一下某城市今天天气如果温度高于 30 度就提醒带伞防晒否则提醒正常出行。这个任务有分支判断能看出三种模式的处理差异。跑 ReAct 时模型通常会在第一轮调get_weather第二轮根据结果给建议两到三步结束。跑 Plan-and-Execute 时它会先规划出查天气→判断温度→给建议三步然后逐步执行步数更多但过程更清晰。跑状态机时parse状态识别意图retrieve状态调天气工具verify状态判断温度分支done输出全程可控。对比时重点看三个指标总模型调用次数、总 token 消耗、以及失败后的恢复能力。我实测下来简单任务 ReAct 调用次数最少Plan-and-Execute 多出一次规划调用状态机因为每个状态都要过一次模型调用次数最多。但任务一旦变复杂ReAct 的调用次数会指数上升因为它每步都要重新理解全局而 Plan-and-Execute 和状态机的增长是线性的。这就是选型的核心依据任务复杂度决定模式。3.3 关键参数怎么设轮数、温度与上下文窗口参数调优这块我踩过不少坑直接给结论。max_steps和max_transitions这类轮数上限经验值是预期步数 × 1.5留出重试空间但别太松。温度参数在 Agent 里要分场景规划阶段用 0.2 到 0.4让计划有一定多样性执行阶段用 0 到 0.2保证工具参数稳定。上下文窗口管理上ReAct 模式必须做裁剪我一般保留最近 6 轮加首条任务Plan-and-Execute 可以把计划单独存不占对话上下文状态机模式每个状态独立上下文天然不会膨胀。还有一个参数是工具调用的并发度。ReAct 里如果模型一轮返回多个工具调用可以并发执行但要注意有依赖关系的工具不能并发。我一般先做依赖分析无依赖的并发有依赖的串行。这个逻辑在三种模式里都要写但状态机模式最容易实现因为依赖关系可以在状态转移图里提前定义好。4. 避坑与排查Agent 循环里最常见的五类翻车这一章是我血泪经验最集中的地方。Agent 的 bug 不像普通程序那样报错它往往是看起来在跑结果不对排查起来很费劲。下面五条按出现频率排序每条都按现象、原因、解决三步写。4.1 模型反复调用同一个工具现象是日志里同一个工具被连续调用五六次参数几乎一样。原因通常是工具返回的结果格式模型没看懂或者返回内容为空模型以为没执行成功就重试。解决办法有两个一是工具返回一定要带明确的状态字段比如{status: ok, data: ...}让模型能判断成功与否二是在上下文里加一句同一工具同一参数不要重复调用的约束。我一般两个都做双保险。4.2 上下文超限导致任务中断现象是跑到一半报 token 超限或者模型开始胡言乱语。原因是 ReAct 模式把所有历史都堆在上下文里轮数一多必然爆。解决办法是分层裁剪首条任务永远保留最近 N 轮保留中间的历史压缩成一句摘要。摘要可以用一次便宜的模型调用生成成本远低于超限重跑。Plan-and-Execute 和状态机模式这个问题轻很多因为它们的状态不都在对话上下文里。4.3 工具参数解析失败现象是模型输出的工具调用参数缺字段或类型不对执行时报错。原因是工具描述里参数说明太模糊或者模型在长上下文里忘记了参数格式。解决办法是把工具描述写成带示例的 JSON Schema参数名用下划线命名保持一致必填字段明确标注。另外在解析层加一层校验参数不对时把错误信息回灌给模型让它重试而不是直接崩。4.4 状态机卡在某个状态出不来现象是状态机模式在某个状态反复转移max_transitions耗尽。原因是转移条件写得太宽或者某个状态的退出条件依赖外部数据但数据一直没到。解决办法是给每个状态加独立的超时和重试计数超过阈值强制转移到兜底状态。状态机的好处就是这种问题容易定位看转移日志就知道卡在哪比 ReAct 的黑匣子好排查得多。4.5 重规划触发过于频繁现象是 Plan-and-Execute 模式里模型不停重新规划任务迟迟不推进。原因是need_replan判定太敏感把正常的中间结果也当成失败。解决办法是把判定规则收紧只在明确的失败信号工具报错、结果为空、校验不通过时触发并且给重规划加次数上限。我一般还会在重规划提示词里带上已尝试过的方案避免模型规划出重复的步骤。5. 进阶技巧用轨迹日志反推该换哪种模式写到这我想分享一个自己常用的判断方法不要凭感觉选模式而是先用最简单的 ReAct 跑一批真实任务把每一步的轨迹完整记下来然后看日志决定要不要升级。轨迹日志至少记四个字段轮次、模型输出、工具调用、工具返回。跑完一批任务后统计三个数字平均轮数、重复工具调用率、失败恢复率。如果平均轮数超过 8 且重复调用率高说明任务需要提前规划该换 Plan-and-Execute如果任务流程固定但模型判断点很多该换状态机。轨迹日志的落地很简单在循环体里加一个 append 就行def log_trace(trace, step, reply, action, observation): # 每轮记录一条方便事后分析模式是否合适 trace.append({ step: step, reply: reply[:200], # 截断避免日志过大 action: action.name if action else None, args: action.args if action else None, observation: observation[:200], })分析时重点看两类模式信号一是模型输出里反复出现相似的思考这是 ReAct 缺全局计划的表现二是工具调用序列高度固定这是可以固化成状态机的信号。我一般会拿 20 到 30 条真实任务跑一遍统计完再决定架构比拍脑袋靠谱得多。还有一个技巧是把轨迹日志喂给模型让它自己分析这个任务更适合哪种模式虽然不能全信但能提供一些你没注意到的视角。最后说个我自己的习惯任何 Agent 项目我都会先写 ReAct 版本跑通再根据轨迹日志决定要不要重构成另外两种。不要一上来就上状态机那会让你在还没搞清楚任务真实形态时就过度设计。模式是手段不是目的能稳定完成任务的循环就是好循环。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。