从 MCP 协议到 ToolCallingAdvisor:构建可编排、可观测的多 Agent 协作系统

2026年6月12日,Spring AI 2.0.0 GA 正式发布。与 1.x 相比,这不仅仅是一次依赖升级,而是一次架构层面的重构:工具调用循环从各 Chat Model 内部提升到 Advisor 链中成为一等公民,MCP 注解从社区孵化项目并入核心模块,ToolSearchToolCallingAdvisor 让上千个工具的动态发现成为可能。

对 Java 技术栈的开发者来说,这意味着:构建生产级多 Agent 系统不再依赖 Python 生态的 LangChain 或 CrewAI——用 Spring Boot 4 + Spring AI 2.0,你可以在一个熟悉的 DI/IoC 模型里,写出类型安全、可测试、可观测的 Agentic 应用。

本文将从"IDE"的视角切入——不是 Visual Studio Code 或 IntelliJ 里的那个 IDE,而是 Agent Development Environment(多 Agent 编排开发环境)——逐一拆解 Spring AI 2.0 如何把每个核心特性变成可组合的积木,并用可运行的代码块串联起一个完整的多 Agent 协作工作流。

🎯 本文目标读者:有 Spring Boot 使用经验的 Java 开发者,想了解如何在 Spring 生态里构建 AI Agent 系统。阅读完本文后你将可以:用 @Tool 定义工具、用 ToolCallingAdvisor 观测调用链路、用 MCP 协议跨服务调用工具、用 Agentic Patterns 编排多 Agent 工作流。

一、架构全景:Spring AI 2.0 的 Agent 编程模型

先看一张心智模型图。Spring AI 2.0 把一次 LLM 调用抽象为一个通过 Advisor 链的请求:

用户请求 → ChatClient → Advisor Chain → Chat Model → 响应

MemoryAdvisor → SafeGuardAdvisor → ToolCallingAdvisor → OutputValidator →
⬆︎ 每个 Advisor 都可以拦截请求、修改 prompt、注入上下文、执行递归循环

这与 Spring Web 的 Filter Chain 或 Spring Security 的过滤器链是同构的设计思想。但关键差异在于:Advisor Chain 支持递归循环。这意味着 ToolCallingAdvisor 可以在一次请求中反复执行"发提示 → 模型请求工具 → 执行工具 → 将结果返回模型 → 模型再请求工具"的循环,直到模型认为它有了足够的信息来给出最终答案。

🧱

ChatClient

统一的入口 API,替代 1.x 中每个模型自行管理的调用逻辑。所有请求都通过 Advisor 链处理。

🔗

Advisor Chain

可插拔的拦截器链,Memory、Tool Calling、Guardrails、Output Validation 都是 Advisor。

🔧

@Tool 注解

在任意 Spring Bean 方法上加 @Tool 即注册为可调用工具,框架自动生成 JSON Schema。

🌐

MCP 协议

Model Context Protocol:跨进程、跨语言的工具调用标准协议。注解驱动,无需手写传输层。

⚡ 与 1.x 的关键区别:在 1.x 中,每个 Chat Model 内部有自己的私有工具调用循环,无法拦截、无法观测、无法替换执行策略。2.0 将工具调用循环从模型中"提升"到 Advisor 链中,使其成为可组合的一等组件。这一点是理解整个 2.0 Agent 模型的基础。

二、上手:一个 50 行的 Tool-Calling Agent

从零开始。创建一个 Spring Boot 4.0 项目,依赖只需要两个:

<!-- pom.xml -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>2.0.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
</dependencies>

2.1 定义工具:@Tool 注解

定义一个查询订单的工具类:

package com.example.agent.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

@Component
public class OrderTools {

    @Tool(description = "查询指定订单的当前状态")
    public OrderStatus getOrderStatus(
        @ToolParam(description = "订单ID,如 ORD-00123") String orderId
    ) {
        // 实际项目中这里查询数据库或下游服务
        return new OrderStatus(orderId, "SHIPPED", "2026-08-10");
    }

    @Tool(description = "查询客户最近N条订单记录")
    public List<OrderSummary> listRecentOrders(
        @ToolParam(description = "客户ID") String customerId,
        @ToolParam(description = "返回数量上限,1-50", required = false) Integer limit
    ) {
        int n = (limit != null && limit > 0) ? Math.min(limit, 50) : 10;
        return orderRepo.findRecent(customerId, n);
    }

    public record OrderStatus(String orderId, String status, String eta) {}
    public record OrderSummary(String orderId, String date, double total) {}
}

几个细节值得注意:

  • description 是给模型看的——模型通过它决定何时调用哪个工具。把它写成清晰的功能说明,而非变量名注释。
  • Java Record 可以直接作为返回类型——框架通过 Jackson 3 序列化为 JSON。
  • @ToolParam(required = false) 会从 JSON Schema 中移除 required 约束,模型可以不传该参数。

2.2 组装 ChatClient

@RestController
public class AgentController {

    private final ChatClient chatClient;

    public AgentController(ChatClient.Builder builder, OrderTools orderTools) {
        this.chatClient = builder
            .defaultTools(orderTools)    // 工具自动注册,框架生成 JSON Schema
            .build();
    }

    @PostMapping("/api/chat")
    public String chat(@RequestBody String message) {
        return chatClient.prompt()
            .user(message)
            .call()
            .content();   // 工具调用循环对业务代码完全透明
    }
}

✅ 不到 50 行业务代码,你得到了什么?
① 模型自动判断何时需要调用工具(无需 if/else 路由)
② 工具执行的完整循环:模型选工具 → 执行 → 结果回传 → 模型继续思考 → 最终回答
③ 类型安全的工具定义,编译期检查参数类型
④ Spring DI 管理工具生命周期,测试时可直接 Mock

三、ToolCallingAdvisor:观测与控制工具调用链

基本工具调用只是起点。真正体现 Spring AI 2.0 架构优势的是 Advisor 的可组合性。你可以通过自定义 Advisor 来观测、拦截、甚至修改工具调用行为。

3.1 观测工具调用:构建 Claude-Style 调用指示器

用过 Claude 或 ChatGPT 的人都知道那个 "Calling tool…" 的动态指示器。在 Spring AI 2.0 中,要实现同样的效果,需要写一个 Advisor 坐到 ToolCallingAdvisor 前面来观察调用事件:

public class ToolCallObservingAdvisor implements CallAdvisor, StreamAdvisor {

    private final Consumer<ToolCallEvent> eventSink;

    public ToolCallObservingAdvisor(Consumer<ToolCallEvent> eventSink) {
        this.eventSink = eventSink;
    }

    @Override
    public int getOrder() {
        // 排序在 ToolCallingAdvisor 之前,确保能看到每次循环
        return ToolCallingAdvisor.DEFAULT_ORDER + 100;
    }

    @Override
    public AdvisedResponse adviseCall(...) {
        // 拦截 advise 调用链:当检测到工具调用时,发射事件
        // 完整实现见 spring-ai-examples/advisors/ 仓库
    }
}

// 在 Controller 中注入观测 Advisor:
chatClient.prompt()
    .user(message)
    .tools(orderTools)
    .advisors(a -> a.advisors(new ToolCallObservingAdvisor(eventSink)))
    .stream().content()
    ...

🔑 Advisor 排序是关键:getOrder() 返回的值决定了 Advisor 在链中的位置。ToolCallingAdvisor.DEFAULT_ORDER + 100 意味着你的 Advisor 在每次工具调用循环中都会被触发——而不仅仅是在请求的入口和出口。这就是"把工具调用循环从黑盒变为白盒"的核心机制。

3.2 扩展到百级工具:ToolSearchToolCallingAdvisor

当工具数量达到几十甚至上百个时,把所有工具的 Schema 都塞进每次请求的 system prompt 会导致:token 消耗巨大、模型选择工具的准确率下降。Spring AI 2.0 给出的方案是 渐进式工具披露

// 用 ToolSearchToolCallingAdvisor 替代默认的 ToolCallingAdvisor
chatClient.prompt()
    .user(message)
    .advisors(a -> a.advisors(
        ToolSearchToolCallingAdvisor.builder()
            .toolCallbacks(allTools)   // 全量工具注册到索引
            .build()
    ))
    .call()
    .content();

它的工作方式是:首次请求时,将所有工具的 description 向量化存入内存索引;模型收到用户消息后,先用一次轻量的语义搜索找到相关的 3-5 个工具,只把这几个工具的 Schema 发给模型。实测中,这个方案可以将单次请求的 token 消耗降低 60-80%。

四、MCP 协议:跨服务工具调用的标准答案

当你需要让运行在 A 服务中的 Agent 调用部署在 B 服务中的工具时,Model Context Protocol(MCP) 就是为此设计的。Spring AI 2.0 将 MCP Java SDK 与注解驱动编程模型深度集成,使跨进程工具调用像本地调用一样简单。

4.1 注解驱动 MCP Server

将你的工具暴露为 MCP Server——只需在已有 @Tool 方法上添加 @McpTool 注解,再加上一个 starter 依赖:

// pom.xml 添加
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

// application.properties
spring.ai.mcp.server.name=order-tools
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.protocol=STREAMABLE          // 推荐,替代已废弃的 SSE
spring.ai.mcp.server.streamable-http.mcp-endpoint=/mcp

// OrderMcpTools.java —— 用 @McpTool 替代 @Tool
@Component
public class OrderMcpTools {

    @McpTool(
        name = "get_order_status",
        description = "Returns the current status of an order"
    )
    public OrderStatus getOrderStatus(
        @McpToolParam(description = "The order ID", required = true) String orderId,
        McpSyncRequestContext context   // 框架自动注入,不在 Schema 中
    ) {
        context.info("Looking up order: " + orderId);
        context.progress(p -> p.progress(0.5).total(1.0).message("Querying database..."));
        return orderService.findById(orderId);
    }
}

4.2 MCP Client:一行配置接入远端工具

在你的 Agent 服务中,只需在配置文件里声明远端 MCP Server 的地址:

// application.yml
spring:
  ai:
    mcp:
      client:
        streamable-http:
          connections:
            order-tools:
              url: http://order-service:8081/mcp
            inventory-tools:
              url: http://inventory-service:8082/mcp

// AgentService.java —— 远端工具自动注入
@Service
public class AgentService {

    private final ChatClient chatClient;

    public AgentService(ChatClient.Builder builder,
                          McpToolCallbackProvider mcpToolProvider) {
        this.chatClient = builder
            .defaultTools(mcpToolProvider)  // 所有 MCP Server 的工具自动注册
            .build();
    }
}
传输方式Starter适用场景注意事项
stdiospring-ai-starter-mcp-server本地开发工具、Claude Code 集成无法多用户/网络化部署
Streamable HTTP (WebMVC)spring-ai-starter-mcp-server-webmvc生产环境标准选择需要 Web 容器
Streamable HTTP (WebFlux)spring-ai-starter-mcp-server-webflux高并发、响应式栈调试难度更高
SSE同上(配置 protocol=SSE)兼容旧客户端⚠ MCP 规范已废弃,新项目不要用

⚠ 错误处理陷阱:工具方法中抛出的普通 RuntimeException 在早期版本中会直接终止 Agent 循环。正确的做法是抛出 ToolExecutionException,框架会将其消息序列化后返回给模型,让模型可以推理错误并决定下一步(重试、让用户澄清等)。异常消息要包含可操作的提示,而非堆栈信息。

五、编排多 Agent:五种 Agentic Pattern 的 Spring AI 实现

Anthropic 在《Building Effective Agents》研究报告中提出了五种工作流模式。Spring AI 官方文档对这五种模式给出了完整的实现参考。下面逐一拆解。

5.1 Chain Workflow(链式工作流)

适用场景:任务有明确的顺序步骤,每步输出是下一步的输入。

public class ChainWorkflow {
    private final ChatClient chatClient;

    public String chain(String userInput, String... prompts) {
        String response = userInput;
        for (String prompt : prompts) {
            response = chatClient.prompt()
                .user("{%s}\n {%s}".formatted(prompt, response))
                .call().content();
        }
        return response;
    }
}

// 示例:合同审查三步骤
String result = chainWorkflow.chain(contractText,
    "Step 1: 识别所有风险条款",
    "Step 2: 对每个风险条款给出修改建议",
    "Step 3: 生成最终的风险评估报告"
);

5.2 Parallelization Workflow(并行工作流)

适用场景:大量独立子任务需要并行处理。

public class ParallelWorkflow {
    public List<String> parallel(String instruction, List<String> inputs, int threads) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            return inputs.stream()
                .map(input -> executor.submit(() ->
                    chatClient.prompt()
                        .user("{%s}\n {%s}".formatted(instruction, input))
                        .call().content()))
                .map(Future::get)
                .toList();
        }
    }
}

// 示例:同时对四个利益方做影响分析
List<String> results = parallelWorkflow.parallel(
    "分析市场变化对该利益方的影响",
    List.of("客户: ...", "员工: ...", "投资人: ...", "供应商: ..."),
    4
);

5.3 Routing Workflow(路由工作流)

适用场景:不同输入类别需要不同专家处理。

public class RoutingWorkflow {
    public String route(String input, Map<String, String> routes) {
        var routing = chatClient.prompt()
            .user("将以下输入分类到: " + routes.keySet() + "\n输入: " + input)
            .call().content();        // 让模型做分类

        return chatClient.prompt()
            .system(routes.get(routing))  // 用对应的专家 System Prompt
            .user(input)
            .call().content();
    }
}

5.4 Orchestrator-Workers(编排器-执行者)

适用场景:子任务无法提前预知,需要动态分解。

public class OrchestratorWorkersWorkflow {
    public WorkerResponse process(String task) {
        // Step 1: 编排器分析任务,动态生成子任务列表
        var subtasks = chatClient.prompt()
            .user("将任务分解为独立子任务: " + task)
            .call().entity(Subtasks.class);   // 结构化输出

        // Step 2: Worker 并行执行每个子任务
        var results = subtasks.items().stream().parallel()
            .map(sub -> chatClient.prompt().user(sub).call().content())
            .toList();

        // Step 3: 汇总结果
        return new WorkerResponse(subtasks.analysis(), results);
    }
}

5.5 Evaluator-Optimizer(评估-优化循环)

适用场景:有明确评估标准,需要迭代优化的任务(代码生成、文案润色等)。

public class EvaluatorOptimizerWorkflow {
    public RefinedResponse loop(String task, int maxRounds) {
        String solution = chatClient.prompt().user(task).call().content();

        for (int i = 0; i < maxRounds; i++) {
            var eval = chatClient.prompt()
                .user("评估以下方案并打分(1-10):\n" + solution)
                .call().entity(Evaluation.class);

            if (eval.score() >= 8) break;  // 达到阈值,退出循环

            solution = chatClient.prompt()
                .user("改进方案,解决以下问题:\n方案: " + solution + "\n问题: " + eval.issues())
                .call().content();
        }
        return new RefinedResponse(solution);
    }
}

💡 选择指南:从最简单开始。Chain 适合 80% 的明确流程场景;Routing 适合多类型输入;Parallel 适合大量独立任务;Orchestrator-Workers 适合开放式复杂任务;Evaluator-Optimizer 适合质量要求极高的生成场景。绝大多数生产系统只需要前三种。

六、全流程实战:构建一个多 Agent 客服系统

现在把以上所有特性组合成一个现实的场景——智能客服系统,包含三个专用 Agent 通过 MCP 协议协作:

主 Agent
(路由 + 编排) → MCP → 订单 Agent
(订单查询/取消)
 → MCP → 库存 Agent
(库存/补货)
 → MCP → 知识库 Agent
(FAQ/文档检索)

6.1 主 Agent:Routing + Orchestrator 混合

@Service
public class CustomerServiceAgent {

    private final ChatClient chatClient;
    private final RoutingWorkflow router;

    // 三个远端 MCP Server 的工具通过 McpToolCallbackProvider 自动注入
    public CustomerServiceAgent(
        ChatClient.Builder builder,
        McpToolCallbackProvider mcpTools,
        ChatMemory chatMemory
    ) {
        this.chatClient = builder
            .defaultTools(mcpTools)
            .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(chatMemory).build(),
                ToolSearchToolCallingAdvisor.builder()   // 60+ 工具,渐进式披露
                    .toolCallbacks(mcpTools.getToolCallbacks())
                    .build(),
                StructuredOutputValidationAdvisor.builder().build()  // 自修正输出
            )
            .build();
    }

    public String handle(String userMessage, String conversationId) {
        // 用 Routing 先分类
        var category = chatClient.prompt()
            .user("分类以下客户消息(订单/库存/产品咨询/投诉/其他): " + userMessage)
            .call().content();

        // 根据分类注入对应的 system prompt
        return chatClient.prompt()
            .system(systemPrompts.getOrDefault(category, defaultPrompt))
            .user(userMessage)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .call().content();
    }
}

6.2 完整对话流转示意

// 用户: "我的订单 ORD-88421 什么时候到?上周买的。"
//
// → Routing: 分类为 "订单"
// → ToolSearchToolCallingAdvisor: 从 60+ 工具中匹配到 getOrderStatus
// → MCP → OrderAgent.getOrderStatus("ORD-88421")
// → OrderAgent 返回: "SHIPPED, ETA 2026-08-10"
// → ChatMemory: 上下文已包含订单信息
// → 模型回答: "您的订单 ORD-88421 已于昨天发货,预计8月10日送达。"
//
// 用户(追问): "能帮我取消吗?"
// → ChatMemory: 知道 "它" = ORD-88421
// → MCP → OrderAgent.cancelOrder("ORD-88421")
// → 返回: ToolExecutionException("订单已发货,无法取消,建议申请退货")
// → 模型: "抱歉,该订单已经发货了所以无法取消。需要我帮您申请退货吗?"

✅ 这个系统的核心价值:
① 主 Agent 与子 Agent 解耦——每个子 Agent 独立部署、独立扩展
② 工具动态发现——新增子 Agent 无需修改主 Agent 代码
③ 对话记忆——ChatMemory 管理上下文,支持多轮追问
④ 错误弹性——ToolExecutionException 让模型能优雅处理业务异常
⑤ 结构化输出自修正——当模型生成的 JSON 不符合 Schema 时自动重试

七、生产部署 Checklist

以下是从开发到生产需要注意的要点:

📊

可观测性

Spring AI 2.0 集成 Micrometer 和 OpenTelemetry。工具调用耗时、Token 消耗、MCP 通信等全部自动埋点。搭配 Grafana 可构建完整的 Agent 监控面板。

🔒

MCP 安全

通过 spring-ai-community/mcp-security 实现 OAuth 2.0 和 API-Key 认证。MCP Server 与 Client 之间的双向认证防止未授权工具调用。

🧪

测试策略

使用 Spring AI Testcontainers 模块,可以在集成测试中启动真实的 Ollama 容器或 Mock 模型响应,验证完整的工具调用链路。

虚拟线程

Spring Boot 4 + Java 21 的虚拟线程对 Agent 系统是绝配:大量工具调用涉及 I/O 等待,虚拟线程避免了传统线程池的瓶颈。

关键配置项

// application.properties —— 推荐的生产配置
# 模型配置
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o
spring.ai.openai.chat.options.temperature=0.3   # Agent 场景建议低温,减少幻觉

# MCP 服务端
spring.ai.mcp.server.protocol=STREAMABLE
spring.ai.mcp.server.name=customer-service-agent
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.streamable-http.mcp-endpoint=/mcp

# 可观测性
management.endpoints.web.exposure.include=health,metrics,prometheus
management.metrics.export.prometheus.enabled=true

# 虚拟线程(Spring Boot 4 默认开启,但显式声明不碍事)
spring.threads.virtual.enabled=true

写在最后

Spring AI 2.0 做对了一件事:把 Agent 开发的复杂性封装进 DI 容器和 Advisor 链中,而不像某些框架那样让你手动管理执行图的状态机。对于已经在用 Spring 生态的 Java 团队来说,这意味着构建 Agent 系统的学习曲线从"学一套全新范式"降到了"理解三个新注解和一个新概念"。

ToolCallingAdvisor 的可组合性、MCP 的跨服务工具调用、五种 Agentic Pattern 的开箱实现——这三个支柱共同构成了一个完整的多 Agent 编程 IDE。它不是要替代 LangChain 或 CrewAI,而是在 JVM 生态里提供了一个类型安全、可观测、生产就绪的替代方案。

2026 年下半年的趋势已经明确:Agent 不再是 Demo 里的玩具,而是要跑在生产环境里、处理真实业务流量、接受 SLA 考核的服务。Spring AI 2.0 的 GA 发布,正是在这个节点上给了 Java 开发者一把趁手的工具。

从 50 行的 Tool-Calling Agent 开始,从那里再到 MCP 多服务编排——每一步都是可落地的。不着急,一步步来。

#Spring AI 2.0 #MCP 协议 #Agentic AI #Tool Calling #ChatClient #Advisor Chain #Java Agent   #Spring Boot 4 #多Agent协作 #ToolSearch

更多推荐