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

过去一段时间我在写一个很小的 Python Agent 学习项目。项目不复杂:一个 LLM 客户端、一个工具注册器、一个搜索工具、一个记忆模块,再加上三个 Agent:ReAct、Plan-and-Solve 和 Reflection。

代码仓库在这里:RayZh0ng/agent-learn。如果你想直接看实现,可以从这三个文件切入:

写完这三个版本后,我更明确地意识到一件事:Agent 的核心并不是"把 Prompt 写长一点",而是把模型调用组织成一个可控的工程流程。不同范式解决的问题不同,适合的任务也不同。本文就结合这个项目,复盘这三种常见 Agent 构建范式。

单轮 LLM 调用的问题

最朴素的 LLM 应用通常长这样:

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

这条链路足够简单,也足够好用。写摘要、改文案、解释概念,大多数时候都可以直接做。但一旦任务变复杂,单轮调用的短板会很快暴露出来。

第一类问题是信息不够。用户问"今天某家公司有什么最新动态",模型不能只靠训练时记住的知识回答,它需要搜索、查数据库或调用外部 API。

第二类问题是结构不稳。复杂任务往往包含多个阶段,比如先分析需求,再查资料,再整理结论。如果让模型一次性回答,它可能跳步、漏步,或者把计划和答案混在一起。

第三类问题是质量不可控。模型第一次生成的代码可能能跑,但边界条件不完整;第一次给出的方案可能有道理,但还没有经过批判性检查。

Agent 范式本质上就是围绕这三个问题做流程设计:

  • 信息不够时,让模型会行动,这就是 ReAct。
  • 任务太大时,让模型先规划,这就是 Plan-and-Solve。
  • 答案不够稳时,让模型先评审再优化,这就是 Reflection。

ReAct:让模型知道下一步该做什么

ReAct 是 Reasoning + Acting 的缩写。它解决的问题是:模型什么时候应该直接回答,什么时候应该先调用工具?

在项目里的 ReActAgent 中,我把模型输出约束成一个很简单的协议:

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

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

一轮 ReAct 循环大概是这样:

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

这个设计里最重要的点是:模型不直接调用工具,模型只声明自己想调用什么工具。真正执行工具的是外层程序。

项目里有一个 ToolExecutor,它负责注册工具、展示工具说明、按名字查找工具函数。Search 工具注册进去之后,模型就可以输出:

text 复制代码
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 的做法是把流程拆成两个阶段:

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

在项目里,PlanAndSolveAgent 本身不直接写太多 Prompt,而是把职责拆给两个组件:

  • Planner:只负责生成计划,不回答问题。
  • Executor:只负责按计划执行,再汇总最终答案。

这看起来只是多拆了两个类,但对 Agent 稳定性很重要。Planner 的输出不是自然语言长文,而是一个 Python 字符串列表:

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

Executor 拿到计划后,不是简单地返回最后一步,而是逐步执行中间步骤。每一步都会看到原始问题、完整计划、历史步骤结果和当前步骤。这样模型不会每一步都从零开始,也不容易忘记前面已经得到的信息。

我在 executor.py 里还做了一个简单的工具接入:如果当前步骤包含"搜索""查询""最新""实时"等关键词,Executor 会调用 Search,然后把工具结果放进当前步骤的 Prompt。这个实现没有 ReAct 灵活,但更容易理解,也更可控。

Plan-and-Solve 的优势是让复杂任务显性化。你可以看到模型如何拆解问题,也可以在计划阶段就发现方向是否跑偏。它适合调研、分析、总结、方案设计这类任务。

它的代价是:计划一旦错了,后面的执行也会跟着错。Planner 如果漏掉关键步骤,Executor 通常不会主动重新规划。所以更完整的系统里,Plan-and-Solve 往往还会加上计划检查、计划修订,甚至结合 ReAct 在某些步骤里动态调用工具。

Reflection:不要相信第一次生成

Reflection 解决的是第三类问题:模型第一次生成的结果不一定可靠,能不能让它自己先审一遍?

我在项目里把 ReflectionAgent 拆成三个 Prompt:

text 复制代码
INITIAL -> REFLECT -> REFINE

也就是:

text 复制代码
先生成第一版代码
  -> 再扮演评审者指出问题
  -> 再根据反馈重写代码
  -> 必要时继续下一轮

这个模式尤其适合代码生成。比如让模型写一个"判断列表是否有重复元素"的函数,第一版可能会用双重循环,时间复杂度是 O(n^2)。Reflection 阶段如果评审到位,就会指出可以用 set 把复杂度优化到 O(n),然后 Refine 阶段再生成新版本。

这里的关键不是"多调用几次模型"这么简单,而是让每一次调用承担不同角色:

  • 初始生成时,模型是 Programmer。
  • 反思阶段,模型是 Reviewer。
  • 优化阶段,模型再回到 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 关注的是结果质量

所以在实践中,我不会问"哪个范式最好",而会先问当前任务最大的风险是什么:

  • 最大风险是信息过期或缺失,就优先考虑 ReAct。
  • 最大风险是任务太大、步骤太多,就优先考虑 Plan-and-Solve。
  • 最大风险是答案初稿不可靠,就优先考虑 Reflection。

真正重要的是工程边界

写这三个 Agent 的过程中,我反而觉得最值得关注的不是 Prompt 本身,而是 Prompt 和程序之间的边界。

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

第二,状态要显式保存。ReAct 有 history,Plan-and-Solve 有步骤结果,Reflection 有每轮反馈轨迹。没有状态,Agent 就退回普通单轮问答;状态混乱,Agent 就会把上一轮的错误带到下一轮。

第三,失败路径必须先想清楚。模型可能返回空响应,可能格式错误,工具可能报错,计划可能为空,反思可能一直改不动。学习 demo 也应该保留这些保护,因为真实系统里最常见的问题往往不是"正常路径怎么跑",而是"模型不按预期输出时怎么办"。

第四,工具调用要由程序接管。模型可以决定要调用工具,但不应该直接拥有执行能力。外层程序需要做解析、权限、超时、异常处理和结果格式化。这也是 Agent 从 demo 走向工程系统时必须补上的一层。

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

单独看三种范式,它们各自都有清晰边界。但真实任务很少只需要一种能力。

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

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

例如做一份技术调研报告,可以先让 Planner 拆出调研维度;执行每个维度时用 ReAct 搜索资料;最后生成报告后,再用 Reflection 检查结构、事实引用和遗漏点。

Agent 不是某个神秘的新抽象,而是一组围绕 LLM 的流程控制方法。你可以从单轮 Prompt 起步,然后根据任务风险逐步加入工具、规划、记忆、反思和失败保护。

如果你正在学习 Agent,我建议不要一开始就追求"大而全"的框架。先自己实现一个最小 ReAct,再实现一个 Plan-and-Solve,再实现一个 Reflection。等你把这三种循环都写过一遍,会更容易看清复杂 Agent 框架背后到底在做什么。

相关推荐
老梁agent1 小时前
Agent 可观测性实战:全链路追踪 + JSON 结构化日志
物联网·agent
想ai抽2 小时前
Hermes Agent vs Loop Agent 技术调研
agent·loop·harness
用户2930750976692 小时前
手写一个 AI 编程助手:Tool Calling 与 ReAct Agent 全解
agent
千帆_Evan2 小时前
🤖 AI 写代码总在重复造轮子?我们把 Utils 复用门禁做进了 Agent 运行时(已开源)
前端·agent
ckjoker2 小时前
23个测试全绿,一个Token没花——LLM应用的单元测试该怎么写?
单元测试·agent
带刺的坐椅4 小时前
代码审查 Agent Harness 实战:AI 自动 Code Review
java·ai·llm·agent·solon·soloncode·harness
米小虾15 小时前
为什么 AI 的下一个突破口,不是更大的模型,而是更好的"世界"?
人工智能·agent
Darling噜啦啦17 小时前
ReAct 框架深度解析:让 AI 真正"自己干活"的思考-行动-观察循环
aigc·agent
不好听61317 小时前
从零打造 AI Agent:第一天,我学到了什么
agent