Spring AI 深度实践:在 Java 项目中统一 Chat、RAG、Tools 与 MCP 能力

项目完整源码地址:https://github.com/zhouByte-hub/java-ai

欢迎 Star / Fork,这篇文章对应的是仓库中的 spring_ai_demo 子项目,后续还会更新 Spring AI Alibaba、LangChain4j 等实战篇。

最近在折腾一个 java-ai 多模块示例仓库,想系统性对比 Spring AI、Spring AI Alibaba、LangChain4j、LangGraph 的不同定位。

本篇先聚焦 Spring AI ,结合 spring_ai_demo 子项目,带你从 0 到 1 跑通:

  • 基础对话:ChatModel vs ChatClient + Advisor
  • RAG:基于 PostgreSQL + pgvector 的检索增强
  • Tools:让大模型"真正动手做事"的函数调用
  • MCP:用标准协议把工具能力从应用中"拆"出去

下文所有代码都可以在仓库里找到,对应路径为:

  • Spring AI Demo:spring_ai_demo
  • MCP Client:spring_ai_demo/mcp-client
  • MCP Server:spring_ai_demo/mcp-server

1. Spring AI 是什么?有什么用?

结合根目录 README.md 的定位,可以把 Spring AI 理解为:

  • Spring 官方维护 的大模型集成项目(类似 Spring Data / Spring Cloud 的角色);
  • 提供统一的 ChatClient / EmbeddingClient / ImageClient / Tool / MCP 等抽象;
  • 强调 "Spring Boot 风格的 AI 接入":配置驱动 + 自动装配,而不是 DSL 式链式编排。

spring_ai_demo 里,你可以看到 Spring AI 主要帮我们做了几件事:

  • 通过 application.yaml 配好模型供应商(这里是本地 Ollama,模型 qwen3 系列);
  • 注入 ChatModel / ChatClient,像用普通 Spring Bean 一样发起对话;
  • Advisor 机制把敏感词过滤、对话记忆、RAG 检索等能力串在一起;
  • spring-ai-rag + vectorstore-pgvector 快速落地 RAG;
  • spring-ai-starter-mcp-* 把 MCP Client / Server 直接融入 Spring Boot。

1.1 使用 Spring AI 的主要优点

  • 官方 & 长期维护:由 Spring 团队维护,API 风格与 Spring Boot 深度统一;
  • 供应商无关:在配置里切换模型(OpenAI / Azure / Ollama / ...),上层代码基本不变;
  • 编程模型简单 :用 ChatClient 发起对话、注入 Advisor,学习成本对 Java / Spring 开发者非常友好;
  • RAG / Tools / MCP 打通 :通过一套 ChatClient 抽象,把向量检索、工具调用、MCP 统一起来。

1.2 目前的一些不足 / 权衡

  • 更偏"LLM 客户端"而不是"编排框架"

    • 复杂 Agent、状态机式对话、图式工作流等更适合交给专门的编排框架处理;
    • Spring AI 更像是"在 Spring 世界里把模型接好"的基座。
  • 生态仍在快速演进

    • 新特性(例如 MCP)更新很快,版本差异带来的 breaking change 需要关注;
    • 文档、社区资料正在丰富中,踩坑时要多看 release note。
  • 国内模型支持需要额外扩展

    • 官方优先支持国际主流厂商,对国内模型的体验可以进一步交给 Spring AI Alibaba 等扩展(将在后续文章展开)。

如果你是 Spring / Spring Boot 开发者,希望在现有系统里快速接入 LLM,Spring AI 是一个非常自然的选择。


2. Demo 总览:Spring AI 在项目里的位置

spring_ai_demo 子项目是一个典型的 Spring Boot 3.5 + Spring AI 示例,主要依赖在 spring_ai_demo/pom.xml

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Ollama 模型客户端 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-ollama</artifactId>
    </dependency>

    <!-- 向量库(pgvector) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>

    <!-- RAG 能力 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-rag</artifactId>
    </dependency>
</dependencies>

核心配置在 spring_ai_demo/src/main/resources/application.yaml(只截取和 Spring AI 相关的部分):

yaml 复制代码
spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen3:0.6b
          temperature: 0.8
      init:
        # never: 从不拉取,when-miss: 没有时拉取,always: 总是拉取
        pull-model-strategy: never
      embedding:
        options:
          model: qwen3-embedding:0.6b

    vectorstore:
      pgvector:
        initialize-schema: true
        dimensions: 1024   # Qwen3-Embedding-0.6B 的向量维度

整体调用关系可以用一张图概括:
Spring Boot 应用(spring_ai_demo) Chat RAG Tools MCP 子模块 ChatClient / ChatModel RAG + ChatClient Embedding / 相似度检索 Tool 调用 MCP 协议 工具 / 资源 mcp-client mcp-server ToolController RagController ChatClientController / ChatModelController 用户 / 前端 Ollama 中的大模型(qwen3 系列) PostgreSQL + pgvector

接下来分别从 Chat、RAG、Tools、MCP 四个能力切入,结合关键代码看一遍完整链路。


3. Chat:从 ChatModel 到 ChatClient + Advisor

3.1 直接使用 ChatModel

最基础的用法是直接注入 ChatModel,然后在 Controller 里调用:

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/chat/ChatModelController.java

java 复制代码
@RestController
@RequestMapping(value = "/chatModel")
@RequiredArgsConstructor
public class ChatModelController {

    private final ChatModel chatModel;

    @GetMapping(value = "/chat")
    public Flux<String> chat(@RequestParam("message") String message) {

        return chatModel.stream(new Prompt(message))
                .map(ChatResponse::getResult)
                .mapNotNull(item -> item.getOutput().getText());
    }
}

流程说明:

  1. Spring Boot 自动根据 spring.ai.ollama 配置创建 ChatModel Bean;
  2. Controller 注入 ChatModel,把用户的 message 包装成 Prompt
  3. 调用 chatModel.stream 获取流式 ChatResponse,再映射为 Flux<String> 返回给前端。

这一层基本等价于"手撸 HTTP 调用大模型 API,但由 Spring AI 帮你封装好了客户端"。

3.2 用 ChatClient 封装更高级的能力

Spring AI 推荐使用更高级的 ChatClient 抽象,它在 ChatModel 之上增加了:

  • Advisor 链:在请求前后插入横切逻辑(敏感词校验、记忆、RAG 等);
  • 更方便的 fluent API:chatClient.prompt().user("...").system("...").stream()

spring_ai_demo 中,ChatClient 是在配置类里定义的:

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/chat/config/OllamaConfig.java

java 复制代码
@Configuration
public class OllamaConfig {

    /**
     * 创建 ChatClient 调用大模型,ChatClient 是比 ChatModel 更高一级的封装
     */
    @Bean("ollamaChatClient")
    public ChatClient ollamaChatClient(ChatModel chatModel,
                                       BaseAdvisor memoriesAdvisor,
                                       ChatMemoryRepository databaseChatMemoryRepository) {

        MessageWindowChatMemory databaseChatMemories = MessageWindowChatMemory.builder()
                .chatMemoryRepository(databaseChatMemoryRepository)
                .maxMessages(10)
                .build();

        MessageChatMemoryAdvisor build = MessageChatMemoryAdvisor.builder(databaseChatMemories)
                .order(0)
                .build();

        return ChatClient.builder(chatModel)
                // 在初始化 ChatClient 时配置全局 Advisor,在具体调用时就不需要再显式配置
                .defaultAdvisors(memoriesAdvisor)       // 也可以切换为 build
                .build();
    }
}

这里做了两件事:

  1. MessageWindowChatMemory + MessageChatMemoryAdvisor 搭了一套"窗口记忆",最多记住最近 10 条对话;
  2. 把记忆相关的 Advisor 配成 ChatClientdefaultAdvisors,之后所有对话都会自动带上这些能力。

3.3 Advisor 示例:敏感词拦截 + 对话记忆

3.3.1 敏感词拦截:SensitiveWordAdviser

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/chat/adviser/SensitiveWordAdviser.java

java 复制代码
@Component
public class SensitiveWordAdviser implements CallAdvisor, StreamAdvisor {

    private final static String[] SENSITIVE_WORDS = {"赌博", "嫖娼", "吸毒"};

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest,
                                         CallAdvisorChain callAdvisorChain) {
        chatClientRequest.prompt().getUserMessages().forEach(message -> {
            if (checkSensitiveWord(message.getText())) {
                throw new RuntimeException("存在敏感词,注意注意!!!");
            }
        });
        ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
        chatClientResponse.context().put("server", "springAi");
        return chatClientResponse;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
                                                 StreamAdvisorChain streamAdvisorChain) {
        chatClientRequest.prompt().getUserMessages().forEach(message -> {
            if (checkSensitiveWord(message.getText())) {
                throw new RuntimeException("存在敏感词,注意注意!!!");
            }
        });
        // 对流式响应逐条添加自定义上下文属性
        return streamAdvisorChain.nextStream(chatClientRequest)
                .doOnNext(resp -> resp.context().put("server", "springAi"));
    }

    private boolean checkSensitiveWord(String content) {
        for (String sensitiveWord : SENSITIVE_WORDS) {
            if (content.contains(sensitiveWord)) {
                return true;
            }
        }
        return false;
    }
}

这段 Advisor 做了两件事:

  • 请求前:检查用户输入是否包含敏感词,命中就直接抛异常中断调用;
  • 流式响应中:在每一条 ChatClientResponsecontext 中写入一个标识。
3.3.2 ChatClient 使用 Adviser 的 Controller

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/chat/ChatClientController.java

java 复制代码
@RestController
@RequestMapping(value = "/chatClient")
@RequiredArgsConstructor
public class ChatClientController {

    private final ChatClient ollamaChatClient;
    private final SensitiveWordAdviser sensitiveWordAdviser;

    /**
     * 使用 ChatClient 实现基本对话
     */
    @GetMapping(value = "/chat")
    public Flux<String> chat(@RequestParam("message") String message) {
        return ollamaChatClient
                .prompt(new Prompt(message))
                .advisors(sensitiveWordAdviser)
                .stream()
                .content();
    }

    @GetMapping(value = "/chatForMemories")
    public Flux<String> chatForMemories(@RequestParam("message") String message,
                                        @RequestParam("sessionId") String sessionId) {
        return ollamaChatClient.prompt(message)
                .advisors(advisorSpec -> advisorSpec.param("chat_memories_session_id", sessionId))
                .stream()
                .content();
    }
}

这里有两个接口:

  • /spring-ai/chatClient/chat:带敏感词 Advisor 的普通聊天;
  • /spring-ai/chatClient/chatForMemories:通过 chat_memories_session_id 把多轮对话串起来交给记忆 Advisor。

结合上述代码,ChatClient 调用流程可以用一张时序图表示:
User ChatClientController ChatClient SensitiveWordAdviser ChatModel / Ollama GET /spring-ai/chatClient/chat?message=... prompt(Prompt(message)).advisors(A) adviseCall / adviseStream(敏感词校验) 校验通过 stream(Prompt) 流式返回内容 Flux<String> SSE / 分片响应 User ChatClientController ChatClient SensitiveWordAdviser ChatModel / Ollama

小结:

在 Spring AI 中,可以把 ChatModel 理解为"最底层的模型客户端",而 ChatClient + Advisor 则是"面向业务的对话编排层"。

敏感词过滤、对话记忆、RAG、Tools、MCP 等能力都可以通过 Advisor 统一挂接。


4. RAG:用 spring-ai-rag + pgvector 做检索增强

根目录 README.md 里已经用 ASCII 图解释了 RAG 的整体流程,本项目在此基础上,用 Spring AI 官方的 spring-ai-ragvectorstore-pgvector 做了一个最小可运行版本。

4.1 什么是 RAG(Retrieval-Augmented Generation)?

简单一句话:

RAG = 检索增强生成 = 「先查资料,再让大模型回答」。

传统的纯 LLM 问答流程是:

  • 把用户问题直接丢给模型,让模型"凭记忆"作答;
  • 模型的知识完全来自预训练参数,更新成本高,而且容易出现"胡说八道"(幻觉)。

RAG 在这个链路中插入了一步 外部知识检索

  1. 用 Embedding 模型把"用户问题"向量化;
  2. 去向量库 / 文档库里查一批相似的文档片段;
  3. 把这些文档拼装成 context,和问题一起交给聊天模型;
  4. 聊天模型"带着资料"回答,既能利用自身的通用能力,又能引用实时 / 私有知识。

4.2 为什么要使用 RAG?

结合根目录 README.md 的总结,RAG 主要是为了解决三类问题:

  • 知识更新成本

    • 模型一旦训练完,参数就很难频繁更新;
    • 业务知识(规章制度、产品文档、代码、FAQ)变化频繁,放在向量库里更合适;
    • 通过更新文档,就能"更新"模型能看到的知识。
  • 上下文长度与成本

    • 直接把所有文档塞给模型,既超出上下文限制,又贵又慢;
    • RAG 只取与当前问题最相关的一小部分内容,控制上下文长度。
  • 安全与隐私

    • 很多资料不能直接用于二次训练,但可以放在自有向量库中;
    • RAG 可以在企业环境内检索敏感数据,并在本地模型中使用。

在本 Demo 里:

  • 大模型走本地 Ollama + qwen3
  • 私有知识放在 PostgreSQL + pgvector 中;
  • Spring AI 帮我们把"检索 + 生成"这一整套流程封装到了 RetrievalAugmentationAdvisor 里。

4.3 使用 RAG 的优点与不足

优点:

  • 减少幻觉:模型回答时有真实文档可以引用,不再完全"想象";
  • 知识可独立演进:只要更新向量库中的文档,无需重新训练 / 微调模型;
  • 模型更小也能用:可以用较小的 Chat 模型 + 外部知识,做出效果不错的问答系统;
  • 可控性好:检索范围、文档来源、过滤规则都可以在工程侧精细控制。

不足 / 需要权衡的点:

  • 检索质量是上限:如果向量召回的文档不相关,再强的 LLM 也难以给出好答案;
  • 工程复杂度更高:需要维护文档切分、embedding、向量库、索引更新等一整套工程;
  • 延迟与成本增加:一次问答通常要调用两次模型(embedding + chat)和至少一次数据库 / 向量库;
  • 领域漂移问题:文档写得不好 / 过时时,RAG 会把"坏知识"放大。

在 Spring AI 中,这些复杂度很大一部分被封装在 RAG Advisor 和 VectorStore 里,业务代码只需要关心"哪些文档要入库、问答接口怎么设计"。

4.4 在 Spring AI Demo 中如何落地 RAG(ChatClient 侧)

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/rag/RagChatClientConfig.java

java 复制代码
@Configuration
public class RagChatClientConfig {

    /**
     * 带 RAG 能力的 ChatClient Bean。
     */
    @Bean
    public ChatClient ragChatClient(ChatModel chatModel, VectorStore pgVectorStore) {

        // 基于 pgVectorStore 的文档检索器:按照向量相似度从向量库中取前 topK 条文档
        VectorStoreDocumentRetriever storeDocumentRetriever = VectorStoreDocumentRetriever.builder()
                .vectorStore(pgVectorStore)
                .topK(5)
                .similarityThreshold(0.1)
                .build();

        // RAG 顾问:每次对话前先用检索器查知识库,并把检索结果注入 Prompt
        RetrievalAugmentationAdvisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
                .order(0)
                .documentRetriever(storeDocumentRetriever)
                .build();

        return ChatClient.builder(chatModel)
                .defaultAdvisors(retrievalAugmentationAdvisor)
                .build();
    }
}

这个配置做的事情其实可以翻译成一句话:

"只要业务通过 ragChatClient 发起对话,就自动先去向量库里查一轮相似文档,把结果拼进 Prompt,再让模型回答。"

4.5 文档导入 + RAG 问答接口

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/rag/RagController.java

java 复制代码
@RestController
@RequestMapping(value = "/rag")
@RequiredArgsConstructor
public class RagController {

    private final VectorStore pgVectorStore;
    private final ChatClient ragChatClient;

    /**
     * 添加文档
     */
    @GetMapping(value = "/import_text")
    public void importTextData(@RequestParam("content") String content) {
        Document document = Document.builder().text(content).build();

        // TokenTextSplitter:按 token 粒度把长文本切成适合做向量检索的小块
        TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(
                50,   // chunkSize:每块最大 token 数
                20,   // minChunkSizeChars:过短的块会和前后合并
                10,   // minChunkLengthToEmbed:短于该值的块不做向量化
                1000, // maxNumChunks:单个文档最大切片数
                false // keepSeparator:是否保留分隔符
        );
        pgVectorStore.add(tokenTextSplitter.split(document));
    }

    /**
     * 执行 RAG 问答
     */
    @GetMapping(value = "/message")
    public Flux<String> message(@RequestParam("query") String message)  {
        return ragChatClient.prompt()
                .system("这里是 springAi 项目,有什么能够帮您?")
                .user(message)
                .stream()
                .content();
    }
}

这两个接口串起来就是一个完整的 RAG 流程:

  1. 导入文档/spring-ai/rag/import_text?content=...

    • 把长文本切成多个 chunk;
    • 对每个 chunk 做 embedding;
    • 写入 PostgreSQL + pgvector。
  2. RAG 问答/spring-ai/rag/message?query=...

    • RetrievalAugmentationAdvisor 用问题做 embedding;
    • 到向量库查 topK 相似文档;
    • 把这些文档拼成 context,一起丢给聊天模型;
    • 模型在"有知识"的前提下给出回答。

用一张图总结 RAG 调用链:
用户问题 /rag/message 生成回答 相似文档 (topK) VectorStore (pgvector) ChatModel / Ollama

小结:

Spring AI 在 RAG 场景里最大的价值,是把"向量检索 + Prompt 拼接"这一整套细节抽象成了一个 Advisor,业务只需要看见一个 ChatClient


5. Tools:用注解把 Java 方法暴露给大模型

在根目录 README.md 里,Tools 被描述为"大模型的执行者",让 LLM 不只是聊天,还能"干活"。

从架构上可以把 Tools 粗略拆成两侧:

  • Client 侧:负责和大模型对话,把 Tool 的描述发给模型,并根据模型发起的 tool call 去实际调用工具;
  • Server 侧:真正实现业务逻辑(查库、调三方、算报价等),对外表现为一个个"可被 LLM 调用的函数"。

spring_ai_demo 这个简单 Demo 里,这两部分都在同一个 Spring Boot 服务里,但职责划分仍然清晰:

Controller + ChatClient 更偏 Client 逻辑,@Tool 标注的方法更偏 Server 逻辑。

5.1 Server 侧:定义 Tool 实现(ZoomTool)

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/tools/ZoomTool.java

java 复制代码
@Component
public class ZoomTool {

    @Tool(description = "通过时区 ID 获取当前时间")
    public String getTimeByZone(@ToolParam(description = "时区 ID,比如 Asia/Shanghai")
                                String zone) {
        ZoneId zoneId = ZoneId.of(zone);
        ZonedDateTime zoneddateTime = ZonedDateTime.now(zoneId);
        return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(zoneddateTime);
    }
}

作为"工具服务端",你只需要两点:

  • @Tool 标记这是一个可以被 LLM 调用的方法,并写上自然语言描述;
  • @ToolParam 标注参数含义,Spring AI 会自动为它生成参数 schema。

5.2 Client 侧:把 Tool 挂到 ChatClient 上

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/tools/ToolChatClientConfig.java

java 复制代码
@Configuration
public class ToolChatClientConfig {

    @Bean
    public ChatClient toolChatClient(ChatModel chatModel, ZoomTool zoomTool) {
        return ChatClient.builder(chatModel)
                .defaultSystem("拿铁咖啡制作需要一分钟,美式咖啡制作需要 1-2分钟,蜜雪冰城出门右转。")
                .defaultTools(zoomTool)
                .build();
    }
}

这里通过 defaultTools(zoomTool)ZoomTool 暴露给 ChatClient:

之后 ChatClient 在调用模型时,会把这个 Tool 的描述一起发给模型,让模型决定 什么时候 需要调用它。

5.3 Client 侧:暴露 HTTP 对话接口

spring_ai_demo/src/main/java/com/zhoubyte/spring_ai_demo/tools/ToolController.java

java 复制代码
@RestController
@RequestMapping(value = "/tools")
@RequiredArgsConstructor
public class ToolController {

    private final ChatClient toolChatClient;

    @GetMapping(value = "/message")
    public Flux<String> chat(@RequestParam("message") String message) {
        return toolChatClient.prompt()
                .user(message)
                .stream()
                .content();
    }
}

现在,假设你在前端调用:

/spring-ai/tools/message?message=请帮我查询一下 Asia/Shanghai 当前时间

大致流程是:

  1. ToolController 调用 toolChatClient.prompt().user(message)
  2. ChatClient 把用户问题和 ZoomTool 的描述一起传给模型;
  3. 模型自己决定 是否调用 getTimeByZone,并根据返回结果组织自然语言回答;
  4. 最终前端拿到的是一段已经填好时间的回答。

用一张时序图表示 Tools 调用链:
"User" "ToolController" "toolChatClient" "LLM" "ZoomTool.getTimeByZone" GET /spring-ai/tools/message?message=... prompt().user(message) 调用模型(携带 Tool 描述) 决定是否调用 getTimeByZone 调用 ZoomTool.getTimeByZone(zone) 返回当前时间 生成带时间的自然语言回答 返回给 Controller 返回给用户 "User" "ToolController" "toolChatClient" "LLM" "ZoomTool.getTimeByZone"

虽然在 Demo 中 Controller、ChatClient、ZoomTool 都在同一个进程里,但从职责上仍可以理解为:

  • Client 侧:Controller + ChatClient + LLM,负责理解用户意图、决定是否调用工具;
  • Server 侧 :被 @Tool 标注的 Java 方法,负责真正执行业务逻辑并返回结构化结果。

小结:

在 Spring AI 中,使用 Tools 的体验非常 Spring:

"写一个 Bean + 加上注解,然后交给 ChatClient,其它都交给框架和模型"。


6. MCP:用标准协议把工具能力"拆"到独立服务

根目录 README.md 中对 MCP(Model Context Protocol)的介绍可以概括为:

MCP 是一种把"工具 / 资源 / Prompt 能力"从单个应用中抽离出来、通过统一协议对外暴露的标准。

换句话说,相比于直接在应用里注册 Tools,MCP 更适合:

  • 多个应用共享同一套工具资产;
  • 工具和资源由独立服务统一治理(认证、审计、灰度、版本);
  • 大模型客户端(不止 Spring AI)都能通过同一协议访问这些能力。

在本项目中,MCP 通过两个子模块演示:

  • spring_ai_demo/mcp-server:真正实现工具的服务;
  • spring_ai_demo/mcp-client:作为 Spring AI 客户端接入 MCP 服务器。

下面就分别从 ServerClient 两个视角来展开。

6.1 MCP Server:暴露工具能力

spring_ai_demo/mcp-server/src/main/java/org/zhoubyte/mcpserver/stdio/ZoomTool.java

java 复制代码
@Component
public class ZoomTool {

    // 使用 @Tool 或 @McpTool 注解都可以
    @McpTool(name = "getTimeByZone", description = "通过时区 ID 获取当前时间")
    public String getTimeByZone(@McpToolParam(description = "时区 ID,比如 Asia/Shanghai")
                                String zone) {
        ZoneId zoneId = ZoneId.of(zone);
        ZonedDateTime zoneddateTime = ZonedDateTime.now(zoneId);
        return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(zoneddateTime);
    }

    @Tool(name = "getCurrentUser", description = "获取当前登录用户的用户名称")
    public String getCurrentUser() {
        return "ZhouByte";
    }
}

这里多了两个 MCP 专用注解:

  • @McpTool:声明这是一个 MCP Tool,Spring AI MCP Server 会为其自动生成 JSON Schema;
  • @McpToolParam:声明参数含义,同样会进入 MCP 的工具描述。

为了让 MCP Server 能够发现这些 Tool,还需要一个 ToolCallbackProvider

spring_ai_demo/mcp-server/src/main/java/org/zhoubyte/mcpserver/stdio/StdioMCPConfig.java

java 复制代码
@Configuration
public class StdioMCPConfig {

    @Bean
    public ToolCallbackProvider toolCallbackProvider(ZoomTool zoomTool) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(zoomTool)
                .build();
    }
}

MCP Server 的关键配置在 mcp-server/src/main/resources/application.yaml

yaml 复制代码
spring:
  ai:
    mcp:
      server:
        enabled: true          # 启用 MCP 服务器
        stdio: true            # 使用 STDIO 传输
        name: mcp-server
        version: 1.0.0
        type: sync
        capabilities:
          resource: true       # 是否暴露资源能力
          tool: true           # 是否暴露工具能力
          prompt: true         # 是否暴露 Prompt 模板
          completion: true

这一套组合在一起,就得到了一个可以通过 MCP 协议暴露工具的"后端服务"。

6.2 MCP Client:把 MCP 工具挂到 ChatClient 上

spring_ai_demo/mcp-client 则是 MCP 客户端,它要做的事情有两步:

  1. 告诉 Spring AI:我要作为 MCP Client 连接到哪个 MCP Server;
  2. 拿到 MCP 提供的 Tools,并把它们挂到 ChatClient 上。

配置在 mcp-client/src/main/resources/application.yaml

yaml 复制代码
spring:
  ai:
    mcp:
      client:
        enabled: true
        name: spring-ai-mcp-client
        version: 1.0.0
        initialized: true
        request-timeout: 20s
        type: sync
        root-change-notification: true
        toolcallback:
          enabled: true      # 将 MCP 工具自动注册为 Spring AI 的 ToolCallback
        stdio:
          servers-configuration: classpath:mcp-server.json

mcp-server.json 指定了如何通过 STDIO 启动 MCP Server(只展示核心结构):

json 复制代码
{
  "mcpServers": {
    "my-mcp-service": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-jar",
        ".../mcp-server-0.0.1-SNAPSHOT.jar"
      ]
    }
  }
}

核心代码在 mcp-client 的配置类和 Controller 中:

spring_ai_demo/mcp-client/src/main/java/org/zhoubyte/mcpclient/config/StdioMcpClientConfig.java

java 复制代码
@Configuration
public class StdioMcpClientConfig {

    @Bean
    public ChatClient stdioMcpChatClient(ChatClient.Builder chatClientBuilder,
                                         ToolCallbackProvider toolCallbackProvider) {
        return chatClientBuilder
                .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
                .build();
    }
}

spring_ai_demo/mcp-client/src/main/java/org/zhoubyte/mcpclient/controller/StdioMcpClientController.java

java 复制代码
@RestController
@RequestMapping(value = "/stdio")
public class StdioMcpClientController {

    private final ChatClient stdioMcpChatClient;

    public StdioMcpClientController(ChatClient stdioMcpChatClient) {
        this.stdioMcpChatClient = stdioMcpChatClient;
    }

    @GetMapping(value = "/chat")
    public Flux<String> chat(@RequestParam("message") String message) {
        return stdioMcpChatClient.prompt()
                .user(message)
                .stream()
                .content();
    }
}

这几行代码的含义可以拆解为:

  • MCP Client 负责和 MCP Server 建立连接、同步 Tool / Resource / Prompt 信息;
  • ToolCallbackProvider 把 MCP 提供的工具转换为 Spring AI 认可的 ToolCallback;
  • ChatClient.Builder 把这些回调挂到默认工具列表上;
  • Controller 看起来和普通 ChatClient 完全一样:对业务是透明的。

用一张时序图总结 MCP 整体调用链:
"User" "McpClient (stdio)" "ChatClient" "LLM" "McpClient" "McpServer" "ZoomTool / getCurrentUser" GET /stdio/chat?message=... prompt().user(message) 调用模型(携带 MCP 工具描述) 决定调用某个 MCP Tool 发起 tool_call 通过 ToolCallback 调用 MCP MCP 请求(STDIO / SSE) 执行 ZoomTool / 其他工具 返回结构化结果 结果返回给 McpClient 结果返回给 ChatClient 把工具结果作为上下文继续生成 最终回答 返回给 Controller 返回给用户 "User" "McpClient (stdio)" "ChatClient" "LLM" "McpClient" "McpServer" "ZoomTool / getCurrentUser"

小结:

直接 Tools 适合"小而美"的单体或少量服务;

MCP 更像一层"工具服务网关",适合在多应用、多团队、多模型场景下集中管理所有工具和知识。


7. 小结:Spring AI 在 Chat / RAG / Tools / MCP 上的体验

结合 spring_ai_demo 子项目,可以给 Spring AI 在这几个能力上的体验做一个简单总结。

7.1 使用感受(优点)

  • 对 Spring Boot 开发者极友好

    几乎所有东西都是"配置 + Bean",Controller 只需要注入 ChatClient 即可。

  • 抽象层次清晰

    • ChatModel:最底层的模型调用;
    • ChatClient:带 Advisor 的对话编排;
    • Advisor:把记忆、RAG、Tools、MCP 串到一条调用链里。
  • RAG 一条龙
    spring-ai-rag + vectorstore-pgvector 把文本切分、向量化、相似度检索封装得比较完整。

  • 工具与 MCP 统一

    直接 Tools 和 MCP Tools 在代码层面体验非常接近,Builder / Advisor 统一收口。

7.2 需要注意的点 / 不足

  • 复杂 Agent 逻辑仍需自己编排

    多 Agent 协作、复杂状态机、工具路由等需要结合其它编排框架或手写逻辑(后续会在 LangChain4j 篇展开)。

  • 版本更新较快

    MCP 等新能力还在快速演进中,升级 Spring AI 版本时要关注配置项和 Starter 的变更。

  • 国内模型生态需要进一步扩展

    对于通义千问等国内模型、以及更丰富的接入姿势,可以结合 Spring AI Alibaba 等扩展使用。


8. 后续计划 & 欢迎交流

这篇文章只是 java-ai 仓库的第一篇 Spring AI 上篇,主要聚焦:

  • 如何在 Spring Boot 项目中用 Spring AI 接入本地大模型(Ollama);
  • 如何基于 Spring AI 实现 Chat、RAG、Tools、MCP 的端到端链路。

接下来会在同一仓库中持续补充:

  • Spring AI Alibaba 实战篇:聚焦国内大模型生态的接入体验;
  • LangChain4j / LangGraph 实战篇:偏 Agent / Chain / 有状态对话编排。

欢迎直接到仓库里留言 / 提 issue:

https://github.com/zhouByte-hub/java-ai

也欢迎在 CSDN 或 GitHub 上关注我,一起把 Java + LLM 的坑踩得更平、更顺。

相关推荐
AI营销资讯站2 小时前
2025社群运营AI工具TOP榜:从自动化话术到AI CRM系统的终极演进
大数据·人工智能
dalalajjl2 小时前
从MCP到PTC Anthropic回归Code Execution路线,AiPy的范式被再次验证
人工智能
零一科技2 小时前
Spring AOP 底层实现:JDK 动态代理与 CGLIB 代理的那点事儿
java·后端·spring
头发还在的女程序员2 小时前
陪诊小程序成品|陪诊系统功能|陪诊系统功能(源码)
java·小程序·his系统
水木姚姚2 小时前
搭建 TensorFlow 在 VScode 下编程环境(Debian)
人工智能·windows·vscode·debian·tensorflow
Hui Baby2 小时前
Embedding和Remark模型探秘
人工智能·语音识别
用户69371750013842 小时前
27.Kotlin 空安全:安全转换 (as?) 与非空断言 (!!)
android·后端·kotlin
数据库知识分享者小北2 小时前
Dify+ADB Supabase+LLM 实现 AI 客服系统
数据库·人工智能·阿里云·adb·postgresql
oak隔壁找我2 小时前
大模型中参数中 topP(核采样)与 topK 参数的区别
人工智能