转载--从单机到分布式:Spring AI Alibaba 多智能体架构实战——以旅游行程规划为例
转发一篇好文档,对于Agent生产环境落地有很大帮助
原文连接:https://mp.weixin.qq.com/s/-i3pq5dFG04Yw_a7tTsBAw
这不是一篇“把三个 Agent 串起来”的 Demo 文章,而是一篇面向生产环境的架构升级指南。我们将从单体多智能体出发,逐步演进到支持高并发、可扩展、可观测、可灰度、可治理的分布式多智能体系统,并以“旅游行程规划”这一典型复合型业务为例,完整讲清楚 Spring AI Alibaba 在企业级场景中的落地方法。
1. 为什么是旅游行程规划?
旅游行程规划是一个很适合多智能体拆分的业务场景,因为它天然具备以下特征:
-
• 任务是复合型的,不是单轮问答
-
• 子任务之间有依赖,但并非完全串行
-
• 每个子领域都需要独立的专业知识和工具
-
• 用户对结果质量、响应速度、可解释性都很敏感
一个真实的用户请求可能是这样的:
“我和家人五一去杭州玩 4 天,预算 8000 元,带一个 6 岁小朋友,希望节奏轻松,最好住西湖附近,帮我规划每天景点、酒店和交通安排。”
表面上这是一个自然语言请求,实际上系统要完成的是一条复杂业务链路:
-
1. 识别用户意图与约束条件
-
2. 补全缺失参数并做结构化建模
-
3. 生成景点候选集合
-
4. 结合亲子、预算、区域偏好做排序
-
5. 规划住宿区域与酒店方案
-
6. 计算景点与酒店之间的动线
-
7. 生成逐日行程
-
8. 对结果做冲突检测、预算校验与兜底修正
如果这些任务全部堆进一个大 Prompt,让一个 Agent 一次性完成,系统会很快碰到以下问题:
-
• 上下文过长,Prompt 成本高且易漂移
-
• 工具过多,模型难以稳定选择正确工具
-
• 错误定位困难,不知道是景点推荐错了还是交通推理错了
-
• 高峰期吞吐有限,单节点无法支撑节假日流量
-
• 某个能力热点升高时无法独立扩容
这正是多智能体架构最有价值的场景。
2. 从“能跑”到“能上线”:多智能体系统真正要解决什么问题
很多团队在做多智能体系统时,第一阶段关注的是“怎么编排”;但到了生产阶段,真正决定系统成败的是下面这几个维度:
-
• 架构层:如何把单节点 Agent 编排平滑升级为分布式协作
-
• 并发层:如何提升吞吐,避免请求堆积和模型侧雪崩
-
• 状态层:如何管理会话记忆、执行状态、任务上下文
-
• 工程层:如何做重试、限流、熔断、降级、超时控制
-
• 运维层:如何观察每个 Agent 的成本、耗时、错误率和调用链
-
• 业务层:如何确保输出结果稳定、可解释、可审计
所以,一套真正可落地的多智能体系统,不应该只回答“如何调用子 Agent”,还必须回答:
-
• 为什么这样拆
-
• 哪些能力必须本地化,哪些应该远程化
-
• 哪些阶段可以并发,哪些必须保持顺序
-
• 如何做状态隔离与多租户治理
-
• LLM 调用失败后如何兜底
-
• 如何避免一个热门 Agent 成为全局瓶颈
3. 架构选型:为什么选择 Agent as Tool,而不是一个超级 Agent
3.1 单 Agent 架构的问题本质
单 Agent 的问题,不只是“大 Prompt 不优雅”,本质是三个方面:
-
1. 认知负载过高
一个 Agent 同时负责景点、酒店、交通、预算、排序、冲突检测,会导致决策边界模糊。 -
2. 上下文污染严重
各领域信息混在一起,模型容易把住宿约束错误地映射到景点规划上,或者把历史对话中的无关信息带入当前推理。 -
3. 工程扩展能力差
单体 Agent 难以独立扩容、灰度、观测和演进。
3.2 多智能体拆分原则
在旅游规划场景中,我们采用“职责单一 + 数据顺流 + 热点独立扩容”的拆分原则,将系统拆为以下核心角色:
-
•
PlannerAgent
负责总体规划、任务分解、结果整合 -
•
AttractionAgent
负责景点筛选、主题匹配、游玩节奏安排 -
•
HotelAgent
负责住宿候选、位置平衡、预算约束 -
•
TransportAgent
负责交通方式、路径衔接、耗时估算 -
•
BudgetGuardAgent
负责预算校验与超支修正 -
•
ReviewAgent
负责对最终计划做一致性检查与可读性优化
3.3 为什么是 Agent as Tool
Spring AI Alibaba 在多智能体编排中,Agent as Tool 是最适合“结构化业务工作流”的模式。
它的核心思想是:
-
• 由一个主 Agent 承担编排职责
-
• 子 Agent 不直接与用户对话,而是作为“具备语义能力的工具”被调用
-
• 主 Agent 根据任务进展决定调用顺序与输入参数
这种模式特别适合旅游规划,因为这是一个“先规划、再约束、再整合”的流程型任务,而不是多专家轮流与用户接管对话的对话型任务。
3.4 为什么新版中更推荐这一模式
从框架设计演进看,SupervisorAgent 这类强绑定监督者语义的实现逐步被更通用的组合方式替代,背后体现的是一个很重要的架构思想:
-
• 调度不是靠特殊类名完成的
-
• 调度是由模型函数调用能力、工具语义和状态流转共同完成的
换句话说,框架在鼓励我们把“编排”看成一种工程能力,而不是单独依赖某个魔法类。
这带来三个直接收益:
-
• 调度逻辑更可组合
-
• 子 Agent 更容易远程化
-
• 每一步调用都更容易做指标采集和链路追踪
4. 架构演进路线:单机版、增强版、分布式版
4.1 阶段一:单机版多智能体
最初我们把所有 Agent 放在一个 Spring Boot 进程中:
用户请求
API Gateway / Controller
PlannerAgent
AttractionAgent
HotelAgent
TransportAgent
BudgetGuardAgent
ReviewAgent
输出完整行程
这个阶段的目标不是一步到位,而是先把业务链路跑通,明确每个 Agent 的输入、输出和边界。
4.2 阶段二:单机增强版
当业务进入测试环境后,需要补足工程能力:
-
• Redis 持久化记忆
-
• 并发执行与线程池隔离
-
• 模型调用限流、熔断、超时
-
• 请求级 traceId、会话级 planId
-
• Prompt 模板与租户配置解耦
这一步非常关键,因为很多系统并不是败在分布式,而是败在“单机阶段就没有做好工程治理”。
4.3 阶段三:分布式版多智能体
当热点能力明显分化,比如景点推荐调用地图 API、POI 检索和实时拥挤度接口,成为最耗时的部分,就应该将其拆成独立服务:
用户
Gateway
Planner Service
Redis Session Store
Nacos
Attraction Agent Service
Hotel Agent Service
Transport Agent Service
Budget Agent Service
Prometheus / Grafana / Loki / Tempo
此时,主服务负责:
-
• 会话入口
-
• 任务编排
-
• 聚合结果
-
• 鉴权与租户隔离
-
• 熔断与降级策略
而子 Agent 服务负责:
-
• 自身领域能力的执行
-
• 领域内工具调用
-
• 局部缓存与提示词治理
-
• 指标暴露与健康检查
5. 领域建模:别急着写 Agent,先把输入输出定清楚
多智能体系统最容易被忽略的一点,不是 Agent 写法,而是领域对象设计。没有稳定的数据契约,Agent 之间只能靠自然语言“猜”,这会直接降低可控性。
5.1 用户请求结构
public record TravelPlanRequest(
String sessionId,
String tenantId,
String destination,
Integer days,
BigDecimal budget,
Integer adults,
Integer children,
String travelStyle,
String hotelAreaPreference,
List<String> preferences,
List<String> constraints) {
}
5.2 标准化的中间结果
public record AttractionPlan(
String city,
List<DailyAttraction> days,
List<String> assumptions,
BigDecimal estimatedTicketCost) {
}
public record HotelPlan(
List<HotelOption> options,
String recommendedHotelId,
BigDecimal estimatedStayCost) {
}
public record TransportPlan(
List<DailyTransport> routes,
BigDecimal estimatedTransportCost) {
}
public record FinalTravelPlan(
TravelPlanRequest request,
AttractionPlan attractionPlan,
HotelPlan hotelPlan,
TransportPlan transportPlan,
BigDecimal totalEstimatedCost,
List<String> warnings,
String markdownItinerary) {
}
5.3 为什么一定要做结构化输出
结构化输出有四个收益:
-
• 让 Agent 之间的数据交互从“自然语言耦合”升级为“契约耦合”
-
• 便于对结果做预算校验、冲突检测、缓存和审计
-
• 便于未来替换某个 Agent 的实现,而不影响整体编排
-
• 为分布式调用与异步执行提供稳定边界
简单说,Prompt 可以是自然语言,但系统内部的数据流尽量要结构化。
6. 单机版核心实现:Spring AI Alibaba 多智能体编排
6.1 Maven 依赖
<properties>
<java.version>17</java.version>
<spring-boot.version>3.3.0</spring-boot.version>
<spring-ai-alibaba.version>1.1.2.2</spring-ai-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
6.2 专家 Agent 的定义
首先定义三个核心专家 Agent。重点不是“让 Prompt 写得很花”,而是把职责边界写清楚。
@Configuration
public class TravelAgentConfiguration {
@Bean
public ReactAgent attractionAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("attraction_agent")
.description("负责景点推荐、游玩顺序和日程节奏规划")
.instruction("""
你是旅游景点规划专家。
目标:
1. 根据目的地、旅行天数、出行人群和用户偏好推荐景点
2. 控制每天游玩强度,避免儿童和老人高强度奔波
3. 输出结构化景点规划,明确上午、下午、晚上安排
约束:
- 不要推荐与用户约束冲突的景点
- 尽量减少跨城区折返
- 若信息不足,明确说明假设
""")
.model(chatModel)
.outputKey("attraction_plan")
.build();
}
@Bean
public ReactAgent hotelAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("hotel_agent")
.description("负责住宿区域选择、酒店推荐和预算平衡")
.instruction("""
你是住宿推荐专家。
输入中会包含景点规划结果:{attraction_plan}
目标:
1. 根据景点动线选择合适住宿区域
2. 根据预算和入住人数给出酒店建议
3. 输出多个候选方案,并说明适合人群
约束:
- 住宿总成本不能脱离用户预算
- 优先保证交通便利与亲子友好
""")
.model(chatModel)
.outputKey("hotel_plan")
.build();
}
@Bean
public ReactAgent transportAgent(ChatModel chatModel) {
return ReactAgent.builder()
.name("transport_agent")
.description("负责交通衔接、耗时预估和成本评估")
.instruction("""
你是交通规划专家。
输入中会包含景点规划:{attraction_plan}
输入中会包含住宿规划:{hotel_plan}
目标:
1. 规划每日景点和酒店之间的交通方式
2. 给出耗时、费用和换乘复杂度
3. 若用户带儿童,优先推荐更平稳的路线
约束:
- 避免推荐明显高风险或低确定性的方案
- 优先选择可执行性高的交通建议
""")
.model(chatModel)
.outputKey("transport_plan")
.build();
}
}
这里有一个非常重要的实践经验:
-
•
description给模型理解“这个 Agent 是干什么的” -
•
instruction约束“这个 Agent 应该怎么做” -
•
outputKey决定“这个 Agent 的输出如何进入状态”
这三者共同构成了 Agent 的可编排边界。
6.3 主 Agent 编排
@Configuration
public class PlannerAgentConfiguration {
@Bean
public ReactAgent plannerAgent(
ChatModel chatModel,
ReactAgent attractionAgent,
ReactAgent hotelAgent,
ReactAgent transportAgent) {
AgentTool attractionTool = AgentTool.builder().agent(attractionAgent).build();
AgentTool hotelTool = AgentTool.builder().agent(hotelAgent).build();
AgentTool transportTool = AgentTool.builder().agent(transportAgent).build();
return ReactAgent.builder()
.name("planner_agent")
.description("负责统筹生成完整旅游行程")
.instruction("""
你是总规划师。
你需要按如下流程执行:
1. 调用 attraction_agent 生成景点规划
2. 调用 hotel_agent 生成住宿规划
3. 调用 transport_agent 生成交通规划
4. 汇总为最终行程方案
输出要求:
- 结果要清晰、完整、可执行
- 明确每日安排、住宿建议、交通建议、预算估算
- 如果预算超支,要主动说明并给出收缩建议
""")
.model(chatModel)
.tools(List.of(attractionTool, hotelTool, transportTool))
.build();
}
}
这个版本已经能跑通,但还不够生产级。真正上线后,第一个遇到的问题几乎一定不是“代码能不能执行”,而是“慢”和“不稳定”。
7. 生产级升级一:把结果生成链路工程化
7.1 应用服务层,而不是 Controller 直接调 Agent
很多 Demo 会在 Controller 里直接执行 Agent。生产环境不建议这么做。更合理的方式是建立应用服务层,统一处理:
-
• 入参校验
-
• traceId 注入
-
• 幂等控制
-
• 会话上下文装载
-
• 熔断降级
-
• 结果落库和缓存
@Service
@RequiredArgsConstructor
public class TravelPlanApplicationService {
private final TravelPlanningOrchestrator orchestrator;
private final SessionMemoryService sessionMemoryService;
private final PlanCacheService planCacheService;
public FinalTravelPlan createPlan(TravelPlanRequest request) {
String cacheKey = buildCacheKey(request);
FinalTravelPlan cached = planCacheService.get(cacheKey);
if (cached != null) {
return cached;
}
ChatContext context = sessionMemoryService.load(request.sessionId(), request.tenantId());
FinalTravelPlan result = orchestrator.execute(request, context);
sessionMemoryService.save(request.sessionId(), request.tenantId(), result);
planCacheService.put(cacheKey, result);
return result;
}
private String buildCacheKey(TravelPlanRequest request) {
return request.tenantId() + ":" + request.destination() + ":" + request.days() + ":" + request.budget();
}
}
7.2 控制器层保持轻薄
@RestController
@RequestMapping("/api/travel-plans")
@RequiredArgsConstructor
public class TravelPlanController {
private final TravelPlanApplicationService applicationService;
@PostMapping
public ResponseEntity<FinalTravelPlan> createPlan(@Valid @RequestBody TravelPlanRequest request) {
return ResponseEntity.ok(applicationService.createPlan(request));
}
}
这一步的意义是把“智能体编排”从 Web 层解耦出来,为后续接入 MQ、异步任务、批量生成和分布式编排打基础。
8. 生产级升级二:并发优化,不是所有步骤都要串行
8.1 串行链路的性能瓶颈
如果景点、酒店、交通全部顺序调用,每一步模型耗时假设为:
-
• 景点规划:3.5 秒
-
• 酒店推荐:2.4 秒
-
• 交通规划:2.1 秒
-
• 汇总整理:1.2 秒
总响应时间大约在 9 到 11 秒之间。
对于一个消费型业务,这个耗时在高峰期是非常危险的。
8.2 能并发的步骤要显式并发
在我们的场景中:
-
•
AttractionAgent必须先执行 -
•
HotelAgent依赖景点规划 -
•
TransportAgent依赖景点规划和酒店推荐
严格来说,交通规划通常会用到酒店位置,因此它不能和酒店完全无依赖并发。
但我们可以做一个更工程化的拆分:
-
1. 先执行
AttractionAgent -
2. 并发执行:
-
•
HotelAgent生成酒店候选 -
•
TransportPreCheckAgent基于景点做交通骨架估算
-
-
3. 再执行
TransportAgent,用酒店推荐结果做最终修正
这样可以显著降低总耗时。
8.3 线程池隔离实现并发编排
下面给出一个更接近生产环境的实现方式,而不是只依赖一个“并行节点”概念示例。
@Configuration
public class PlannerExecutorConfiguration {
@Bean("agentExecutor")
public ThreadPoolTaskExecutor agentExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(64);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("agent-exec-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
}
@Service
@RequiredArgsConstructor
public class TravelPlanningOrchestrator {
private final AttractionService attractionService;
private final HotelService hotelService;
private final TransportService transportService;
private final BudgetGuardService budgetGuardService;
private final ReviewService reviewService;
@Qualifier("agentExecutor")
private final Executor agentExecutor;
public FinalTravelPlan execute(TravelPlanRequest request, ChatContext context) {
AttractionPlan attractionPlan = attractionService.plan(request, context);
CompletableFuture<HotelPlan> hotelFuture =
CompletableFuture.supplyAsync(() -> hotelService.plan(request, attractionPlan, context), agentExecutor);
CompletableFuture<TransportSkeleton> skeletonFuture =
CompletableFuture.supplyAsync(() -> transportService.preCheck(request, attractionPlan, context), agentExecutor);
HotelPlan hotelPlan = hotelFuture.join();
TransportSkeleton skeleton = skeletonFuture.join();
TransportPlan transportPlan = transportService.finalizePlan(request, attractionPlan, hotelPlan, skeleton, context);
FinalTravelPlan draft = budgetGuardService.rebalance(request, attractionPlan, hotelPlan, transportPlan);
return reviewService.polish(draft, context);
}
}
8.4 为什么这里不用“一切都让 LLM 自己决定”
这是多智能体系统设计里的一个关键原则:
-
• 业务依赖关系应该由工程代码显式定义
-
• 语义推理与内容生成由 Agent 负责
如果把流程依赖也交给模型自己判断,系统会出现以下问题:
-
• 执行顺序不稳定
-
• 难以做性能优化
-
• 难以做 SLA 约束
-
• 难以重试和回放
所以,生产系统里最好的模式通常是:
-
• 流程控制由代码掌握
-
• 领域生成由 Agent 完成
这是一种“可控 Agentic Architecture”。
9. 生产级升级三:状态、记忆与多租户治理
9.1 为什么不能只用内存记忆
单机 Demo 中,很多会话状态都存在 JVM 内存里。这在生产环境中有三个明显问题:
-
• Pod 重启后记忆丢失
-
• 多副本之间状态不共享
-
• 无法支持同一用户请求打到不同实例
9.2 Redis 会话记忆设计
推荐把会话上下文拆成两类:
-
• 短期执行态
当前链路中的中间结果,比如景点规划、酒店候选、预算估算 -
• 中期会话态
用户偏好、历史目的地、常见出行风格等
public record ChatContext(
String sessionId,
String tenantId,
Map<String, Object> facts,
List<String> memorySummary,
Instant lastUpdatedAt) {
}
@Service
@RequiredArgsConstructor
public class SessionMemoryService {
private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper;
public ChatContext load(String sessionId, String tenantId) {
String key = key(sessionId, tenantId);
String raw = stringRedisTemplate.opsForValue().get(key);
if (raw == null) {
return new ChatContext(sessionId, tenantId, new HashMap<>(), new ArrayList<>(), Instant.now());
}
try {
return objectMapper.readValue(raw, ChatContext.class);
} catch (Exception ex) {
throw new IllegalStateException("Failed to load chat context", ex);
}
}
public void save(String sessionId, String tenantId, FinalTravelPlan result) {
ChatContext context = new ChatContext(
sessionId,
tenantId,
Map.of(
"lastDestination", result.request().destination(),
"lastBudget", result.request().budget(),
"recommendedHotelId", result.hotelPlan().recommendedHotelId()),
List.of("用户最近一次关注亲子轻松行程"),
Instant.now());
try {
stringRedisTemplate.opsForValue().set(key(sessionId, tenantId), objectMapper.writeValueAsString(context), Duration.ofDays(7));
} catch (Exception ex) {
throw new IllegalStateException("Failed to save chat context", ex);
}
}
private String key(String sessionId, String tenantId) {
return "travel:session:" + tenantId + ":" + sessionId;
}
}
9.3 多租户需要隔离什么
多租户不仅是“数据分开”这么简单。对多智能体系统来说,至少要隔离以下几个维度:
-
• Prompt 模板
-
• 可用模型
-
• 工具权限
-
• Token 限额
-
• 敏感词和内容策略
-
• 缓存命名空间
-
• 观测指标标签
因此建议设计一层租户配置中心:
public record TenantAgentPolicy(
String tenantId,
String plannerModel,
String expertModel,
int maxConcurrentTasks,
boolean mapApiEnabled,
boolean hotelApiEnabled,
BigDecimal maxBudgetLimit) {
}
主编排服务在执行前读取租户策略,把模型、工具和限流参数动态注入执行上下文。
10. 生产级升级四:高并发治理体系
多智能体系统的高并发,不只是 JVM 扛住请求,而是整条链路都要扛住:
-
• API 网关
-
• 应用线程池
-
• Redis
-
• 模型服务
-
• 外部工具服务
-
• 远程 Agent 服务
10.1 常见瓶颈点
在旅游规划系统中,最常见的瓶颈通常在:
-
• DashScope 等模型调用耗时和配额限制
-
• 地图 / 酒店 / 门票接口的外部依赖延迟
-
• Planner 服务线程池被长耗时调用占满
-
• Redis 热 key 与会话大对象读写
-
• 热门节假日目的地缓存击穿
10.2 限流、熔断、隔离、超时要成套使用
限流
保护模型配额和下游服务,避免高峰期把整个系统拖死。
熔断
下游服务异常持续上升时,快速失败而不是持续阻塞。
舱壁隔离
不同 Agent 使用不同线程池或连接池,避免一个热点能力拖垮全局。
超时
每个外部调用都必须有边界,否则链路耗时会不断叠加。
10.3 Resilience4j 实战配置
resilience4j:
ratelimiter:
instances:
dashscope:
limit-for-period: 80
limit-refresh-period: 1s
timeout-duration: 200ms
circuitbreaker:
instances:
attractionAgent:
sliding-window-size: 50
minimum-number-of-calls: 20
failure-rate-threshold: 50
slow-call-rate-threshold: 60
slow-call-duration-threshold: 4s
wait-duration-in-open-state: 20s
hotelAgent:
sliding-window-size: 50
failure-rate-threshold: 50
wait-duration-in-open-state: 15s
timelimiter:
instances:
attractionAgent:
timeout-duration: 6s
hotelAgent:
timeout-duration: 4s
transportAgent:
timeout-duration: 4s
10.4 带降级逻辑的领域服务
@Service
@RequiredArgsConstructor
public class HotelService {
private final HotelAgentGateway hotelAgentGateway;
@CircuitBreaker(name = "hotelAgent", fallbackMethod = "fallbackHotelPlan")
@TimeLimiter(name = "hotelAgent")
public CompletableFuture<HotelPlan> planAsync(
TravelPlanRequest request,
AttractionPlan attractionPlan,
ChatContext context) {
return CompletableFuture.supplyAsync(() ->
hotelAgentGateway.generate(request, attractionPlan, context));
}
public HotelPlan plan(TravelPlanRequest request, AttractionPlan attractionPlan, ChatContext context) {
return planAsync(request, attractionPlan, context).join();
}
private CompletableFuture<HotelPlan> fallbackHotelPlan(
TravelPlanRequest request,
AttractionPlan attractionPlan,
ChatContext context,
Throwable throwable) {
HotelPlan degraded = new HotelPlan(
List.of(new HotelOption("fallback-hotel-1", "西湖东侧经济型酒店", new BigDecimal("420"), "交通便利,适合预算受限场景")),
"fallback-hotel-1",
new BigDecimal("1680"));
return CompletableFuture.completedFuture(degraded);
}
}
10.5 高并发下的缓存策略
缓存不是为了“偷懒”,而是为了降低模型成本和高峰期抖动。建议至少做三层缓存:
-
• 请求结果缓存
相同目的地、预算、天数的标准行程模板 -
• POI 与酒店候选缓存
减少地图与酒店接口频繁访问 -
• Prompt 片段缓存
将技能说明、租户模板、工具描述做模板化复用
但要注意,个性化请求不适合粗暴全量缓存,适合做“模板骨架缓存 + 个性化二次生成”。
11. 生产级升级五:A2A + Nacos,把单体升级为分布式 Agent 网络
11.1 为什么要拆成远程 Agent
当系统进入真实生产阶段后,通常会出现能力热点不均衡:
-
• 景点 Agent:调用地图、景区热度、天气、亲子标签等多个工具,最重
-
• 酒店 Agent:依赖较少,但涉及价格和位置权衡
-
• 交通 Agent:主要是计算和规则判断,较轻
如果它们都绑在一个进程里,扩容只能整体扩,成本很高,也不灵活。
这时就应该引入远程 Agent,把热点能力独立成服务。
11.2 分布式拆分原则
不是所有 Agent 都值得拆。一般满足以下任意条件才建议远程化:
-
• 计算耗时显著更高
-
• 外部依赖明显更多
-
• 调用量与其他 Agent 不均衡
-
• 需要独立扩缩容
-
• 需要独立灰度发布
在旅游规划场景中,优先拆分:
-
•
AttractionAgent -
•
HotelAgent
主 PlannerAgent 则保留在中心服务中,负责流程控制。
11.3 A2A Server 暴露远程专家能力
server:
port: 8082
spring:
application:
name: attraction-agent-service
ai:
alibaba:
a2a:
registry:
enabled: true
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
server:
version: 1.0.0
card:
name: attraction-agent
description: 景点规划专家服务
url: http://attraction-agent-service:8082
@Configuration
public class AttractionA2AServerConfiguration {
@Bean
public A2AServer attractionA2AServer(ReactAgent attractionAgent) {
return A2AServer.builder()
.agent(attractionAgent)
.build();
}
}
11.4 Planner 服务通过远程 Agent 代理调用
@Configuration
public class RemoteAgentConfiguration {
@Bean
public A2aRemoteAgent remoteAttractionAgent(A2AClient a2aClient) {
return A2aRemoteAgent.builder()
.name("remote_attraction_agent")
.description("远程景点规划专家")
.url("http://attraction-agent-service:8082/a2a")
.client(a2aClient)
.build();
}
}
如果结合 Nacos,就可以把远程 Agent 的地址从固定 URL 提升为动态服务发现。
11.5 远程 Agent 化后的新挑战
分布式不是简单的“拆开部署”,而是会新增一批问题:
-
• 网络调用失败率上升
-
• 调用时延波动加大
-
• 多 Agent 之间版本兼容性问题
-
• 远程上下文过大导致序列化成本升高
-
• 链路追踪复杂度明显增加
所以远程化后有两个重要原则:
-
1. 远程调用的数据要尽量小
传结构化摘要,不要把整段历史对话直接扔给下游 -
2. 远程 Agent 自身也要有降级能力
不能把所有问题都留给主服务兜底
12. 上下文工程:高质量多智能体系统的真正核心
很多人把多智能体问题理解成“多几个 Prompt”,其实真正拉开质量差距的是上下文工程。
12.1 什么信息应该传给子 Agent
不是所有信息都应该全量下发。推荐原则:
-
• 与当前子任务强相关的信息必须传
-
• 与决策约束相关的信息必须传
-
• 历史会话只传摘要,不传全文
-
• 其他 Agent 的原始输出优先传结构化摘要
例如传给 HotelAgent 的上下文应该是:
-
• 目的地和出行天数
-
• 预算区间
-
• 入住人数
-
• 景点动线摘要
-
• 用户区域偏好
而不需要把景点规划的完整自然语言长文全部下发。
12.2 Prompt 模板建议
一个生产级 Prompt 模板建议拆成三层:
-
• 角色层
你是谁,你的职责是什么 -
• 规则层
你有哪些硬约束和优先级 -
• 输出层
你必须输出什么结构
例如:
角色:
你是亲子旅游住宿规划专家。
规则:
1. 优先考虑景点动线与通勤便利
2. 优先选择评分稳定、儿童友好的酒店
3. 住宿总成本必须尽量落在预算范围内
输出:
返回 JSON 结构,包括候选酒店、推荐理由、总成本估算和风险提示。
12.3 为什么推荐 JSON 结构输出
因为后面还有预算校验、缓存和分布式调用。
如果中间结果全是自然语言,后续每个节点都要再做一次解析,复杂度和错误率都会上升。
13. 结果校验:不要让 Agent 输出直接裸奔到用户面前
一套生产级多智能体系统,最终面向用户的结果最好不是“最后一个 Agent 的原始输出”,而应该经过校验与修正层。
13.1 预算校验
@Service
public class BudgetGuardService {
public FinalTravelPlan rebalance(
TravelPlanRequest request,
AttractionPlan attractionPlan,
HotelPlan hotelPlan,
TransportPlan transportPlan) {
BigDecimal total = attractionPlan.estimatedTicketCost()
.add(hotelPlan.estimatedStayCost())
.add(transportPlan.estimatedTransportCost());
List<String> warnings = new ArrayList<>();
HotelPlan adjustedHotelPlan = hotelPlan;
if (request.budget() != null && total.compareTo(request.budget()) > 0) {
warnings.add("当前方案超出预算,系统已自动切换为更保守的住宿方案");
adjustedHotelPlan = downgradeHotelPlan(hotelPlan);
total = attractionPlan.estimatedTicketCost()
.add(adjustedHotelPlan.estimatedStayCost())
.add(transportPlan.estimatedTransportCost());
}
return new FinalTravelPlan(
request,
attractionPlan,
adjustedHotelPlan,
transportPlan,
total,
warnings,
renderMarkdown(request, attractionPlan, adjustedHotelPlan, transportPlan, total, warnings));
}
private HotelPlan downgradeHotelPlan(HotelPlan original) {
HotelOption fallback = original.options().stream()
.min(Comparator.comparing(HotelOption::pricePerNight))
.orElse(original.options().get(0));
BigDecimal stayCost = fallback.pricePerNight().multiply(BigDecimal.valueOf(4));
return new HotelPlan(List.of(fallback), fallback.id(), stayCost);
}
private String renderMarkdown(
TravelPlanRequest request,
AttractionPlan attractionPlan,
HotelPlan hotelPlan,
TransportPlan transportPlan,
BigDecimal total,
List<String> warnings) {
return """
## 行程总览
- 目的地:%s
- 天数:%s 天
- 预算估算:%s 元
## 风险提示
%s
""".formatted(
request.destination(),
request.days(),
total,
warnings.isEmpty() ? "- 无明显风险" : warnings.stream().map(w -> "- " + w).collect(Collectors.joining("\n")));
}
}
13.2 一致性检查
推荐增加一个轻量的 ReviewAgent 或规则校验器,检查:
-
• 同一天景点数量是否过多
-
• 酒店区域是否与景点动线矛盾
-
• 交通耗时是否明显不合理
-
• 总预算是否越界
-
• 文案是否有自相矛盾表达
这一步对用户体验提升非常明显,因为 Agent 系统最常见的问题不是“完全胡说”,而是“局部合理、全局不一致”。
14. 可观测性建设:没有观测,就没有生产级 AI 系统
多智能体系统一旦变成分布式,问题就不再是“报错了没”,而是:
-
• 哪个 Agent 慢
-
• 哪个 Agent 贵
-
• 哪个 Agent 错
-
• 哪个租户最消耗资源
-
• 哪个阶段最容易重试
14.1 核心指标设计
建议至少监控以下指标:
-
•
agent_execution_duration_ms
每个 Agent 的执行耗时 -
•
agent_execution_total
每个 Agent 的调用次数 -
•
agent_error_total
每个 Agent 的失败数 -
•
llm_token_input_total
输入 Token 量 -
•
llm_token_output_total
输出 Token 量 -
•
travel_plan_cache_hit_total
缓存命中数 -
•
a2a_call_duration_ms
远程 Agent 调用耗时
14.2 Micrometer 埋点示例
@Component
@RequiredArgsConstructor
public class AgentMetricsRecorder {
private final MeterRegistry meterRegistry;
public <T> T record(String agentName, Supplier<T> supplier) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
T result = supplier.get();
sample.stop(Timer.builder("agent.execution.duration")
.tag("agent", agentName)
.tag("status", "success")
.register(meterRegistry));
meterRegistry.counter("agent.execution.total", "agent", agentName, "status", "success").increment();
return result;
} catch (Exception ex) {
sample.stop(Timer.builder("agent.execution.duration")
.tag("agent", agentName)
.tag("status", "error")
.register(meterRegistry));
meterRegistry.counter("agent.execution.total", "agent", agentName, "status", "error").increment();
throw ex;
}
}
}
14.3 链路追踪
推荐把以下 ID 贯穿整条链路:
-
•
traceId
整个 HTTP 请求链路 -
•
sessionId
用户会话维度 -
•
planId
本次行程生成任务 -
•
tenantId
租户维度
这样在 Grafana、Loki 或 Tempo 里排查问题时,你可以从用户反馈直接定位到某次具体的 Agent 执行记录。
14.4 观测面板建议
Grafana 面板建议按三层设计:
-
• 业务层
行程生成成功率、平均耗时、超预算率、缓存命中率 -
• 系统层
CPU、内存、线程池队列、Redis RT、QPS -
• 模型层
Token 消耗、模型调用耗时、错误率、限流次数
15. Kubernetes 云原生部署:让系统具备弹性
15.1 为什么多智能体系统特别适合 K8s
因为它天然是一个“多角色、异构负载”的系统:
-
• 有些 Agent 偏 CPU
-
• 有些 Agent 偏 I/O
-
• 有些 Agent 调用外部服务更多
-
• 有些 Agent 只负责汇总和编排
Kubernetes 正好擅长处理这类异构工作负载。
15.2 主编排服务 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: planner-service
spec:
replicas: 3
selector:
matchLabels:
app: planner-service
template:
metadata:
labels:
app: planner-service
spec:
containers:
- name: planner-service
image: registry.example.com/travel/planner-service:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: SPRING_DATA_REDIS_HOST
value: redis-master
- name: SPRING_AI_ALIBABA_A2A_NACOS_SERVER_ADDR
value: nacos:8848
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "2Gi"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 15
15.3 热点 Agent 独立扩容
景点 Agent 通常是最热点的服务,因此可以独立设置更高副本数:
apiVersion: apps/v1
kind: Deployment
metadata:
name: attraction-agent-service
spec:
replicas: 6
selector:
matchLabels:
app: attraction-agent-service
template:
metadata:
labels:
app: attraction-agent-service
spec:
containers:
- name: attraction-agent-service
image: registry.example.com/travel/attraction-agent-service:1.0.0
ports:
- containerPort: 8082
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "4"
memory: "4Gi"
15.4 HPA 自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: attraction-agent-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: attraction-agent-service
minReplicas: 4
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: agent_execution_duration_p95
target:
type: AverageValue
averageValue: 3000m
实践里建议不要只盯 CPU,还要结合业务指标,比如:
-
• 队列积压长度
-
• P95 延迟
-
• 限流触发次数
-
• 活跃会话数
因为 AI 系统中,CPU 不一定能真实反映业务压力。
16. 真实场景演示:一次亲子杭州四日游请求是如何被执行的
用户请求:
{
"sessionId": "sess-20260501-1001",
"tenantId": "ota-vip",
"destination": "杭州",
"days": 4,
"budget": 8000,
"adults": 2,
"children": 1,
"travelStyle": "轻松亲子",
"hotelAreaPreference": "西湖附近",
"preferences": ["亲子", "拍照", "美食"],
"constraints": ["避免太赶", "每日步行不超过15000步"]
}
系统内部执行流程:
-
1.
PlannerService接收请求,生成traceId、planId -
2. 从 Redis 加载该用户历史偏好摘要
-
3. 调用
AttractionAgent
产出 4 天景点骨架与门票预算 -
4. 并发触发:
-
•
HotelAgent生成 3 个候选酒店 -
•
TransportPreCheckAgent计算每日交通骨架
-
-
5.
TransportAgent根据最终酒店方案修正路线 -
6.
BudgetGuardAgent发现总价 8450,超过预算 450 -
7. 自动将酒店从高端亲子酒店切换为次优方案,预算回落到 7820
-
8.
ReviewAgent检查每日强度与文案一致性 -
9. 输出最终 Markdown 行程和结构化 JSON
最终用户看到的是一份可执行、预算受控、带风险提示的完整行程,而不是多个 Agent 的零散结果拼接。
17. 压测与容量规划:从架构图走向可交付 SLA
很多文章到这里就结束了,但真正上线前,还必须回答一个问题:
系统到底能扛多少?
17.1 推荐的压测维度
-
• 单租户稳定压测
-
• 多租户混合压测
-
• 节假日热点目的地压测
-
• 外部依赖异常注入压测
-
• Redis 故障切换压测
-
• 某个远程 Agent 全部超时压测
17.2 一组典型容量参考
以下是一套示意性容量规划结果,供架构设计参考:
|
指标 |
单体版 |
分布式版 |
|---|---|---|
|
平均响应时间 |
8.7s |
4.9s |
|
P95 响应时间 |
13.2s |
7.4s |
|
峰值吞吐 |
90 RPM |
320 RPM |
|
缓存命中率 |
18% |
41% |
|
景点能力扩容粒度 |
整体扩容 |
独立扩容 |
|
单次请求平均 Token 成本 |
高 |
中 |
这里有两个关键观察:
-
1. 分布式化不一定直接带来“单次更快”,但会显著提升整体吞吐与资源利用率
-
2. 结构化上下文 + 缓存 + 分阶段并发,往往比“单纯多开几个 Pod”更能降低成本
18. 常见踩坑与规避建议
18.1 不要把所有 Agent 都做成远程服务
远程化会带来额外的网络开销、序列化成本和运维复杂度。
只有真正的热点能力、独立演进能力才值得拆。
18.2 不要让所有上下文全量透传
这会导致:
-
• Token 成本快速膨胀
-
• 远程调用负载增大
-
• 子 Agent 决策噪声增多
正确做法是“摘要透传 + 结构化中间结果”。
18.3 不要把流程控制全部交给模型
流程编排、重试、超时、依赖关系这些,应该由代码层显式管理。
否则系统很难稳定、也很难调优。
18.4 不要忽视降级路径
高峰期最危险的不是返回一个保守结果,而是全链路阻塞。
对于旅游规划这种业务,返回“稍弱但可用”的方案,通常比完全失败更符合业务价值。
18.5 不要没有观测就直接上生产
AI 系统的问题常常是“偶发性偏差”,没有链路和指标,你很难复盘到底是哪一环出了问题。
19. 总结:一套生产级 Spring AI Alibaba 多智能体方案应该长什么样
如果把全文收敛成一句架构结论,可以这样概括:
多智能体系统的本质不是“多个 Prompt 协作”,而是“用结构化编排把复杂认知任务拆成多个可治理、可扩展、可观测的专业能力单元”。
在本文这套旅游行程规划方案中,真正支撑其从 Demo 走向生产的,不是某个单一框架能力,而是下面这组组合拳:
-
• 用
Agent as Tool建立清晰的角色边界 -
• 用结构化输出打通 Agent 间的数据流
-
• 用应用服务层承接工程治理
-
• 用并发编排缩短关键路径耗时
-
• 用 Redis 承担跨实例记忆与状态共享
-
• 用 Resilience4j 实现限流、熔断、超时和降级
-
• 用 A2A + Nacos 完成远程 Agent 化和服务发现
-
• 用 Kubernetes 完成弹性伸缩和异构负载部署
-
• 用 Micrometer + Prometheus + Grafana + Loki + Tempo 建立完整可观测性
这才是一套真正“能上线”的 Spring AI Alibaba 多智能体架构。
20. 结语
从单机到分布式,并不是简单地把一个 Agent 拆成多个 Agent,也不是把多个服务扔到 Kubernetes 上就结束了。真正的升级,是把“智能体能力”纳入我们熟悉的软件工程体系中:
-
• 有边界
-
• 有契约
-
• 有并发控制
-
• 有故障治理
-
• 有成本意识
-
• 有观测和回放能力
如果你正在用 Spring AI Alibaba 落地多智能体系统,我的建议是:
-
1. 先在单机内把角色边界和结构化数据流设计清楚
-
2. 再补足线程池、缓存、限流、记忆和观测
-
3. 最后只把真正有价值的热点 Agent 远程化
这样演进,你得到的不会是一个“会说话的 Demo”,而是一套真正可以服务业务增长的生产级智能体系统。
附录:生产落地检查清单
上线前建议逐项核对:
-
• 是否有结构化的请求、响应和中间结果定义
-
• 是否区分了流程编排与 Agent 推理职责
-
• 是否有线程池隔离与超时边界
-
• 是否对模型调用做了限流、熔断、降级
-
• 是否实现了 Redis 会话记忆与多副本共享
-
• 是否支持 traceId、sessionId、tenantId 全链路透传
-
• 是否对热点 Agent 做了独立扩容设计
-
• 是否在 Grafana 中有 Agent 级延迟、错误率和 Token 成本监控
-
• 是否有缓存策略与热 key 保护方案
-
• 是否有预算校验、一致性检查和结果兜底机制
-
• 是否做过高峰压测和依赖故障注入演练
当这些问题都能回答清楚时,你的多智能体系统才真正具备了生产级工程品质。
更多推荐




所有评论(0)