Skip to content

集成SpringAI ​

引入依赖 ​

在项目的父工程中的pom文件中导入springai的依赖:

xml
<!-- 依赖声明 -->
<dependencyManagement>
    <dependencies>
        <!-- SpringAI坐标,版本统一管理-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.1.8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

在使用的业务模块中的pom.xml文件中添加依赖:

xml
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

很多大模型的对接都兼容了openai的api,所以添加的是openai的依赖 , 典型的百炼大模型就已经集成了openai。

配置 ​

在application.yml文件中增加如下配置:

yml
spring:
    ai:
      # 使用openai规范连接阿里云百炼平台模型
      openai:
        base-url: ${DASHSCOPE_BASE_URL}
        api-key: ${DASHSCOPE_API_KEY}
        chat:
          options:
            model: qwen3.8-flash

使用注意

使用openai规范调用阿里云百炼平台, SpringAI会自动在base-url后面拼接/v1/chat/completions

所以base-url后面不能带 /v1 后缀

聊天对话 ​

在SpringAI中,ChatClient是核心关键点,负责与大模型交互,而得到ChatClient对象是通过ChatClient.Builder构建得到的,并且需要将ChatClient对象放入到Spring容器,方便注入使用。

所以,需要写一个配置类,来完成上述的事情。

java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Bean : 在Spring容器创建的时候会自动执行被@Bean注解标注的方法,将方法的返回值对象装入到Spring容器中
 * 方法执行过程中如果需要参数 , 会自动从Spring容器中查询匹配类型的Bean进行注入(与@Autowired规则一样)
 */
@Configuration
public class SpringAIConfig {

    @Bean
    // 注意:这里是将返回的返回值对象也就是ChatClient对象装入到Spring容器中
    public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel){
        return ChatClient.builder(openAiChatModel).build();
    }
}

代码解读:

  • ChatClient.builder:会得到一个ChatClient.Builder工厂对象,利用它可以自由选择模型、添加各种自定义配置
  • OpenAiChatModel :如果你引入了openai的starter,这里就可以自动注入OpenAiChatModel 对象。同理,Ollama也是一样的用法。

接下来,我们定义一个Controller,在其中接收用户发送的提示词,然后把提示词发送给大模型,交给大模型处理,拿到结果后返回,代码如下:

java
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/ai")
public class ChatController {

    @Autowired
    private ChatClient chatClient;

    @PostMapping(path = "/chat", produces = "text/html;charset=utf-8")
    public String chat(String prompt) {
        String content = chatClient
                .prompt()
                .user(prompt)  // 设置用户提示词
                .call()        // 调用大模型
                .content();
        return content;
    }
}

流式对话 ​

java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/ai")
public class ChatController {

    @Autowired
    private ChatClient chatClient;

    @RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
    public Flux<String> chat(String prompt) {
        return chatClient.prompt()
                .user(prompt)
                .stream()
                .content();
    }
}

System角色设定 ​

我们希望AI按照新的设定工作,就需要给它设置System背景信息。

在SpringAI中,设置System信息非常方便,不需要在每次发送时封装到Message,而是创建ChatClient时指定即可。

我们修改SpringAIConfig中的代码,给ChatClient设定默认的System信息:

java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringAIConfig {

    /**
     * 创建并返回一个ChatClient的Spring Bean实例。
     * @param openAiChatModel
     * @return
     */
    @Bean
    public ChatClient chatClient(OpenAiChatModel openAiChatModel) {
        return ChatClient
                .builder(openAiChatModel)
                .defaultSystem("您是一个优秀的智能助手,名字叫小聪。请以友好乐于助人和愉快的方式解答用户的各种问题。")
                .build();
    }
}

SpringAI对接其他模型 ​

集成Ollama ​

导入依赖

XML
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>

在application-dev.yml中集成ollama

YAML
spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: deepseek-r1:8b

核心配置类

Java
@Bean
public ChatClient ollamaChatClient(OllamaChatModel ollamaChatModel) {
    return ChatClient
            .builder(ollamaChatModel)
            .build();
}

集成DeepSeek ​

导入依赖

XML
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

在application-dev.yml中集成DeepSeek

YAML
spring:
  ai:
    deepseek:
      base-url: https://api.deepseek.com
      api-key: ${DEEPSEEK_API_KEY}
      chat:
        options:
          model: deepseek-flash

需要在电脑的环境变量中添加DEEPSEEK_API_KEY

核心配置类

Java
@Bean
public ChatClient deepSeekChatClient(DeepSeekChatModel deepSeekChatModel) {
    return ChatClient
            .builder(deepSeekChatModel)
            .build();
}

ToolCalling ​

toolcalling运行的原理如下图:

流程解读:

  1. 提前把这些操作定义为Function(SpringAI中叫Tool)
  2. 然后将Function的名称、作用、需要的参数等信息都封装为Prompt提示词与用户的提问一起发送给大模型
  3. 大模型在与用户交互的过程中,根据用户交流的内容判断是否需要调用Function
  4. 如果需要则返回Function名称、参数等信息
  5. Java解析结果,判断要执行哪个函数,代码执行Function,把结果再次封装到Prompt中发送给AI
  6. AI继续与用户交互,直到完成任务

听起来是不是挺复杂,还要解析响应结果,调用对应函数。

不过,有了SpringAI,中间这些复杂的步骤大家就都不用做了!

下面是SpringAI中ToolCalling的使用流程:

角色设定 ​

java
public class SystemConstants {

    public static final String prompt = """
            您是一个优秀的聊天助手,你的名字叫小智。请以友好、乐于助人和愉快的方式解答用户的各种问题。
            如果需要查询护理项目时,要调用find_project_tool工具          
            """;
}

引入提示词:

java
@Bean
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
    return ChatClient
            .builder(openAiChatModel)
            .defaultSystem(SystemConstants.prompt)
            .build();
}

定义Tool ​

java
import com.xhzb.nursing.domain.vo.NursingProjectVo;
import com.xhzb.nursing.service.INursingProjectService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class NursingProjectTool {

    @Autowired
    private INursingProjectService nursingProjectService;

    @Tool(name = "find_project_tool", description = "查询护理项目")
    public List<NursingProjectVo> findAllProjectVo() {
        //查询所有护理项目
        return nursingProjectService.listAll();
    }
}

注册Tool ​

java
@Configuration
public class SpringAIConfig {

    /**
     * 创建并返回一个ChatClient的Spring Bean实例。
     * @param openAiChatModel
     * @return
     */
    @Bean
    public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel, NursingProjectTool nursingProjectTool) {
        return ChatClient
                .builder(openAiChatModel)
                .defaultTools(nursingProjectTool)
                .defaultSystem(SystemConstants.prompt)
                .build();
    }
}

会话记忆和历史记录 ​

Advisors 运行原理 ​

Spring AI Advisors提供了一种灵活且强大的方式,可以在 Spring 应用中轻松拦截、调整和增强基于AI的交互操作。通过使用Advisors,可以构建更复杂、可重用且易于维护的AI组件,从而提升应用的功能性和简化开发流程,使项目更加高效和整洁。

SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能。所有的增强通知都需要实现Advisor接口。

Spring提供了一些Advisor的默认实现,来实现一些基本的增强功能:

  • SimpleLoggerAdvisor:日志记录的Advisor
  • MessageChatMemoryAdvisor:会话记忆的Advisor
  • QuestionAnswerAdvisor:实现RAG的Advisor

日志Advisor ​

首先,我们需要修改SpringAIConfig,给ChatClient添加日志Advisor:

Java
@Configuration
public class SpringAIConfig {

    /**
     * 创建并返回一个ChatClient的Spring Bean实例。
     * @param openAiChatModel
     * @return
     */
    @Bean
    public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel, NursingProjectTool nursingProjectTool) {
        return ChatClient
                .builder(openAiChatModel)
                .defaultTools(nursingProjectTool)
                .defaultSystem(SystemConstants.prompt)
                .defaultAdvisors(new SimpleLoggerAdvisor())  //添加默认的advisor 记录日志
                .build();
    }
}

接下来,我们在application-dev.yml中添加日志配置,更新日志级别:

YAML
# 日志配置
logging:
  level:
    com.xhzb: debug
    org.springframework: warn
    org.springframework.ai: debug # AI对话的日志级别

重启项目,再次聊天就能看到AI对话的日志信息了~

会话聊天记忆 ​

我们的AI聊天机器人是没有记忆功能的,上一次聊天的内容,下一次就忘掉了。

SpringAI自带了会话记忆功能,可以帮我们把历史会话保存下来,下一次请求AI时会自动拼接,非常方便。

会话记忆功能同样是基于AOP实现,Spring提供了一个MessageChatMemoryAdvisor的通知,我们可以像之前添加日志通知一样添加到ChatClient即可。

不过,要注意的是,MessageChatMemoryAdvisor需要指定一个ChatMemory实例,也就是会话历史保存的方式。

ChatMemory接口声明如下()

java
public interface ChatMemory {
    String DEFAULT_CONVERSATION_ID = "default";
    String CONVERSATION_ID = "chat_memory_conversation_id";

    default void add(String conversationId, Message message) {
        Assert.hasText(conversationId, "conversationId cannot be null or empty");
        Assert.notNull(message, "message cannot be null");
        this.add(conversationId, List.of(message));
    }

    void add(String conversationId, List<Message> messages);

    List<Message> get(String conversationId);

    void clear(String conversationId);
}

可以看到,所有的会话记忆都是与conversationId有关联的,也就是会话Id,将来不同会话id的记忆自然是分开管理的。

目前,在SpringAI中有两个ChatMemory的实现:

  • MessageWindowChatMemory:
  • RedisChatMemory:会话保存在Redis非关系数据库中

开启会话记忆和会话隔离 ​

开启基于内存的会话记忆

TypeScript
@Configuration
public class SpringAIConfig {

    /**
     * 创建并返回一个ChatClient的Spring Bean实例。
     * @param openAiChatModel
     * @return
     */
    @Bean
    public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel, 
                NursingProjectTool nursingProjectTool
                ,ChatMemory chatMemory) {
        return ChatClient
                .builder(openAiChatModel)
                .defaultTools(nursingProjectTool)
                .defaultSystem(SystemConstants.prompt)
                .defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
}

还要记得要进行会话隔离,不同会话直接的数据是相互隔离互不影响的

如何隔离呢,怎么实现会话隔离呢?做法就是修改ChatController,传递上下文的会话id

Java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/ai")
public class ChatController {

    @Autowired
    private ChatClient openAiChatClient;


    @RequestMapping(value = "/chat",produces = "text/html;charset=UTF-8")
    public Flux<String> chat(String prompt,String chatId) {

        Flux<String> content = openAiChatClient
                .prompt()
                .user(prompt)
                .advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))
                .stream()
                .content();

        return content;
    }
}

注意,这里传递chatId给Advisor的方式是通过AdvisorContext,也就是以key-value形式存入上下文:

java
chatClient...advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))

基于Redis的会话记忆 ​

由于基于内存保存会话记忆和会话历史会导致数据丢失,所以我们可以选择更好的存储方式,用上redis方式,这样既可以保证效率也能兼顾数据的可靠性。

不过,前面的方案,都面临一个问题,SpringAI中的Message类未实现Serializable接口,也没提供public的构造方法,无法实现序列化。所以一般的最佳实践是定义一个可序列化的Message类,方便后续持久化。

现在在vo包中新建一个Msg类:

java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Msg {
    MessageType messageType;
    String text;
    Map<String, Object> metadata;
    private LocalDateTime createTime;

    public Msg(Message message) {
        this.messageType = message.getMessageType();
        this.text = message.getText();
        this.metadata = message.getMetadata();
        createTime = LocalDateTime.now();
    }

    public Message toMessage() {
        return switch (messageType) {
            case SYSTEM -> new SystemMessage(text);
            case USER -> UserMessage.builder().text(text).media(List.of()).metadata(metadata).build();
            case ASSISTANT -> AssistantMessage.builder().content(text).properties(metadata).media(List.of()).build();
            default -> throw new IllegalArgumentException("Unsupported message type: " + messageType);
        };
    }
}

这个类中有两个关键方法:

  • 构造方法:实现将SpringAI的Message转为我们的Msg的功能
  • toMessage方法:实现将我们的Msg转为SpringAI的Message

自定义ChatMemory ​

java
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.xhzb.nursing.domain.vo.Msg;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 基于Redis的ChatMemory实现
 * 数据类型 : List<Msg的JSON字符串>
 * Key的规则 : chat:memory:{conversationId}
 */
@Service
public class RedisChatMemory implements ChatMemory {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String KEY_TEMPLATE = "chat:memory:%s";

    /**
     * 新增聊天消息
     *
     * @param conversationId
     * @param messages
     */
    @Override
    public void add(String conversationId, List<Message> messages) {
        // 1. 判断消息列表是否为空
        if (CollUtil.isEmpty(messages)) {
            return;
        }

        // 2. 将聊天消息转化为JSON保存到Redis中
        String key = String.format(KEY_TEMPLATE, conversationId);

        // 2.1 将消息列表中的每个Message对象 先转化为 Msg对象 再转化为JSON字符串
        List<String> msgList = messages.stream()
                .map(message -> new Msg(message))
                .map(msg -> JSONUtil.toJsonStr(msg))
                .toList();

        redisTemplate.opsForList().leftPushAll(key, msgList);
    }

    /**
     * 获取聊天消息
     *
     * @param conversationId
     * @return
     */
    @Override
    public List<Message> get(String conversationId) {
        // 再Redis中根据会话id查询会话历史
        String key = String.format(KEY_TEMPLATE, conversationId);
        // 获取会话记录列表
        List<String> msgList = redisTemplate.opsForList().range(key, 0, -1);
        // 如果为空直接返回空集合
        if (CollUtil.isEmpty(msgList)) {
            return List.of();
        }
        // 如果不为空,将数据先转化为 Msg对象  再转化为Message对象
        List<Message> messageList = msgList.stream()
                .map(str -> JSONUtil.toBean(str, Msg.class))
                .sorted((msg1, msg2) -> msg1.getCreateTime().compareTo(msg2.getCreateTime()))
                .map(msg -> msg.toMessage())
                .toList();

        return messageList;
    }

    /**
     * 清空聊天消息
     * @param conversationId
     */
    @Override
    public void clear(String conversationId) {
        String key = String.format(KEY_TEMPLATE, conversationId);
        redisTemplate.delete(key);
    }
}