这篇博文的"理论线"参考了 《AI Agent 设计范式的演进之路:从工具调用到多智能体协作》,"工程线"基于开源项目 nacos-learn-example 的实际代码。读者可以对照源码阅读本文。
一、背景:为什么是 Plan & Execute?
那篇理论文章 梳理了 AI Agent 的一条清晰演进链:
Tool Calling (地基)
↓
ReAct --- 边想边做
↓
Plan & Execute --- 先计划后执行 ← 我们在这里
↓
Reflection --- 自我反思
↓
Multi-Agent --- 多角色协作
文中说:"理解这条演进路径,远比死记硬背概念更重要。" 而我们的项目正是沿着这条路径走到了 Plan & Execute(第二范式),原因很直接:
- 用户请求是明确的多步骤任务(查天气 → 计算温差 → 生成报告),而非开放式探索
- 需要流程可追溯------用户想看到"AI 打算怎么做"之后再执行
- 追求稳定性------纯 ReAct 不设边界时,LLM 可能在 10 轮 tool call 后迷路
那篇文章的工程原则说得好:"从不复杂的范式开始,随着需求增长逐步演进。"
二、架构全景:四层 + 三阶段
整个系统跨越四个独立模块,源码见 agentic/、nacos-mcp-router/、mcp-server/、agentic-ui/:
┌──────────────────────────────────────────────────────────────┐
│ agentic-ui (Vue 3) │
│ WebSocket 流式渲染 / REST 对话管理 │
└──────────────────────┬───────────────────────────────────────┘
│ WS /api/ws
▼
┌──────────────────────────────────────────────────────────────┐
│ agentic (Python + FastAPI) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Planner │──▶│Validator │──▶│ Executor │ │
│ │ (拆解) │ │(校验) │ │(逐步执行) │ │
│ └──────────┘ └──────────┘ └────┬─────┘ │
│ │ │
│ ToolRegistry ◀── McpRouterClient │
│ (预发现工具) (SSE 长连接) │
└──────────────────────┬───────────────────────────────────────┘
│ MCP Protocol (SSE)
▼
┌──────────────────────────────────────────────────────────────┐
│ nacos-mcp-router (Docker) │
│ Nacos 服务发现代理层 --- 三大元工具: │
│ search_mcp_server / add_mcp_server / use_tool │
└────┬─────────────────────────────────────────────────────┬───┘
│ Nacos 注册 │
▼ ▼
┌────────────────┐ ┌──────────────────┐
│ mcp-server │ │ 更多 MCP 服务 │
│ (Python) │ │ (Java/Go/...) │
│ hello │ │ │
│ calculator │ │ │
│ weather │ │ │
└────────────────┘ └──────────────────┘
三阶段的执行流
系统遵循 Plan → Validate → Execute → Summarize 四阶段流水线,这与那篇文章描述的 Plan & Execute 核心流程一致:
那篇文章:"Planner:将用户目标拆解为有序的子任务列表 → Executor:依次执行每个子任务 → Replanner:当执行失败或信息变化时,重新生成计划"
我们增加了一个Validate 阶段作为"代码门禁"------这是那篇文章没有讨论到的工程实践:
用户: "北京和深圳今天的气温差多少?"
Phase 1 --- Plan (agentic/src/agentic/planner.py)
LLM 思考(流式输出 thinking token)→ 调用 submit_plan 函数提交结构计划
┌─────────────────────────────────────────────┐
│ Step 1: 查北京的天气 → get_weather(北京) │
│ Step 2: 查深圳的天气 → get_weather(深圳) │
│ Step 3: 计算温差 → calculator(减法) │
│ Step 4: 生成报告 → (无需工具) │
└─────────────────────────────────────────────┘
Phase 1.5 --- Validate (agentic/src/agentic/validator.py)
代码级检查:每一步的 suggested_tool 是否在 ToolRegistry 中?
→ 如果工具不可用,触发 replan(最多 2 次)
→ 如果全部不可用,直接返回错误
Phase 2 --- Execute (agentic/src/agentic/executor.py)
┌─ Step 1 ──────────────────────────────────────┐
│ LLM 收到完整上下文 + 当前步骤描述 │
│ → 调用 weather("北京") → 得到 27℃ │
│ → 确认完成 │
└───────────────────────────────────────────────┘
┌─ Step 2 ──────────────────────────────────────┐
│ → 调用 weather("深圳") → 得到 32℃ │
│ → 确认完成 │
└───────────────────────────────────────────────┘
┌─ Step 3 ──────────────────────────────────────┐
│ → 调用 calculator(27, 32, "subtract") → 得到 -5│
│ → 或通过 modify_plan 动态调整后续步骤 │
└───────────────────────────────────────────────┘
Phase 3 --- Summarize (agentic/src/agentic/executor.py:summarize)
LLM 不加载工具,基于所有步骤结果生成自然语言回复
"北京 27℃,深圳 32℃,温差 5℃,深圳比北京暖和一些~"
💡 那篇文章提到 Eino ADK 的 Plan & Execute 使用了三个独立的 Agent(Planner、Executor、Replanner)。我们的项目选择了更轻量的方式------同一条 LLM 换 prompt 切换角色,降低了通信复杂度和 token 成本。这是工程权衡的结果。
三、关键设计决策
3.1 外圈代码驱动 + 内圈 LLM 驱动
这是本项目最有特色的设计模式。那篇文章描述 Plan & Execute 时提到"规划与执行分离",但没有讨论"分离的粒度"。我们的答案是:
外圈 (代码驱动): while plan.pointer < len(plan.steps):
执行单步
plan.pointer += 1
内圈 (LLM 驱动): for turn in range(max_turns):
LLM 推理 → 调用工具 → 观察结果 → 继续推理
直到 LLM 不再调用工具就 break
这两种驱动方式的对比:
| 维度 | 纯 LLM 驱动(Eino ADK 风格) | 代码 + LLM 混合(我们的方案) |
|---|---|---|
| 步骤边界 | LLM 自行约束 | 代码强制保障 |
| 单步复杂度 | 可能一口气做太多 | 限定为原子操作 |
| 可观察性 | 中间状态模糊 | 每一步开始/结束/状态清晰可见 |
| 错误恢复 | 依赖 LLM 自省 | 代码级 retry + 自动跳过不可用步骤 |
那篇文章的演进全景图将 ReAct 和 Plan & Execute 列为不同的范式,但在我们的实践中,它们不是互斥的,而是嵌套的:
Plan & Execute (外层,代码控制)
└── ReAct (内层,每步 LLM 驱动)
└── Tool Calling (原子动作)
└── MCP Protocol (基础设施)
那篇文章在后来的"组合使用"章节也补充了这一点:"这些范式不是互斥的,而是可以组合使用。" 这恰好印证了我们的架构选择。
3.2 两阶段 Prompt 设计
同一个 LLM,在不同阶段看到不同的工具信息:
Planner 看到: " • get_weather --- 查询指定城市的实时天气
• calculator --- 执行四则运算" ← 紧凑描述
Executor 看到: {name: "get_weather", parameters: {city: {type: "string"}}} ← 完整定义
规划阶段不需要参数细节,执行阶段才需要精确的函数签名。分离后,规划 prompt 更短更专注------这是一个容易被忽视的 token 优化手段。
3.3 验证器(Validator)作为代码门禁
在 Planner 和 Executor 之间,插入纯代码验证层:
Planner 输出计划
│
▼
Validator 检查: suggested_tool 都在 ToolRegistry 中?
│ │
│ 全部可用 │ 部分不可用
▼ ▼
进入 Execute 触发 Replan(告知 LLM 哪些不可用,重规划)
│
│ 重试 2 次后仍有不可用工具
▼
返回错误:"当前不支持此工作"
那篇文章提到 Plan & Execute 时没有讨论"工具可用性校验"这一环节。在 MCP 生态中,工具可用性是运行时基础设施问题,不是推理问题------代码门禁比 LLM 自省更适合处理它。
3.4 预发现(Pre-discovery)
系统启动时自动连接 nacos-mcp-router,全量拉取所有已注册 MCP 服务的工具定义。源码在 agentic/src/agentic/api.py 的 lifespan 阶段:
python
services = await mcp_client.discover_services()
for server_name in services:
tools = await mcp_client.fetch_service_tools_via_add(server_name)
for tool_name, description, input_schema in tools:
tool_registry.register(server_name, tool_name, description, input_schema)
这与那篇文章的案例不同------那篇文章假设工具已经就绪,而在实际 MCP 生态中,工具发现是启动时的基础设施操作,不应让 LLM 在运行时分心。
四、Nacos MCP 生态的角色分工
mcp-server(工具生产者)
源码见 mcp-server/src/mcp_server/main.py。通过 NacosMCP SDK 自动注册到 Nacos MCP Hub:
python
@mcp.tool()
def weather(city: str) -> str:
"""查询指定城市的实时天气"""
return fetch_weather(city, api_key)
nacos-mcp-router(工具代理 + 服务发现)
Docker 部署,作为 Nacos MCP 生态的代理网关,对外暴露三个元工具:
| 元工具 | 功能 |
|---|---|
search_mcp_server |
搜索 Nacos 上已注册的 MCP 服务 |
add_mcp_server |
获取特定服务的工具定义 |
use_tool |
代理调用某个服务的某个工具 |
启动方式(docker-compose.yml 见 nacos-mcp-router/):
bash
docker compose -f nacos-mcp-router/docker-compose.yml up -d
agentic(工具消费者 + LLM 编排)
Plan & Execute 的核心。同时承担两个角色:
- MCP Client:启动时通过 SSE 连接 router,拉取工具定义
- LLM Orchestrator:将工具定义注入 LLM,编排 Plan → Execute → Summarize 流程
工具调用链路:
LLM 决定调用 "get_weather"
→ ToolRegistry.lookup("get_weather") → server_name="mcp-server-python"
→ agentic 调用 router 的 use_tool
→ router 转发给 mcp-server-python
→ mcp-server 执行 get_weather("北京")
→ 结果一路返回给 LLM
五、与范式理论的对照
那篇文章的演进全景表,映射到我们的项目:
| 演进阶段 | 核心范式 | 项目中的体现 |
|---|---|---|
| 地基 | Tool Calling | OpenAI function calling → MCP Router → MCP 服务 |
| 第一阶 | ReAct | agent.py:stream_llm() 每步最多 10 轮 Thought→Action→Observation |
| 第二阶 | Plan & Execute | Orchestrator 四阶段流水线(主导范式) |
| 第三阶 | Reflection | 轻量:Executor 把所有步骤结果 + 错误反馈喂给 LLM |
| 第四阶 | Multi-Agent | 未使用(单一 LLM 换 prompt 切换角色,见文末讨论) |
为什么不走 Multi-Agent?
那篇文章介绍了 AutoGen、CrewAI 等多个 Multi-Agent 框架。我们选择不走 Multi-Agent,原因在那篇文章中其实已经给出了答案:
💡 工程原则:从不复杂的范式开始,随着需求增长逐步演进。不要一开始就搭建 Multi-Agent,那很可能是过度设计。
单一 LLM 已经能通过换 prompt 胜任 Planner、Executor、Summarizer 三个角色。引入三个独立 Agent 会带来通信复杂度和成本翻倍------当前阶段不值得。
六、一些有用的工程细节
6.1 动态修改计划
那篇文章提到 Plan & Execute 需要 Replanner 角色。我们在执行阶段提供了 modify_plan 工具(见 executor.py),允许 LLM 在执行中动态追加/跳过/替换步骤:
LLM 发现 Step 3 的结果不符合预期
→ 调用 modify_plan(action="append", new_step={...})
→ 计划在 Step 3 后追加 Step 4
→ 继续执行
这是那篇文章中 Replanner 角色的具体实现------不是独立的 Agent,而是一个元工具。
6.2 Retry 策略
- 单步 retry:最多 3 次,每次重试时 LLM 看到上一次的错误信息
- Replan retry:最多 2 次,通知 LLM 哪些工具不可用,要求绕过
七、总结
我们从一段 Agent 范式理论出发,走通了一条从理论到工程的完整路径:
理论范式 → 架构设计 → 模块实现 → 服务部署
Plan & Orchestrator agentic/ Docker +
Execute 四阶段流水线 + MCP 工具 Nacos 注册
开始探索
bash
git clone https://gitee.com/bytesifter/nacos-learn-example.git
cd nacos-learn-example
# 启动 Nacos MCP Router
docker compose -f nacos-mcp-router/docker-compose.yml up -d
# 启动 MCP Server
cd mcp-server && uv run mcp-server
# 启动 Agentic 后端
cd agentic && uv run uvicorn agentic.api:app
# 启动前端
cd agentic-ui && bun dev
核心收获
- Plan & Execute 适合明确的多步骤任务------提供可追溯、可控的执行流程
- 代码 + LLM 混合驱动比纯 LLM 驱动更稳定------代码管边界,LLM 管内容(对照那篇文章的 Plan & Execute 案例,这是我们的差异化实践)
- MCP 协议天然适配 Plan & Execute------工具发现是启动时的基础设施操作,执行阶段 LLM 只需专注"调用什么"
- 范式不是单选题------Plan & Execute 的外圈包裹 ReAct 的内圈,正如那篇文章总结的:"这些范式可以组合使用"
参考资源
- 理论文章:AI Agent 设计范式的演进之路:从工具调用到多智能体协作
- 项目源码:https://gitee.com/bytesifter/nacos-learn-example
- 关键模块:agentic/src/agentic/(Orchestrator、Planner、Executor、Validator)