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"
   ]
 }
相关推荐
canonical_entropy3 小时前
最小变更成本 vs 最小信息表达:第一性原理的比较
后端
渣哥3 小时前
代理选错,性能和功能全翻车!Spring AOP 的默认技术别再搞混
javascript·后端·面试
间彧4 小时前
Java泛型详解与项目实战
后端
间彧4 小时前
PECS原则在Java集合框架中的具体实现有哪些?举例说明
后端
间彧4 小时前
Java 泛型擦除详解和项目实战
后端
间彧4 小时前
在自定义泛型类时,如何正确应用PECS原则来设计API?
后端
间彧4 小时前
能否详细解释PECS原则及其在项目中的实际应用场景?
后端
武子康4 小时前
大数据-132 Flink SQL 实战入门 | 3 分钟跑通 Table API + SQL 含 toChangelogStream 新写法
大数据·后端·flink
李辰洋4 小时前
go tools安装
开发语言·后端·golang