java 有了Spring AI的扶持下

前言

随着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:对应的请求路径后缀
  1. 例如我们需要配置一个通义千问的配置如下所示:
yaml 复制代码
spring:          
  ai:
    openai:
      base-url: https://dashscope.aliyuncs.com/compatible-mode
      api-key: sk-xxxxxxxxxxxxxxx
      chat:
        options:
          model: qwen-plus
  1. 例如我们要配置一个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流式返回

通过获取到OpenAiChatModelbean(封装了与 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

  1. 通过builder去构造实现获取ChatClient.builder(chatModel).build()
  2. 通过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": "闭包是指一个函数记住了它外部变量的环境......"  } ]

构建的请求中的输入和输出

  1. 通过produces = MediaType.TEXT_EVENT_STREAM_VALUE来表示返回内容类型为 text/event-stream,用于支持 SSE(Server-Sent Events) ,实现服务器向浏览器持续推送数据

  2. 返回类型是 Flux<String> ,通过Reactor 响应式编程库,来支持异步、非阻塞、流式传输

  3. 设置了.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);
  1. .text(...):定义提示词模板,使用 {actor} 占位符。
  2. .param("actor", "Tom Hanks"):填充参数 → 最终提示变为:"Generate the filmography of 5 movies for Tom Hanks."
  3. .call():同步调用 LLM(如 GPT)
  4. .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(即工具)交互来扩展其能力,其工具具体的应用场景为:信息的检索(对外部信息进行一个搜寻和获取,来扩展模型的现有知识)和执行操作(邮件发送/数据库操作/触发任务等)

  1. 对于方法的限制,可以通过使用springAi封装的@Tool注解,注解包含如下几个参数
  • name: 工具名称。若不指定,默认使用方法名称。AI 模型通过此名称识别调用工具,因此不允许在同一类中存在同名工具
  • description:工具描述,用于指导模型判断何时及如何调用该工具。若未指定,默认使用方法名称作为工具描述。但强烈建议提供详细描述,因为这对模型理解工具用途和使用方式至关重要。若描述不充分,可能导致模型在该调用工具时未调用,或错误调用工具
  • returnDirect:控制工具结果直接返回客户端(true)还是传回模型(false)。
  • resultConverter:用于将工具调用结果转换为字符串对象的 ToolCallResultConverter 实现,该字符串将返回至 AI 模型。
  1. 对于参数的限制,可以通过@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 客户端请求的超时时长, 默认20s
  • type: 客户端类型,(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"
      ]
    }
  }
}

此时,我用pythonuv搭建的虚拟环境可以运行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 aiservermcp工具的实现,此时我们应该如何暴露对外使用呢?还是通过客户端clientspring ai 的client / 第三方的工具)去连接。

此时我们使用spring ai client的形式去连接我们的服务, 可以通过使用STDIO形式,或者可以通过SSEurl形式

  1. 我们先用SSE的形式,将我们的serverspringboot的服务形式启动起来,例如设定端口8000 ,url:http://localhost:8000 ,此时我们就可以通过SSE的形式来连接
yaml 复制代码
spring:
  ai:
    mcp:
      client:
        sse:
          connections:
            mcp-server-test:
              url: http://localhost:8000
  1. 如果我们是以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"
   ]
 }
相关推荐
東雪木14 分钟前
Spring Boot 2.x 集成 Knife4j (OpenAPI 3) 完整操作指南
java·spring boot·后端·swagger·knife4j·java异常处理
天使街23号25 分钟前
go-dongle v1.2.0 发布,新增 SM2 非对称椭圆曲线加密算法支持
开发语言·后端·golang
用户69371750013841 小时前
Kotlin 协程基础入门系列:从概念到实战
android·后端·kotlin
Moonbit2 小时前
MoonBit Pearls Vol.14:哈希表避坑指南
后端·算法·编程语言
Moonbit2 小时前
MoonBit Pearls Vol.13: 使用 MoonBit 开发一个 HTTP 文件服务器
服务器·后端·http
一 乐2 小时前
个人博客|博客app|基于Springboot+微信小程序的个人博客app系统设计与实现(源码+数据库+文档)
java·前端·数据库·spring boot·后端·小程序·论文
LucianaiB2 小时前
Qoder 降价,立即生效!首购 2 美金/月
后端
微学网络3 小时前
基于 PVE 8.1 的 CentOS / Ubuntu / Docker / Kubernetes 部署手册
后端
Main121383 小时前
JDK 8 Stream API 教程文档
后端
火山引擎开发者社区3 小时前
Vibe Coze-企业 AI 应用赛道开启
后端