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系列的相关教程,有问题欢迎在评论区讨论~

相关推荐
hqxstudying41 分钟前
Java异常处理
java·开发语言·安全·异常
代码老y42 分钟前
ASP.NET Core 高并发万字攻防战:架构设计、性能优化与生产实践
后端·性能优化·asp.net
我命由我123454 小时前
Kotlin 数据容器 - List(List 概述、创建 List、List 核心特性、List 元素访问、List 遍历)
java·开发语言·jvm·windows·java-ee·kotlin·list
武子康6 小时前
Java-80 深入浅出 RPC Dubbo 动态服务降级:从雪崩防护到配置中心秒级生效
java·分布式·后端·spring·微服务·rpc·dubbo
舒一笑6 小时前
我的开源项目-PandaCoder迎来史诗级大更新啦
后端·程序员·intellij idea
@昵称不存在7 小时前
Flask input 和datalist结合
后端·python·flask
zhuyasen8 小时前
Go 分布式任务和定时任务太难?sasynq 让异步任务从未如此简单
后端·go
东林牧之8 小时前
Django+celery异步:拿来即用,可移植性高
后端·python·django
YuTaoShao8 小时前
【LeetCode 热题 100】131. 分割回文串——回溯
java·算法·leetcode·深度优先
源码_V_saaskw9 小时前
JAVA图文短视频交友+自营商城系统源码支持小程序+Android+IOS+H5
java·微信小程序·小程序·uni-app·音视频·交友