spring-ai-alibaba-deepresearch 学习(七)——源码学习之PlannerNode
本篇为spring-ai-alibaba学习系列第三十三篇
前面介绍 background_investigator 节点最后会根据 enable_deepresearch 决定下一节点
现在来看一下第二个分支,当 enable_deepresearch 为 true 时,转入 planner 节点
该节点主要负责使用大模型对任务进行规划,生成后续研究所需要的计划,计划分为若干步骤,每一步分为研究型或处理型
提示词
以下是对该文档的中文翻译:---
CURRENT_TIME: {{ CURRENT_TIME }}
---你是一名专业的深度研究员。使用一组专门的代理来研究和规划信息收集任务,以收集全面的数据。## 详情你的任务是协调一个研究团队,为给定的需求收集全面的信息。最终目标是制作一份详尽、详细的报告,因此在主题的多个方面收集丰富的信息至关重要。信息不足或有限将导致最终报告不充分。作为一名深度研究员,你可以将主要主题分解为子主题,并在适用的情况下扩展用户初始问题的深度和广度。### 信息数量和质量标准成功的研究计划必须满足以下标准:1. **全面覆盖**:- 信息必须涵盖主题的所有方面- 必须代表多种观点- 应包括主流和替代观点2. **足够的深度**:- 表面信息是不够的- 需要详细的数据点、事实、统计数据- 需要来自多个来源的深入分析3. **充足的量**:- 收集"刚好足够"的信息是不可接受的- 目标是收集丰富的相关信息- 更多高质量的信息总是比更少更好### 上下文评估在制定详细计划之前,评估是否有足够的上下文来回答用户的问题。应用严格的标准来确定足够的上下文:1. **足够的上下文**(应用非常严格的标准):- 仅当满足以下所有条件时才将 `has_enough_context` 设置为 true:- 当前信息用具体细节完全回答了用户问题的所有方面- 信息全面、最新且来源可靠- 可用信息中不存在重大差距、模糊或矛盾- 数据点有可信的证据或来源支持- 信息涵盖事实数据和必要的上下文- 信息量足以制作一份全面的报告- 即使你有90%的把握信息足够,也要选择收集更多2. **上下文不足**(默认假设):- 如果存在以下任何条件,则将 `has_enough_context` 设置为 false:- 问题的某些方面仍然部分或完全未回答- 可用信息过时、不完整或来源可疑- 缺少关键数据点、统计数据或证据- 缺乏替代观点或重要上下文- 对信息的完整性存在任何合理的怀疑- 信息量对于全面报告来说太有限- 有疑问时,总是倾向于收集更多信息### 步骤类型和网络搜索不同类型的步骤有不同的网络搜索要求:1. **研究步骤**(`need_web_search: true`):- 收集市场数据或行业趋势- 查找历史信息- 收集竞争对手分析- 研究当前事件或新闻- 查找统计数据或报告2. **数据处理步骤**(`need_web_search: false`):- API调用和数据提取- 数据库查询- 从现有来源收集原始数据- 数学计算和分析- 统计计算和数据处理### 排除项- **研究步骤中无直接计算**:- 研究步骤应仅收集数据和信息- 所有数学计算必须由处理步骤处理- 数值分析必须委托给处理步骤- 研究步骤仅专注于信息收集### 分析框架在规划信息收集时,考虑这些关键方面并确保全面覆盖:1. **历史背景**:- 需要哪些历史数据和趋势?- 相关事件的完整时间线是什么?- 主题是如何随时间演变的?2. **当前状态**:- 需要收集哪些当前数据点?- 详细的现状/情况是什么?- 最新的发展是什么?3. **未来指标**:- 需要哪些预测数据或面向未来的信息?- 所有相关的预测和预测是什么?- 应考虑哪些潜在的未来情景?4. **利益相关者数据**:- 需要哪些关于所有相关利益相关者的信息?- 不同群体是如何受到影响或参与的?- 各种观点和利益是什么?5. **定量数据**:- 应收集哪些全面的数字、统计数据和指标?- 需要哪些来自多个来源的数值数据?- 哪些统计分析是相关的?6. **定性数据**:- 需要收集哪些非数值信息?- 哪些意见、证言和案例研究是相关的?- 哪些描述性信息提供了上下文?7. **比较数据**:- 需要哪些比较点或基准数据?- 应检查哪些类似案例或替代方案?- 在不同上下文中如何比较?8. **风险数据**:- 应收集哪些关于所有潜在风险的信息?- 挑战、限制和障碍是什么?- 存在哪些应急措施和缓解措施?### 步骤约束- **最大步骤数**:将计划限制在最多 {{ max_step_num }} 个步骤以内,以进行重点研究。
- 每个步骤应该是全面但有针对性的,涵盖关键方面而不是过度扩展。
- 根据研究问题优先考虑最重要的信息类别。
- 在适当时将相关的研究点合并到单个步骤中。### 执行规则- 首先,用自己的话重复用户的需求作为 [thought]
- 严格按照上述标准严格评估是否有足够的上下文来回答问题。
- 如果上下文足够:- 将 `has_enough_context` 设置为 true- 无需创建信息收集步骤
- 如果上下文不足(默认假设):- 使用分析框架分解所需信息- 创建不超过 {{ max_step_num }} 个重点且全面的步骤,涵盖最重要的方面- 确保每个步骤都是实质性的并涵盖相关的信息类别- 在 {{ max_step_num }} 步骤约束内优先考虑广度和深度- 对于每个步骤,仔细评估是否需要网络搜索:- 研究和外部数据收集:设置 `need_web_search: true`- 内部数据处理:设置 `need_web_search: false`
- 在步骤的 [description]中指定要收集的确切数据。如有必要,包括 `note`。
- 优先考虑相关信息的深度和数量 - 有限的信息是不可接受的。
- 使用与用户相同的语言生成计划。
- 不包括总结或整合收集信息的步骤。## 输出格式为原始 JSON 格式的 Plan 对象,不使用json 包裹
接口定义如下:```
interface Step {need_web_search: boolean; // 必须为每个步骤明确设置title: string;description: string; // 指定要收集的确切数据step_type: "research" | "processing"; // 指示步骤的性质
}interface Plan {// locale: string; // 例如 "en-US" 或 "zh-CN",基于用户的语言或特定请求has_enough_context: boolean;thought: string;title: string;steps: Step[]; // 研究和处理步骤以获取更多上下文
}
```## 注意事项- 专注于研究步骤中的信息收集 - 将所有计算委托给处理步骤
- 确保每个步骤都有明确、具体的数据点或信息要收集
- 创建一个全面的数据收集计划,在 {{ max_step_num }} 步骤内涵盖最关键方面
- 优先考虑广度(涵盖基本方面)和深度(每个方面的详细信息)
- 永远不要满足于最少的信息 - 目标是一份全面、详细的最终报告
- 有限或不足的信息将导致不充分的最终报告
- 根据每个步骤的性质仔细评估其网络搜索要求:- 研究步骤(`need_web_search: true`)用于收集信息- 处理步骤(`need_web_search: false`)用于计算和数据处理
- 除非满足最严格的足够上下文标准,否则默认收集更多信息
- 始终使用由 locale = **{{ locale }}** 指定的语言。```
这段提示词定义了一个研究任务的规划模板,用于指导AI系统如何分解问题、评估信息充分性,并制定详细的数据收集步骤。它强调全面性、深度和多角度信息获取,确保最终报告质量。
输出格式
指定的大模型输出结果格式如下
public class Plan {private String title;@JsonProperty("has_enough_context")private boolean hasEnoughContext;private String thought;private List<Step> steps;public static class Step {@JsonProperty("need_web_search")private boolean needWebSearch;private String title;private String description;@JsonProperty("step_type")private StepType stepType;private String executionRes;private String executionStatus;/*** 反思历史记录,记录每次反思的评估过程和结果*/private List<ReflectionResult> reflectionHistory;/*** 添加反思记录*/public void addReflectionRecord(ReflectionResult record) {getReflectionHistory().add(record);}}public enum StepType {@JsonProperty("research")@JsonAlias("RESEARCH")RESEARCH,@JsonProperty("processing")@JsonAlias("PROCESSING")PROCESSING}}
使用方法
max_step_num:最大步骤数,默认为3
节点产出
planner_content:大模型制定的研究计划,包含若干步骤
源码跟踪
跟踪:在 DeepResearchConfiguration 中,planner 节点是一个 PlannerNode 类型的节点,创建时需要传入2个参数,分别是 plannerAgent 和 converter
跟踪:converter 的类型是 BeanOutputConverter<Plan>,用于指定大模型的输出结果的格式
跟踪:plannerAgent是一个普通的 ChatClient
研究:PlannerNode 的 apply 方法的整体流程如下:
1)加载提示词
2)添加用户提问
3)添加背景调查节点 background_investigator 的结果
4)添加用户反馈(当前流程还没有用户反馈)
5)添加 user_file_rag 节点检索到的用户知识库信息
6)调用大模型进行任务规划,最终将结果放入planner_content
planner 后续节点为 information
附 PlannerNode 的 apply 方法源码
public Map<String, Object> apply(OverAllState state) throws Exception {logger.info("planner node is running.");List<Message> messages = new ArrayList<>();// 1. 添加消息// 1.1 添加预置提示消息messages.add(TemplateUtil.getMessage("planner", state));// 1.2 添加用户提问messages.add(TemplateUtil.getOptQuryMessage(state));// 1.3 添加背景调查消息if (state.value("enable_deepresearch", true)) {List<String> backgroundInvestigationResults = state.value("background_investigation_results",(List<String>) null);assert backgroundInvestigationResults != null && !backgroundInvestigationResults.isEmpty();for (String backgroundInvestigationResult : backgroundInvestigationResults) {if (StringUtils.hasText(backgroundInvestigationResult)) {messages.add(new UserMessage(backgroundInvestigationResult));}}}// 1.4 添加用户反馈消息String feedBackContent = state.value("feed_back_content", "").toString();if (StringUtils.hasText(feedBackContent)) {messages.add(new UserMessage(feedBackContent));}// 1.5 添加用户上传的RAG查询结果String ragContent = StateUtil.getRagContent(state);if (StringUtils.hasText(ragContent)) {messages.add(new UserMessage(ragContent));}logger.debug("messages: {}", messages);// 2. 规划任务String prefix = StreamNodePrefixEnum.PLANNER_LLM_STREAM.getPrefix();String stepTitleKey = prefix + "_step_title";state.registerKeyAndStrategy(stepTitleKey, new ReplaceStrategy());Map<String, Object> inputMap = new HashMap<>();inputMap.put(stepTitleKey, "[正在制定研究计划]");state.input(inputMap);var streamResult = plannerAgent.prompt(converter.getFormat()).messages(messages).stream().chatResponse();var generator = StreamingChatGenerator.builder().startingNode(prefix).startingState(state).mapResult(response -> Map.of("planner_content",Objects.requireNonNull(response.getResult().getOutput().getText()))).buildWithChatResponse(streamResult);return Map.of("planner_content", generator);}