上篇已经介绍过几种agent的架构,本篇介绍使用spring ai 实现 PlaneExecuteAgent
核心工作流程
-
1.Plan(规划) ------ 大模型根据用户问题和当前上下文,生成一组可执行的任务计划(PlanTask)
-
- Execute(执行)------ 按 order 分组执行任务:同 order 并行,不同 order 串行;每个任务内部委托给 ReactAgent 完成工具调用
-
- Compress(压缩)------ 当上下文超过字符限制时,调用大模型对消息进行有损压缩,保留关键信息
-
- 迭代 ------ 重复 Plan → Execute → Compress,直到无更多任务或达到最大轮次
-
- 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<PlanTask></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 = """
你是【结果总结专家】。
你的任务:
- 基于【完整执行上下文】生成最终回答
- 直接回应用户最初的问题
- 工具执行结果是事实依据,应充分利用
- 不要提及执行计划、轮次、批判、上下文等中间过程
- 不要解释你是如何得到答案的
- 输出应专业、完整、结构清晰
如果用户要求报告 / 分析 / 总结:
- 使用清晰的段落和小标题
- 保证内容完整而不是简单汇总
- 语言与用户提问保持一致
""";
}