大家好,今天继续分享Spring AI Workflow系列中的“编排器-工作者(Orchestrator-Workers)”工作流模式,并通过一个实际案例-AI 旅行行程规划助手,带大家一步步看懂它如何巧妙地实现任务的动态分解与并行处理。
对 Spring AI Workflow 其他模式感兴趣的朋友,可以查看我之前发布的文章:
- Spring AI 评估-优化器模式完整指南Spring AI Chain工作流模式完整指南
“编排器-工作者”工作流模式概述
简单来说, “编排器-工作者(Orchestrator-Workers)”工作流模式 就是它能够分析一个复杂问题,将其拆解成多个定义清晰的子任务,然后将这些任务分派给专门的“工作者”代理去独立完成。
这里我举一个场景示例来便于大家理解:比如你是一位项目经理,手下有一支由各类专家组成的团队。现在,客户提出了一个复杂的需求,比如“从零到一搭建一个新的营销网站”。你肯定不会把这个庞大的任务直接丢给某一个人。相反,你 (作为经理) 会先分析需求,然后进行任务分解:
- “我需要一位 UI/UX 设计师来制作原型图。”“我需要一位文案来撰写网站内容。”“我需要一位后端开发者来搭建内容管理系统 (CMS)。”“我需要一位前端开发者来完成网站的构建。”
你会把这些子任务分派给最合适的专家。等他们都完成了各自的工作,你再把所有成果汇总起来,最终交付完整的产品。
“编排器-工作者”工作流模式的原理与此如出一辙,只不过合作的对象换成了大语言模型 (LLM)。它主要由三个关键部分组成:
- 编排器 (The Orchestrator): 这是一个扮演“经理”角色的 LLM。它唯一的职责就是分析用户最初提出的复杂请求,并将其拆解成一个清晰的、由多个独立子任务组成的列表。它负责决定要做什么。工作者 (The Workers): 它们是专门化的 LLM (或者是调用 LLM 的函数),负责从编排器接收一个明确、聚焦的子任务。每个工作者都是其特定领域的专家,它们专注于如何做好整个大任务中的某一个环节。合成器 (The Synthesizer): 这是最后一步,负责收集所有工作者的产出,并将它们整合成一个统一、连贯的最终结果,呈现给用户。
“编排器-工作者”工作流模式与“并行化”模式最核心的区别在于其动态性。在并行化模式中,需要并行执行的任务是预先定义好的。而在“编排器-工作者”工作流模式中,任务是由编排器 LLM 在运行时根据用户的具体输入动态决定的。这使得它在处理那些无法预知具体步骤的复杂任务时更加灵活。
我在这里简单概括了以下“编排器-工作者”工作流模式使用场景:
- 任务非常复杂,需要动态地进行分解。子任务无法预先确定 (例如,每个人的旅行偏好不同)。同一个问题需要采用多种不同的专业方法来解决。需要系统具备自适应解决问题的能力。希望从多个不同角度来审视同一个任务。
实践案例:AI 旅行行程规划助手
文章下面的章节内容中,我通过这个具体的示例来帮助大家进一步理解“编排器-工作者”工作流模式。 这个旅行规划助手的功能是:为任何目的地创建详尽的旅行计划。当输入旅行偏好相关的信息后,会按照以下步骤工作:
- 编排器:分析旅行请求,确定需要规划哪些方面 (如住宿、活动、餐饮、交通等)。工作者:为旅行的每一个方面创建专门的建议。合成器:将所有工作成果整合成一份完整的、按天规划的旅行行程计划。
以下是这个项目的 Spring Boot 应用目录结构:
spring-ai-orchestrator-workers-workflow├── src│ └── main│ ├── java│ │ └── com│ │ └──autogenerator │ │ ├── controller│ │ │ └── TravelController.java│ │ ├── service│ │ │ └── TravelPlanningService.java│ │ ├── workflow│ │ │ └── TravelOrchestratorWorkflow.java│ │ ├── dto│ │ │ └── TravelRequest.java│ │ │ └── TravelItinerary.java│ │ │ └── OrchestratorAnalysis.java│ │ │ └── PlanningTask.java│ │ ├── SpringAiOrchestratorWorkersWorkflowApplication.java│ └── resources│ └── application.yml└── pom.xml
在这里,我对整个项目的目录结构做一个简要的说明:
SpringAiOrchestratorWorkersWorkflowApplication.java
:Spring Boot 应用的启动入口。TravelController.java
:REST 控制器,负责暴露 /api/travel/plan
端点来接收用户请求。TravelPlanningService.java
:服务层,作为控制器与核心工作流逻辑之间的桥梁。TravelOrchestratorWorkflow.java
:核心代码实现,包含“编排器-工作者”模式的逻辑和相关提示词prompt。TravelRequest.java
:数据传输对象 (DTO),代表用户最初的旅行规划请求。TravelItinerary.java
:DTO,代表最终合成并返回给用户的旅行计划。PlanningTask.java
:DTO,代表由编排器为某个工作者生成的单个子任务。OrchestratorAnalysis.java
:DTO,用于映射编排器 LLM 输出的结构化 JSON 数据。application.yml
:Spring AI 相关配置。pom.xml
:Maven 项目依赖。第 1 步:添加 Maven 依赖、配置应用属性
关于这个项目的Maven依赖项以及Spring AI的相关配置,可参考Spring AI Chain工作流模式完整指南
这篇文章。项目的技术栈主要使用的是JDK17、Spring boot3.5以及Google的gemini-2.5-flash模型版本。
第 2 步:定义数据传输对象 (DTO)
在编写工作流逻辑之前,需要先定义好传输数据的结构,确保数据在从 API 接口、 LLM 调用之间流转以及最终返回给用户的整个过程中,始终保持简洁、结构化和类型安全。这里我使用的是 Java 的 Record 类型,因为它的简洁、不可变特性。
用户输入 DTO :作为整个数据流的起点,该record类用于存放用户旅行计划的所有关键信息,包括目的地、天数、预算、人数等等。
public record TravelRequest( String destination, Integer numberOfDays, String budgetRange, String travelStyle, String groupSize, String specialInterests ) {}
编排器的输出 DTO :数据模型设计中最重要的部分,要求编排器 LLM必须以这种精确的 JSON 格式返回响应。
public record OrchestratorAnalysis( String analysis, // 对旅行请求的理解分析 String travelStrategy, // 本次旅行的总体策略 List<PlanningTask> tasks // 需要执行的具体规划任务列表) {}
工作者的任务 DTO: 代表一个由编排器生成并分配给某个专门worker定义明确的“工作指令”。每个 PlanningTask
都是一条独立的指令,它为工作者提供了高效完成任务所需的所有信息,而无需工作者去理解整个旅行计划。
public record PlanningTask( String taskType, // 例如:"accommodation", "activities", "dining" String description, // 该任务需要完成什么 String specialization // 该任务的具体关注点) {}
最终行程 DTO:汇集了整个流程的成果——从编排器的初步分析,到每个专业工作者的分工,再到最终的完整计划。
public record TravelItinerary( String destination, String travelStrategy, String analysis, List<String> planningResults, // 每个工作者的产出结果 String finalItinerary, // 整合后的每日行程计划 long processingTimeMs) {}
第 3 步:“编排器-工作者”工作流的实现
TravelOrchestratorWorkflow这个类是实现该模式的核心部分。它接收结构化的用户请求,编排整个多步骤 流程,并最终生成一份完善的旅行计划。
@Componentpublic class TravelOrchestratorWorkflow { private final ChatClient chatClient; public TravelOrchestratorWorkflow(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder.build(); } /** * 编排器负责协调端到端的旅行规划工作流。 */ public TravelItinerary createTravelPlan(TravelRequest request) { long startTime = System.currentTimeMillis(); // 步骤 1:编排器分析旅行请求 System.out.println("🎯 编排器正在分析目的地为 " + request.destination() + " 的旅行请求..."); String orchestratorPrompt = String.format( ORCHESTRATOR_PROMPT_TEMPLATE, request.destination(), request.numberOfDays(), request.budgetRange(), request.travelStyle() != null ? request.travelStyle() : "general exploration", request.groupSize() != null ? request.groupSize() : "general", request.specialInterests() != null ? request.specialInterests() : "general sightseeing" ); OrchestratorAnalysis analysis = chatClient.prompt() .user(orchestratorPrompt) .call() .entity(OrchestratorAnalysis.class); System.out.println("📋 旅行策略:" + analysis.travelStrategy()); System.out.println("📝 已识别出 " + analysis.tasks().size() + " 个规划任务。"); // 步骤 2:工作者们并行处理旅行规划的各个方面 System.out.println("⚡️ 工作者们正在创建专属建议..."); List<CompletableFuture<String>> workerFutures = analysis.tasks().stream() .map(task -> CompletableFuture.supplyAsync(() -> executePlanningTask(request, task))) .toList(); // 等待所有工作者完成任务并收集结果 List<String> planningResults = workerFutures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); // 步骤 3:将所有建议合成为最终的行程计划 System.out.println("🔧 正在合成最终的旅行行程..."); String finalItinerary = synthesizeItinerary(request, analysis, planningResults); long processingTime = System.currentTimeMillis() - startTime; System.out.println("✅ 旅行行程创建完毕,耗时 " + processingTime + "ms"); return new TravelItinerary( request.destination(), analysis.travelStrategy(), analysis.analysis(), planningResults, finalItinerary, processingTime ); } /** * 执行单个规划任务 (如住宿、活动等) */ private String executePlanningTask(TravelRequest request, PlanningTask task) { System.out.println("🔧 工作者正在处理:" + task.taskType()); String workerPrompt = String.format( WORKER_PROMPT_TEMPLATE, request.destination(), request.numberOfDays(), task.taskType(), task.description(), task.specialization(), request.budgetRange(), request.travelStyle() != null ? request.travelStyle() : "general exploration", request.groupSize() != null ? request.groupSize() : "general", request.specialInterests() != null ? request.specialInterests() : "general sightseeing" ); return chatClient.prompt() .user(workerPrompt) .call() .content(); } /** * 将所有规划任务的结果整合成一个最终的行程 */ private String synthesizeItinerary(TravelRequest request, OrchestratorAnalysis analysis, List<String> planningResults) { String combinedResults = String.join("\n\n", planningResults); String synthesisPrompt = String.format( SYNTHESIZER_PROMPT_TEMPLATE, request.destination(), request.numberOfDays(), analysis.travelStrategy(), combinedResults, request.numberOfDays() ); return chatClient.prompt() .user(synthesisPrompt) .call() .content(); } // 提示词模板 private static final String ORCHESTRATOR_PROMPT_TEMPLATE = """ 请你扮演一位专业的旅行规划师。请分析以下旅行请求,并确定需要规划哪些方面: 目的地:%s 行程天数:%s 天 预算:%s 旅行风格:%s 团队规模:%s 特殊兴趣:%s 基于以上信息,请制定一个旅行策略,并将其分解为 3-4 个具体的规划任务。 每个任务应分别负责旅行的不同方面 (如住宿、活动、餐饮、交通)。 请以 JSON 格式返回你的响应: { "analysis": "你对目的地和旅行者偏好的分析", "travelStrategy": "针对此次旅行类型和目的地的总体策略", "tasks": [ { "taskType": "accommodation", "description": "根据预算和偏好寻找合适的住宿地点", "specialization": "重点关注指定预算下的地理位置、设施和性价比" }, { "taskType": "activities", "description": "推荐符合旅行风格的活动和景点", "specialization": "重点关注符合旅行风格和兴趣的体验" } ] } """; private static final String WORKER_PROMPT_TEMPLATE = """ 请根据以下要求创建旅行建议: 目的地:%s 旅行天数:%s 天 规划重点:%s 任务描述:%s 专业领域:%s 预算范围:%s 旅行风格:%s 团队类型:%s 特殊兴趣:%s 请提供详细、实用、可落地的旅行建议。 在可能的情况下,请包含具体名称、地点和有用的贴士。 """; private static final String SYNTHESIZER_PROMPT_TEMPLATE = """ 请利用以下规划结果,创建一个详尽的每日旅行行程: 目的地:%s 行程天数:%s 天 旅行策略:%s 规划结果: %s 请将所有建议整合成一个连贯的 %s 日行程。 按天组织,并包含时间安排、地点、活动间的衔接等实用细节。 使其易于遵循且对旅行者来说切实可行。 """;}
代码逻辑解析:createTravelPlan
这个公共方法负责管理从头到尾的整个流程。
- 步骤 1:编排器 (The “Manager”)
首先,让扮演“经理”的角色来分析用户请求。通过 使用
ORCHESTRATOR_PROMPT_TEMPLATE
这个System prompt提示词指令,分析出需要规划哪些事项 (例如,酒店、活动、餐饮),并生成一份待办清单。随后,Spring AI 会自动将 LLM 返回的 JSON 格式的计划转换为我们的 OrchestratorAnalysis
Java 对象。步骤 2:工作者 (The “Specialists”)接下来,将待办清单中的每一项任务分发给一个专业的“工作者”worker。这部分的代码中通过使用
CompletableFuture
,让所有工作者能够并行 执行任务。每个工作者都使用 WORKER_PROMPT_TEMPLATE
来专注于自己的单一任务,比如寻找最佳餐厅。步骤 3:合成器 (The “Editor”) 通过 SYNTHESIZER_PROMPT_TEMPLATE
,将所有Worker执行任务的结果进行整合,最终生成一份精美、易读、按天规划的行程计划。最后,该方法会将所有结果——包括初始分析、工作者的原始报告以及最终润色过的计划——全部打包进 TravelItinerary
对象,并返回给用户。
辅助函数与提示词
- executePlanningTask() :这是每个工作者的“职责描述”。它接收清单中的一个任务,并生成一份详细的建议。synthesizeItinerary() :这个函数负责执行合成器的工作,请求 LLM 将所有内容组装成最终的计划。提示词 (…_PROMPT_TEMPLATE):是我们为 LLM 在每个阶段提供的详细文本指令,用于引导其思考,确保我们能得到期望的输出。
第 4 步:创建服务类TravelPlanningService service层主要负责Controller和具体的工作流逻辑之间的衔接;
@Servicepublic class TravelPlanningService { private final TravelOrchestratorWorkflow orchestratorWorkflow; public TravelPlanningService(TravelOrchestratorWorkflow orchestratorWorkflow) { this.orchestratorWorkflow = orchestratorWorkflow; } public TravelItinerary planTrip(TravelRequest request) { // 处理任务委托给工作流 return orchestratorWorkflow.createTravelPlan(request); }}
第 5 步:创建控制器 这一步,通过创建一个控制器TravelController
,用于将POST /api/travel/plan
url端点暴露出去,接收用户发送的restful 请求。
@RestController@RequestMapping("/api/travel")public class TravelController { private final TravelPlanningService travelService; public TravelController(TravelPlanningService travelService) { this.travelService = travelService; } @PostMapping("/plan") public ResponseEntity<TravelItinerary> createItinerary(@RequestBody TravelRequest request) { try { TravelItinerary itinerary = travelService.planTrip(request); return ResponseEntity.ok(itinerary); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().build(); } catch (Exception e) { System.err.println("创建旅行行程报错:" + e.getMessage()); return ResponseEntity.internalServerError().build(); } }}
第 6 步:应用入口点
最后定义启动 Spring Boot 应用的主类, 用于Spring boot初始化所有组件,并启动内嵌的Tomcat容器。
@SpringBootApplicationpublic class SpringAiOrchestratorWorkersWorkflowApplication { public static void main(String[] args) { SpringApplication.run(SpringAiOrchestratorWorkersWorkflowApplication.class, args); } // 通过 Logbook 启用对所有发往 LLM API 的出站 HTTP 请求的日志记录 @Bean public RestClientCustomizer restClientCustomizer(Logbook logbook) { return restClientBuilder -> restClientBuilder.requestInterceptor(new LogbookClientHttpRequestInterceptor(logbook)); }}
“编排器-工作者”工作流模式的分步运行机制
在介绍完上述的关键代码后,我使用浏览器的http调试工具来说明一下整个执行过程:从提交旅行偏好设置,到最终收到一份细节满满的行程计划。
“编排器-工作者”工作流模式时序图
- 请求发起: 向
/api/travel/plan
端点发送一个 POST 请求,请求体中包含旅行详情,如目的地、预算和旅行风格等字段信息。- 控制器处理:
TravelController
接收到这个请求。Spring 框架会自动将 JSON 数据转换为一个 TravelRequest
DTO 对象。随后,控制器将这个 DTO 传递给 TravelPlanningService
。服务委派: TravelPlanningService
接收到 TravelRequest
后,立即调用 TravelOrchestratorWorkflow
中的 createTravelPlan
方法。编排器分析 (第 1 步): TravelOrchestratorWorkflow
开始执行。它将用户的偏好连同一个专门的“编排器”提示词一起发送给 LLM。LLM 分析该请求后,返回一份结构化的行动计划——一个子任务列表 (例如,规划住宿、寻找活动、推荐餐厅)。工作者并行执行 (第 2 步): 接着,工作流将每个子任务分派给一个“工作者”。利用 CompletableFuture
为每个任务触发一次独立的 LLM 调用。所有的工作者并行执行——一个在找酒店,另一个在搜罗美食。行程合成 (第 3 步): 一旦所有工作者都完成了各自的任务,工作流便会收集它们各自的报告。然后,它发起最后一次“合成器” LLM 调用,将所有工作者的产出汇总起来,并请求 LLM 将它们整理成一份统一、连贯、按天规划的旅行计划。响应交付: 最终生成的 TravelItinerary
对象包含了完整的行程计划,会沿着调用链从工作流返回到服务层,再到控制器。控制器将其包装在一个状态为 200 OK 的 ResponseEntity
中,作为最终的 JSON 响应发送回用户。最终返回的结构
当然,这个模式虽然强大,但并非“银弹”。它的灵活性也带来了一些工程上的挑战,如果考虑不周,很容易“踩坑”。在实践中,我总结了以下几点需要特别注意:
- 编排器提示词的可靠性: 必须通过精巧的提示词工程,确保能稳定地输出约定好的JSON结构。如果编排器未能生成正确的 JSON 结构,整个工作流就会中断报错或者产出未知的结果。成本与延迟: 这种模式会产生大量调用 (1 个编排器 + N 个工作者 + 1 个合成器),导致消耗的token数量较多,这会直接影响到费用成本和总响应时间。虽然并行化工作者有助于降低延迟,但费用是累加的。上线前务必做好成本预估,并持续监控。稳健的错误处理机制: 如果一个工作者失败了,而其他工作者都成功了,该怎么办?如果合成器生成的结果质量不佳,又该如何处理?为这些异常场景设计好重试或优雅降级策略,是系统健壮性的关键。工作者提示词的设计: 每个工作者的提示词都应高度聚焦于其特定任务。要确保向每个工作者提供了来自原始请求的充足上下文 (如预算或用户偏好),以便它能给出相关的建议,同时也要避免提示词过于宽泛导致不同工作者的产出内容重叠。合成器策略的选择: 最后的合成步骤可以很简单,比如用代码拼接字符串,也可以是另一次完整的 LLM 调用,取决于你的业务需求和预算。
写在最后
总体来看,“编排器-工作者”工作流提供了一种全新的、更工程化的LLM交互范式:不再依赖一个单一、复杂的提示词,而是创建了一个由“代理”组成的团队,它们分工协作,共同解决一个问题的不同部分。借助这种模式,可以构建出真正“智能”的应用,能够思考、规划、分工,并最终将所有成果汇集在一起。
好了,今天的分享就这么多,后续会继续分享Spring AI系列的相关教程,有问题欢迎在评论区讨论~