上篇学习了ReACT,今天继续学习PlanAndExecute模式

与ReACT模式的关键区别如下:
对比维度 | ReAct Agent | Plan-and-Execute Agent |
|---|---|---|
思考模式 | 单步思考-行动循环 | 两阶段分离:先规划后执行 |
执行流程 | Thought → Action → Observation (循环) | Plan → Execute Step 1 → Execute Step 2 → ... |
规划范围 | 只规划下一步 | 一次性规划完整执行路径 |
LLM调用 | 每次循环都需要LLM | 规划时一次,执行时可能多次 |
示例代码:
定义规划器Planner
1 public interface Planner {
2
3 @SystemMessage("""
4 你是一个任务规划专家。请将用户的任务分解为详细的执行步骤。
5
6 输出格式必须是严格的JSON格式:
7 {
8 "plan_name": "任务名称",
9 "steps": [
10 {
11 "step_number": 1,
12 "description": "步骤描述",
13 "tool": "使用的工具名称",
14 "parameters": {"参数名": "参数值"}
15 }
16 ],
17 "expected_output": "预期输出"
18 }
19
20 可用工具列表:
21 1. add - 两数相加
22 2. getWeather - 天气查询
23 3. calculateCircleArea - 计算圆的面积
24 4. getCurrentDateTime - 获取当前时间
25 5. calculateCuboidVolume - 计算长方体体积
26 6. multiply - 两数相乘
27 7. divide - 两数相除
28 8. queryExpressOrder - 查询快递单
29 9. queryRefundProgress - 查询退款进度
30 """)
31 @UserMessage("问:{{request}}")
32 @Agent("基于用户提供的问题生成计划")
33 String createPlan(@V("request") String request);
34 }注:20-29行硬编码的方式指定需要用到的工具列表,也可以去掉,在运行时,类似ReAct一样,一股脑把sampleTools全扔给Planner调用的LLM,从运行结果来看,一样能跑通,但是使用token量就会大很多。
定义执行器
1 public interface Executor {
2
3 @SystemMessage("""
4 你是一个任务执行器。根据给定的步骤执行任务。
5
6 每次只执行一个步骤,然后等待下一个指令。
7 执行完成后,报告结果和下一步建议。
8
9 输出格式:
10 步骤 {n}: [工具名称]
11 输入: {参数}
12 输出: {结果}
13 状态: [成功/失败]
14
15 如果步骤失败,请说明原因和建议。
16 """)
17 @UserMessage("{{step}}")
18 @Agent("基于用户提供的问题生成计划")
19 String executeStep(@V("step") String step);
20 }定义协调器
1 /**
2 * @author junmingyang
3 */
4 public class Coordinator {
5
6 private final Planner planner;
7 private final Executor executor;
8 private final SampleTools tools;
9 private final Map<String, Object> context;
10
11
12 public Coordinator(ChatModel model, SampleTools tools) {
13
14 this.tools = tools;
15 this.context = new HashMap<>();
16
17 //创建规划器
18 this.planner = AgenticServices.agentBuilder(Planner.class)
19 .chatModel(model)
20 .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(15))
21 // 如果明确知道工具的列表,可以显式提供,这里就无需再绑定,以减少token使用
22 //.tools(this.tools)
23 .build();
24
25 //创建执行器
26 this.executor = AgenticServices.agentBuilder(Executor.class)
27 .chatModel(model)
28 .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(15))
29 .tools(this.tools)
30 .build();
31 }
32
33
34 public Map<String, Object> executeTask(String task) {
35 System.out.println("\n" + "=".repeat(80));
36 System.out.println("🎯 任务: " + task);
37 System.out.println("=".repeat(80));
38
39 Map<String, Object> result = new LinkedHashMap<>();
40 result.put("task", task);
41 result.put("start_time", LocalDateTime.now().toString());
42
43 try {
44 // 阶段1: 规划
45 System.out.println("\n📋 阶段1: 任务规划");
46 System.out.println("-".repeat(40));
47 String planJson = planner.createPlan(task);
48 System.out.println("生成的计划:\n" + planJson);
49
50 // 解析计划(简化版,实际应该使用JSON解析)
51 List<Map<String, String>> steps = parsePlan(planJson);
52 result.put("plan", steps);
53
54 // 阶段2: 执行
55 System.out.println("\n⚡ 阶段2: 执行计划");
56 System.out.println("-".repeat(40));
57
58 List<Map<String, Object>> executionResults = new ArrayList<>();
59
60 for (int i = 0; i < steps.size(); i++) {
61 Map<String, String> step = steps.get(i);
62 System.out.printf("\n📝 步骤 %d/%d: %s%n",
63 i + 1, steps.size(), step.get("description"));
64
65 // 构建步骤指令
66 String stepInstruction = buildStepInstruction(step);
67
68 // 执行步骤
69 String stepResult = executor.executeStep(stepInstruction);
70 System.out.println("执行结果:\n" + stepResult);
71
72 // 保存结果
73 Map<String, Object> stepResultMap = new HashMap<>();
74 stepResultMap.put("step_number", i + 1);
75 stepResultMap.put("description", step.get("description"));
76 stepResultMap.put("tool", step.get("tool"));
77 stepResultMap.put("result", stepResult);
78 executionResults.add(stepResultMap);
79
80 // 更新上下文
81 updateContext(task, step, stepResult);
82
83 // 短暂暂停,避免过快执行
84 Thread.sleep(1000);
85 }
86
87 result.put("execution_results", executionResults);
88 result.put("status", "completed");
89
90 } catch (Exception e) {
91 System.err.println("❌ 任务执行失败: " + e.getMessage());
92 result.put("status", "failed");
93 result.put("error", e.getMessage());
94 }
95
96 result.put("end_time", LocalDateTime.now().toString());
97 return result;
98 }
99
100 private List<Map<String, String>> parsePlan(String planJson) {
101 // 简化的计划解析(实际应用中应该使用完整的JSON解析)
102 List<Map<String, String>> steps = new ArrayList<>();
103
104 // 使用正则表达式提取步骤信息
105 Pattern stepPattern = Pattern.compile(
106 "\"step_number\":\\s*(\\d+).*?" +
107 "\"description\":\\s*\"([^\"]+)\".*?" +
108 "\"tool\":\\s*\"([^\"]+)\"",
109 Pattern.DOTALL
110 );
111
112 Matcher matcher = stepPattern.matcher(planJson);
113 while (matcher.find()) {
114 Map<String, String> step = new HashMap<>();
115 step.put("step_number", matcher.group(1));
116 step.put("description", matcher.group(2));
117 step.put("tool", matcher.group(3));
118 steps.add(step);
119 }
120
121 // 如果没有匹配到,创建默认步骤
122 if (steps.isEmpty()) {
123 Map<String, String> defaultStep = new HashMap<>();
124 defaultStep.put("step_number", "1");
125 defaultStep.put("description", "执行任务: " + planJson);
126 defaultStep.put("tool", "analyzeText");
127 steps.add(defaultStep);
128 }
129
130 return steps;
131 }
132
133 private String buildStepInstruction(Map<String, String> step) {
134 return String.format(
135 "执行步骤 %s:\n" +
136 "描述: %s\n" +
137 "工具: %s\n" +
138 "请使用指定工具完成此步骤。",
139 step.get("step_number"),
140 step.get("description"),
141 step.get("tool")
142 );
143 }
144
145 private void updateContext(String task, Map<String, String> step, String result) {
146 // 将步骤结果存入上下文,供后续步骤使用(可选)
147 String key = MurmurHash.murmur3_32Hash(task) + "_step_" + step.get("step_number") + "_result";
148 context.put(key, result);
149 }
150
151 public void printContext() {
152 System.out.println("-".repeat(50) + "\n上下文: ");
153 context.forEach((key, value) -> System.out.println(key + " => \n" + value + "\n" + "-".repeat(30)));
154 }
155 }注:22行,由于在Planner中明确指定了工具列表,所以在协调器中,创建planner实例时,无需刻意绑定Tools.
完整示例:
1 @SpringBootApplication
2 public class PlanAndExecuteApplication {
3
4 public static void main(String[] args) throws IOException {
5 ConfigurableApplicationContext context = SpringApplication.run(AgentDesignPatternApplication.class, args);
6 ChatModel model = context.getBean("ollamaChatModel", ChatModel.class);
7 SampleTools sampleTools = context.getBean("sampleTools", SampleTools.class);
8
9 String[] testTasks = {
10 "计算 15 加上 27 等于多少?",
11 "北京现在的天气怎么样?",
12 "计算半径为5的圆的面积",
13 "现在是几点?",
14 "计算长方体的体积,长10,宽5,高3",
15 "帮我算一下 (25 × 4) ÷ 2 等于多少?",
16 "快递单123456,现在到哪了?",
17 "我的订单56789,退款到账了没?"
18 };
19
20 Coordinator coordinator = new Coordinator(model, sampleTools);
21
22 for (int i = 0; i < testTasks.length; i++) {
23 System.out.printf("\n📦 测试用例 %d/%d%n", i + 1, testTasks.length);
24
25 Map<String, Object> result = coordinator.executeTask(testTasks[i]);
26
27 // 打印总结
28 System.out.println("\n✅ 任务完成总结:");
29 System.out.println("-".repeat(40));
30 System.out.println("任务: " + result.get("task"));
31 System.out.println("状态: " + result.get("status"));
32 System.out.println("耗时: " + calculateDuration(
33 (String) result.get("start_time"),
34 (String) result.get("end_time")
35 ));
36
37 if (result.containsKey("execution_results")) {
38 @SuppressWarnings("unchecked")
39 List<Map<String, Object>> executions =
40 (List<Map<String, Object>>) result.get("execution_results");
41 System.out.println("执行步骤数: " + executions.size());
42 }
43
44 System.out.println("=".repeat(60));
45
46 // 任务间暂停
47 try {
48 Thread.sleep(2000);
49 } catch (InterruptedException e) {
50 Thread.currentThread().interrupt();
51 }
52 }
53
54 coordinator.printContext();
55 }
56
57
58 private static String calculateDuration(String start, String end) {
59 try {
60 LocalDateTime startTime = LocalDateTime.parse(start);
61 LocalDateTime endTime = LocalDateTime.parse(end);
62 Duration duration = Duration.between(startTime, endTime);
63 return String.format("%d秒", duration.getSeconds());
64 } catch (Exception e) {
65 return "未知";
66 }
67 }
68 }运行结果:
📦 测试用例 1/8
================================================================================
🎯 任务: 计算 15 加上 27 等于多少?
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "加法计算","steps": [ { "step_number": 1, "description": "使用add工具计算15加27的结果", "tool": "add", "parameters": {"a": 15, "b": 27} }],"expected_output": "42"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/1: 使用add工具计算15加27的结果
[工具调用] 加法: 15.00 + 27.00 = 42.00
执行结果:
步骤 1: add
输入: {"a": 15, "b": 27}
输出: 42.00
状态: 成功
计算完成!15加27的结果是42.00。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 计算 15 加上 27 等于多少?
状态: completed
耗时: 5秒
执行步骤数: 1
============================================================
📦 测试用例 2/8
================================================================================
🎯 任务: 北京现在的天气怎么样?
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "查询北京天气","steps": [ { "step_number": 1, "description": "使用getWeather工具查询北京当前的天气情况", "tool": "getWeather", "parameters": {"city": "北京"} }],"expected_output": "北京当前的天气信息,包括温度、湿度、天气状况等"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/1: 使用getWeather工具查询北京当前的天气情况
[工具调用] 查询天气: 北京
执行结果:
步骤 1: getWeather
输入: {"city": "北京"}
输出: 北京的天气:晴转多云,温度22-28°C,湿度65%,东南风2级
状态: 成功
天气查询完成!北京当前天气为晴转多云,温度22-28°C,湿度65%,东南风2级。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 北京现在的天气怎么样?
状态: completed
耗时: 4秒
执行步骤数: 1
============================================================
📦 测试用例 3/8
================================================================================
🎯 任务: 计算半径为5的圆的面积
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "计算圆的面积","steps": [ { "step_number": 1, "description": "使用calculateCircleArea工具计算半径为5的圆的面积", "tool": "calculateCircleArea", "parameters": {"radius": 5} }],"expected_output": "78.5"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/1: 使用calculateCircleArea工具计算半径为5的圆的面积
[工具调用] 圆面积计算: 半径=5.00, 面积=78.54
执行结果:
步骤 1: calculateCircleArea
输入: {"radius": 5}
输出: 半径为 5.00 的圆的面积是 78.54
状态: 成功
计算完成!半径为5的圆的面积是78.54。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 计算半径为5的圆的面积
状态: completed
耗时: 5秒
执行步骤数: 1
============================================================
📦 测试用例 4/8
================================================================================
🎯 任务: 现在是几点?
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "获取当前时间","steps": [ { "step_number": 1, "description": "使用getCurrentDateTime工具获取当前时间", "tool": "getCurrentDateTime", "parameters": {} }],"expected_output": "当前的时间信息,包括年、月、日、小时、分钟、秒等"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/1: 使用getCurrentDateTime工具获取当前时间
[工具调用] 当前时间: 2026-02-01 13:43:35
执行结果:
步骤 1: getCurrentDateTime
输入: {}
输出: 2026-02-01 13:43:35
状态: 成功
时间获取完成!当前时间是2026-02-01 13:43:35。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 现在是几点?
状态: completed
耗时: 5秒
执行步骤数: 1
============================================================
📦 测试用例 5/8
================================================================================
🎯 任务: 计算长方体的体积,长10,宽5,高3
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "计算长方体体积","steps": [ { "step_number": 1, "description": "使用calculateCuboidVolume工具计算长方体的体积,长10,宽5,高3", "tool": "calculateCuboidVolume", "parameters": {"length": 10, "width": 5, "height": 3} }],"expected_output": "150"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/1: 使用calculateCuboidVolume工具计算长方体的体积,长10,宽5,高3
[工具调用] 长方体体积: 10.00×5.00×3.00=150.00
执行结果:
步骤 1: calculateCuboidVolume
输入: {"length": 10, "width": 5, "height": 3}
输出: 长 10.00、宽 5.00、高 3.00 的长方体体积是 150.00
状态: 成功
计算完成!长10、宽5、高3的长方体体积是150.00。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 计算长方体的体积,长10,宽5,高3
状态: completed
耗时: 5秒
执行步骤数: 1
============================================================
📦 测试用例 6/8
================================================================================
🎯 任务: 帮我算一下 (25 × 4) ÷ 2 等于多少?
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "复合运算计算","steps": [ { "step_number": 1, "description": "使用multiply工具计算25乘以4的结果", "tool": "multiply", "parameters": {"a": 25, "b": 4} }, { "step_number": 2, "description": "使用divide工具将上一步的结果除以2", "tool": "divide", "parameters": {"a": 100, "b": 2} }],"expected_output": "50"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/2: 使用multiply工具计算25乘以4的结果
[工具调用] 乘法: 25.00 × 4.00 = 100.00
执行结果:
步骤 1: multiply
输入: {"a": 25, "b": 4}
输出: 25.00 × 4.00 = 100.00
状态: 成功
计算完成!25乘以4的结果是100.00。
请提供下一步指令。
📝 步骤 2/2: 使用divide工具将上一步的结果除以2
[工具调用] 除法: 100.00 ÷ 2.00 = 50.00
执行结果:
步骤 2: divide
输入: {"a": 100, "b": 2}
输出: 100.00 ÷ 2.00 = 50.00
状态: 成功
计算完成!100除以2的结果是50.00。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 帮我算一下 (25 × 4) ÷ 2 等于多少?
状态: completed
耗时: 9秒
执行步骤数: 2
============================================================
📦 测试用例 7/8
================================================================================
🎯 任务: 快递单123456,现在到哪了?
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "查询快递物流状态","steps": [ { "step_number": 1, "description": "使用queryExpressOrder工具查询快递单号123456的物流状态", "tool": "queryExpressOrder", "parameters": {"orderNumber": "123456"} }],"expected_output": "快递单号123456的当前物流状态,包括位置、派送进度等信息"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/1: 使用queryExpressOrder工具查询快递单号123456的物流状态
[工具调用] 快递单: 123456,已经在运输途中,预订明天送达
执行结果:
步骤 1: queryExpressOrder
输入: {"expressOrderNo": "123456"}
输出: 快递单: 123456,已经在运输途中,预订明天送达
状态: 成功
查询完成!快递单号123456的物流状态显示:已经在运输途中,预订明天送达。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 快递单123456,现在到哪了?
状态: completed
耗时: 4秒
执行步骤数: 1
============================================================
📦 测试用例 8/8
================================================================================
🎯 任务: 我的订单56789,退款到账了没?
================================================================================
📋 阶段1: 任务规划
----------------------------------------
生成的计划:
```json{
"plan_name": "查询退款进度","steps": [ { "step_number": 1, "description": "使用queryRefundProgress工具查询订单56789的退款进度", "tool": "queryRefundProgress", "parameters": {"orderNumber": "56789"} }],"expected_output": "订单56789的退款进度信息,包括退款状态、预计到账时间等"}
⚡ 阶段2: 执行计划
----------------------------------------
📝 步骤 1/1: 使用queryRefundProgress工具查询订单56789的退款进度
[工具调用] 订单: 56789,退款已审批通过,预计1-3个工作日按原路退回
执行结果:
步骤 1: queryRefundProgress
输入: {"orderNo": "56789"}
输出: 订单: 56789,退款已审批通过,预计1-3个工作日按原路退回
状态: 成功
查询完成!订单56789的退款进度显示:退款已审批通过,预计1-3个工作日按原路退回。
请提供下一步指令。
✅ 任务完成总结:
----------------------------------------
任务: 我的订单56789,退款到账了没?
状态: completed
耗时: 4秒
执行步骤数: 1时序图-AI生成

文中示例代码:
https://github.com/yjmyzz/agentic_turoial_with_langchain4j
参考:
Building Effective AI Agents \ Anthropic
[译] AI Workflow & AI Agent:架构、模式与工程建议(Anthropic,2024)