在前文ReAct架构基础上,本篇聚焦Planning Agent(规划型Agent) ——核心是让大模型先对复杂任务做「全局结构化拆解」,生成清晰的执行步骤(规划),再按步骤逐一执行。本文选择「多维度购物清单计算」作为核心示例,充分体现任务拆解的价值。
一、Planning Agent核心:先规划,再执行(与ReAct的核心差异)
| 架构 | 核心逻辑 | 执行方式 | 适用场景 |
|---|---|---|---|
| ReAct | 走一步看一步(思考→行动→观察) | 多轮单步调用,无全局规划 | 步骤少、逻辑简单的任务 |
| Planning Agent | 先拆所有步骤→再按步骤执行 | 先全局规划,再批量执行 | 多步骤、多依赖、有规则的复杂任务 |
示例选择:多维度购物清单计算
原始指令:「买2斤苹果(8元/斤)、3盒牛奶(5元/盒)、1袋大米(50元/袋),满100减10元,计算最终总价」
- • Planning Agent会先拆解为:
- 1. 计算苹果总价 → 2*8
- 2. 计算牛奶总价 → 3*5
- 3. 计算大米总价 → 1*50
....
... 计算满减后价格 → 总价-10- • 再按步骤逐一执行,过程可控、可解释。
二、完整Planning Agent Demo
完全对齐前文的代码风格、注释规范、非流式调用逻辑:
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/sashabaranov/go-openai"
)
// ==================== 1. Agent 核心结构体 ====================
// PlanStep 结构化存储拆解后的单个规划步骤。
type PlanStep struct {
StepID string `json:"step_id"` // 步骤ID(如 step1)
Desc string `json:"desc"` // 步骤描述(如"计算苹果总价")
ToolName string `json:"tool_name"` // 该步骤调用的工具名
Parameter string `json:"parameter"` // 工具参数(如 "2 * 8")
}
// PlanningAgent 规划型 Agent,先全局拆解任务,再按步骤顺序执行。
type PlanningAgent struct {
UserPrompt string // 用户原始指令(复杂任务)
PlanSteps []PlanStep // 全局拆解的规划步骤
StepResults []string // 各步骤执行结果
FinalAnswer string // 最终输出答案
TotalLatency int64 // 总耗时(毫秒)
}
// ==================== 2. 四则运算工具(Agent 的「手脚」) ====================
// parseCalculation 从文本解析「数字1 运算符 数字2」。
func parseCalculation(text string) (num1 float64, op rune, num2 float64, err error) {
_, err = fmt.Sscanf(strings.TrimSpace(text), "%f %c %f", &num1, &op, &num2)
return num1, op, num2, err
}
// calculator 执行加减乘除四则运算,返回可读的计算结果字符串。
func calculator(expression string) string {
num1, op, num2, err := parseCalculation(expression)
if err != nil {
return fmt.Sprintf("解析失败:%v", err)
}
var res float64
switch op {
case '+':
res = num1 + num2
case '-':
res = num1 - num2
case '*':
res = num1 * num2
case '/':
if num2 == 0 {
return "错误:除数不能为0"
}
res = num1 / num2
default:
return fmt.Sprintf("不支持运算符:%c", op)
}
return fmt.Sprintf("%.2f %c %.2f = %.2f", num1, op, num2, res)
}
// ==================== 3. 工具定义说明 ====================
// 注意:Planning Agent 的规划阶段不需要向 LLM 传 Tools 字段。
// 与 ReAct 不同,Planning 的核心是「先全局规划,再顺序执行」:
// - 规划阶段:LLM 在 content 中输出结构化 JSON 计划,工具的 schema 已在
// System Prompt 中用文字描述(tool_name、parameter 格式),无需 Tools 字段;
// - 执行阶段:由 Go 代码直接调用 calculator 函数,不经过 LLM。
// 若传 Tools,LLM 会倾向用 tool_calls 返回(ReAct 模式),与输出 JSON 计划
// 的指令冲突,行为不可控。
// ==================== 4. Planning System Prompt ====================
// planningSystemPrompt 强制 LLM 输出结构化的全局规划步骤(JSON 数组)。
const planningSystemPrompt = `你是一个擅长拆解复杂任务的 Planning Agent,专注于购物清单价格计算。
面对购物清单计算任务,你需要:
1. 全局拆解:将任务拆分为「有依赖、按顺序」的单步计算步骤,覆盖「单品计算 → 汇总 → 优惠抵扣」全流程;
2. 格式要求:必须只返回一个 JSON 数组,不要输出任何其他文字、不要使用代码块标记;
3. 每个数组元素格式为:{"step_id":"步骤ID","desc":"步骤描述","tool_name":"calculator","parameter":"单步表达式"};
4. 规则约束:
- 每步仅调用 calculator 工具,且仅计算一个表达式;
- 表达式格式必须为「数字 运算符 数字」(如 2 * 8、16 + 15);
- 涉及前序结果的表达式,可用数字占位(如汇总后总价 31 + 50);
- 步骤需逻辑连贯,无遗漏、无冗余。
示例:
输入:买2斤苹果(8元/斤),计算总价
输出:[{"step_id":"step1","desc":"计算苹果总价","tool_name":"calculator","parameter":"2 * 8"}]`
// ==================== 5. JSON 提取与解析 ====================
// extractJSONArray 从 LLM 输出中提取首个 '[' 到末尾 ']' 的子串。
// LLM 可能夹带说明文字或代码块标记,这里做容错提取。
func extractJSONArray(content string) string {
s := strings.TrimSpace(content)
// 去除可能的代码块标记
s = strings.TrimPrefix(s, "```json")
s = strings.TrimPrefix(s, "```")
s = strings.TrimSuffix(s, "```")
s = strings.TrimSpace(s)
start := strings.Index(s, "[")
end := strings.LastIndex(s, "]")
if start == -1 || end == -1 || end <= start {
return ""
}
return s[start : end+1]
}
// ==================== 6. 单轮 LLM 调用(规划阶段:拆解任务) ====================
// callLLMForPlan 非流式调用 LLM,生成全局规划步骤。
func (a *PlanningAgent) callLLMForPlan(client *openai.Client, messages []openai.ChatCompletionMessage) ([]PlanStep, error) {
resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
Model: "deepseek-v4-flash", // DeepSeek 官方模型名,第三方模型替换对应名称
Messages: messages,
Temperature: 0.1, // 低温度保证规划精准
})
if err != nil {
return nil, fmt.Errorf("规划阶段 LLM 调用失败:%w", err)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("LLM 未返回规划结果")
}
// 提取并解析 JSON 规划
rawContent := resp.Choices[0].Message.Content
jsonStr := extractJSONArray(rawContent)
if jsonStr == "" {
return nil, fmt.Errorf("未找到 JSON 数组,原始内容:%s", rawContent)
}
var steps []PlanStep
if err := json.Unmarshal([]byte(jsonStr), &steps); err != nil {
return nil, fmt.Errorf("规划步骤解析失败(JSON 格式错误):%w,原始内容:%s", err, jsonStr)
}
if len(steps) == 0 {
return nil, fmt.Errorf("LLM 未生成任何规划步骤")
}
return steps, nil
}
// ==================== 7. 工具执行分发 ====================
// executeTool 根据规划步骤执行对应工具,返回结果字符串。
func executeTool(step PlanStep) string {
if step.ToolName != "calculator" {
return fmt.Sprintf("未知工具「%s」", step.ToolName)
}
if step.Parameter == "" {
return "参数为空"
}
return calculator(step.Parameter)
}
// ==================== 8. Planning Agent 核心流程:规划 → 执行 → 汇总 ====================
// maxPlanSteps 限制最大规划步骤数,防止异常。
const maxPlanSteps = 10
// RunPlanning 运行 Planning Agent 完整流程:
// 1. 规划阶段:调用 LLM 一次性拆解出全局步骤;
// 2. 执行阶段:按步骤顺序逐一执行工具;
// 3. 汇总阶段:整合结果生成最终答案。
func (a *PlanningAgent) RunPlanning() {
fmt.Println("===== Planning Agent 启动 =====")
fmt.Printf("用户指令:%s\n\n", a.UserPrompt)
// 初始化客户端
apikey := os.Getenv("DEEPSEEK_API_KEY")
if apikey == "" {
a.FinalAnswer = "DEEPSEEK_API_KEY 环境变量未设置"
return
}
cfg := openai.DefaultConfig(apikey)
cfg.BaseURL = "https://api.deepseek.com"
client := openai.NewClientWithConfig(cfg)
start := time.Now()
// --------------- 阶段1:全局规划(拆解任务)---------------
fmt.Println("----- 阶段1:全局任务拆解 -----")
planMessages := []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: planningSystemPrompt},
{Role: openai.ChatMessageRoleUser, Content: a.UserPrompt},
}
planSteps, err := a.callLLMForPlan(client, planMessages)
if err != nil {
a.FinalAnswer = fmt.Sprintf("规划阶段失败:%v", err)
return
}
// 立即按最大步数截断,避免后续遍历越界或异常
if len(planSteps) > maxPlanSteps {
planSteps = planSteps[:maxPlanSteps]
}
a.PlanSteps = planSteps
// 打印拆解的规划步骤
fmt.Println("✅ 任务拆解完成,规划步骤:")
for i, step := range a.PlanSteps {
fmt.Printf(" 步骤%d:%s(工具:%s,参数:%s)\n", i+1, step.Desc, step.ToolName, step.Parameter)
}
// --------------- 阶段2:按步骤执行 ---------------
fmt.Println("\n----- 阶段2:按规划步骤执行 -----")
for i, step := range a.PlanSteps {
fmt.Printf("\n执行步骤%d:%s\n", i+1, step.Desc)
result := executeTool(step)
fmt.Printf(" 执行结果:%s\n", result)
a.StepResults = append(a.StepResults, fmt.Sprintf("步骤%d:%s → %s", i+1, step.Desc, result))
}
// --------------- 阶段3:结果汇总 ---------------
fmt.Println("\n----- 阶段3:结果汇总 -----")
a.TotalLatency = time.Since(start).Milliseconds()
// 拼接规划步骤
planStr := ""
for i, step := range a.PlanSteps {
planStr += fmt.Sprintf(" %d. %s(参数:%s)\n", i+1, step.Desc, step.Parameter)
}
if planStr == "" {
planStr = "无规划步骤"
}
// 拼接执行结果
resultStr := ""
for _, res := range a.StepResults {
resultStr += fmt.Sprintf(" %s\n", res)
}
if resultStr == "" {
resultStr = "无执行结果"
}
a.FinalAnswer = fmt.Sprintf(
"✅ Planning Agent 任务完成\n\n【全局规划步骤】\n%s\n【步骤执行结果】\n%s\n总耗时:%dms",
planStr, resultStr, a.TotalLatency,
)
}
// ==================== 主函数:测试 Planning Agent ====================
func main() {
// 测试用例1:多维度购物清单计算(核心示例,多步骤 + 优惠规则)
agent := &PlanningAgent{
UserPrompt: "买2斤苹果(8元/斤)、3盒牛奶(5元/盒)、1袋大米(50元/袋),满100减10元,计算最终需要支付的总价",
}
agent.RunPlanning()
fmt.Println("\n===== Planning Agent 最终输出 =====")
fmt.Println(agent.FinalAnswer)
}三、核心逻辑解析
1. 结构化规划步骤(PlanStep)
定义PlanStep结构体,将LLM生成的文本规划转为可执行的结构化数据,解决「规划步骤无法直接执行」的问题:
- •
StepID:步骤唯一标识,便于追踪; - •
Desc:人类可读的步骤描述,提升可解释性; - •
ToolName:绑定执行工具,实现「步骤→工具」的映射; - •
Parameter:工具执行参数,确保每步计算精准。
2. 非流式规划阶段(核心差异)
移除流式输出,采用「单轮LLM调用+JSON解析」的方式生成全局规划:
- • 通过
planningSystemPrompt强制LLM输出标准JSON格式; - • 清理LLM输出的冗余内容(如「思考:」、代码块标记);
- • 解析JSON为
PlanStep数组,实现「文本规划→结构化步骤」的转换。
3. 步骤执行引擎
按拆解后的步骤逐一执行,对齐参考Demo的工具执行逻辑:
- • 工具分发:通过
executeTool函数统一处理工具调用; - • 错误兜底:单个步骤执行失败不中断整体流程,仅记录错误;
- • 步数限制:通过
maxPlanSteps防止LLM生成异常多的步骤。
4. 结果汇总
整合「规划步骤+执行结果+耗时统计」,生成清晰的最终答案,便于用户理解全流程。
四、运行效果(非流式+规划→执行→汇总)
测试用例1:多维度购物清单计算
===== Planning Agent 启动 =====
用户指令:买2斤苹果(8元/斤)、3盒牛奶(5元/盒)、1袋大米(50元/袋),满100减10元,计算最终需要支付的总价
----- 阶段1:全局任务拆解 -----
✅ 任务拆解完成,规划步骤:
步骤1:计算苹果总价(工具:calculator,参数:2 * 8)
步骤2:计算牛奶总价(工具:calculator,参数:3 * 5)
步骤3:计算大米总价(工具:calculator,参数:1 * 50)
步骤4:汇总苹果和牛奶价格(工具:calculator,参数:16 + 15)
步骤5:加上大米得到商品总价(工具:calculator,参数:31 + 50)
步骤6:商品总价未满100,不享受优惠,最终支付(工具:calculator,参数:81 - 0)
----- 阶段2:按规划步骤执行 -----
执行步骤1:计算苹果总价
执行结果:2.00 * 8.00 = 16.00
执行步骤2:计算牛奶总价
执行结果:3.00 * 5.00 = 15.00
执行步骤3:计算大米总价
执行结果:1.00 * 50.00 = 50.00
执行步骤4:汇总苹果和牛奶价格
执行结果:16.00 + 15.00 = 31.00
执行步骤5:加上大米得到商品总价
执行结果:31.00 + 50.00 = 81.00
执行步骤6:商品总价未满100,不享受优惠,最终支付
执行结果:81.00 - 0.00 = 81.00
----- 阶段3:结果汇总 -----
===== Planning Agent 最终输出 =====
✅ Planning Agent 任务完成
【全局规划步骤】
1. 计算苹果总价(参数:2 * 8)
2. 计算牛奶总价(参数:3 * 5)
3. 计算大米总价(参数:1 * 50)
4. 汇总苹果和牛奶价格(参数:16 + 15)
5. 加上大米得到商品总价(参数:31 + 50)
6. 商品总价未满100,不享受优惠,最终支付(参数:81 - 0)
【步骤执行结果】
步骤1:计算苹果总价 → 2.00 * 8.00 = 16.00
步骤2:计算牛奶总价 → 3.00 * 5.00 = 15.00
步骤3:计算大米总价 → 1.00 * 50.00 = 50.00
步骤4:汇总苹果和牛奶价格 → 16.00 + 15.00 = 31.00
步骤5:加上大米得到商品总价 → 31.00 + 50.00 = 81.00
步骤6:商品总价未满100,不享受优惠,最终支付 → 81.00 - 0.00 = 81.00
总耗时:10273ms
五、核心总结
1. Planning Agent核心价值
- • 全局可控:先拆解所有步骤,再执行,避免ReAct「走一步看一步」的混乱;
- • 可解释性强:用户能清晰看到「为什么拆这些步骤、每步做什么」;
- • 非流式优势:相比流式输出,单轮调用更简洁、易调试,符合生产环境开发习惯。
2. 示例选择原则
- • 选「多步骤、有依赖、带业务规则(如满减)」的任务(购物清单、旅行预算、项目成本核算);
- • 避免单步骤任务(体现不出规划的价值);
- • 贴近实际场景,易理解,且能直观展示「规划→执行」的闭环。
3. 工程化扩展方向
- • 步骤依赖校验:确保步骤按依赖顺序执行(如步骤3必须在步骤1、2之后);
- • 步骤重试机制:失败步骤自动重试;
- • 多工具支持:扩展工具映射(如计算器+汇率转换+运费计算);
- • 规划优化:LLM自动合并冗余步骤,提升执行效率。
Planning Agent是复杂任务场景下的核心架构,相比ReAct更适合企业级应用——通过「先规划、再执行」,让Agent的行为更可控、更可解释,是工业级LLM Agent的必备能力。
MiaoAll