转发一篇好文档,对于Agent生产环境落地有很大帮助

原文连接:https://mp.weixin.qq.com/s/-i3pq5dFG04Yw_a7tTsBAw

这不是一篇“把三个 Agent 串起来”的 Demo 文章,而是一篇面向生产环境的架构升级指南。我们将从单体多智能体出发,逐步演进到支持高并发、可扩展、可观测、可灰度、可治理的分布式多智能体系统,并以“旅游行程规划”这一典型复合型业务为例,完整讲清楚 Spring AI Alibaba 在企业级场景中的落地方法。


1. 为什么是旅游行程规划?

旅游行程规划是一个很适合多智能体拆分的业务场景,因为它天然具备以下特征:

  • • 任务是复合型的,不是单轮问答

  • • 子任务之间有依赖,但并非完全串行

  • • 每个子领域都需要独立的专业知识和工具

  • • 用户对结果质量、响应速度、可解释性都很敏感

一个真实的用户请求可能是这样的:

“我和家人五一去杭州玩 4 天,预算 8000 元,带一个 6 岁小朋友,希望节奏轻松,最好住西湖附近,帮我规划每天景点、酒店和交通安排。”

表面上这是一个自然语言请求,实际上系统要完成的是一条复杂业务链路:

  1. 1. 识别用户意图与约束条件

  2. 2. 补全缺失参数并做结构化建模

  3. 3. 生成景点候选集合

  4. 4. 结合亲子、预算、区域偏好做排序

  5. 5. 规划住宿区域与酒店方案

  6. 6. 计算景点与酒店之间的动线

  7. 7. 生成逐日行程

  8. 8. 对结果做冲突检测、预算校验与兜底修正

如果这些任务全部堆进一个大 Prompt,让一个 Agent 一次性完成,系统会很快碰到以下问题:

  • • 上下文过长,Prompt 成本高且易漂移

  • • 工具过多,模型难以稳定选择正确工具

  • • 错误定位困难,不知道是景点推荐错了还是交通推理错了

  • • 高峰期吞吐有限,单节点无法支撑节假日流量

  • • 某个能力热点升高时无法独立扩容

这正是多智能体架构最有价值的场景。


2. 从“能跑”到“能上线”:多智能体系统真正要解决什么问题

很多团队在做多智能体系统时,第一阶段关注的是“怎么编排”;但到了生产阶段,真正决定系统成败的是下面这几个维度:

  • • 架构层:如何把单节点 Agent 编排平滑升级为分布式协作

  • • 并发层:如何提升吞吐,避免请求堆积和模型侧雪崩

  • • 状态层:如何管理会话记忆、执行状态、任务上下文

  • • 工程层:如何做重试、限流、熔断、降级、超时控制

  • • 运维层:如何观察每个 Agent 的成本、耗时、错误率和调用链

  • • 业务层:如何确保输出结果稳定、可解释、可审计

所以,一套真正可落地的多智能体系统,不应该只回答“如何调用子 Agent”,还必须回答:

  • • 为什么这样拆

  • • 哪些能力必须本地化,哪些应该远程化

  • • 哪些阶段可以并发,哪些必须保持顺序

  • • 如何做状态隔离与多租户治理

  • • LLM 调用失败后如何兜底

  • • 如何避免一个热门 Agent 成为全局瓶颈


3. 架构选型:为什么选择 Agent as Tool,而不是一个超级 Agent

3.1 单 Agent 架构的问题本质

单 Agent 的问题,不只是“大 Prompt 不优雅”,本质是三个方面:

  1. 1. 认知负载过高
    一个 Agent 同时负责景点、酒店、交通、预算、排序、冲突检测,会导致决策边界模糊。

  2. 2. 上下文污染严重
    各领域信息混在一起,模型容易把住宿约束错误地映射到景点规划上,或者把历史对话中的无关信息带入当前推理。

  3. 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. 1. 先执行 AttractionAgent

  2. 2. 并发执行:

    • • HotelAgent 生成酒店候选

    • • TransportPreCheckAgent 基于景点做交通骨架估算

  3. 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. 1. 远程调用的数据要尽量小
    传结构化摘要,不要把整段历史对话直接扔给下游

  2. 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. 1. PlannerService 接收请求,生成 traceIdplanId

  2. 2. 从 Redis 加载该用户历史偏好摘要

  3. 3. 调用 AttractionAgent
    产出 4 天景点骨架与门票预算

  4. 4. 并发触发:

    • • HotelAgent 生成 3 个候选酒店

    • • TransportPreCheckAgent 计算每日交通骨架

  5. 5. TransportAgent 根据最终酒店方案修正路线

  6. 6. BudgetGuardAgent 发现总价 8450,超过预算 450

  7. 7. 自动将酒店从高端亲子酒店切换为次优方案,预算回落到 7820

  8. 8. ReviewAgent 检查每日强度与文案一致性

  9. 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. 1. 分布式化不一定直接带来“单次更快”,但会显著提升整体吞吐与资源利用率

  2. 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. 1. 先在单机内把角色边界和结构化数据流设计清楚

  2. 2. 再补足线程池、缓存、限流、记忆和观测

  3. 3. 最后只把真正有价值的热点 Agent 远程化

这样演进,你得到的不会是一个“会说话的 Demo”,而是一套真正可以服务业务增长的生产级智能体系统。


附录:生产落地检查清单

上线前建议逐项核对:

  • • 是否有结构化的请求、响应和中间结果定义

  • • 是否区分了流程编排与 Agent 推理职责

  • • 是否有线程池隔离与超时边界

  • • 是否对模型调用做了限流、熔断、降级

  • • 是否实现了 Redis 会话记忆与多副本共享

  • • 是否支持 traceId、sessionId、tenantId 全链路透传

  • • 是否对热点 Agent 做了独立扩容设计

  • • 是否在 Grafana 中有 Agent 级延迟、错误率和 Token 成本监控

  • • 是否有缓存策略与热 key 保护方案

  • • 是否有预算校验、一致性检查和结果兜底机制

  • • 是否做过高峰压测和依赖故障注入演练

当这些问题都能回答清楚时,你的多智能体系统才真正具备了生产级工程品质。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐