spring ai 实战 手搓 PlaneExecuteAgent

上篇已经介绍过几种agent的架构,本篇介绍使用spring ai 实现 PlaneExecuteAgent

核心工作流程

  • 1.Plan(规划) ------ 大模型根据用户问题和当前上下文,生成一组可执行的任务计划(PlanTask)

    1. Execute(执行)------ 按 order 分组执行任务:同 order 并行,不同 order 串行;每个任务内部委托给 ReactAgent 完成工具调用
    1. Compress(压缩)------ 当上下文超过字符限制时,调用大模型对消息进行有损压缩,保留关键信息
    1. 迭代 ------ 重复 Plan → Execute → Compress,直到无更多任务或达到最大轮次
    1. Summarize(总结)------ 基于完整执行上下文,生成最终回答

Agent 整体流程代码

代码相对于reactAgent 还是相对复杂的,我使用AI 生成了注释,可以看看注释

java 复制代码
/**
 * 基于 "Plan-and-Execute"(规划-执行)模式实现的多轮迭代智能 Agent。
 * <p>
 * 核心工作流程:
 * <pre>
 *   1. Plan(规划)  ------ 大模型根据用户问题和当前上下文,生成一组可执行的任务计划(PlanTask)
 *   2. Execute(执行)------ 按 order 分组执行任务:同 order 并行,不同 order 串行;
 *                        每个任务内部委托给 ReactAgent 完成工具调用
 *   3. Compress(压缩)------ 当上下文超过字符限制时,调用大模型对消息进行有损压缩,保留关键信息
 *   4. 迭代           ------ 重复 Plan → Execute → Compress,直到无更多任务或达到最大轮次
 *   5. Summarize(总结)------ 基于完整执行上下文,生成最终回答
 * </pre>
 * <p>
 * 与 ReactAgent 的区别:
 * <ul>
 *   <li>ReactAgent 是"边推理边行动"的单循环模式,适合简单任务</li>
 *   <li>PlanExecuteAgent 是"先规划再执行"的多循环模式,适合需要多步骤、多工具协作的复杂任务</li>
 * </ul>
 *
 */
@Slf4j
public class PlanExecuteAgent {

    /** Spring AI 的 ChatModel,负责与大模型进行交互(规划、压缩、总结均需调用大模型) */
    private ChatModel chatModel;

    /** 当前 Agent 可用的工具回调列表,每个 ToolCallback 封装了一个可调用的外部工具 */
    private List<ToolCallback> tools;

    /** 最大迭代轮次(Plan → Execute 循环次数),<=0 表示不限制 */
    private int maxRounds;

    /** 上下文字符数上限,超过此值时触发上下文压缩(compressIfNeeded) */
    private int contextCharLimit;

    /** 工具并发执行信号量,用于控制同时执行的工具调用数量,防止资源过载 */
    private Semaphore toolSemaphore;

    /** 单个任务执行失败时的最大重试次数 */
    private int maxToolRetries;

    /** Prompt 工厂,持有规划、执行、压缩、总结等各阶段的提示词模板(支持自定义覆盖) */
    private PlanExecutePromptsFactory planExecutePrompts;

    /** 聊天记忆,用于在多轮对话中保存历史消息,实现上下文连续 */
    private ChatMemory chatMemory;

    /**
     * 构造一个 PlanExecuteAgent 实例。
     *
     * @param chatModel            底层大模型,如 DashScopeChatModel
     * @param tools                可用工具列表,每个工具以 ToolCallback 形式注册
     * @param maxRounds            最大 Plan-Execute 迭代轮次,<=0 表示不限制
     * @param contextCharLimit     上下文字符数上限,超过后触发压缩
     * @param toolSemaphore        工具并发信号量,控制同时执行的工具数量
     * @param maxToolRetries       单个任务失败时的最大重试次数
     * @param planExecutePrompts   Prompt 模板工厂,传 null 则使用默认提示词
     * @param chatMemory           聊天记忆,传 null 则不启用记忆
     */
    public PlanExecuteAgent(ChatModel chatModel, List<ToolCallback> tools, int maxRounds, int contextCharLimit, Semaphore toolSemaphore, int maxToolRetries, PlanExecutePromptsFactory planExecutePrompts, ChatMemory chatMemory) {
        this.chatModel = chatModel;
        this.tools = tools;
        this.maxRounds = maxRounds;
        this.contextCharLimit = contextCharLimit;
        this.toolSemaphore = toolSemaphore;
        this.maxToolRetries = maxToolRetries;
        this.planExecutePrompts = planExecutePrompts;
        this.chatMemory = chatMemory;
    }

    /**
     * 执行一次完整的 Plan-and-Execute 流程,返回最终回答。
     * <p>
     * 流程概述:
     * <ol>
     *   <li>初始化全局状态(OverAllState),加载聊天记忆</li>
     *   <li>进入 while 循环,每轮依次执行:
     *     <ul>
     *       <li>createPlan() ------ 调用大模型生成本轮的任务执行计划</li>
     *       <li>executePlans() ------ 按 order 分组并行/串行执行任务</li>
     *       <li>compressIfNeeded() ------ 若上下文超限则压缩</li>
     *     </ul>
     *   </li>
     *   <li>循环结束后调用 summarize() 生成最终回答</li>
     * </ol>
     *
     * @param conversationId 会话 ID,用于关联聊天记忆;为 null 时不启用记忆
     * @param question       用户提出的问题
     * @return 模型生成的最终回答文本
     */
    public String call(String conversationId, String question) {

        // 判断是否启用聊天记忆:conversationId 和 chatMemory 均非空时启用
        boolean useMemory = Objects.nonNull(conversationId) && Objects.nonNull(chatMemory);

        // 初始化全局状态,记录会话 ID、用户问题、消息列表和当前轮次
        OverAllState overallState = new OverAllState(conversationId, question);
        if (useMemory) {
            // 从记忆中加载历史消息,追加到全局状态的消息列表中
            overallState.getMessages().addAll(Optional.of(chatMemory.get(conversationId)).orElse(new ArrayList<>()));
            // 将本轮用户问题存入记忆
            chatMemory.add(conversationId, new UserMessage(question));
        }
        // 将用户问题加入全局状态的消息列表
        overallState.add(new UserMessage(question));


        // ===== Plan-Execute 主循环 =====
        while (maxRounds <= 0 || overallState.getRound() < maxRounds) {
            overallState.nextRound();
            log.info("Round {}", overallState.getRound());


            // 1. 规划阶段:调用大模型生成本轮的任务执行计划
            List<PlanTask> planTasks = createPlan(overallState);
            log.info("PlanTasks: {}", planTasks);
            // 将执行计划作为 AssistantMessage 追加到上下文,供后续阶段参考
            overallState.add(new AssistantMessage("[execution plan]\n" + planTasks));
            // 如果计划为空或所有任务 id 均为 null,说明无需执行工具,直接跳出循环进入总结
            if (planTasks.isEmpty() || planTasks.stream().allMatch(t -> t.id() == null)) {
                log.info("===== No execution needed, direct answer =====");
                break;
            }
            // 2. 执行阶段:按 order 分组执行任务
            executePlans(planTasks, overallState);
            // 3. 压缩阶段:若上下文超过字符限制,调用大模型进行有损压缩
            compressIfNeeded(overallState);

        }
        // 达到最大轮次时的兜底提示
        if (overallState.round == maxRounds) {

            log.info("===== Max rounds reached, force finish =====");
        }

        // 4. 总结阶段:基于完整执行上下文生成最终回答
        return summarize(overallState);


    }


    /**
     * 总结阶段:将用户原始问题和完整执行上下文提交给大模型,生成最终回答。
     * <p>
     * 该方法的职责:
     * <ul>
     *   <li>使用 summarizePrompt 模板构建 Prompt</li>
     *   <li>将全局状态中的所有消息渲染为文本,作为执行上下文传入</li>
     *   <li>调用大模型生成最终回答</li>
     *   <li>将回答存入聊天记忆(如果启用了记忆)</li>
     * </ul>
     *
     * @param state 全局状态,包含完整的消息历史和用户问题
     * @return 大模型生成的最终回答文本
     */
    private String summarize(OverAllState state) {
        Prompt prompt = new Prompt(List.of(
                // 系统提示词:定义总结角色的行为规范
                new SystemMessage(PlanExecutePromptsFactory.buildPrompts(planExecutePrompts).getSummarizePrompt()),
                // 用户消息:包含原始问题和渲染后的完整执行上下文
                new UserMessage("""
                        【用户原始问题】
                        %s
                        
                        【执行上下文(含工具结果)】
                        %s
                        """.formatted(
                        state.getQuestion(),
                        renderMessages(state.getMessages())
                ))
        ));

        String answer = chatModel.call(prompt).getResult().getOutput().getText();
        // 将最终回答追加到聊天记忆,供后续对话使用
        if (state.conversationId != null && chatMemory != null) {
            chatMemory.add(state.conversationId, new AssistantMessage(answer));
        }
        return answer;
    }


    /**
     * 上下文压缩:当消息总字符数超过 contextCharLimit 时,调用大模型对上下文进行有损压缩。
     * <p>
     * 压缩策略:
     * <ul>
     *   <li>将当前所有消息渲染为文本,提交给大模型</li>
     *   <li>大模型按照 compressPrompt 的要求,保留关键信息(用户目标、已完成任务、工具结果等),删除冗余内容</li>
     *   <li>压缩完成后清空原始消息列表,用压缩后的快照(SystemMessage)替代</li>
     * </ul>
     * 这样可以有效防止上下文无限膨胀导致的 token 超限和性能问题。
     *
     * @param state 全局状态
     */
    private void compressIfNeeded(OverAllState state) {

        // 如果当前上下文未超限,无需压缩,直接返回
        if (state.currentChars() < contextCharLimit) {
            return;
        }

        log.warn("===== Context too large, compressing ,size is {} =====", state.currentChars());

        // 构建压缩 Prompt:系统提示词包含字符数硬限制 + 压缩规则模板
        Prompt prompt = new Prompt(List.of(
                new SystemMessage("""                             
                             ## 最大压缩限制(必须遵守)
                             - 你输出的最终内容【总字符数(包含所有标签、空格、换行)】
                                不得超过:%s
                             - 这是硬性上限,不是建议
                             - 如超过该限制,视为压缩失败
                        
                        """.formatted(contextCharLimit) + PlanExecutePromptsFactory.buildPrompts(planExecutePrompts).getCompressPrompt()),

                // 用户消息:将当前所有消息渲染为纯文本,交由大模型压缩
                new UserMessage(renderMessages(state.getMessages()))
        ));

        // 调用大模型执行压缩,获取压缩后的快照文本
        String snapshot = chatModel.call(prompt)
                .getResult()
                .getOutput()
                .getText();

        // 清空原始消息列表,用压缩后的快照替代(作为新的 SystemMessage)
        state.clearMessages();
        state.add(new SystemMessage("【Compressed Agent State】\n" + snapshot));
        log.warn("===== Context compress has completed, size is {} =====", state.currentChars());
    }

    /**
     * 执行阶段:按 order 分组执行任务计划。
     * <p>
     * 执行策略:
     * <ul>
     *   <li>相同 order 的任务并行执行(通过 CompletableFuture 异步提交)</li>
     *   <li>不同 order 的任务按 order 值从小到大串行执行</li>
     *   <li>每个任务通过信号量(toolSemaphore)控制并发度,防止资源过载</li>
     *   <li>每个任务内部委托给 ReactAgent 完成实际的工具调用</li>
     *   <li>前序任务的结果会作为 "Available Results" 传递给后续任务,实现任务间依赖</li>
     * </ul>
     *
     * @param planTasks    本轮的任务计划列表
     * @param overallState 全局状态,执行结果会追加到其消息列表中
     * @return 任务 ID → 执行结果的映射(当前实现返回 null,结果通过 overallState 传递)
     */
    private Map<String, TaskResult> executePlans(List<PlanTask> planTasks, OverAllState overallState) {

        // 存储所有任务的执行结果(线程安全,因为可能并行写入)
        Map<String, TaskResult> results = new ConcurrentHashMap<>();

        // 按 order 分组:order 相同的 task 可并行执行
        Map<Integer, List<PlanTask>> grouped =
                planTasks.stream().collect(Collectors.groupingBy(PlanTask::order));

        // 累积已成功执行的任务结果,用于传递给后续依赖任务
        Map<String, String> accumulatedResults = new ConcurrentHashMap<>();
        // 按 order 值从小到大遍历(TreeSet 保证有序)
        for (Integer i : new TreeSet<>(grouped.keySet())) {

            // 渲染当前已累积的任务结果快照,作为后续任务的依赖上下文
            String renderDependencySnapshot = renderDependencySnapshot(accumulatedResults);
            List<PlanTask> plans = grouped.get(i);
            // 将同 order 的任务并行提交执行
            plans.stream().map(task ->
                    CompletableFuture.runAsync(() -> {
                        try {
                            // 获取信号量许可,控制并发工具调用数量
                            toolSemaphore.acquire();
                            log.info("Executing task {}", task.id());
                            // 跳过 id 为空的任务(无需执行)
                            if (StrUtil.isBlank(task.id())) {
                                return;
                            }
                            // 执行任务(含重试机制),获取执行结果
                            TaskResult taskResult = executeWithRetry(task, renderDependencySnapshot);
                            results.put(task.id, taskResult);
                            // 如果任务执行成功且有输出,将结果累积到依赖结果中
                            if (taskResult.success() && Objects.nonNull(taskResult.output())) {
                                accumulatedResults.put(task.id(), taskResult.output());
                            }
                            // 将任务执行结果作为 AssistantMessage 追加到全局状态
                            overallState.add(new AssistantMessage("""
                                    【Completed Task Result】
                                    taskId: %s
                                    success: %s
                                    result:
                                    %s
                                    error:
                                    %s
                                    【End Task Result】
                                    """.formatted(
                                    task.id(),
                                    taskResult.success(),
                                    taskResult.output(),
                                    taskResult.error()
                            )));


                        } catch (InterruptedException e) {
                            // 线程被中断时,恢复中断标志并记录失败结果
                            Thread.currentThread().interrupt();

                            results.put(task.id(),
                                    new TaskResult(
                                            task.id(),
                                            false,
                                            null,
                                            "Task execution interrupted"
                                    ));

                        } finally {
                            // 无论成功或失败,都必须释放信号量许可
                            toolSemaphore.release();
                        }

                    })
            );


        }


        return null;
    }

    /**
     * 带重试机制的任务执行方法。
     * <p>
     * 每个任务内部创建一个 ReactAgent 实例,由 ReactAgent 完成实际的工具调用。
     * 如果执行失败,最多重试 maxToolRetries 次。
     *
     * @param task               待执行的任务计划
     * @param dependencySnapshot 前序任务的执行结果快照(作为当前任务的依赖上下文)
     * @return 任务执行结果,包含任务 ID、是否成功、输出内容和错误信息
     */
    private TaskResult executeWithRetry(PlanTask task, String dependencySnapshot) {

        int attempt = 0;
        Throwable lastError = null;

        // 重试循环:最多执行 maxToolRetries 次
        while (attempt < maxToolRetries) {
            attempt++;
            try {

                // 为每个任务创建一个独立的 ReactAgent 实例来执行工具调用
                // 参数:chatMemory=null(不启用记忆), maxRounds=2(最多两轮推理)
                ReactAgent reactAgent = new ReactAgent(null, chatModel, 2, "react-agent", "你是一个专业的研究助手", tools);
                // 调用 ReactAgent,将依赖快照和当前任务指令传入
                String result = reactAgent.call("""
                        【Available Results】
                        %s
                        
                        【Current Task】
                        %s
                        """.formatted(
                        dependencySnapshot.isBlank() ? "NONE" : dependencySnapshot,
                        task.instruction
                ), null);

                return new TaskResult(task.id(), true, result, null);
            } catch (Exception e) {
                lastError = e;
                log.warn("Task {} failed attempt {}/{}", task.id(), attempt, maxToolRetries, e);
            }
        }

        // 所有重试均失败,返回失败结果
        return new TaskResult(
                task.id(),
                false,
                null,
                lastError == null ? "unknown error" : lastError.getMessage()
        );
    }


    /**
     * 将已完成任务的结果渲染为可读的文本快照,用于传递给后续依赖任务。
     * <p>
     * 格式示例:
     * <pre>
     * - taskId: task-1
     *   output:
     *   北京: 晴, 5°C
     * </pre>
     *
     * @param results 任务 ID → 输出内容的映射
     * @return 渲染后的文本快照;如果结果为空则返回空字符串
     */
    private String renderDependencySnapshot(Map<String, String> results) {

        if (results.isEmpty()) {
            return "";
        }

        StringBuilder sb = new StringBuilder();

        results.forEach((taskId, output) -> {
            sb.append("- taskId: ")
                    .append(taskId)
                    .append("\n")
                    .append("  output:\n")
                    .append(output)
                    .append("\n\n");
        });

        return sb.toString();
    }

    /**
     * 规划阶段:调用大模型生成本轮的任务执行计划。
     * <p>
     * 该方法会:
     * <ul>
     *   <li>收集所有可用工具的描述信息,供大模型参考</li>
     *   <li>使用 BeanOutputConverter 将大模型的 JSON 输出反序列化为 List&lt;PlanTask&gt;</li>
     *   <li>将当前时间、轮次、工具说明、输出格式和 planPrompt 模板组合为系统提示词</li>
     *   <li>将对话历史渲染为文本,作为用户消息传入</li>
     * </ul>
     *
     * @param overallState 全局状态,包含当前轮次和消息历史
     * @return 生成的任务计划列表
     */
    private List<PlanTask> createPlan(OverAllState overallState) {

        // 获取所有可用工具的名称和描述,拼接为文本供大模型参考
        String toolDes = getToolDes();

        // 使用 BeanOutputConverter 将大模型输出的 JSON 文本转换为 List<PlanTask>
        // ParameterizedTypeReference 用于保留泛型类型信息
        BeanOutputConverter<List<PlanTask>> converter = new BeanOutputConverter<>(new ParameterizedTypeReference<>() {
        });

        Prompt prompt = new Prompt(List.of(
                // 系统提示词:包含当前时间、轮次、工具说明、输出格式和规划规则
                new SystemMessage("""
                            当前时间是:%s。
                        
                            当前是迭代的第 %s 轮次。
                        
                            ## 可用工具说明(仅用于规划参考)
                            %s
                        
                            ## 输出format
                            %s
                        
                        """.formatted(LocalDateTime.now(ZoneId.of("Asia/Shanghai")), overallState.getRound(), toolDes, converter.getFormat())
                        + PlanExecutePromptsFactory.buildPrompts(planExecutePrompts).getPlanPrompt()),
                // 用户消息:将对话历史渲染为文本
                new UserMessage("【对话历史】\n\n" + renderMessages(overallState.getMessages()))
        ));

        // 调用大模型生成计划(JSON 格式),然后反序列化为 PlanTask 列表
        String text = chatModel.call(prompt).getResult().getOutput().getText();
        List<PlanTask> planTasks = converter.convert(text);

        return planTasks;
    }


    /**
     * 将消息列表渲染为可读的纯文本格式。
     * <p>
     * 每条消息的格式为:
     * <pre>
     * [消息类型]
     *
     * 消息内容
     * </pre>
     *
     * @param messages 消息列表
     * @return 渲染后的文本
     */
    private String renderMessages(List<Message> messages) {
        StringBuilder sb = new StringBuilder();
        for (Message m : messages) {
            sb.append("\n\n[").append(m.getMessageType()).append("]\n\n")
                    .append(m.getText());
        }
        return sb.toString();
    }

    /**
     * 获取所有可用工具的描述信息,用于在规划阶段告知大模型有哪些工具可用。
     * <p>
     * 格式示例:
     * <pre>
     * --getWeather:根据城市名称查询天气信息
     * --search:搜索工具
     * </pre>
     *
     * @return 工具描述文本;如果无可用工具则返回提示信息
     */
    private String getToolDes() {
        if (CollectionUtil.isEmpty(tools)) {

            return "当前无工具可以使用";
        }

        StrBuilder sb = new StrBuilder();
        for (ToolCallback tool : tools) {
            sb.append("--").append(tool.getToolDefinition().name()).append(":").append(tool.getToolDefinition().description()).append("\n");
        }
        return sb.toString();
    }


    /**
     * 全局状态容器,贯穿整个 Plan-Execute 流程。
     * <p>
     * 持有以下信息:
     * <ul>
     *   <li>conversationId --- 会话 ID,用于关联聊天记忆</li>
     *   <li>question --- 用户原始问题</li>
     *   <li>messages --- 完整的消息历史(包含用户消息、助手消息、工具结果等)</li>
     *   <li>round --- 当前迭代轮次</li>
     * </ul>
     * 该状态在 Plan → Execute → Compress 各阶段之间共享和传递。
     */
    @Getter
    public static class OverAllState {

        /** 会话 ID,用于关联聊天记忆 */
        private final String conversationId;
        /** 用户原始问题 */
        private final String question;
        /** 完整的消息历史列表,贯穿整个 Plan-Execute 流程 */
        private final List<Message> messages = new ArrayList<>();
        /** 当前迭代轮次(Plan-Execute 循环次数) */
        private int round = 0;

        public OverAllState(String conversationId, String question) {
            this.question = question;
            this.conversationId = conversationId;
        }

        /** 轮次递增 1,进入下一轮迭代 */
        public void nextRound() {
            round++;
        }

        /** 向消息列表追加一条消息 */
        public void add(Message m) {
            messages.add(m);
        }

        /**
         * 计算当前消息列表的总字符数(用于判断是否需要触发上下文压缩)。
         *
         * @return 所有消息文本的字符数之和
         */
        public int currentChars() {
            return messages.stream()
                    .mapToInt(m -> m.getText() == null ? 0 : m.getText().length())
                    .sum();
        }

        /** 清空消息列表(压缩前调用,压缩后用快照替代) */
        public void clearMessages() {
            messages.clear();
        }
    }

    /**
     * 任务计划记录,表示一个待执行的子任务。
     *
     * @param id          任务唯一标识(如 "task-1"),为 null 时表示无需执行
     * @param instruction 任务指令,描述需要调用哪个工具、执行什么操作
     * @param order       执行顺序,相同 order 的任务可并行执行,不同 order 按值从小到大串行执行
     */
    public record PlanTask(String id, String instruction, int order) {
    }

    /**
     * 任务执行结果记录。
     *
     * @param taskId  任务唯一标识
     * @param success 是否执行成功
     * @param output  执行成功时的输出内容
     * @param error   执行失败时的错误信息
     */
    public record TaskResult(
            String taskId,
            boolean success,
            String output,
            String error
    ) {
    }


    /**
     * 入口方法,用于本地测试 Plan-Execute Agent 的完整工作流。
     * <p>
     * 本示例中:
     * <ul>
     *   <li>使用阿里云通义千问(DashScope)作为底层大模型</li>
     *   <li>注册了天气查询(WeatherService)和搜索(SearchService)两个工具</li>
     *   <li>最大迭代轮次设为 3,上下文限制 1000 字符,工具并发数 5,单任务重试 2 次</li>
     *   <li>要求 Agent 综合天气查询和搜索工具,生成一份综合天气分析报告</li>
     * </ul>
     *
     * @param args 命令行参数(未使用)
     */
    public static void main(String[] args) {
        // 1. 构建 DashScope(通义千问)ChatModel 实例,需替换为真实的 API Key
        ChatModel chatModel = DashScopeChatModel.builder()
                .dashScopeApi(DashScopeApi.builder()
                        .apiKey("xxxx")
                        .build())
                .build();

        // 2. 将 WeatherService 和 SearchService 中的 @Tool 方法转换为 ToolCallback 数组
        ToolCallback[] toolCallbacks = ToolCallbacks.from(new WeatherService(), new SearchService());

        // 3. 创建滑动窗口聊天记忆,最多保留 20 条消息
        ChatMemory chatMemory = MessageWindowChatMemory.builder().maxMessages(20).build();

        // 4. 创建 PlanExecuteAgent 实例
        //    参数:chatModel, 工具列表, 最大轮次=3, 上下文限制=1000字符, 信号量=5, 重试=2次, 默认Prompt, 聊天记忆
        PlanExecuteAgent agent = new PlanExecuteAgent(chatModel, Arrays.asList(toolCallbacks), 3, 1000, new Semaphore(5), 2, PlanExecutePromptsFactory.builder().build(), chatMemory);

        // 5. 调用 Agent,传入复杂的多步骤问题
        //    该问题需要依次:查询天气 → 搜索预警 → 搜索景点 → 生成报告
        String result = agent.call("""
                请你先查询北京今天的天气,再搜索本周末北京天气的预警情况,并基于本周末北京的天气预警情况,搜索北京本周末适合旅游打卡的景点有哪些,最终生成一份不少于 500 字的综合天气分析报告。
                """, "111");
        log.info("{}", result);

    }
}

提示词模板

java 复制代码
/**
 * Plan-Execute Agent 各阶段的 Prompt 模板工厂。
 * <p>
 * 持有 Agent 工作流中各阶段所需的提示词模板:
 * <ul>
 *   <li>planPrompt --- 规划阶段:指导大模型生成任务执行计划</li>
 *   <li>executePrompt --- 执行阶段:指导 ReactAgent 执行具体任务</li>
 *   <li>critiquePrompt --- 批判阶段:评估当前执行结果是否满足目标</li>
 *   <li>compressPrompt --- 压缩阶段:指导大模型对上下文进行有损压缩</li>
 *   <li>summarizePrompt --- 总结阶段:指导大模型生成最终回答</li>
 * </ul>
 * <p>
 * 支持自定义覆盖:通过 {@link #buildPrompts(PlanExecutePromptsFactory)} 方法,
 * 可以只覆盖部分阶段的提示词,未指定的阶段自动使用默认值。
 */
@Getter
@Builder
public class PlanExecutePromptsFactory {

    /** 规划阶段的提示词模板,用于指导大模型生成任务执行计划 */
    private String planPrompt;

    /** 执行阶段的提示词模板,用于指导 ReactAgent 执行具体工具调用任务 */
    private String executePrompt;

    /** 批判阶段的提示词模板,用于评估当前执行结果是否满足用户目标 */
    private String critiquePrompt;

    /** 压缩阶段的提示词模板,用于指导大模型对上下文进行有损压缩 */
    private String compressPrompt;

    /** 总结阶段的提示词模板,用于指导大模型基于执行上下文生成最终回答 */
    private String summarizePrompt;

    /**
     * 使用 DefaultPrompts 中的默认提示词构建一个完整的 Prompt 工厂实例。
     *
     * @return 包含所有默认提示词的 PlanExecutePromptsFactory 实例
     */
    public static PlanExecutePromptsFactory buildPrompts() {
        return PlanExecutePromptsFactory.builder()
                .planPrompt(DefaultPrompts.PLAN)
                .executePrompt(DefaultPrompts.EXECUTE)
                .critiquePrompt(DefaultPrompts.CRITIQUE)
                .compressPrompt(DefaultPrompts.COMPRESS)
                .summarizePrompt(DefaultPrompts.SUMMARIZE)
                .build();
    }

    /**
     * 基于默认提示词构建 Prompt 工厂,并用自定义实例中的非空字段覆盖对应阶段的提示词。
     * <p>
     * 如果 custom 为 null,则直接返回默认实例。
     * 对于 custom 中为 null 的字段,自动回退到默认值,实现"部分覆盖"的效果。
     *
     * @param custom 自定义的 Prompt 工厂实例,其中非 null 的字段会覆盖默认值
     * @return 合并后的 PlanExecutePromptsFactory 实例
     */
    public static PlanExecutePromptsFactory buildPrompts(PlanExecutePromptsFactory custom) {
        PlanExecutePromptsFactory defaults = buildPrompts();

        if (custom == null) {
            return defaults;
        }

        return PlanExecutePromptsFactory.builder()
                .planPrompt(custom.planPrompt != null ? custom.planPrompt : defaults.planPrompt)
                .executePrompt(custom.executePrompt != null ? custom.executePrompt : defaults.executePrompt)
                .critiquePrompt(custom.critiquePrompt != null ? custom.critiquePrompt : defaults.critiquePrompt)
                .compressPrompt(custom.compressPrompt != null ? custom.compressPrompt : defaults.compressPrompt)
                .summarizePrompt(custom.summarizePrompt != null ? custom.summarizePrompt : defaults.summarizePrompt)
                .build();
    }
}

各阶段默认的提示词

java 复制代码
/**
 * Plan-Execute Agent 各阶段的默认提示词常量集合。
 * <p>
 * 本类定义了 Agent 工作流中五个阶段的默认提示词模板:
 * <ul>
 *   <li>{@link #PLAN} --- 规划阶段:指导大模型生成结构化的任务执行计划(JSON 格式)</li>
 *   <li>{@link #EXECUTE} --- 执行阶段:指导工具执行助手基于依赖结果执行具体任务</li>
 *   <li>{@link #CRITIQUE} --- 批判阶段:评估当前执行结果是否满足用户目标</li>
 *   <li>{@link #COMPRESS} --- 压缩阶段:指导大模型对上下文进行有损压缩,保留关键信息</li>
 *   <li>{@link #SUMMARIZE} --- 总结阶段:指导大模型基于完整执行上下文生成最终回答</li>
 * </ul>
 * 这些常量通过 {@link PlanExecutePromptsFactory#buildPrompts()} 加载为默认值,
 * 用户可通过自定义 PlanExecutePromptsFactory 覆盖任意阶段的提示词。
 */
public final class DefaultPrompts {

    /** 私有构造器,防止实例化(工具类) */
    private DefaultPrompts() {
    }

    /**
     * 规划阶段的默认提示词。
     * <p>
     * 定义"执行计划生成器"的角色和行为规范:
     * <ul>
     *   <li>只规划工具调用型任务,严禁规划纯文本任务(如总结、分析)</li>
     *   <li>支持并行(相同 order)和串行(不同 order)两种执行模式</li>
     *   <li>如果无需工具调用,返回 id=null 的特殊任务</li>
     *   <li>输出必须是严格的 JSON 数组格式</li>
     * </ul>
     */
    public static final String PLAN = """
            你是【执行计划生成器】。
                        
            你的职责:
            - 判断是否需要【调用工具】来推进问题解决;
            - 如果不需要任何工具调用,返回"无需执行计划";
            - 如果需要,生成【仅包含工具调用的执行计划】。
            - 尤其需要关注最近一次的【Critique Feedback】提出的反馈意见,补充增量的执行计划。

            ## 重要规则(必须严格遵守)

            1. 你只能规划【工具调用型任务】;
               - 每一个 task 都必须明确对应一个具体工具;
               - instruction 中必须显式包含工具名称。

            2. 严禁规划以下内容:
               - 总结、分析、对比、写报告、生成结论;
               - 整合信息、输出答案、给出建议;
               - 任何不直接调用工具的纯文本任务。

            3. 如果问题已经具备作答条件,或是简单问题,无需执行计划:
               - 返回一个对象,且 id = null;
               - 表示"无需生成工具执行计划"。

            4. 支持并行与串行:
               - order 相同表示可并行执行;
               - 如果没有明确依赖关系,尽量并行(order 相同);
               - 如果是有先后关系,order数字小的先执行,并在后续指令中也尽可能的指明依赖前序的工具结果信息。

            5. 输出必须是严格的 JSON 数组:
               - 不要任何额外文字、解释或注释;
               - 不要输出 tool_call 或函数调用。

            6. instruction 只能是自然语言的【工具调用指令】,
               用于指导后续执行模块解析并调用工具。
                              
            ## 输出格式(严格 JSON)
            示例1:无需工具执行计划
            [
              {
                "id": null,
                "instruction": "无需调用任何工具",
                "order": 0
              }
            ]

            示例2:需要工具执行计划(并行)
            [
              {
                "id": "task-1",
                "instruction": "调用 <工具名> 工具,执行 <明确查询或操作>",
                "order": 1
              },
              {
                "id": "task-2",
                "instruction": "调用 <工具名> 工具,执行 <明确查询或操作>",
                "order": 1
              }
            ]
            
            示例3:具有先后关系的执行计划(串行)
            [
              {
                "id": "task-1",
                "instruction": "调用 <工具名> 工具,执行 <明确查询或操作>,获取XX结果",
                "order": 1
              },
              {
                "id": "task-2",
                "instruction": "根据task-1的执行结果,调用 <工具名> 工具,执行 <明确查询或操作>",
                "order": 2
              }
            ]
            
            示例4:具有先后关系的执行计划(并行+串行)
            [
               {"id":"task-1","instruction":"调用 XXX 工具,执行<明确查询或操作>","order":1},
               {"id":"task-2","instruction":"调用 XXX 工具,执行<明确查询或操作>","order":1},
               {"id":"task-3","instruction":"根据 task1 和 task-2 的结果,调用 XXX 工具,执行<明确查询或操作>","order":2}
             ]
            """;

    /**
     * 执行阶段的默认提示词。
     * <p>
     * 定义"工具执行助手"的角色:只能基于已有的依赖结果和当前任务指令执行任务,禁止假设未明确给出的信息。
     */
    public static final String EXECUTE = """
            你是一个专业的工具执行助手。
            你只能基于提供的依赖结果和当前任务指令执行任务,
            禁止假设任何未明确给出的信息。
            """;

    /**
     * 批判阶段的默认提示词。
     * <p>
     * 定义"任务批判评估专家"的角色:基于完整上下文判断是否已满足用户目标。
     * 输出格式为 JSON,包含 passed(是否通过)和 feedback(改进建议)。
     */
    public static final String CRITIQUE = """
            你是【任务批判评估专家】。
            基于完整上下文判断是否已满足用户目标。

            只允许输出 JSON:
            {
              "passed": true | false,
              "feedback": "如果未通过,给出明确改进建议,建议不要过长,描述清楚问题即可。"
            }
            """;

    /**
     * 压缩阶段的默认提示词。
     * <p>
     * 定义"上下文内容压缩器"的角色和详细压缩规则:
     * <ul>
     *   <li>压缩目标:在不丢失关键信息的前提下,生成支持下一轮正确决策的最小状态</li>
     *   <li>必须保留的信息:用户最终目标、已完成任务及结论、工具执行结果、最近一次批判反馈、未解决问题</li>
     *   <li>压缩规则:删除冗余内容,保留事实和结论,禁止引入新信息</li>
     *   <li>输出格式:按固定分区(User Goal / Completed Work / Key Tool Results / Last Critique / Open Issues)输出</li>
     * </ul>
     */
    public static final String COMPRESS = """
             你是【上下文内容压缩器】。
             
             你的输出将直接作为 Agent 的下一轮上下文输入,
             用于继续规划、判断和工具调用。
             这是工作记忆压缩,不是给人类阅读的摘要。
             
             ## 压缩目标
             将当前上下文压缩为:
             在不丢失关键信息的前提下,支持 Agent 下一轮正确决策的最小状态。
             
             ## 必须保留的信息(不可丢失)
             ### 1. 用户最终目标
             - 保留用户的原始问题或最终确认的目标
             - 不得改变语义,不得抽象或泛化
             
             ### 2. 已完成的关键任务(任务级别)
             - 只保留已经实际执行的任务
             - 每个任务必须包含明确结论或结果
             - 不得保留计划、假设或未执行内容
             
             ### 3. 工具执行结果(必须完整)
             - 每一次工具调用都必须保留:
               - 工具名称
               - 关键输入参数
               - 输出中的关键事实、数据或结论
             - 不得仅保留总结而丢失工具来源
             - 不得合并多个工具结果为模糊描述
             
             ### 4. 最近一次 Critique / Reflection(如存在)
             - 是否通过(Passed: true / false)
             - 如果未通过,明确失败原因和改进要求
             
             ### 5. 当前未解决的问题
             - 明确缺失的信息或未完成的条件
             - 不得引入新的任务或推理
             
             ## 压缩规则
             - 删除冗余对话、重复解释和思考过程
             - 保留事实、结论、判断、约束和失败原因
             - 不得使用模糊指代(如"之前提到的""上一步")
             - 不得引入任何新信息、新结论或新推理
             - 不得生成计划、建议或下一步行动
             
             ## 超限时的压缩优先级(仅在接近或超过上限时使用)
             - 优先压缩或删除:
                1) 较早且对当前决策影响较小的已完成任务
                2) 工具输出中的描述性或重复性文本,仅保留关键事实
                3) Critique / Reflection 中的细节描述(但 Passed 字段必须保留)
             - 禁止删除或改写用户最终目标
             
             ## 输出格式(严格遵守)
             【User Goal】
             <用户原始问题或最终目标>
             
             【Completed Work】
             - Task: <已执行的任务> 
               Conclusion: <结论或结果>
             - ...
             
             【Key Tool Results】 
             - Tool: <tool_name> 
               Input: <关键输入参数> 
               Result: <关键事实、数据或结论>
             - ...
             
             【Last Critique】 
             - Passed: true / false 
             - Feedback: <失败原因或通过结论;如不存在填写 NONE>
             
             【Open Issues】 
             - <尚未解决的问题或缺失信息>
            """;

    /**
     * 总结阶段的默认提示词。
     * <p>
     * 定义"结果总结专家"的角色:基于完整执行上下文生成最终回答,直接回应用户问题。
     * 要求输出专业、完整、结构清晰,不提及中间执行过程。
     */
    public static final String SUMMARIZE = """
            你是【结果总结专家】。

            你的任务:
            - 基于【完整执行上下文】生成最终回答
            - 直接回应用户最初的问题
            - 工具执行结果是事实依据,应充分利用
            - 不要提及执行计划、轮次、批判、上下文等中间过程
            - 不要解释你是如何得到答案的
            - 输出应专业、完整、结构清晰

            如果用户要求报告 / 分析 / 总结:
            - 使用清晰的段落和小标题
            - 保证内容完整而不是简单汇总
            - 语言与用户提问保持一致
            """;
}
相关推荐
火山引擎开发者社区1 小时前
火山引擎 AgentKit 获评中国信通院 2026 智能原生软件“银弹”标杆实践
人工智能
米小虾1 小时前
RSI 走到哪一步了:拆开递归自我改进的三个可写面、五条定律,和那个没人做的对照实验
人工智能·agent
这料鬼有毒1 小时前
二刷hot100-73.矩阵置零
java
caoerzhong1 小时前
JeeWMS 开源仓库管理系统二次开发与接口对接实战:Java WMS 如何与 ERP、MES 和自动化设备打通
java·开源
ACP广源盛139246256731 小时前
GSV9001E 国产 4K 视频处理器,AI 多模态可视化大屏多路画面合成方案解析
人工智能·硬件架构·国产芯片·ai服务器
长谷深风1111 小时前
评测AI Agent:三种裁判各司其职
java·大数据·开发语言·人工智能·ai agent
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day42|项目周开发技术汇总 1 思维导图复盘
java·开发语言·spring boot·学习·spring
武雄(小星Ai)1 小时前
9月大模型超级发布周:GPT-6 Astra、Gemini 3.8 Flash等4款模型选型对比实录
ai·大模型·对比评测
火山引擎开发者社区1 小时前
火山方舟Agent Plan上线最新生图生视频模型
人工智能