Agent 设计模式系列

Agent 设计模式 11:Plan-and-Execute:先规划,再执行

把任务规划、单步执行与协调控制分开,阅读结构化计划和原文八组历史输出。

上一讲介绍了 ReAct。本讲继续阅读 Plan-and-Execute:先生成计划,再按步骤执行,由协调器连接规划器与执行器。

Plan-and-Execute 模式

原文配图:Plan-and-Execute 模式。

以下沿用原文对两种模式的比较,未做性能或适用性复测:

对比维度ReAct AgentPlan-and-Execute Agent
思考模式单步思考与行动循环先规划,再执行
执行流程Thought → Action → Observation(循环)Plan → Execute Step 1 → Execute Step 2 → …
规划范围规划下一步预先规划完整执行路径
LLM 调用每次循环需要 LLM规划时一次,执行时可能多次

原文归纳的优势

  • 效率更高:减少重复的"思考"步骤
  • 可预测性:预先知道完整执行路径
  • 易于调试:每个步骤都有明确的状态
  • 适合复杂任务:结构化处理多步骤流程

原文列举的适用场景

  • ✅ 数据处理的ETL流程
  • ✅ 自动化工作流
  • ✅ 需要审计的合规任务
  • ✅ 批量处理任务
  • ❌ 探索性任务(路径不确定)
  • ❌ 需要实时交互的对话

示例代码

规划器 Planner

public interface Planner {

    @SystemMessage("""
           你是一个任务规划专家。请将用户的任务分解为详细的执行步骤。

           输出格式必须是严格的JSON格式:
           {
               "plan_name": "任务名称",
               "steps": [
                   {
                       "step_number": 1,
                       "description": "步骤描述",
                       "tool": "使用的工具名称",
                       "parameters": {"参数名": "参数值"}
                   }
               ],
               "expected_output": "预期输出"
           }

           可用工具列表:
           1. add - 两数相加
           2. getWeather - 天气查询
           3. calculateCircleArea - 计算圆的面积
           4. getCurrentDateTime - 获取当前时间
           5. calculateCuboidVolume - 计算长方体体积
           6. multiply - 两数相乘
           7. divide - 两数相除
           8. queryExpressOrder - 查询快递单
           9. queryRefundProgress - 查询退款进度
          """)
    @UserMessage("问:{{request}}")
    @Agent("基于用户提供的问题生成计划")
    String createPlan(@V("request") String request);
}

原文第 20—29 行在提示词中直接列出工具。原作者还尝试过像 ReAct 一样,把 sampleTools 的工具描述传给规划器,并报告能够运行、但 token 用量增加;本站没有复测这一对比。

执行器 Executor

public interface Executor {

    @SystemMessage("""
            你是一个任务执行器。根据给定的步骤执行任务。
            
            每次只执行一个步骤,然后等待下一个指令。
            执行完成后,报告结果和下一步建议。
            
            输出格式:
            步骤 {n}: [工具名称]
            输入: {参数}
            输出: {结果}
            状态: [成功/失败]
            
            如果步骤失败,请说明原因和建议。
            """)
    @UserMessage("{{step}}")
    @Agent("基于用户提供的问题生成计划")
    String executeStep(@V("step") String step);
}

协调器 Coordinator

/**
 * @author junmingyang
 */
public class Coordinator {

    private final Planner planner;
    private final Executor executor;
    private final SampleTools tools;
    private final Map<String, Object> context;


    public Coordinator(ChatModel model, SampleTools tools) {

        this.tools = tools;
        this.context = new HashMap<>();

        //创建规划器
        this.planner = AgenticServices.agentBuilder(Planner.class)
                .chatModel(model)
                .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(15))
                // 如果明确知道工具的列表,可以显式提供,这里就无需再绑定,以减少token使用
                //.tools(this.tools)
                .build();

        //创建执行器
        this.executor = AgenticServices.agentBuilder(Executor.class)
                .chatModel(model)
                .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(15))
                .tools(this.tools)
                .build();
    }


    public Map<String, Object> executeTask(String task) {
        System.out.println("\n" + "=".repeat(80));
        System.out.println("🎯 任务: " + task);
        System.out.println("=".repeat(80));

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("task", task);
        result.put("start_time", LocalDateTime.now().toString());

        try {
            // 阶段1: 规划
            System.out.println("\n📋 阶段1: 任务规划");
            System.out.println("-".repeat(40));
            String planJson = planner.createPlan(task);
            System.out.println("生成的计划:\n" + planJson);

            // 解析计划(简化版,实际应该使用JSON解析)
            List<Map<String, String>> steps = parsePlan(planJson);
            result.put("plan", steps);

            // 阶段2: 执行
            System.out.println("\n⚡ 阶段2: 执行计划");
            System.out.println("-".repeat(40));

            List<Map<String, Object>> executionResults = new ArrayList<>();

            for (int i = 0; i < steps.size(); i++) {
                Map<String, String> step = steps.get(i);
                System.out.printf("\n📝 步骤 %d/%d: %s%n",
                        i + 1, steps.size(), step.get("description"));

                // 构建步骤指令
                String stepInstruction = buildStepInstruction(step);

                // 执行步骤
                String stepResult = executor.executeStep(stepInstruction);
                System.out.println("执行结果:\n" + stepResult);

                // 保存结果
                Map<String, Object> stepResultMap = new HashMap<>();
                stepResultMap.put("step_number", i + 1);
                stepResultMap.put("description", step.get("description"));
                stepResultMap.put("tool", step.get("tool"));
                stepResultMap.put("result", stepResult);
                executionResults.add(stepResultMap);

                // 更新上下文
                updateContext(task, step, stepResult);

                // 短暂暂停,避免过快执行
                Thread.sleep(1000);
            }

            result.put("execution_results", executionResults);
            result.put("status", "completed");

        } catch (Exception e) {
            System.err.println("❌ 任务执行失败: " + e.getMessage());
            result.put("status", "failed");
            result.put("error", e.getMessage());
        }

        result.put("end_time", LocalDateTime.now().toString());
        return result;
    }

    private List<Map<String, String>> parsePlan(String planJson) {
        // 简化的计划解析(实际应用中应该使用完整的JSON解析)
        List<Map<String, String>> steps = new ArrayList<>();

        // 使用正则表达式提取步骤信息
        Pattern stepPattern = Pattern.compile(
                "\"step_number\":\\s*(\\d+).*?" +
                        "\"description\":\\s*\"([^\"]+)\".*?" +
                        "\"tool\":\\s*\"([^\"]+)\"",
                Pattern.DOTALL
        );

        Matcher matcher = stepPattern.matcher(planJson);
        while (matcher.find()) {
            Map<String, String> step = new HashMap<>();
            step.put("step_number", matcher.group(1));
            step.put("description", matcher.group(2));
            step.put("tool", matcher.group(3));
            steps.add(step);
        }

        // 如果没有匹配到,创建默认步骤
        if (steps.isEmpty()) {
            Map<String, String> defaultStep = new HashMap<>();
            defaultStep.put("step_number", "1");
            defaultStep.put("description", "执行任务: " + planJson);
            defaultStep.put("tool", "analyzeText");
            steps.add(defaultStep);
        }

        return steps;
    }

    private String buildStepInstruction(Map<String, String> step) {
        return String.format(
                "执行步骤 %s:\n" +
                        "描述: %s\n" +
                        "工具: %s\n" +
                        "请使用指定工具完成此步骤。",
                step.get("step_number"),
                step.get("description"),
                step.get("tool")
        );
    }

    private void updateContext(String task, Map<String, String> step, String result) {
        // 将步骤结果存入上下文,供后续步骤使用(可选)
        String key = MurmurHash.murmur3_32Hash(task) + "_step_" + step.get("step_number") + "_result";
        context.put(key, result);
    }

    public void printContext() {
        System.out.println("-".repeat(50) + "\n上下文: ");
        context.forEach((key, value) -> System.out.println(key + " => \n" + value + "\n" + "-".repeat(30)));
    }
}

代码第 22 行创建规划器。原文已在 Planner 提示词中列出工具,因此这里没有额外绑定工具实例。

完整示例

@SpringBootApplication
public class PlanAndExecuteApplication {

    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext context = SpringApplication.run(AgentDesignPatternApplication.class, args);
        ChatModel model = context.getBean("ollamaChatModel", ChatModel.class);
        SampleTools sampleTools = context.getBean("sampleTools", SampleTools.class);

        String[] testTasks = {
                "计算 15 加上 27 等于多少?",
                "北京现在的天气怎么样?",
                "计算半径为5的圆的面积",
                "现在是几点?",
                "计算长方体的体积,长10,宽5,高3",
                "帮我算一下 (25 × 4) ÷ 2 等于多少?",
                "快递单123456,现在到哪了?",
                "我的订单56789,退款到账了没?"
        };

        Coordinator coordinator = new Coordinator(model, sampleTools);

        for (int i = 0; i < testTasks.length; i++) {
            System.out.printf("\n📦 测试用例 %d/%d%n", i + 1, testTasks.length);

            Map<String, Object> result = coordinator.executeTask(testTasks[i]);

            // 打印总结
            System.out.println("\n✅ 任务完成总结:");
            System.out.println("-".repeat(40));
            System.out.println("任务: " + result.get("task"));
            System.out.println("状态: " + result.get("status"));
            System.out.println("耗时: " + calculateDuration(
                    (String) result.get("start_time"),
                    (String) result.get("end_time")
            ));

            if (result.containsKey("execution_results")) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> executions =
                        (List<Map<String, Object>>) result.get("execution_results");
                System.out.println("执行步骤数: " + executions.size());
            }

            System.out.println("=".repeat(60));

            // 任务间暂停
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        coordinator.printContext();
    }


    private static String calculateDuration(String start, String end) {
        try {
            LocalDateTime startTime = LocalDateTime.parse(start);
            LocalDateTime endTime = LocalDateTime.parse(end);
            Duration duration = Duration.between(startTime, endTime);
            return String.format("%d秒", duration.getSeconds());
        } catch (Exception e) {
            return "未知";
        }
    }
}

原文运行输出

展开原文日志或请求/响应(未复测)
📦 测试用例 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 生成)

Plan-and-Execute:先规划,再执行:简化时序图

原文配图:Plan-and-Execute:先规划,再执行:简化时序图。