三种构建范式的工程复盘:从单轮 Prompt 到 Agent

时间:2026-07-09 08:28:41 来源:互联网

在构建小型 Python Agent 学习项目时,我实现了三种典型的 Agent 范式,涵盖 ReAct、Plan-and-Solve 以及 Reflection,并从中意识到,Agent 的核心并非简单地延长 Prompt,而是将模型调用组织成一个可控的工程流程。本文结合该项目的实践经验,深入复盘这三种常见的 Agent 构建范式。

从单轮 Prompt 到 Agent:三种构建范式的工程复盘

代码仓库位于 RayZh0ng/agent-learn,若希望直接了解实现细节,可优先查看以下三个关键文件:

  1. re_act_agent.py:ReAct 范式,推理与工具行动交替进行。
  2. PlanAndSolveAgent.py:Plan-and-Solve 范式,先进行规划,随后再执行。
  3. ReflectionAgent.py:Reflection 范式,先生成内容,再进行反思与优化。

在完成这三个版本的开发后,我更加明确地认识到,Agent 的核心并非“将 Prompt 编写得更长”,而是将模型调用组织成一个可控的工程流程。不同范式所解决的问题各不相同,所适应的任务类型也有所差异。本文将结合该项目,对这三种常见的 Agent 构建范式进行全面的复盘分析。

单轮 LLM 调用的问题

最基础的 LLM 应用通常呈现出以下结构:

 复制代码用户问题 -> Prompt -> 模型回答

这一链路足够简单,也足够实用,适用于摘要撰写、文案修改、概念解释等多种场景。然而,当任务变得复杂时,单轮调用的局限性会迅速显现。

第一类问题是信息不足。例如,用户询问“今天某家公司有哪些最新动态”,模型不能仅依赖训练时积累的知识进行回答,它需要通过搜索、查询数据库或调用外部 API 来获取所需信息。

第二类问题是结构不稳定。复杂的任务通常包含多个阶段,例如先分析需求,再查阅资料,最后整理结论。若让模型一次性完成回答,它可能会跳过某些步骤、遗漏环节,甚至将计划与答案混杂在一起。

第三类问题是质量不可控。模型初次生成的代码可能能够运行,但其边界条件可能并不完整;初次给出的方案可能看似合理,却未经批判性的检查与验证。

Agent 范式本质上围绕这三类问题进行了流程设计:

  1. 信息不足时,让模型具备行动能力,这就是 ReAct。
  2. 任务过于庞大时,让模型先进行规划,这就是 Plan-and-Solve。
  3. 答案不够稳定时,让模型先进行评审再进行优化,这就是 Reflection。

ReAct:让模型明确下一步的行动方向

ReAct 是 Reasoning 与 Acting 的缩写。它所解决的问题是:模型何时应直接给出回答,何时应先调用工具?

在项目的 ReActAgent 中,我将模型的输出约束为一个非常简单的协议:

 复制代码Thought: 用一句话说明当前判断
Action: Search[搜索关键词] 或 Finish[最终答案]

程序只执行 Action,不执行 ThoughtThought 用于帮助模型整理上下文,而 Action 才是 Python 程序真正解析并执行的指令。

一轮 ReAct 循环的基本流程如下:

 复制代码Question
  -> LLM 输出 Thought / Action
  -> Python 解析 Action
  -> 如果是 Search,则调用搜索工具
  -> 工具结果记录为 Observation
  -> 下一轮将 Observation 放回 Prompt
  -> 直到模型输出 Finish

该设计中最为关键的一点是:模型并不直接调用工具,它仅仅声明自己想调用何种工具。实际执行工具的是外层程序。

项目中包含一个 ToolExecutor,它负责注册工具、展示工具的使用说明,并根据工具名称查找对应的函数。当 Search 工具注册后,模型可以输出:

 复制代码Action: Search[OpenAI 最新消息]

随后,Python 程序解析出工具名称及其输入参数,找到真实的 search(...) 函数并执行,最后将执行结果作为 Observation 放回下一轮的上下文。

这也正是 ReAct 相较于普通问答的优势所在:它并不要求模型“一次性掌握所有答案”,而是允许模型在解决实际问题的过程中不断获取新信息。

然而,ReAct 在工程实现上也面临一些棘手问题。模型可能不遵循既定格式,可能输出了 Thought 却没有合法的 Action;也可能反复调用工具,迟迟不愿执行 Finish。因此,代码中必须加入解析容错机制和最大步数限制。在实现中,我保留了 max_steps 参数,以防止 Agent 陷入无限循环。

根据我的经验,只要任务需要依赖外部世界的信息,ReAct 就是一种自然而然的选择。搜索、查阅资料、读取文件、查询数据库或调用业务接口等场景,都非常适合采用 ReAct 范式。反之,如果任务本身不需要外部信息,仅需编写一个普通函数或整理已有的材料,ReAct 反而可能显得过于臃肿。

Plan-and-Solve:将复杂任务先行拆解

Plan-and-Solve 旨在解决另一个问题:复杂任务应如何合理拆分?

很多时候,模型并非不知道答案,而是由于一次性处理过多内容,导致最终结果不够稳定。例如,在“调研一个技术方向并给出学习路线”这一任务中,至少包含理解问题、收集信息、筛选重点、组织结构和输出建议等多个环节。若直接让模型回答,往往得到的结果虽然看起来完整,但其生成过程却缺乏透明度。

Plan-and-Solve 将流程划分为两个阶段:

 复制代码Plan:  Planner 先生成计划
Solve: Executor 按计划逐步执行
Final: 汇总各步骤结果,生成最终答案

在项目中,PlanAndSolveAgent 并未直接编写冗长的 Prompt,而是将职责分配给两个组件:

  1. Planner:仅负责生成计划,不进行回答问题。
  2. Executor:仅负责按计划执行,并汇总出最终答案。

这看似只是多拆分了两个类,但对于 Agent 的稳定性却有着重要意义。Planner 的输出并非自然语言的长篇大论,而是一个 Python 字符串列表:

 复制代码[
    "分析用户问题和目标",
    "收集或核验必要信息",
    "整合信息并生成最终答案",
]

Executor 在获取计划后,并非简单地返回最后一步的执行结果,而是逐步执行每一个中间步骤。每一步骤都能看到原始问题、完整计划、历史步骤的结果以及当前步骤的目标。这种做法确保了模型不会每次从零开始,也不易遗忘前面已经获取的信息。

executor.py 中,我还加入了一个简单的工具接入逻辑:如果当前步骤包含“搜索”“查询”“最新”“实时”等关键词,Executor 会调用 Search 工具,并将工具结果整合到当前步骤的 Prompt 中。这种实现方式虽然不如 ReAct 灵活,但更易于理解,也更具可控性。

Plan-and-Solve 的优势在于将复杂任务显性化。通过它,我们可以清晰地看到模型如何拆解问题,并且在计划阶段就能发现方向是否出现偏差。它特别适合调研、分析、总结、方案设计等类型的任务。

它的代价也很明显:一旦计划出错,后续的执行也会随之偏离正轨。如果 Planner 遗漏了关键步骤,Executor 通常不会主动进行重新规划。因此,在更完善的系统中,Plan-and-Solve 往往会结合计划检查、计划修订机制,甚至在某些步骤中融入 ReAct 以动态调用工具。

Reflection:不可轻信初次生成的结果

Reflection 旨在解决第三类问题:模型初次生成的结果未必可靠,能否让它在输出前先自行审视一番?

在项目中,我将 ReflectionAgent 拆分为三个 Prompt:

 复制代码INITIAL -> REFLECT -> REFINE

具体流程如下:

 复制代码先生成第一版代码
  -> 再扮演评审者的角色,指出其中存在的问题
  -> 然后根据反馈重写代码
  -> 必要时可进行下一轮迭代

这种模式特别适用于代码生成场景。例如,让模型编写一个“判断列表是否存在重复元素”的函数,第一版可能会使用双重循环,其时间复杂度为 O(n^2)。在 Reflection 阶段,若评审到位,就会指出可以使用 set 将复杂度优化为 O(n),随后 Refine 阶段再生成新版本。

这里的关键并非简单的“多调用几次模型”,而是让每一次调用分别承担不同的角色:

  1. 初始生成时,模型扮演 Programmer 的角色。
  2. 反思阶段,模型扮演 Reviewer 的角色。
  3. 优化阶段,模型再次扮演 Programmer 的角色,但必须依据 Reviewer 的反馈进行修改。

项目中的 ReflectionStep 会记录每一轮的代码、反馈、优化后的代码以及是否发生了变化。这一轨迹记录对于学习非常有帮助,因为它让我们能够看到最终答案是如何逐步推导出来的,而不仅仅是一个最终结果。

当然,Reflection 也有其明显的局限性。评审者与生成者本质上是同一个模型,如果模型本身就不了解某个边界条件,它在反思阶段也难以发现问题。在真实的工程实践中,我更倾向于将 Reflection 与测试、静态分析、类型检查等手段相结合:模型负责提出修改方向,而程序则负责提供客观的反馈依据。

换句话说,Reflection 非常适合作为一个质量改进层,但它不应被视为质量保证的全部手段。

三种范式如何选择

这三种范式并非“谁更高级”的关系,而是针对不同问题的工具。我的选型判断大致如下:

范式主要解决问题核心循环适合任务主要风险
ReAct缺乏外部信息,需要工具支持Thought -> Action -> Observation搜索、查库、调用API、多轮查询格式解析失败、工具循环过多
Plan-and-Solve任务复杂,需先拆分步骤Plan -> Execute -> Final Answer调研、分析、方案设计、复杂问答计划质量决定执行质量
Reflection初稿质量不稳定,需自我修正Initial -> Reflect -> Refine代码生成、答案优化、性能改进自我评审可能遗漏盲点

概括而言:

ReAct 聚焦于行动能力,Plan-and-Solve 聚焦于任务结构,Reflection 聚焦于结果质量

因此,在实践中,我不会追问“哪个范式最好”,而是先分析当前任务面临的最大风险是什么:

  1. 若最大风险是信息过期或缺失,则优先考虑 ReAct。
  2. 若最大风险是任务规模过大、步骤过多,则优先考虑 Plan-and-Solve。
  3. 若最大风险是答案初稿不可靠,则优先考虑 Reflection。

真正重要的是工程边界

在开发这三个 Agent 的过程中,我愈发感到,最值得关注的并非 Prompt 本身,而是 Prompt 与程序之间的边界。

第一,Prompt 应像协议一样规范。ReAct 要求模型输出 Action: Tool[input],Planner 要求模型输出 Python 列表,Reflection 要求评审阶段不可直接重写代码。这些约束并非文字游戏,而是在定义模型与程序之间如何协作。

第二,状态信息需要显式保存。ReAct 有 history,Plan-and-Solve 有步骤结果,Reflection 有每轮的反馈轨迹。如果没有状态,Agent 便会退化为普通的单轮问答;如果状态混乱,Agent 则会将上一轮的错误带入下一轮。

第三,失败路径必须先进行预判并设计应对方案。模型可能返回空响应或格式错误;工具可能报错;计划可能为空;反思过程可能始终无法生效。即使是学习用的 demo,也应保留这些保护机制,因为真实系统中最常见的问题往往不是“正常路径如何运行”,而是“当模型不按预期输出时应如何处理”。

第四,工具调用需由程序接管。模型可以决定是否需要调用工具,但不应直接拥有执行权限。外层程序需要负责解析、权限控制、超时处理、异常管理和结果格式化。这是 Agent 从 demo 走向工程系统时必须弥补的关键环节。

更真实的 Agent 往往是组合式的

单独审视这三种范式,它们各自拥有清晰的边界。然而,真实的任务往往需要多种能力的协同配合。

一个更完整的流程可能是:

 复制代码Plan-and-Solve 先拆解任务
  -> 某些步骤内部使用 ReAct 调用工具
  -> 最终答案再经过 Reflection 进行优化

例如,在制作一份技术调研报告时,可以首先让 Planner 拆解出具体的调研维度;在执行每个维度的调研时,使用 ReAct 搜索资料;最后,在生成报告后,再利用 Reflection 检查整体结构、事实引用是否存在遗漏。

Agent 并非某种神秘的新抽象概念,而是一组围绕 LLM 的流程控制方法。可以从单轮 Prompt 起步,随后根据任务风险逐步引入工具、规划、记忆、反思以及失败保护机制。

总结来说,Agent 构建的本质是从单轮 Prompt 向结构化流程的演进,而掌握这三种范式并灵活组合,是走向更复杂 Agent 系统的关键起点。如果你正在学习 Agent,我建议先从实现一个最小化的 ReAct 开始,再逐步实现 Plan-and-Solve 和 Reflection,通过亲手编写这三种循环,你会更容易理解复杂 Agent 框架背后的核心逻辑。