1. 为什么单 Agent 搞不定复杂旅游行程如果你用 Spring AI 写过一个旅游助手大概率遇到过这种场景用户丢来一句「两人去厦门玩三天偏好海滨景点不吃辣人均预算 1500 以内」你希望它一次性输出行程、天气穿搭、预算明细。结果模型要么漏掉预算要么把天气建议塞进行程里改一处提示词又崩了另一处。问题不在模型能力而在架构。单个 Agent 承载行程规划、天气查询、预算核算、餐饮推荐全部职责时系统提示词会膨胀到几百字模型在长上下文里容易混淆边界。你想新增「签证提醒」功能就得回头改那一大坨提示词牵一发动全身。多 Agent 协作的核心思路是职责拆分、专人专岗。用一个 Supervisor 调度 Agent 作为唯一入口接收用户需求后拆解任务分发给行程、天气、预算三个垂直子 Agent最后汇总整合成完整方案。类比企业团队前台调度接单行程专员、天气专员、预算专员各干各的调度再把三份结果拼成一份交付物。这篇要跑通的就是 Spring AI 里 Supervisor 模式的多 Agent 协作骨架。适合已经写过单 Agent、想往多 Agent 架构迁移的 Java 后端也适合正在做旅游、客服、综合问答类 AI 应用需要低耦合扩展业务模块的开发者。下面从环境准备到本地验证一步步给出可复制的配置和代码。2. TaoToken 前置准备模型接入与 Key 获取多 Agent 协作对模型的指令遵循能力要求比单 Agent 高因为 Supervisor 要准确拆解任务、子 Agent 要严格守住职责边界。我实测下来用统一接入层管理模型调用会更省心TaoToken 就是这样一个入口它把多家模型的调用方式统一成 OpenAI 兼容格式Spring AI 侧只需要改 base-url 和 api-key。先拿到调用凭证。打开控制台地址登录后进入 API Keys 页面创建一个新 Key复制保存后面配置里要用https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建 Key 时建议按项目命名比如springai-multiagent-dev方便后续区分环境。Key 只在创建时完整显示一次记得先存到密码管理器或本地.env文件别直接提交到 Git。接入文档在这里里面有 Spring AI 的配置示例和可用模型列表遇到参数不确定时对照查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你只是想先验证模型对话效果不想写代码可以直接在模型对话页面试几条旅游需求观察模型对「拆解任务」的响应质量https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteAPI 基础地址统一用https://taotoken.net/api注意这个地址不带 UTM 参数直接写进配置文件即可。Spring AI 的 OpenAI 兼容 starter 会在这个 base-url 后面自动拼接/v1/chat/completions等路径。3. 可复制的多 Agent 配置骨架3.1 项目依赖与配置文件项目基于 Spring Boot 3.5.3 Spring AI 1.0.0JDK 17。pom.xml里核心依赖是 OpenAI 兼容 starter 和 chat clientdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-client-chat/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencyapplication.properties里配置 TaoToken 的接入信息把your-api-key换成上一步创建的 Keyspring.application.namespringai-multiagent-demo server.port8080 spring.ai.openai.api-keyyour-api-key spring.ai.openai.base-urlhttps://taotoken.net/api spring.ai.openai.chat.options.modelgpt-4o-mini spring.ai.openai.chat.options.temperature0.3 logging.level.org.springframework.aiINFO logging.level.com.exampleDEBUGtemperature 设 0.3 是故意的多 Agent 场景下子 Agent 需要稳定输出结构化内容温度太高会导致格式漂移。模型名按你账号下可用的填接入文档里有完整列表。3.2 三个子 Agent 的职责边界子 Agent 的关键是提示词里写死「只做什么、不做什么」。行程 Agent 只输出每日景点、交通、停留时长明确禁止输出预算和天气Component RequiredArgsConstructor public class ItineraryAgent { private final ChatClient chatClient; public String generateRoute(String demand) { String sysPrompt 你是专业行程规划子Agent只负责输出每日景点、交通路线、游玩时长。 禁止输出预算明细禁止输出天气穿搭建议。 用户需求%s 输出格式按天划分上午/下午/晚间三段标注景点、交通方式、建议停留时长。 .formatted(demand); return chatClient.prompt() .system(sysPrompt) .user(demand) .call() .content(); } }天气 Agent 绑定一个时区查询 Tool让它先拿到目的地当前时间再给穿搭建议。Tool 用Tool注解声明Spring AI 会自动注册Component public class TimeMethodTool { Tool(description 根据城市名称获取对应时区当前日期时间) public String getCityCurrentTime( ToolParam(description 城市名称如北京、成都、厦门) String city) { MapString, String zoneMap Map.of( 北京, Asia/Shanghai, 成都, Asia/Shanghai, 厦门, Asia/Shanghai, 伦敦, Europe/London ); String zone zoneMap.getOrDefault(city, Asia/Shanghai); return city 当前时间 LocalDateTime.now(ZoneId.of(zone)); } }天气 Agent 调用时通过.tools(timeTool)把工具挂上去模型会在需要时自动触发调用Component RequiredArgsConstructor public class WeatherAgent { private final ChatClient chatClient; private final TimeMethodTool timeTool; public String getWeatherAdvice(String destination) { String sysPrompt 你是天气穿搭子Agent先调用工具获取目的地当前时间 再根据季节时段给出出行穿搭、防晒防雨建议简洁输出。 禁止输出行程安排和预算明细。 目的地%s .formatted(destination); return chatClient.prompt() .system(sysPrompt) .user(查询 destination 出行穿搭建议) .tools(timeTool) .call() .content(); } }预算 Agent 只核算人均总花费拆成门票、餐饮、交通、住宿四项不碰行程和天气。3.3 Supervisor 调度与结果汇总Supervisor 是整条链路的编排者。它先串行调用三个子 Agent 拿到独立输出再用一次模型调用把三份结果整合成通顺的完整方案Component RequiredArgsConstructor public class SupervisorAgent { private final ItineraryAgent itineraryAgent; private final WeatherAgent weatherAgent; private final BudgetAgent budgetAgent; private final ChatClient chatClient; public String dispatchAndSummary(String userDemand) { String routeResult itineraryAgent.generateRoute(userDemand); String dest extractCity(userDemand); String weatherResult weatherAgent.getWeatherAdvice(dest); String budgetResult budgetAgent.calcBudget(userDemand); String summaryPrompt 你是总规划师整合下面三份专业输出生成一份完整通顺的旅游方案。 【每日行程安排】%s 【目的地天气与穿搭】%s 【费用预算明细】%s 要求结构清晰分三大块排版语言亲切适合出行参考。 .formatted(routeResult, weatherResult, budgetResult); return chatClient.prompt() .system(summaryPrompt) .user(整合完整旅游方案) .call() .content(); } private String extractCity(String demand) { if (demand.contains(厦门)) return 厦门; if (demand.contains(成都)) return 成都; if (demand.contains(北京)) return 北京; return demand.split(游玩)[0]; } }extractCity这里用了最朴素的字符串匹配生产环境建议换成正则或让模型做实体抽取。Controller 层只暴露一个 GET 接口通过 Service 中转保持分层解耦RestController RequestMapping(/multiAgent) RequiredArgsConstructor public class MultiAgentController { private final MultiAgentService multiAgentService; GetMapping(/travelPlan) public MapString, Object travelPlan(RequestParam(demand) String demand) { String fullPlan multiAgentService.generateFullTravelPlan(demand); return Map.of( code, 200, msg, 多Agent协作生成完整旅游方案成功, userDemand, demand, travelPlan, fullPlan, agentMode, Supervisor调度 行程/天气/预算三子Agent ); } }4. 本地验证请求与成功结果启动项目后浏览器或 curl 访问下面这个地址把需求作为demand参数传入http://localhost:8080/multiAgent/travelPlan?demand两人去厦门玩三天偏好海滨景点不吃辣人均预算1500以内执行流程可以这样复盘Supervisor 先提取城市为「厦门」行程 Agent 生成三天海滨路线天气 Agent 调用 Time 工具拿到厦门当前时间后输出夏季穿搭防晒建议预算 Agent 拆分住宿、餐饮、门票并控制在人均 1500 附近最后 Supervisor 把三份内容整合排版。返回的 JSON 里travelPlan字段是完整方案结构大致如下{ agentMode: Supervisor调度 行程/天气/预算三子Agent, code: 200, travelPlan: 【厦门旅游方案】\n\n一、行程安排\n第一天鼓浪屿风情之旅...\n第二天海滨休闲与宗教文化体验...\n第三天历史遗迹与自然生态探索...\n\n二、目的地天气与穿搭\n当前厦门时间为...建议穿着轻薄透气衣物携带防晒用品...\n\n三、费用预算明细\n门票约280元/人餐饮约150元/人交通约200元/人住宿约900元/人总计约1620元/人, userDemand: 两人去厦门玩三天偏好海滨景点不吃辣人均预算1500以内, msg: 多Agent协作生成完整旅游方案成功 }看到三大块内容都齐了说明 Supervisor 的分发和汇总链路跑通了。如果预算超出用户设定可以在预算 Agent 的提示词里加一句「若超出用户预算给出压缩建议」让子 Agent 自己处理约束。5. 本篇常见错误排查子 Agent 输出串味行程里混进了预算数字或者天气里出现了景点推荐。根因是提示词边界不够硬。在子 Agent 的 system prompt 里显式写「禁止输出 XX 内容」比只写「只负责 XX」更有效。实测下来加一句否定约束能明显降低串味概率。Tool 没有被调用天气 Agent 直接编了个时间没走 TimeMethodTool。检查两点一是.tools(timeTool)是否挂上二是 Tool 方法的description是否说清了「什么时候该调用」。描述写成「根据城市名称获取对应时区当前日期时间」模型才知道在需要当前时间时触发。Supervisor 汇总时丢内容三份子结果只整合了两份。常见原因是 summaryPrompt 里的占位符顺序和formatted参数顺序不一致或者某份子结果为空字符串。在汇总前打印三份结果的长度做校验能快速定位是哪一步空了。接口返回 401 或 403Key 无效或额度不足。去 API Keys 页面确认 Key 状态必要时重新创建一个。base-url 确认是https://taotoken.net/api不要多写或少写路径。响应特别慢三个子 Agent 是串行调用的总耗时约等于三次模型调用之和。如果对延迟敏感可以把行程、天气、预算改成并行调用用CompletableFuture包一层Supervisor 等三个都完成后再汇总。注意并行时 ChatClient 是线程安全的可以放心共用。6. 继续深入的方向跑通这个骨架后往生产走还有几块可以补。一是给 Supervisor 加对话记忆把用户长期偏好持久化下次规划时自动带上「不吃辣」「偏好海滨」这类约束。二是动态路由让 Supervisor 自己判断这次需求需要调用哪几个子 Agent而不是固定全调省 token 也省时间。三是给每个子 Agent 绑专属知识库行程 Agent 接景点知识库预算 Agent 接实时票价数据用 RAG 把输出质量再抬一档。如果你打算长期做编码类或 Agent 类项目可以看看 Coding Plan里面有更完整的工程化实践和额度方案https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite多 Agent 的价值不在于 Agent 数量多而在于每个 Agent 的职责足够窄、边界足够清。先把 Supervisor 加三个子 Agent 这条最小链路跑稳再往上叠记忆、路由、RAG架构不会乱。