Spring AI 实战:用“编排-工作者”模式打造 AI 旅行规划助手

大家好,今天继续分享Spring AI Workflow系列中的"编排器-工作者(Orchestrator-Workers)"工作流模式,并通过一个实际案例-AI 旅行行程规划助手,带大家一步步看懂它如何巧妙地实现任务的动态分解与并行处理。

对 Spring AI Workflow 其他模式感兴趣的朋友,可以查看我之前发布的文章:

  • Spring AI 评估-优化器模式完整指南
  • Spring AI Chain工作流模式完整指南

"编排器-工作者"工作流模式概述

简单来说, "编排器-工作者(Orchestrator-Workers)"工作流模式 就是它能够分析一个复杂问题,将其拆解成多个定义清晰的子任务,然后将这些任务分派给专门的"工作者"代理去独立完成。

这里我举一个场景示例来便于大家理解:比如你是一位项目经理,手下有一支由各类专家组成的团队。现在,客户提出了一个复杂的需求,比如"从零到一搭建一个新的营销网站"。你肯定不会把这个庞大的任务直接丢给某一个人。相反,你 (作为经理) 会先分析需求,然后进行任务分解:

  • "我需要一位 UI/UX 设计师来制作原型图。"
  • "我需要一位文案来撰写网站内容。"
  • "我需要一位后端开发者来搭建内容管理系统 (CMS)。"
  • "我需要一位前端开发者来完成网站的构建。"

你会把这些子任务分派给最合适的专家。等他们都完成了各自的工作,你再把所有成果汇总起来,最终交付完整的产品。

"编排器-工作者"工作流模式的原理与此如出一辙,只不过合作的对象换成了大语言模型 (LLM)。它主要由三个关键部分组成:

  1. 编排器 (The Orchestrator): 这是一个扮演"经理"角色的 LLM。它唯一的职责就是分析用户最初提出的复杂请求,并将其拆解成一个清晰的、由多个独立子任务组成的列表。它负责决定要做什么
  2. 工作者 (The Workers): 它们是专门化的 LLM (或者是调用 LLM 的函数),负责从编排器接收一个明确、聚焦的子任务。每个工作者都是其特定领域的专家,它们专注于如何做好整个大任务中的某一个环节。
  3. 合成器 (The Synthesizer): 这是最后一步,负责收集所有工作者的产出,并将它们整合成一个统一、连贯的最终结果,呈现给用户。

"编排器-工作者"工作流模式与"并行化"模式最核心的区别在于其动态性 。在并行化模式中,需要并行执行的任务是预先定义好的。而在"编排器-工作者"工作流模式中,任务是由编排器 LLM 在运行时根据用户的具体输入动态决定的。这使得它在处理那些无法预知具体步骤的复杂任务时更加灵活。

我在这里简单概括了以下"编排器-工作者"工作流模式使用场景:

  • 任务非常复杂,需要动态地进行分解。
  • 子任务无法预先确定 (例如,每个人的旅行偏好不同)。
  • 同一个问题需要采用多种不同的专业方法来解决。
  • 需要系统具备自适应解决问题的能力。
  • 希望从多个不同角度来审视同一个任务。

实践案例:AI 旅行行程规划助手

文章下面的章节内容中,我通过这个具体的示例来帮助大家进一步理解"编排器-工作者"工作流模式。 这个旅行规划助手的功能是:为任何目的地创建详尽的旅行计划。当输入旅行偏好相关的信息后,会按照以下步骤工作:

  • 编排器:分析旅行请求,确定需要规划哪些方面 (如住宿、活动、餐饮、交通等)。
  • 工作者:为旅行的每一个方面创建专门的建议。
  • 合成器:将所有工作成果整合成一份完整的、按天规划的旅行行程计划。

以下是这个项目的 Spring Boot 应用目录结构:

css 复制代码
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类用于存放用户旅行计划的所有关键信息,包括目的地、天数、预算、人数等等。

arduino 复制代码
public record TravelRequest(
        String destination,
        Integer numberOfDays,
        String budgetRange, 
        String travelStyle, 
        String groupSize,   
        String specialInterests 
) {}

编排器的输出 DTO :数据模型设计中最重要的部分,要求编排器 LLM必须以这种精确的 JSON 格式返回响应。

arduino 复制代码
public record OrchestratorAnalysis(
        String analysis,        // 对旅行请求的理解分析
        String travelStrategy,  // 本次旅行的总体策略
        List<PlanningTask> tasks // 需要执行的具体规划任务列表
) {}

工作者的任务 DTO : 代表一个由编排器生成并分配给某个专门worker定义明确的"工作指令"。每个 PlanningTask 都是一条独立的指令,它为工作者提供了高效完成任务所需的所有信息,而无需工作者去理解整个旅行计划。

arduino 复制代码
public record PlanningTask(
        String taskType,        // 例如:"accommodation", "activities", "dining"
        String description,     // 该任务需要完成什么
        String specialization   // 该任务的具体关注点
) {}

最终行程 DTO:汇集了整个流程的成果------从编排器的初步分析,到每个专业工作者的分工,再到最终的完整计划。

arduino 复制代码
public record TravelItinerary(
        String destination,
        String travelStrategy,
        String analysis,
        List<String> planningResults, // 每个工作者的产出结果
        String finalItinerary,        // 整合后的每日行程计划
        long processingTimeMs
) {}

第 3 步:"编排器-工作者"工作流的实现

TravelOrchestratorWorkflow这个类是实现该模式的核心部分。它接收结构化的用户请求,编排整个多步骤 流程,并最终生成一份完善的旅行计划。

scss 复制代码
@Component
public 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和具体的工作流逻辑之间的衔接;

kotlin 复制代码
@Service
public 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/planurl端点暴露出去,接收用户发送的restful 请求。

kotlin 复制代码
@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容器。

typescript 复制代码
@SpringBootApplication
public 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调试工具来说明一下整个执行过程:从提交旅行偏好设置,到最终收到一份细节满满的行程计划。

"编排器-工作者"工作流模式时序图

  1. 请求发起:/api/travel/plan 端点发送一个 POST 请求,请求体中包含旅行详情,如目的地、预算和旅行风格等字段信息。
  1. 控制器处理: TravelController 接收到这个请求。Spring 框架会自动将 JSON 数据转换为一个 TravelRequest DTO 对象。随后,控制器将这个 DTO 传递给 TravelPlanningService
  2. 服务委派: TravelPlanningService 接收到 TravelRequest 后,立即调用 TravelOrchestratorWorkflow 中的 createTravelPlan 方法。
  3. 编排器分析 (第 1 步): TravelOrchestratorWorkflow 开始执行。它将用户的偏好连同一个专门的"编排器"提示词一起发送给 LLM。LLM 分析该请求后,返回一份结构化的行动计划------一个子任务列表 (例如,规划住宿、寻找活动、推荐餐厅)。
  4. 工作者并行执行 (第 2 步): 接着,工作流将每个子任务分派给一个"工作者"。利用 CompletableFuture为每个任务触发一次独立的 LLM 调用。所有的工作者并行执行------一个在找酒店,另一个在搜罗美食。
  5. 行程合成 (第 3 步): 一旦所有工作者都完成了各自的任务,工作流便会收集它们各自的报告。然后,它发起最后一次"合成器" LLM 调用,将所有工作者的产出汇总起来,并请求 LLM 将它们整理成一份统一、连贯、按天规划的旅行计划。
  6. 响应交付: 最终生成的 TravelItinerary 对象包含了完整的行程计划,会沿着调用链从工作流返回到服务层,再到控制器。控制器将其包装在一个状态为 200 OK 的 ResponseEntity 中,作为最终的 JSON 响应发送回用户。

最终返回的结构

当然,这个模式虽然强大,但并非"银弹"。它的灵活性也带来了一些工程上的挑战,如果考虑不周,很容易"踩坑"。在实践中,我总结了以下几点需要特别注意:

  • 编排器提示词的可靠性: 必须通过精巧的提示词工程,确保能稳定地输出约定好的JSON结构。如果编排器未能生成正确的 JSON 结构,整个工作流就会中断报错或者产出未知的结果。
  • 成本与延迟: 这种模式会产生大量调用 (1 个编排器 + N 个工作者 + 1 个合成器),导致消耗的token数量较多,这会直接影响到费用成本和总响应时间。虽然并行化工作者有助于降低延迟,但费用是累加的。上线前务必做好成本预估,并持续监控。
  • 稳健的错误处理机制: 如果一个工作者失败了,而其他工作者都成功了,该怎么办?如果合成器生成的结果质量不佳,又该如何处理?为这些异常场景设计好重试或优雅降级策略,是系统健壮性的关键。
  • 工作者提示词的设计: 每个工作者的提示词都应高度聚焦于其特定任务。要确保向每个工作者提供了来自原始请求的充足上下文 (如预算或用户偏好),以便它能给出相关的建议,同时也要避免提示词过于宽泛导致不同工作者的产出内容重叠。
  • 合成器策略的选择: 最后的合成步骤可以很简单,比如用代码拼接字符串,也可以是另一次完整的 LLM 调用,取决于你的业务需求和预算。

写在最后

总体来看,"编排器-工作者"工作流提供了一种全新的、更工程化的LLM交互范式:不再依赖一个单一、复杂的提示词,而是创建了一个由"代理"组成的团队,它们分工协作,共同解决一个问题的不同部分。借助这种模式,可以构建出真正"智能"的应用,能够思考、规划、分工,并最终将所有成果汇集在一起。

好了,今天的分享就这么多,后续会继续分享Spring AI系列的相关教程,有问题欢迎在评论区讨论~

相关推荐
蓝澈1121几秒前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
Kali_077 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0219 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习25 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl38 分钟前
深度解读jdk8 HashMap设计与源码
java
Falling4242 分钟前
使用 CNB 构建并部署maven项目
后端
guojl44 分钟前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文1 小时前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端