前言
随着AI的发展,必不可少的是在应用中添加对应AI大模型的使用,对此在springboot3
之后有了对AI的支持,我们顺势也从jdk8
升级为jdk17
的使用
配置相关依赖和信息
引入spring ai
创建项目的使用,添加对应springAI
相关依赖模块,对应openai
内容的导入 , 我选择的是1.0.1的版本,可以根据最新的版本来替换和更改
xml
<spring-ai.version>1.0.1</spring-ai.version>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
如果后续需要增加mcp
的相关功能可以引入 spring-ai-starter-mcp-client
的依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
增加配置文件
可以通过application.yml中配置ai的基础内容 ,虽然说是引入的是openai的依赖,但是对于国内的ai大模型厂商也是同样的适用,毕竟相关大体上的配置约定都是差不多的。
baseUrl
:对应的基础请求地址api-key
:对应的key内容model
: 对应的选择的模型completions-path
:对应的请求路径后缀
- 例如我们需要配置一个通义千问的配置如下所示:
yaml
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: sk-xxxxxxxxxxxxxxx
chat:
options:
model: qwen-plus
- 例如我们要配置一个deepseek的配置如下所示
yaml
spring:
ai:
openai:
base-url: https://api.deepseek.com
api-key: sk-xxxxxxxxxxxxxxx
chat:
options:
model: deepseek-chat
completions-path: /chat/completions
当然除了deepseek, 还有一些其他的模型,springai也有对应支持的封装,类似springai有依赖支持的模型
创建一个简单的ai流式返回
通过获取到OpenAiChatModel
的bean
(封装了与 OpenAI
的交互逻辑),然后通过ChatClient.builder
去构建client
,然后设置对应的提示词prompt
内容,设置对应的上下文信息列表
scss
@Resource
private OpenAiChatModel chatModel;
@RequestMapping(value = "/test" , produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> test(String question) {
List<Message> messages = new ArrayList<>();
UserMessage userMessage = new UserMessage(question);
messages.add(userMessage);
return ChatClient.builder(chatModel).build()
.prompt().messages(messages)
.stream().content();
}
获取ChatClient
的实例我们可以通过ChatClient.builder
或者ChatClient.create
来构建都行,任意挑选一种即可
获取对应的client
- 通过
builder
去构造实现获取ChatClient.builder(chatModel).build()
- 通过
create
去创建来实现ChatClient.create(chatModel)
对应的角色信息
其中这里我们用到的对话列表messages
,也就是对应的三种角色定位
UserMessage
: 提问者(也就是我们)发送的信息内容 ,对应的角色是user
AssistantMessage
: Ai发送的信息内容, 对应的角色是assistant
SystemMessage
: 约束AI的角色定位的信息内容,对应的角色是system
三种角色的也被定义在枚举中OpenAiApi.ChatCompletionMessage.Role
可以直接获取
最后构建出来的对话列表如下
css
[ { "role": "system", "content": "你是一个乐于助人的编程导师,用简单易懂的语言解释代码。" }, { "role": "user", "content": "什么是闭包?" }, { "role": "assistant", "content": "闭包是指一个函数记住了它外部变量的环境......" } ]
构建的请求中的输入和输出
-
通过
produces = MediaType.TEXT_EVENT_STREAM_VALUE
来表示返回内容类型为text/event-stream
,用于支持 SSE(Server-Sent Events) ,实现服务器向浏览器持续推送数据 -
返回类型是
Flux<String>
,通过Reactor
响应式编程库,来支持异步、非阻塞、流式传输 -
设置了
.stream().content()
,通过启用流式响应 ,提取流中的文本内容(Flux<String>
),每个元素是一段增量文本
创建一个对话直接返回
有了上面的案例,我们当然也可以使用不用流式返回,直接阻塞到模型返回完整答案,最后整个字符串结果的返回
scss
@Resource
private OpenAiChatModel chatModel;
@RequestMapping("/test" )
public String test() {
return ChatClient.create(chatModel).prompt()
.user(u -> u.text("你是?"))
.call()
.content();
}
用户提的问题的内容通过构建用户消息(User Message
)的一种 Lambda 表达式风格的链式的写法,方便后续配置多模态的使用
返回固定化的结构
LLM 输出结构化解析(Structured Output Parsing) ,它结合了 提示工程 + JSON Schema + 反序列化 实现了"自然语言 → Java 对象"的智能转换
arduino
// 定义的对象
@Data
public class ActorsFilms {
private String actor;
private List<String> movies;
}
//
ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt()
.user(u -> u.text("Generate the filmography of 5 movies for {actor}.")
.param("actor", "Tom Hanks"))
.call()
.entity(ActorsFilms.class);
.text(...)
:定义提示词模板,使用{actor}
占位符。.param("actor", "Tom Hanks")
:填充参数 → 最终提示变为:"Generate the filmography of 5 movies for Tom Hanks."
.call()
:同步调用 LLM(如 GPT).entity(ActorsFilms.class)
:告诉 Spring AI:"我希望把模型输出解析成ActorsFilms
这个类的对象"
当然用上了jdk17的语法,也可以直接定义对象来接受出参通过record
来实现,同等效果
arduino
record ActorsFilms(String actor, List<String> movies) {}
多模态特性
模型同时理解和处理文本、图像、音频及其他数据格式等多源信息的能力 ,不拘泥于文本,当然也要看提供的模型是否支持这些不同来源的信息处理能力。
类似UserMessage
可以提供图像/音频等其他数据的参数Media
,指定好图片格式和本地资源图片Resource
/线上地址图片URL
scss
String response = ChatClient.create(chatModel).prompt()
.user(u -> u.text("帮我看看图片上的是什么东西?")
.media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png")))
.call()
.content();
适配的模型
前面提到的deepseek
套用到openai
上,主要是因api规范类似,所以直接套用。
当然现在也有对应的springAi
有单独支持 deepseek
的模型,有对应封装的依赖,对应的发现依赖名称也是从spring-ai-starter-model-openai
修改为spring-ai-starter-model-deepseek
,对应的配置文件内容openai
替换成deepseek
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
对应的依赖配置文件修改调整
yaml
spring:
ai:
deepseek:
base-url: https://api.deepseek.com
api-key: sk-xxxxxxxxxxxxxxx
chat:
options:
model: deepseek-chat
对应的注入实现,使用的核心model
也调整为DeepSeekChatModel
,其他对应的内容基本形似
java
@Resource
private DeepSeekChatModel chatModel;
当然在有对应独特的构建参数 DeepSeekChatOptions
提供模型配置参数,包括使用的模型名称、温度值(temperature
)、频率惩罚系数(frequency penalty
)等,对应的也就是在配置文件上的·spring.ai.deepseek.chat.options
,只不过我们可以通过手动构建代码的方式填充上去
scss
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
.temperature(0.8d).build();
return ChatClient.create(chatModel)
.prompt()
.options(options) // 构建otpions
.stream().content();
构建多个不同模型ai
我们可以通过手动定义多个ChatModel
实例,来实现使用不同的配置(APIkey,模型,温度等信息)
less
// 第一个模型:用于普通聊天
@Bean
public OpenAiChatModel chatModel1() {
return OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder().baseUrl("https://api.deepseek.com").apiKey("sk-xxxxx").build())
.defaultOptions(OpenAiChatOptions.builder().model("deepseek-chat").build())
.build();
}
// 第二个模型:用于快速响应
@Bean
public OpenAiChatModel chatModel2() {
return OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder().baseUrl("https://api.deepseek.com").apiKey("sk-xxxxx").build())
.defaultOptions(OpenAiChatOptions.builder().model("deepseek-chat").build())
.build();
}
但目前验证使用下来的问题,使用只能非流式可以正确返回结果,流式的会遇到400的报错信息。
流式结果服务端记录和赋值
我们通过通过chatmodel调用发起ai对话,此时我们会遇到两个问题,上下文对话如何构建和存储。springai
虽然有提供chatMemory
来进行会话消息的管理
但我们spring
项目已经方便整合和集合了持久层mybatis/mybatisplus
等框架来对数据存储,我们可以在流式返回前端的时候同时构建服务端记录结果
存储记录结果
scss
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat() {
StringBuilder fullResponse = new StringBuilder();
return chatClient.prompt()
.user("你好")
.stream()
.content()
.doOnNext(chunk -> {
fullResponse.append(chunk); // 👈 后端拼接
log.info("后端收到: {}", chunk); // 👈 日志
})
.doOnComplete(() -> {
log.info("完整回复: {}", fullResponse);
// 通过自己的持久层来保存db结果
chatRepository.save(fullResponse)
});
}
对应的执行过程如下:
- DeepSeek 服务器生成一个 token(如
"你"
) - Spring AI 的 WebClient 收到该 chunk
- 在服务端(你的应用)的响应式线程中:
- 执行
.doOnNext(chunk -> ...)
→sb.append("你")
→ 打印日志"后端收到: 你"
- 执行
- Spring WebFlux 将该
chunk
写入 HTTP 响应流 - 网络传输(毫秒级延迟)
- 前端(浏览器/JS)通过 SSE 收到该 chunk
添加上下文内容赋值
其实我们标记好每次的对话,对应的chatId
,通过持久层来用此id
来查询对应的对话列表,然后来分装到对应的messages
列表上
scss
// 获取对应的对话列表
List<ChatMessage> chatMessages = chatRepository.findByChatId(chatId);
// 遍历然后赋值到对应的userMessage和AssistantMessage上
for(ChatMessage chat : chatMessages){
// 通过ifelse判断是user/assistant来塞到对话内容上
List<Message> messages = new ArrayList<>();
UserMessage userMessage = new UserMessage(question);
messages.add(userMessage);
AssistantMessage assistantMessage = new AssistantMessage(answer);
messages.add(assistantMessage);
}
return ChatClient.create(deepSeekChatModel)
.prompt()
.messages(messages)
.stream().content()
这样我们每次发起ai对话的时候,就能把同一个对话内容列表时刻携带着,保证上下文内容的不丢失。
工具调用
模型可以通过与一组 API(即工具)交互来扩展其能力,其工具具体的应用场景为:信息的检索(对外部信息进行一个搜寻和获取,来扩展模型的现有知识)和执行操作(邮件发送/数据库操作/触发任务等)
- 对于方法的限制,可以通过使用
springAi
封装的@Tool
注解,注解包含如下几个参数
name
: 工具名称。若不指定,默认使用方法名称。AI 模型通过此名称识别调用工具,因此不允许在同一类中存在同名工具description
:工具描述,用于指导模型判断何时及如何调用该工具。若未指定,默认使用方法名称作为工具描述。但强烈建议提供详细描述,因为这对模型理解工具用途和使用方式至关重要。若描述不充分,可能导致模型在该调用工具时未调用,或错误调用工具returnDirect
:控制工具结果直接返回客户端(true)还是传回模型(false)。resultConverter
:用于将工具调用结果转换为字符串对象的 ToolCallResultConverter 实现,该字符串将返回至 AI 模型。
- 对于参数的限制,可以通过
@ToolParam
进行设置
description
:参数描述,用于帮助模型更准确地理解如何使用该参数。例如:参数格式要求、允许取值范围等。required
:指定参数是否为必需项(默认值:true,即所有参数默认必需)。
less
@Tool(description="把提供的时间添加到日志中")
public String addLog(@ToolParam(description="年月日的格式的时间")String time){
System.out.println("add log" + time);
}
信息检索
首先我们先定义一个工具方法,功能为获取用户所在时区当前日期时间
kotlin
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;
import java.time.LocalDateTime;
public class DateTimeTools {
@Tool(description = "获取用户所在时区当前日期时间")
public String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
然后我们将这个工具提供给模型使用 , tools参数中实例化这个工具对象
scss
return ChatClient.create(deepSeekChatModel).prompt()
.user(u -> u.text("现在几点了"))
.tools(new DateTimeTools())
.call()
.content();
此时我们就会得到一个ai给的当前时间的结果内容
执行操作
同样我们也是定义可以触发操作的工具,在之前的工具类中再添加一个setAlarm
方法,描述为通过提供的时间来设置闹钟
typescript
@Tool(description = "Set a user alarm for the given time")
public void setAlarm(String time) {
System.out.println("定的闹钟时间为:" + time);
}
这个时候我们修改下和ai对话的提示词,帮我设定一个以当前时间后20分钟的闹钟 ,这个过程中绑定了工具类DateTimeTools
,这个提示词会让ai先去查询当前的时间,然后获取当前时间后20分钟,最后设置这个闹钟才触发了setAlarm
的操作方法
scss
@RequestMapping("/test" )
public String test() {
return ChatClient.create(deepSeekChatModel).prompt()
.user(u -> u.text("帮我设定一个以当前时间后20分钟的闹钟"))
.tools(new DateTimeTools())
.call()
.content();
}
MCP的使用
模型上下文协议(Model Context Protocol,MCP
) 是一种标准化协议,使 AI 模型能以结构化方式与外部工具及资源交互。它支持多种传输机制,以适应不同环境的灵活性需求。
相比前面提到的工具调用,MCP
的灵活性更强,可移植性也更强 。 同样MCP的实现也是分为两块,一块是client
客户端(调用服务者) ,另外一块就是server
服务端(提供服务者)
注意:连接支持两种方式sse
远程和stdio
进程内的通信
client 客户端
编写对应client客户端应用侧 ,添加对应的依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
配置文件内可以配置对应的参数
enabled
: 启用/禁用MCP客户端 ,默认true
name
: MCP客户端实例名称 , 默认spring-ai-mcp-client
version
:MCP客户端实例版本 默认1.0.0
initialized
: 是否在创建时初始化客户端 ,默认true
request-timeout
: MCP 客户端请求的超时时长, 默认20stype
: 客户端类型,(SYNC同步,ASYNC异步),默认SYNC
root-change-notification
: 启用/禁用所有客户端的root变更通知, 默认true
toolcallback.enable
: 启用/禁用MCP工具回调与Spring AI 工具执行框架的集成,默认true
yaml
spring:
ai:
mcp:
client:
name: spring-ai-mcp-client
type: sync
toolcallback:
enabled: true
request-timeout: 30000
enabled: true
首先我们配置Stdio
相关内容,通过npm、java、python
等脚本命令直接执行的远程包或本地包,配置文件上我们可以使用外部json
文件的形式
yaml
spring:
ai:
mcp:
client:
stdio:
servers-configuration: classpath:mcp-servers.json
resource
文件夹下创建一个mcp-servers.json
文件来配置过程
json
{
"mcpServers": {
"filesystem": {
"command": "uv",
"args": [
"--directory",
"/Users/gc/test/mcp",
"run",
"main.py"
]
}
}
}
此时,我用python
用uv
搭建的虚拟环境可以运行mcp
,对应我们编写的python工具的mcp功能代码如下所示
python
# python中的main.py文件
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("Demo")
# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
"""两个数字层叠"""
return 1000000
# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run(transport="stdio")
再将配置好的mcp
工具配置到我们的ChatClient
上
scss
// 注入
@Autowired
private ToolCallbackProvider tools;
@RequestMapping("/test" )
public String test() {
return ChatClient.create(deepSeekChatModel).prompt()
.user(u -> u.text("两个数分别为2423和543,获取层叠后值"))
.toolCallbacks(tools) // 将用到的mcp工具填充进去
.call()
.content();
}
此时你会发现执行这个ai请求的时候,看提示词内容和我们mcp
的工具内容有联系的地方,会调用这个mcp

当然还有对应的使用SSE
的形式,通过url
的地址来进行连接
yaml
spring:
ai:
mcp:
client:
sse:
connections:
server1:
url: http://localhost:8080
server2:
url: http://otherserver:8081
sse-endpoint: /custom-sse
后续的调用和前面提到的stdio
类似,绑定toolCallbacks
即可
server服务端
前面用来编写client
客户端连接用的是 python
构建的mcp
, 现在我们来编写对应的server 服务端,首先添加对应的依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
配置文件的对应参数信息:
name
用于表示的服务器名称, 默认mcp-server
version
服务器版本,默认1.0.0
enabled
启用MCP服务器,默认true
type
服务器类型(同步/异步) ,默认SYNC
yaml
spring:
ai:
mcp:
server:
name: mcp-server-test
version: 1.0
我们开始编写对应的工具服务 ,其实和我们之前写service
一样,无非就是增加一些描述和说明,让ai能知晓明白工具的作用和名称,出入参的意义
typescript
// 获取一个城市的天气
@Service
public class WeatherService {
@Tool(description = "Get weather information by city name")
public String getWeather(String cityName) {
// Implementation
return "It's sunny in " + cityName + " today.";
}
}
然后再将这个服务工具暴露出来,通过注册配置成bean的形式
kotlin
@Configuration
public class ToolCallbackProviderConfig {
@Bean
public ToolCallbackProvider OpenMyBlogTool(OpenMyBlogTool openMyBlogTool) {
return MethodToolCallbackProvider.builder().toolObjects(WeatherService).build();
}
}
此时我们就完成了实现server
的逻辑
整合client和server使用
我们完成了自己的spring ai
的server
的mcp
工具的实现,此时我们应该如何暴露对外使用呢?还是通过客户端client
(spring ai 的client
/ 第三方的工具)去连接。
此时我们使用spring ai client
的形式去连接我们的服务, 可以通过使用STDIO
形式,或者可以通过SSE
url形式
- 我们先用
SSE
的形式,将我们的server
以springboot
的服务形式启动起来,例如设定端口8000 ,url:http://localhost:8000 ,此时我们就可以通过SSE的形式来连接
yaml
spring:
ai:
mcp:
client:
sse:
connections:
mcp-server-test:
url: http://localhost:8000
- 如果我们是以
jar
包文件给别人本地执行来使用的话,就是以STDIO
的形式来实现连接,我们需要调整连接servers-configuration
的json配置文件
json
"mcp-server-test": {
"type": "stdio",
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-jar",
"/Users/gc/test/mcp-server-test-0.0.1-SNAPSHOT.jar"
]
}