章
目
录
一、引言
之前在介绍SpringAI技术现状,以及RAG、Function Call、向量数据库等相关知识时,用的还是SpringAI 1.0.0 – M3版本。最近再看SpringAI官方网站,好家伙,变化可真不小,版本都到1.0.0 – M6了。这更新速度,不得不说Spring在AI领域发力真猛!
在新版本里,有个挺让人意外的改动,Function Call被标记为废弃了。仔细研究文档才知道,现在Spring更推荐用Tool Calling。这俩功能其实差不多,就是换了套注解。用Tool Calling后,能像下面这样把它绑定到ChatClient上:
class DateTimeTools {
// 使用@Tool注解描述该方法的功能,即获取用户所在时区的当前日期和时间
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
// 返回当前日期和时间,通过获取系统当前时间,结合用户时区进行转换
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
// 注册工具到大模型上面
ChatModel chatModel =...
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(dateTimeTools)
.build();
Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
chatModel.call(prompt);
为啥要把Function Call改成Tool Call呢?我琢磨着,主要是SpringAI想支持MCP协议。MCP协议规定服务端程序得包含Prompt、Resource、Tools这三个要素,所以就把Function Call的API全改成Tool Call了。要是想了解这俩API的使用对比,可以参考https://docs.spring.io/spring-ai/reference/1.0/api/tools.html,改动规模还挺大,以后升级SpringAI大版本,估计不少研发人员又得忙着改API了。
二、MCP协议详解
MCP(Model Context Protocol),也就是模型上下文协议,它就好比AI世界里的“万能插座”。这个协议的作用是在AI模型和外部数据源、工具之间搭建沟通的桥梁,标准化应用程序给LLM提供上下文的方式,让不同的AI组件和系统能顺畅协作。这就跟Jdbc协议类似,通过制定规范,让中间件厂商去实现。
从SpringAI官网的图示能看到:大模型应用程序可以通过MCP Client访问外部资源,采用的是典型的Client – Server架构。这实现了大模型工具实现和工具调用的分离,就像有个第三方管家统一管理函数调用一样。在我看来,MCP比Function Calling更高级,是实现智能体Agent的基础。
目前MCP服务端有两种类型:标准IO和SSE模式。标准IO的服务端和大模型应用是一对一的关系,而SSE模式下,服务端能和多个客户端通信。说不定以后,服务端开发人员不用再对接前端页面,只需要对接大模型的McpClient就行,所有服务端程序都得接入MCP协议,更好地服务大模型和智能体。
三、体验MCP功能
下面通过一个简单的demo,感受下SpringAI的MCP能力。我们要让大模型通过MCP Client具备访问数据库和本地文件系统的能力,实际效果如下:
这个demo先不开发MCP服务端应用程序,用https://modelcontextprotocol.io/introduction提供的开源MCP服务端实现。考虑到很多人用的是SpringAI 1.0.0 – M5版本,这里先用这个版本演示,后面再讲讲1.0.0 – M6版本的功能,方便大家对比两个版本的API变化。
3.1 安装npx和uvx工具
npx是nodeJs下的工具,能执行Ts或JS脚本,甚至运行应用程序。uvx和它功能类似,是Python环境下执行脚本的工具。因为要用的文件服务器MCP服务端是用Ts代码写的,访问数据库的MCP服务端程序是用Python写的,所以得安装这俩工具,才能让MCP Client正常调用。简单说,TypeScript编写的MCP server用npx命令运行,Python编写的用uvx命令运行。
uvx的安装命令如下:
# 确保你已经安装了python
pip install uvx
npx一般随npm一起安装,装了npm就有npx工具。
3.2 利用大模型生成前端页面
这步不难,我用DeepSeek生成前端页面。不过有个小注意点,Prompt得好好打磨。Deepseek有识图能力,也能直接发图。记得在提示词里告诉大模型,别用Vue或React框架,因为功能简单,用不上这么重量级的框架。还可以让大模型用EventSource接收服务端数据,实现打字机效果。
3.3 配置MCP相关代码
// 配置类,用于声明MCP相关的Bean
@Configuration
public class McpConfig {
// 定义一个Bean,将McpSyncClient中的工具转换为McpFunctionCallback列表
@Bean
public List<McpFunctionCallback> functionCallbacks(List<McpSyncClient> mcpSyncClients) {
List<McpFunctionCallback> list = new ArrayList<>();
for (McpSyncClient mcpSyncClient : mcpSyncClients) {
list.addAll(mcpSyncClient.listTools(null)
.tools()
.stream()
.map(tool -> new McpFunctionCallback(mcpSyncClient, tool))
.toList());
}
return list;
}
// 定义一个Bean,创建用于访问文件系统的McpSyncClient
@Bean(destroyMethod = "close")
public McpSyncClient mcpFileSysClient() {
// 配置文件系统服务端的参数,注意路径要改成自己的真实路径
var stdioParams = ServerParameters.builder("D:\\software\\nodeJs\\npx.cmd")
.args("-y", "@modelcontextprotocol/server-filesystem", "D:\\工作日志")
.build();
// 创建McpSyncClient,设置请求超时时间为10秒
var mcpClient = McpClient.using(new StdioClientTransport(stdioParams))
.requestTimeout(Duration.ofSeconds(10)).sync();
// 初始化McpSyncClient,并打印初始化结果
var init = mcpClient.initialize();
System.out.println("mcpFileSysClient loading init=" + init);
return mcpClient;
}
// 定义一个Bean,创建用于访问数据库的McpSyncClient
@Bean(destroyMethod = "close")
public McpSyncClient mcpDbClient() {
// 配置数据库服务端的参数,注意路径要改成自己的真实路径
var stdioParams = ServerParameters.builder("D:\\Program Files\\python3.12.3\\Scripts\\uvx.exe")
.args("mcp-server-sqlite", "--db-path", "D:\\work-space-study\\spring-ai-mcp-demo\\mcp-client\\src\\main\\resources\\test.db")
.build();
// 创建McpSyncClient,设置请求超时时间为10秒
var mcpClient = McpClient.using(new StdioClientTransport(stdioParams))
.requestTimeout(Duration.ofSeconds(10)).sync();
// 初始化McpSyncClient,并打印初始化结果
var init = mcpClient.initialize();
System.out.println("mcpDbClient loading init=" + init);
return mcpClient;
}
}
这段代码声明了两个用标准IO方式连接MCP服务端的McpClient,用@Bean(destroyMethod = "close")
让Spring容器关闭时释放客户端资源。最后把两个服务端的tools转成List集合,供后续定义ChatClient使用。
3.4 集成大模型客户端
// 构造函数,用于创建ChatClient并注入相关依赖
public FileSysController(ChatClient.Builder chatClientBuilder,
List<McpFunctionCallback> functionCallbacks) {
this.chatClient = chatClientBuilder
// 将之前定义的functionCallbacks注入到ChatClient中
.defaultFunctions(functionCallbacks.toArray(new McpFunctionCallback[0]))
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
.defaultOptions(DashScopeChatOptions.builder().withTopP(0.7).build())
.build();
}
这里创建ChatClient,利用Spring的依赖注入,把前面McpConfig
类中声明的functionCallbacks
注入进去。这样,大模型ChatClient就有调用MCP服务端的能力了。用大模型生成的前端页面输入问题,访问下面的Controller接口就能测试。
// 处理前端请求,生成聊天响应流
@RequestMapping(value = "/generate", method = RequestMethod.GET)
public Flux<ChatResponse> generateStream(HttpServletResponse response, @RequestParam("id") String id, @RequestParam("prompt") String prompt) {
// 设置响应编码为UTF-8
response.setCharacterEncoding("UTF-8");
// 创建消息聊天记忆顾问,用于管理聊天记忆
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
// 调用ChatClient的prompt方法,生成聊天响应流
return this.chatClient.prompt(prompt)
.advisors(messageChatMemoryAdvisor).stream().chatResponse();
}
四、基于SpringAI开发服务端应用程序
前面用的是开源的MCP服务端,那怎么用Java自己开发一个MCP服务端程序呢?下面讲讲主要步骤。注意,得把SpringAI升级到1.0.0 – M6版本,低版本对MCP服务端调用支持不太好。
4.1 开发MCP – Server
开发MCP – server要引入SpringAI的依赖,具体选哪种,得看用什么通信方式:
<!--标准IO通信类型的MCP服务端,适合命令行形式的桌面工具,例如之前的案例中的文件助手 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>
<!--基于Http协议通信类型的MCP服务端 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
</dependency>
<!--基于SSE通信类型的MCP服务端 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
</dependency>
一个标准的MCP服务端程序包含Tools、Prompts、Resources这三个主要部分:
- 资源(Resources):就是AI能读取的数据,像文件内容、数据库查询结果、API响应这些。比如说,AI可以通过资源获取你的日历事件列表。
- 工具(Tools):是AI能调用的函数,用来执行特定操作,像添加新任务、发邮件之类的。使用工具一般得用户先批准,保证安全。
- 提示词(Prompts):是服务器给AI的预写消息或模板,帮助AI明白怎么用资源和工具。比如服务器可能告诉AI:“你可以添加任务,试试说‘添加任务:买牛奶’”,这样用户就能更轻松地完成任务。虽然提示词是给AI的,但实际上是间接帮用户的,AI会根据提示词告诉用户怎么操作。
在SpringAI中,这三个要素对应的API代码如下:
// 配置类,用于配置MCP服务端相关的Bean
@Configuration
@EnableWebMvc
public class McpServerConfig implements WebMvcConfigurer {
// 定义一个Bean,注册工具
@Bean
public ToolCallbackProvider bookServiceTools(BookService bookService) {
// 通过MethodToolCallbackProvider.builder()方法构建工具回调提供者,将BookService注册为工具
return MethodToolCallbackProvider.builder().toolObjects(bookService).build();
}
// 定义一个Bean,注册资源
@Bean
public List<McpServerFeatures.SyncResourceRegistration> resourceRegistrations() {
// 这里返回一个包含资源注册的List,实际使用时需要根据具体情况添加资源注册
return List.of(resourceRegistration);
}
// 定义一个Bean,注册Prompt
@Bean
public List<McpServerFeatures.SyncPromptRegistration> promptRegistrations() {
// 这里返回一个包含提示词注册的List,实际使用时需要根据具体情况添加提示词注册
return List.of(promptRegistration);
}
}
这里的BookService
可以是自己写的服务,比如实现查询数据库、调用其他微服务等功能。
最后,还得在application.yaml
里配置服务暴露:
# Using spring-ai-mcp-server-webmvc-spring-boot-starter
spring:
ai:
mcp:
server:
name: webmvc-mcp-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/messages
到这儿,服务端基本就开发好了。
4.2 开发MCP – Client
开发好MCP – server后,怎么让大模型调用它呢?和SpringAI 1.0.0 – M5版本比,1.0.0 – M6版本的McpClient集成方式变化很大,配置更简单了。
首先在application.yaml
里做如下配置:
server:
port: 9999
spring:
ai:
mcp:
client:
enabled: true
name: call-mcp-server
sse:
connections:
server1:
# 这里是你的Mcp服务端的暴露地址
url: http://127.0.0.1:8080
dashscope:
api-key: {通义千问API-Key}
接着在pom
文件里引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
最后就可以直接用了,代码如下:
// 定义一个RestController,处理与聊天相关的请求
@RestController
@RequestMapping("/dashscope/chat-client")
public class ChatController {
// 注入ChatClient
private final ChatClient chatClient;
// 定义聊天记忆,这里使用InMemoryChatMemory
private final ChatMemory chatMemory = new InMemoryChatMemory();
// 构造函数,通过依赖注入获取ChatClient.Builder、McpSyncClients和ToolCallbackProvider
public ChatController(ChatClient.Builder chatClientBuilder,
List<McpSyncClient> mcpSyncClients,
ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
// 使用defaultTools方法注入工具,与之前版本API不同
.defaultTools(tools)
.defaultOptions(DashScopeChatOptions.builder().withTopP(0.7).build())
.build();
}
// 处理前端请求,生成聊天响应流
@RequestMapping(value = "/generate_stream", method = RequestMethod.GET)
public Flux<ChatResponse> generateStream(HttpServletResponse response, @RequestParam("id") String id, @RequestParam("prompt") String prompt) {
// 设置响应编码为UTF-8
response.setCharacterEncoding("UTF-8");
// 创建消息聊天记忆顾问,用于管理聊天记忆
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
// 调用ChatClient的prompt方法,生成聊天响应流
return this.chatClient.prompt(prompt)
.advisors(messageChatMemoryAdvisor)
.stream()
.chatResponse();
}
}
对比1.0.0 – M5版本的客户端实现,现在不用自己定义McpClient了,引入spring - ai - mcp - client - spring - boot - starter
后,会自动识别配置的mcpClient信息并自动装配。而且创建ChatClient用的API也从defaultFunctions
变成了defaultTools
。
要是想配置超时时间,可以实现SpringAI提供的McpSyncClientCustomizer
接口,在里面写定制逻辑就行。
最后,先启动Mcp – Server,再启动Mcp – client应用程序,就可以开始提问了。实际效果如下:
在上图中大模型通过了McpClient调用了我在Mcp-Server提供了BookService,然后按照名字查询到了对应的书籍,这些书籍信息是我在代码中预制的信息,大模型输出这个信息也很好的证明了确实调用到了我们的Mcp服务端接口。