项目完整源码地址: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 跑通:
- 基础对话:
ChatModelvsChatClient+ 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());
}
}
流程说明:
- Spring Boot 自动根据
spring.ai.ollama配置创建ChatModelBean; - Controller 注入
ChatModel,把用户的message包装成Prompt; - 调用
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();
}
}
这里做了两件事:
- 用
MessageWindowChatMemory+MessageChatMemoryAdvisor搭了一套"窗口记忆",最多记住最近 10 条对话; - 把记忆相关的 Advisor 配成
ChatClient的defaultAdvisors,之后所有对话都会自动带上这些能力。
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 做了两件事:
- 请求前:检查用户输入是否包含敏感词,命中就直接抛异常中断调用;
- 流式响应中:在每一条
ChatClientResponse的context中写入一个标识。
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-rag 和 vectorstore-pgvector 做了一个最小可运行版本。
4.1 什么是 RAG(Retrieval-Augmented Generation)?
简单一句话:
RAG = 检索增强生成 = 「先查资料,再让大模型回答」。
传统的纯 LLM 问答流程是:
- 把用户问题直接丢给模型,让模型"凭记忆"作答;
- 模型的知识完全来自预训练参数,更新成本高,而且容易出现"胡说八道"(幻觉)。
RAG 在这个链路中插入了一步 外部知识检索:
- 用 Embedding 模型把"用户问题"向量化;
- 去向量库 / 文档库里查一批相似的文档片段;
- 把这些文档拼装成
context,和问题一起交给聊天模型; - 聊天模型"带着资料"回答,既能利用自身的通用能力,又能引用实时 / 私有知识。
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 流程:
-
导入文档 :
/spring-ai/rag/import_text?content=...- 把长文本切成多个 chunk;
- 对每个 chunk 做 embedding;
- 写入 PostgreSQL + pgvector。
-
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 当前时间
大致流程是:
ToolController调用toolChatClient.prompt().user(message);- ChatClient 把用户问题和
ZoomTool的描述一起传给模型; - 模型自己决定 是否调用
getTimeByZone,并根据返回结果组织自然语言回答; - 最终前端拿到的是一段已经填好时间的回答。
用一张时序图表示 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 服务器。
下面就分别从 Server 和 Client 两个视角来展开。
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 客户端,它要做的事情有两步:
- 告诉 Spring AI:我要作为 MCP Client 连接到哪个 MCP Server;
- 拿到 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:
也欢迎在 CSDN 或 GitHub 上关注我,一起把 Java + LLM 的坑踩得更平、更顺。