第13篇-Plan-and-Execute模式-先规划后执行
【AI Agent 与 Super Agent 构建实战】第 13 篇:Plan-and-Execute 模式 — 先规划后执行
本系列定位:用 Go 语言从零构建各类 AI Agent,覆盖 ReAct、Plan-and-Execute、Multi-Agent、Super Agent 等核心设计模式的完整工程实现。
本篇你将学到
- Plan-and-Execute 模式的核心架构与"先规划后执行"的设计哲学
- Planner(规划器)与 Executor(执行器)的职责分离实现
- Go 语言实现 Plan 数据结构——支持步骤列表、依赖关系、状态追踪
- 完整的 Plan-and-Execute 执行引擎,并与 ReAct 做适用场景对比
学完本篇,你将进入模块 3 的核心:让 Agent 从"走一步看一步"升级为"先想清楚再动手"。
一、为什么需要 Plan-and-Execute
1.1 ReAct 的局限性
在模块 2 中,我们实现了完整的 ReAct Agent。ReAct 的核心循环是 Thought → Action → Observation,每一步都依赖 LLM 的实时推理来决定下一步做什么。这种"边想边做"的模式简单直接,但在复杂任务面前有明显短板。
ReAct 有三个结构性问题:
| 问题 | 表现 | 影响 |
|---|---|---|
| 短视决策 | 每步只看上一步结果,无全局视图 | 任务执行到中途才发现方向错了 |
| Token 浪费 | 每一步都把完整上下文塞给 LLM | 10 步任务的 Token 消耗线性增长 |
| 不可复用 | 计划隐含在推理过程中,无法提取 | 无法缓存、无法并行、无法预审 |
1.2 Plan-and-Execute 的核心思想
Plan-and-Execute 模式来自 LangChain 团队提出的经典架构,核心思路是将规划和执行解耦为两个独立阶段:
关键区别在于:Planner 一次性生成完整的步骤列表,Executor 则像流水线一样逐步执行。规划只调用一次 LLM(或少量几次),执行阶段则可以高效推进。
1.3 两种模式适用场景对比
| 维度 | ReAct | Plan-and-Execute |
|---|---|---|
| 决策频率 | 每步都调 LLM | 规划时调,执行时少调 |
| 全局视野 | 弱,逐步推进 | 强,先看全局 |
| Token 成本 | 高(N 步 N 次完整推理) | 低(1 次规划 + N 次轻量执行) |
| 适合任务 | 探索性、不可预测的任务 | 步骤清晰、可预见的任务 |
| 典型例子 | “帮我调试这个报错” | “调研 3 个竞品并写报告” |
一句话总结:ReAct 适合探险,Plan-and-Execute 适合工程。 实际生产中两者经常组合使用——大计划用 Plan-and-Execute,每个步骤内部用 ReAct。
二、Plan 数据结构设计
2.1 核心模型
一个计划由多个步骤组成,每个步骤有依赖关系、状态、执行结果。我们先定义清晰的数据模型:
2.2 Go 实现
// internal/planner/plan.go
package planner
import (
"time"
)
// StepStatus 步骤状态枚举
type StepStatus string
const (
StatusPending StepStatus = "pending" // 等待执行
StatusRunning StepStatus = "running" // 正在执行
StatusDone StepStatus = "done" // 执行成功
StatusFailed StepStatus = "failed" // 执行失败
StatusSkipped StepStatus = "skipped" // 被跳过(依赖失败等)
)
// PlanStep 单个计划步骤
type PlanStep struct {
Index int `json:"index"` // 步骤序号,从 0 开始
Task string `json:"task"` // 步骤描述(自然语言)
DependsOn []int `json:"depends_on"` // 依赖的步骤序号列表
ToolNames []string `json:"tool_names"` // 预计使用的工具
Status StepStatus `json:"status"` // 当前状态
Result string `json:"result"` // 执行结果
Error string `json:"error"` // 错误信息
Retries int `json:"retries"` // 已重试次数
StartedAt *time.Time `json:"started_at"` // 开始时间
DoneAt *time.Time `json:"done_at"` // 完成时间
}
// Plan 完整任务计划
type Plan struct {
ID string `json:"id"` // 计划唯一标识
Goal string `json:"goal"` // 原始目标
Steps []PlanStep `json:"steps"` // 步骤列表
Status string `json:"status"` // plan 整体状态
CreatedAt time.Time `json:"created_at"` // 创建时间
}
// NewPlan 创建新计划
func NewPlan(goal string, steps []PlanStep) *Plan {
// 初始化每个步骤的状态
for i := range steps {
steps[i].Index = i
steps[i].Status = StatusPending
}
return &Plan{
ID: generateID(),
Goal: goal,
Steps: steps,
Status: "pending",
CreatedAt: time.Now(),
}
}
// generateID 生成简易唯一 ID
func generateID() string {
return time.Now().Format("20060102-150405.000")
}
// IsDone 判断计划是否全部完成
func (p *Plan) IsDone() bool {
for i := range p.Steps {
s := &p.Steps[i]
if s.Status != StatusDone && s.Status != StatusSkipped {
return false
}
}
return true
}
// IsFailed 判断是否有致命失败
func (p *Plan) IsFailed() bool {
for i := range p.Steps {
if p.Steps[i].Status == StatusFailed {
return true
}
}
return false
}
// NextReady 找出下一个可执行步骤(依赖已全部完成)
func (p *Plan) NextReady() []int {
var ready []int
for i := range p.Steps {
s := &p.Steps[i]
if s.Status != StatusPending {
continue
}
if p.depsSatisfied(s) {
ready = append(ready, i)
}
}
return ready
}
// depsSatisfied 检查步骤依赖是否全部满足
func (p *Plan) depsSatisfied(s *PlanStep) bool {
for _, depIdx := range s.DependsOn {
if depIdx < 0 || depIdx >= len(p.Steps) {
return false // 依赖越界
}
status := p.Steps[depIdx].Status
if status != StatusDone && status != StatusSkipped {
return false
}
}
return true
}
// MarkRunning 标记步骤开始执行
func (p *Plan) MarkRunning(idx int) {
now := time.Now()
p.Steps[idx].Status = StatusRunning
p.Steps[idx].StartedAt = &now
}
// MarkDone 标记步骤成功完成
func (p *Plan) MarkDone(idx int, result string) {
now := time.Now()
p.Steps[idx].Status = StatusDone
p.Steps[idx].Result = result
p.Steps[idx].DoneAt = &now
}
// MarkFailed 标记步骤失败
func (p *Plan) MarkFailed(idx int, errMsg string) {
p.Steps[idx].Status = StatusFailed
p.Steps[idx].Error = errMsg
}
// MarkSkipped 标记步骤被跳过(通常因为依赖失败)
func (p *Plan) MarkSkipped(idx int, reason string) {
p.Steps[idx].Status = StatusSkipped
p.Steps[idx].Error = reason
}
// Progress 返回进度信息(已完成/总数)
func (p *Plan) Progress() (done, total int) {
total = len(p.Steps)
for i := range p.Steps {
s := p.Steps[i].Status
if s == StatusDone || s == StatusSkipped {
done++
}
}
return done, total
}
这套数据结构有几个设计要点:
- 指针接收者:
MarkDone、MarkFailed等方法修改状态,用指针接收者避免拷贝 - 内部取地址:遍历
range p.Steps时对&p.Steps[i]取地址而非循环变量,避免经典的 Go 闭包陷阱 - 依赖检查:
depsSatisfied是并行执行的基础,下一章会基于它实现 DAG 调度 - 进度追踪:
Progress方法支持运行时可视化
三、Planner — 让 LLM 生成完整计划
3.1 Planner 的职责
Planner 的核心任务是接收用户的自然语言目标,输出结构化的步骤列表。我们用结构化输出(Structured Output)技术让 LLM 直接返回 JSON。
3.2 Planner 实现
// internal/planner/planner.go
package planner
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/yourname/agentforge/internal/provider"
)
// Planner 规划器:调用 LLM 生成任务计划
type Planner struct {
llm provider.LLMProvider
model string
toolsInfo []ToolInfo // 可用工具的描述信息
}
// ToolInfo 工具元信息(用于提示 LLM)
type ToolInfo struct {
Name string
Description string
}
// NewPlanner 创建规划器
func NewPlanner(llm provider.LLMProvider, model string, tools []ToolInfo) *Planner {
return &Planner{
llm: llm,
model: model,
toolsInfo: tools,
}
}
// planRequest LLM 需要返回的结构
type planRequest struct {
Thought string `json:"thought"` // 规划思路
Steps []rawStep `json:"steps"` // 步骤列表
}
// rawStep LLM 返回的原始步骤(可能缺字段,需后处理)
type rawStep struct {
Task string `json:"task"`
DependsOn []int `json:"depends_on"`
ToolNames []string `json:"tool_names"`
}
// CreatePlan 生成完整任务计划
func (pl *Planner) CreatePlan(ctx context.Context, goal string) (*Plan, error) {
prompt := pl.buildPrompt(goal)
// 使用结构化输出模式请求 LLM
resp, err := pl.llm.Chat(ctx, provider.ChatRequest{
Model: pl.model,
Messages: []provider.Message{
{Role: "system", Content: plannerSystemPrompt(pl.toolsInfo)},
{Role: "user", Content: prompt},
},
ResponseFormat: provider.ResponseFormatJSON,
})
if err != nil {
return nil, fmt.Errorf("规划请求失败: %w", err)
}
// 解析 LLM 返回的 JSON
var pr planRequest
if err := json.Unmarshal([]byte(resp.Content), &pr); err != nil {
return nil, fmt.Errorf("计划解析失败: %w, 原始内容: %s", err, resp.Content)
}
// 转换为 Plan 结构并校验
steps, err := normalizeSteps(pr.Steps)
if err != nil {
return nil, err
}
return NewPlan(goal, steps), nil
}
// buildPrompt 构建规划提示
func (pl *Planner) buildPrompt(goal string) string {
return fmt.Sprintf(`请为以下目标制定详细的执行计划:
目标:%s
要求:
1. 将目标分解为 3-8 个具体、可执行的步骤
2. 每个步骤用一句话清晰描述要做什么
3. 用 depends_on 字段标明步骤间的依赖(值为前置步骤的序号,从 0 开始)
4. 没有依赖的步骤 depends_on 设为空数组 []
5. tool_names 填写该步骤预计用到的工具名
输出 JSON 格式:
{
"thought": "你的规划思路(1-2 句话)",
"steps": [
{
"task": "步骤描述",
"depends_on": [],
"tool_names": ["tool_a"]
}
]
}`, goal)
}
// plannerSystemPrompt 构建系统提示(含工具清单)
func plannerSystemPrompt(tools []ToolInfo) string {
var sb strings.Builder
sb.WriteString("你是一个任务规划专家。你的职责是将用户目标分解为清晰的执行步骤。\n\n")
sb.WriteString("可用工具:\n")
for _, t := range tools {
sb.WriteString(fmt.Sprintf("- %s: %s\n", t.Name, t.Description))
}
sb.WriteString("\n请确保计划合理、步骤之间依赖关系正确。")
return sb.String()
}
// normalizeSteps 校验并规范化 LLM 返回的步骤
func normalizeSteps(raw []rawStep) ([]PlanStep, error) {
if len(raw) == 0 {
return nil, fmt.Errorf("计划为空,至少需要一个步骤")
}
if len(raw) > 20 {
return nil, fmt.Errorf("步骤过多(%d),建议不超过 20 个", len(raw))
}
steps := make([]PlanStep, len(raw))
for i, r := range raw {
if strings.TrimSpace(r.Task) == "" {
return nil, fmt.Errorf("第 %d 步缺少任务描述", i)
}
// 校验依赖索引合法性
for _, dep := range r.DependsOn {
if dep < 0 || dep >= len(raw) {
return nil, fmt.Errorf("第 %d 步依赖了非法索引 %d", i, dep)
}
if dep >= i {
return nil, fmt.Errorf("第 %d 步依赖了自身或后续步骤 %d", i, dep)
}
}
steps[i] = PlanStep{
Index: i,
Task: r.Task,
DependsOn: r.DependsOn,
ToolNames: r.ToolNames,
Status: StatusPending,
}
}
return steps, nil
}
normalizeSteps 做了三层校验:步骤数量、描述非空、依赖索引合法且不形成前向依赖(dep >= i 即指向自己或后续步骤,必然构成环)。这是防止 LLM 输出混乱数据的第一道防线。
四、Executor — 逐步执行计划
4.1 执行器职责
Executor 接收一个 Plan,按依赖顺序逐步执行每个步骤。每个步骤本质上是一次 ReAct 子循环——但范围被限定在该步骤的 Task 描述内。
4.2 Executor 实现
// internal/planner/executor.go
package planner
import (
"context"
"fmt"
"strings"
"github.com/yourname/agentforge/internal/provider"
)
// StepExecutor 单步执行函数类型
// 输入:步骤描述 + 已完成步骤的结果摘要
// 输出:步骤执行结果
type StepExecutor func(ctx context.Context, step *PlanStep, context_ string) (string, error)
// Executor 计划执行器
type Executor struct {
llm provider.LLMProvider
model string
maxStepIter int // 单步最大迭代
}
// NewExecutor 创建执行器
func NewExecutor(llm provider.LLMProvider, model string) *Executor {
return &Executor{
llm: llm,
model: model,
maxStepIter: 5, // 单步最多 5 次 LLM 调用
}
}
// Execute 执行整个计划(同步顺序执行)
func (ex *Executor) Execute(ctx context.Context, plan *Plan) error {
plan.Status = "running"
for !plan.IsDone() && !plan.IsFailed() {
ready := plan.NextReady()
if len(ready) == 0 {
// 没有可执行步骤且未全部完成 → 死锁(可能有环)
return fmt.Errorf("计划陷入死锁,无法继续")
}
for _, idx := range ready {
step := &plan.Steps[idx]
plan.MarkRunning(idx)
// 构建上下文:已完成步骤的结果摘要
stepCtx := ex.buildStepContext(plan, idx)
// 执行该步骤
result, err := ex.executeStep(ctx, step, stepCtx)
if err != nil {
plan.MarkFailed(idx, err.Error())
// 级联跳过依赖此步骤的后续步骤
ex.cascadeSkip(plan, idx)
} else {
plan.MarkDone(idx, result)
}
}
}
if plan.IsFailed() {
plan.Status = "failed"
return fmt.Errorf("计划执行失败")
}
plan.Status = "done"
return nil
}
// executeStep 执行单个步骤(内部 ReAct 循环)
func (ex *Executor) executeStep(ctx context.Context, step *PlanStep, stepCtx string) (string, error) {
prompt := fmt.Sprintf(`你正在执行一个多步骤计划的第 %d 步。
步骤任务:%s
前置步骤结果:
%s
请完成这一步并返回结果。`, step.Index+1, step.Task, stepCtx)
// 简化版:直接调用 LLM(实际项目中可接入工具调用)
resp, err := ex.llm.Chat(ctx, provider.ChatRequest{
Model: ex.model,
Messages: []provider.Message{
{Role: "system", Content: "你是一个任务执行器,专注完成分配给你的单一步骤。"},
{Role: "user", Content: prompt},
},
})
if err != nil {
return "", err
}
return resp.Content, nil
}
// buildStepContext 构建步骤上下文(汇总前置步骤结果)
func (ex *Executor) buildStepContext(plan *Plan, idx int) string {
var sb strings.Builder
for _, depIdx := range plan.Steps[idx].DependsOn {
dep := &plan.Steps[depIdx]
sb.WriteString(fmt.Sprintf("步骤 %d(%s)的结果:\n%s\n\n",
depIdx+1, dep.Task, dep.Result))
}
if sb.Len() == 0 {
return "(无前置步骤)"
}
return sb.String()
}
// cascadeSkip 级联跳过依赖失败步骤的后续步骤
func (ex *Executor) cascadeSkip(plan *Plan, failedIdx int) {
for i := range plan.Steps {
s := &plan.Steps[i]
if s.Status != StatusPending {
continue
}
for _, dep := range s.DependsOn {
if dep == failedIdx {
plan.MarkSkipped(i, fmt.Sprintf("依赖步骤 %d 失败", failedIdx))
// 递归跳过依赖此步骤的步骤
ex.cascadeSkip(plan, i)
break
}
}
}
}
执行器有几个关键设计:
- 同步顺序执行:当前版本按顺序执行,下一章会升级为基于 DAG 的并行执行
- 级联跳过:某步失败后,所有直接或间接依赖它的步骤会被标记为
Skipped,避免无效执行 - 死锁检测:如果
NextReady返回空但计划未完成,说明依赖关系有环(正常情况不应出现,但 LLM 生成的计划可能出错)
五、Plan-and-Execute 引擎整合
5.1 引擎封装
把 Planner 和 Executor 组合为统一入口:
// internal/planner/engine.go
package planner
import (
"context"
"fmt"
"github.com/yourname/agentforge/internal/provider"
)
// PlanEngine Plan-and-Execute 引擎
type PlanEngine struct {
planner *Planner
executor *Executor
callbacks EngineCallbacks // 执行回调(用于日志/UI)
}
// EngineCallbacks 执行过程中的回调钩子
type EngineCallbacks struct {
OnPlanCreated func(plan *Plan)
OnStepStart func(step *PlanStep)
OnStepDone func(step *PlanStep)
OnStepFailed func(step *PlanStep)
OnPlanComplete func(plan *Plan)
}
// NewPlanEngine 创建引擎
func NewPlanEngine(llm provider.LLMProvider, model string, tools []ToolInfo) *PlanEngine {
return &PlanEngine{
planner: NewPlanner(llm, model, tools),
executor: NewExecutor(llm, model),
}
}
// WithCallbacks 设置回调
func (e *PlanEngine) WithCallbacks(cb EngineCallbacks) *PlanEngine {
e.callbacks = cb
return e
}
// Run 一键运行:规划 + 执行
func (e *PlanEngine) Run(ctx context.Context, goal string) (*Plan, error) {
// 阶段一:规划
plan, err := e.planner.CreatePlan(ctx, goal)
if err != nil {
return nil, fmt.Errorf("规划阶段失败: %w", err)
}
if e.callbacks.OnPlanCreated != nil {
e.callbacks.OnPlanCreated(plan)
}
// 阶段二:执行
plan.Status = "running"
for !plan.IsDone() && !plan.IsFailed() {
ready := plan.NextReady()
if len(ready) == 0 {
break
}
for _, idx := range ready {
step := &plan.Steps[idx]
plan.MarkRunning(idx)
if e.callbacks.OnStepStart != nil {
e.callbacks.OnStepStart(step)
}
stepCtx := e.executor.buildStepContext(plan, idx)
result, err := e.executor.executeStep(ctx, step, stepCtx)
if err != nil {
plan.MarkFailed(idx, err.Error())
if e.callbacks.OnStepFailed != nil {
e.callbacks.OnStepFailed(step)
}
} else {
plan.MarkDone(idx, result)
if e.callbacks.OnStepDone != nil {
e.callbacks.OnStepDone(step)
}
}
}
}
if e.callbacks.OnPlanComplete != nil {
e.callbacks.OnPlanComplete(plan)
}
return plan, nil
}
// PrintPlan 打印计划(调试用)
func PrintPlan(plan *Plan) {
fmt.Printf("\n📋 计划: %s\n", plan.Goal)
fmt.Println("──────────────────────────────")
for i := range plan.Steps {
s := &plan.Steps[i]
depStr := "无"
if len(s.DependsOn) > 0 {
deps := make([]string, len(s.DependsOn))
for j, d := range s.DependsOn {
deps[j] = fmt.Sprintf("%d", d+1)
}
depStr = fmt.Sprintf("步骤 %s", joinInts(s.DependsOn))
}
statusIcon := map[StepStatus]string{
StatusPending: "⏳",
StatusRunning: "🔄",
StatusDone: "✅",
StatusFailed: "❌",
StatusSkipped: "⏭️",
}[s.Status]
fmt.Printf(" %s [%d] %s (依赖: %s)\n", statusIcon, i+1, s.Task, depStr)
if s.Result != "" {
fmt.Printf(" → %s\n", truncate(s.Result, 80))
}
}
fmt.Println("──────────────────────────────")
done, total := plan.Progress()
fmt.Printf("进度: %d/%d\n\n", done, total)
}
// joinInts 辅助函数
func joinInts(nums []int) string {
result := ""
for i, n := range nums {
if i > 0 {
result += ", "
}
result += fmt.Sprintf("%d", n+1)
}
return result
}
// truncate 截断字符串
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max] + "..."
}
5.2 运行示例
// cmd/plan_demo/main.go
package main
import (
"context"
"fmt"
"log"
"github.com/yourname/agentforge/internal/provider"
"github.com/yourname/agentforge/internal/planner"
)
func main() {
// 初始化 LLM Provider(复用第 02 篇的抽象层)
llm, err := provider.New("openai", loadConfig())
if err != nil {
log.Fatal(err)
}
tools := []planner.ToolInfo{
{Name: "web_search", Description: "搜索互联网获取最新信息"},
{Name: "file_write", Description: "将内容写入文件"},
{Name: "summarize", Description: "对长文本生成摘要"},
}
engine := planner.NewPlanEngine(llm, "gpt-4o", tools).
WithCallbacks(planner.EngineCallbacks{
OnPlanCreated: func(p *planner.Plan) {
fmt.Println("✨ 计划已生成!")
planner.PrintPlan(p)
},
OnStepStart: func(s *planner.PlanStep) {
fmt.Printf("\n▶ 开始执行步骤 %d: %s\n", s.Index+1, s.Task)
},
OnStepDone: func(s *planner.PlanStep) {
fmt.Printf("✔ 步骤 %d 完成\n", s.Index+1)
},
OnPlanComplete: func(p *planner.Plan) {
fmt.Println("\n🎉 计划执行完毕!")
planner.PrintPlan(p)
},
})
// 运行一个复杂任务
goal := "调研 LangChain、LlamaIndex、AutoGen 三个 AI Agent 框架的核心特性,对比它们的差异,生成一份中文分析报告"
plan, err := engine.Run(context.Background(), goal)
if err != nil {
log.Printf("执行出错: %v", err)
}
fmt.Printf("\n最终状态: %s\n", plan.Status)
}
运行后预期输出大致如下:
$ go run cmd/plan_demo/main.go
✨ 计划已生成!
📋 计划: 调研 LangChain、LlamaIndex、AutoGen 三个 AI Agent 框架的核心特性...
──────────────────────────────
⏳ [1] 搜索 LangChain 的核心特性和架构 (依赖: 无)
⏳ [2] 搜索 LlamaIndex 的核心特性和架构 (依赖: 无)
⏳ [3] 搜索 AutoGen 的核心特性和架构 (依赖: 无)
⏳ [4] 对比三个框架的差异和适用场景 (依赖: 步骤 1, 2, 3)
⏳ [5] 生成中文分析报告 (依赖: 步骤 4)
──────────────────────────────
进度: 0/5
▶ 开始执行步骤 1: 搜索 LangChain 的核心特性和架构
✔ 步骤 1 完成
▶ 开始执行步骤 2: 搜索 LlamaIndex 的核心特性和架构
✔ 步骤 2 完成
...
🎉 计划执行完毕!
最终状态: done
注意:步骤 1/2/3 没有相互依赖,理论上可以并行执行。当前版本是顺序执行的,下一章我们会用 DAG 调度让它们并行跑起来。
本篇小结
| 知识点 | 核心内容 |
|---|---|
| Plan-and-Execute 架构 | 规划与执行分离:Planner 一次生成完整计划,Executor 逐步执行 |
| ReAct vs Plan-Execute | ReAct 适合探索性任务,Plan-Execute 适合步骤清晰的工程任务 |
| Plan 数据结构 | 步骤列表 + 依赖关系 + 五种状态(Pending/Running/Done/Failed/Skipped) |
| Planner 实现 | LLM + 结构化输出 + 依赖校验(防环、防越界) |
| Executor 实现 | 按依赖顺序执行 + 级联跳过失败步骤的后续 + 死锁检测 |
| 引擎封装 | PlanEngine 整合 Planner/Executor + 回调钩子 + 可视化打印 |
下篇预告
第 14 篇:任务图与 DAG 执行 — 复杂任务的并行化
本篇的 Executor 是顺序执行的,步骤 1/2/3 明明可以并行却串行等待。下一篇我们将计划建模为 DAG 有向无环图,用拓扑排序 + goroutine 实现真正的并行执行引擎。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。
更多推荐



所有评论(0)