简介:面向AI智能体设计与软件开发者的源码资源包,聚焦ReAct(推理-行动)与Plan-and-Execute(规划-执行)两种主流运行模式,系统梳理了二者的核心思想、工作流程、优劣势及适用场景。资源通过简洁的HTML演示页面,直观展示ReAct边推理边行动、可即时调整的特点,以及Plan-and-Execute先规划后执行、全局清晰的优势,帮助读者快速识别动态任务与结构化任务之间的选型差异。包内共3个文件,包含HTML演示源码、inscode配置及gitignore辅助文件,整体仅10KB,轻量易读,适合作为学习模板或二次开发基础。读者还可以借助源码中的示例,进一步理解两种模式结合使用以提升策略灵活性的思路,并参考其中关于按任务动态变化或步骤明确程度选择模式的建议。目前已有53人学习下载,对于正在调研智能体架构、对比模式差异或快速搭建演示原型的开发者,是一份省时高效的入门资料。 把Agent的Prompt调好,只是迈过了第一道门槛。真正决定它能不能稳定跑在业务里的,是运行模式。我最近整理了一版Agent运行模式的对比笔记,把每种模式的核心逻辑抽成了可复现的源码片段,今天拿出来聊一聊。文章默认你已经有LLM调用和工具调用的基础,但不要求用过任何特定框架。我会按单轮直答、ReAct、Plan-and-Execute、反射、多Agent协作几种常见模式依次拆解,穿插源码和踩坑记录,最后给一张选型对照表。想自己写Agent而不是只会调框架的话,这篇应该能省不少时间。
1. Agent运行模式到底在对比什么
1.1 为什么“跑通”不等于“跑好”
很多人第一次跑通Agent时特别兴奋,但放到真实场景里,问题会一个接一个冒出来:同样一个任务,有时结果正确,有时完全跑偏;工具被莫名其妙调用几十次;上下文越来越长,最终超限。这些问题十有八九不是模型能力不行,而是运行模式没选对。
运行模式本质上决定三件事:模型在一个任务里被调用几次、调用之间如何传递信息、流程控制权在模型手里还是在你写的代码手里。单轮直答模式把决策权完全交给模型,一次生成最终答案;ReAct模式把控制权分散到每一轮思考和动作之间;Plan-and-Execute模式把“拆任务”和“执行子任务”分开,用流程框住模型。这三者在代码层面的差异很小,行为差异却极大。
我自己的经验是,任何Agent项目的第一个版本,都应该先用最简单的模式跑通,再观察失败案例集中在哪,逐步升级。直接上多Agent协作看起来很酷,但排查问题时会很痛苦。模式选对了,平庸的模型也能交出稳定结果;选错了,再强的模型也救不回来。
1.2 从输入输出看模式的本质差异
对比运行模式有一个很实用的视角:看每次调用LLM时,messages列表里到底放了什么。单轮模式通常只有一条user消息加一条assistant消息;ReAct的messages里会交替出现thought、action、observation,每轮都要把之前所有步骤重新发给模型;Plan-and-Execute一开始就把完整计划放进上下文,执行过程中再逐步追加每步结果;反射模式把上一轮答案和评价一起发给模型,让它修正。
所以“运行模式”和“上下文管理策略”几乎是同一个概念。选模式之前,先想清楚三个问题:任务需要几步?每一步是否依赖外部工具的结果?能不能容忍模型犯错并自己纠正?这三个问题想清楚了,模式自然就出来了。举个例子,要做“根据用户问题查询订单状态”的客服Agent,单轮模式只能让模型生成一段看起来合理的SQL,它不会真的去查;换成ReAct,模型会先调用查订单工具,再把真实结果组织成回答。单轮只能“说”,ReAct才能“做”,但代价是每次工具调用都要把历史重新发给模型,Token成本成倍上涨。
2. 三种主流运行模式的源码级拆解
2.1 单轮直答模式:最简单也最容易低估
单轮直答模式的源码短到有点不好意思:
def run_single_round(prompt: str, llm_func) -> str: messages = [{"role": "user", "content": prompt}] response = llm_func(messages) return response[-1]["content"]严格来说这不算Agent,但它是所有Agent模式的地基,适用范围远比想象中广。文本分类、信息抽取、格式转换、翻译、简单问答,单轮模式完全够用,延迟低、成本低、结果可预期,出了问题也最好排查。
这里有个容易被忽略的点:单轮模式也能调用工具,只是调用逻辑写在你的业务代码里。比如先调一个模型判断意图,再根据意图走不同业务分支,这是一种“外部编排的单轮模式”。LLM只负责其中一小段,流程控制交给硬编码。这种方案可靠且简单,适合大部分生产场景。很多开发者一上来就上ReAct,觉得Agent就得让模型自主决策,但我见过太多项目因为ReAct不可控,又改回硬编码路由。我的建议是:能硬编码的路由,就不要让模型自由发挥。
2.2 ReAct模式:边想边做,用源码看推理循环
ReAct(Reasoning and Acting)的思路是让模型在推理和行动之间交替,后来成了各类Agent框架的主流。核心代码可以浓缩成一个while循环:
def run_react(task, llm_func, tools, max_steps=10): messages = [{"role": "user", "content": task}] action_map = {t.name: t for t in tools} for step in range(max_steps): response = llm_func(messages) messages.append({"role": "assistant", "content": response}) action = parse_action(response) if action["type"] == "finish": return action["answer"] tool = action_map.get(action["name"]) if tool is None: observation = f"Error: unknown tool '{action['name']}'" else: try: observation = tool.run(**action["args"]) except Exception as e: observation = f"Error: {e}" messages.append({"role": "user", "content": f"Observation: {observation}"}) return {"error": "max_steps exceeded", "messages": messages}这段代码有四处细节决定它能不能真跑。第一,parse_action要容错。模型输出的Action经常不规整,可能带Markdown代码块,可能把参数写成JSON数组。我一般让模型严格按“Action: 工具名\nAction Input: JSON”的格式输出,同时写一个parse函数兼容常见格式错误。第二,Observation必须短。工具可能返回几千行日志,直接丢给模型会让下一轮上下文爆炸。我会在工具层截断到500字,并注明这是截断结果。第三,max_steps不能省。模型很可能陷入“调用工具->拿到结果->再调用同样工具”的死循环,没有上限,Token会烧到你怀疑人生。第四,未知工具和异常也要作为Observation返回,让模型自己修正,这比外部校验更自然。
ReAct最适合“任务步骤不明确,必须根据中间结果动态决定下一步”的场景,比如“查这个仓库里最近更新的Python文件”,模型需要先列目录、再看文件、再读内容,每一步都依赖上一步输出。这种任务用单轮完全做不了,用Plan-and-Execute又因为步骤不确定而计划不准。代价是Token消耗随时间线性增长,优化手段是定期把早期Thought和Observation压缩成摘要,只保留最近几步的完整内容。
2.3 Plan-and-Execute模式:先计划再执行
Plan-and-Execute和ReAct最大的区别,是先让模型生成完整执行计划,再逐条执行。核心代码:
def run_plan_execute(task, planner, executor, max_steps=10): plan = planner(task) current_plan = plan[:max_steps] executed = [] for i, step in enumerate(current_plan): result = executor(step, plan=plan, executed=executed) if result.status == "need_replan": new_plan = planner(task, feedback=result.error, plan=plan, executed=executed) current_plan[i:] = new_plan plan = current_plan if result.status == "done": executed.append(result.output) return synthesize(executed)planner和executor一般复用同一个LLM,但system prompt完全不同。planner只负责输出步骤列表,不执行;executor只按步骤执行,不能跳步。这个模式的优势是可预测,出了问题可以直接定位到某一步,也方便并行执行无依赖的子任务。我经常用它做“数据清洗->分析->生成报告”这类多阶段任务。
但计划本身可能出错,这是它最大的坑。任务需求有歧义时,planner生成的计划可能完全跑偏。解决办法是executor执行出错时触发replan,而不是直接抛异常。另一种技巧是让planner给每个步骤加“验收条件”,executor执行完先自查,不满足就反馈给planner调整。用一句话区分:任务像“做菜”且菜谱清晰,就用Plan-and-Execute;连菜谱都不知道,得边看食材边摸索,就用ReAct。
3. 进阶模式:记忆、反射与多智能体协作
3.1 带记忆的对话模式:状态从哪来
Agent跑起来就会产生状态,如果全部堆在messages里,上下文窗口迟早被撑爆。我常用的方案是“向量检索+摘要改写”。任务开始前,根据当前输入检索相关历史,拼进context;任务结束后,把这轮关键信息压缩成结构化记录,再写入记忆库:
def agent_with_memory(task, memory_store, llm_func, top_k=3): records = memory_store.search(task, top_k) memory_text = "\n".join(f"[{r.time}] {r.summary}" for r in records) messages = [{ "role": "user", "content": f"Related history:\n{memory_text}\n\nCurrent task: {task}" }] answer = llm_func(messages) memory_store.add(summary=compact_summary(task, answer, llm_func)) return answer这个模式的关键不是向量库选型,而是什么时候写、怎么写。把原始对话直接塞进向量库,检索出来多半是噪音。我让LLM在每次任务结束后把“用户目标、最终结果、关键限制条件”压缩成三行摘要再入库,检索更干净。top_k也别设太大,对话任务里最相关的历史通常只有两三条,top_k超过10反而让模型分不清主次。我一般设3到5,同时给每条记忆加时间戳。
3.2 反射与自我修正模式:让Agent学会复盘
反射模式是在Agent给出答案后,再让模型或校验工具对答案做审查,发现问题就重新生成。最常见的实现:
def run_reflective(task, llm_func, rounds=2): answer = llm_func(task) for _ in range(rounds): critique = llm_func(f"审查以下答案的问题:\n{answer}") answer = llm_func(f"根据审查意见改写答案:\n{critique}") return answer但直接这么用,模型往往看不出自己问题,critique经常是“内容完整”这种空话。我把反射分成两类:基于规则的反射,让模型按明确清单检查,
本文还有配套的精品资源,点击获取