基于Spring AI 的智能聊天机器人

目录

[1. 项目介绍](#1. 项目介绍)

[1.1 项目背景和目标](#1.1 项目背景和目标)

[1.2 核心功能](#1.2 核心功能)

[2. 界面设计](#2. 界面设计)

[3. 搭建环境](#3. 搭建环境)

[3.1 创建项目](#3.1 创建项目)

[3.2 添加依赖&写启动类](#3.2 添加依赖&写启动类)

[3.3 添加配置文件](#3.3 添加配置文件)

[4. 简单对话](#4. 简单对话)

[4.1 定义接口](#4.1 定义接口)

[4.2 代码实现](#4.2 代码实现)

[5. 对话记忆](#5. 对话记忆)

[5.1 Chat Memory](#5.1 Chat Memory)

[6. 会话历史](#6. 会话历史)

[6.1 定义会话列表](#6.1 定义会话列表)

[6.2 获取会话列表](#6.2 获取会话列表)

[6.3 获取会话记录](#6.3 获取会话记录)

[6.4 删除会话记录](#6.4 删除会话记录)


代码:spring-ai-first-term-practice/spring-ai-chat-bot · 巨蛇神/JavaEE进阶 - 码云 - 开源中国

1. 项目介绍

1.1 项目背景和目标

**时代背景:**现在大模型能力飞速提高,使用范围也逐渐扩大,为了跟上时代,必须要体验一把运用AI大模型的感觉,感受它的强大并提升自己的能力。此刻心想 "DeekSeek比较便宜,性能也出色,就选他了!"

技术背景: 在Spring AI 中引入的模型 默认 不具备上下文,因此同一个对话下你与AI交互时AI无法回答这样的问题,第一次输入:"我有100元,现在花了10元,还剩多少元?" 输出:"剩90元";第二次输入:"我又花了10元,还剩多少元?" 输出:"你好,我不知道你有多少钱,你可以先告诉我"。为了具备上下文,开发者可以添加上下文相关的advisor,或使用数据库保存每次输入输出信息。

产品目标:

  • 提供流畅,自然的对话体验
  • 支持多轮对话及上下文理解
  • 记录和管理用户历史对话

1.2 核心功能

1. 对话

支持用户与机器人流畅对话。

实时响应用户输入,流式输出自然语言。

2. 多轮对话

能够理解和处理多轮对话

支持基于上下文的智能应答

3. 历史记录

自动保存用户与机器人的对话历史

支持 添加新对话、查看历史和删除历史对话

2. 界面设计

该项目只关注后端的智能体构建,前端代码在:巨蛇神/JavaEE进阶 - 码云 - 开源中国

3. 搭建环境

3.1 创建项目

创建项目:spring-ai-chat-bot

3.2 添加依赖&写启动类

添加pom依赖:

复制代码
<!--  加入spring boot  -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.1.1</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

添加bom:

复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>2.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

启动类:

3.3 添加配置文件

复制代码
server:
  port: 8080
spring:
  application:
    name: spring-ai-chat-bot
  ai:
    openai:
      api-key: ${DEEPSEEK_API_KEY_SPRING_AI} #DeepSeek api-key
      base-url: https://api.deepseek.com 
      chat:
        options:
          model: deepseek-flash #目前代表的是DeepSeek-V4.1-Flash
          temperature: 0.7 #温度越高,分子越容易发散——》AI生成的内容越不固定(后一次生成的内容和前一次不大相同)
logging:
  level:
    org.springframework.ai.chat.client.advisor: debug
  pattern:
    console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

4. 简单对话

4.1 定义接口

请求

GET/POST /chat/stream

参数

prompt(String):用 户输入的消息内容

响应

Flux 流式返回机器人回复的内容 (text/html;charset=utf-8)

4.2 代码实现

1.配置ChatClient:

复制代码
@Configuration
public class CommonConfiguration {
    @Bean
    public ChatClient chatClient(ChatModel model){
        return ChatClient.builder(model)
                .defaultSystem("你叫小蛇,是学生在学习英语的顶级魔王护,英语能力超雅思9分的高级学习助理")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

2.流式返回:

复制代码
@RequestMapping("/chat")
@RestController
public class ChatController {
    private final ChatClient chatClient;

    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

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

直到这里,已经可以启动后端进行人机交互了。但是你会发现AI没有记忆!在同一会话中进行多轮对话,它会出现如下情况:

默认情况下大模型是没有记忆的,但是Spring AI 官方提供了实现对话记忆的接口,下面介绍。

5. 对话记忆

让AI记住上下文,就要给它每次人机交互的信息。Spring AI 官方定义了三种角色消息类型。

Spring AI角色消息类型

在对话系统,尤其是大语言模型应用中,SystemMessage、UserMessage和AssistantMessage 是三种核心角色消息类型,用于构建上下文感知的对话框架。

  • SystemMessage:系统消息,用于设定对话背景、角色、行为准则等,在系统启动前设置。
  • UserMessage:用户消息,用户输入的文本内容。
  • AssistantMessage:助理消息,模型生成返回的信息。

例:

复制代码
SystemMessage:"你叫小蛇,是学生在学习英语的顶级魔王护,英语能力超雅思9分的高级学习助理"
UserMessage:"你是谁"                                   
AssistantMessage:"我是小蛇,你的英语学习“顶级魔王护”,也是你的高级学习助理!"

5.1 Chat Memory

大语言模型是无状态的,也就是说他不会记得以前的数据。为了解决这个问题,Spring AI 官方提供了对话内存功能,定义了ChatMemory接口,允许和方便 开发者创建、维护上下文。Spring AI内置了InMemoryChatMemoryRepository,它是spring ai默认提供的bean,它把记忆存储到内存中,重启服务之前的记忆就没了。

复制代码
ChatMemory接口的抽象函数:

|------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| void add(String conversationId, List<Message> messages); | 第一个 参数是会话ID,每个会话ID都要是独一无二的(常用UUID),不传会抛 IllegalArgumentException ;第二个参数是人机交互产生的信息,同一个会话中的信息在存储时带有的相同的ID。 |
| List<Message> get(String conversationId); | 根据ID返回会话所有信息,再由 MessageWindowChatMemoryzhe这类实现类在内部根据窗口大小获取信息和处理信息 |
| void clear(String conversationId); | 清空指定会话存储 |

实现上下文:

InMemoryChatMemoryRepository 不能单独使用,要配合 MessageWindowChatMemory

第一步修改接口:

请求

GET/POST /chat/stream

参数

prompt(String): 用户输入 的消息内容
chatId (String) : 会话标识 ID, 由前端生成 , 不重复 , 新建会话时 , 创建新的 chatID

响应

Flux 流式返回的机器人回复内容 (text/html;charset=utf-8)

java 复制代码
    @RequestMapping(value = "/stream",produces = "text/html;charset=utf-8")
    public Flux<String> stream(String prompt,String chatId){ //前后端生成UUID都可以,这里前端生成并返回UUID
        return chatClient.prompt()
                .user(prompt)
                .advisors(spec-> spec.param(ChatMemory.CONVERSATION_ID,chatId))//必须设置
                .stream()
                .content();
    }

第二步加入advisors:

**方式一.**若手动创建InMemoryChatMemoryRepository bean,InMemoryChatMemoryRepository就要与MessageWindowChatMemory 搭配才能能添加到advisors里。如下图"第一步"可知,它俩搭配后转化为ChatMemory类型的数据。


方式二. 对于上面的手动创建或许有人在Spring AI官网:Chat Memory :: Spring AI Reference 看到下面这张图的写法也正常,这和上面一样引入的都是默认提供的 InM...Repository ,只不过隐式完成了"第一步"。


方式三. spring DI 自动注入ChatMemory,开发者不需要手动"第一步",spring已经隐式帮我们完成了。DI如下:

6. 会话历史

后端使用ChatMemory维护了一段上下文,人机可进行多轮有记忆对话。现在为前端实现会话列表,下图是前端实现的历史会话列表,一刷新就没了。


6.1 定义会话列表

总目标:1. 后端需实现 历史会话列表,前端接收后端信息,展示列表数据,点进会话看见以前的交互信息**;2.** 每个会话显示最后一轮用户发送的信息**;3.**当从历史会话列表删除会话记录,同时删除会话内的信息。

功能列表:

  1. 历史会话列表
  2. 根据会话ID,返回历史信息
  3. 删除历史会话

第一步:模仿ChatMemory,写一个包含add、get和clear方法的ChatHistoryRepository接口。

java 复制代码
public interface ChatHistoryRepository {
    void add(String chatId,String title);
    List<ChatInfo> get();
    void clear(String chatId);
}

第二步:实现实体类,定义会话列表信息

第三步:MemoryChatHistoryRepository实现ChatHistoryRepository接口

java 复制代码
@Repository
public class MemoryChatHistoryRepository implements ChatHistoryRepository{
    Map<String,String> chatInfos = new LinkedHashMap<>(); //有顺序的Map,返回给前端时元素的顺序与用户创建会话的顺序一致

    /**
     * 保存会话记录
     * 如果首次创建会话,则新增数据
     * 若会话已存在,则修改数据
     */
    @Override
    public void add(String chatId, String title) {
        chatInfos.put(chatId,title);
    }
    /**
     *返回会话列表
     */
    @Override
    public List<ChatInfo> get() {
        return chatInfos.entrySet().stream()
                //转化成ChatInfo类型
                .map(entry->new ChatInfo(entry.getKey(), entry.getValue()))
                .collect(Collectors.toList());
    }
    /**
     * 根据会话ID删除会话
     */
    @Override
    public void clear(String chatId) {
        chatInfos.remove(chatId);
    }
}

第四步:修改ChatController

复制代码
@Autowired
private ChatHistoryRepository memoryChatHistoryRepository;
复制代码
@RequestMapping(value = "/stream",produces = "text/html;charset=utf-8")
public Flux<String> stream(String prompt,String chatId){
    log.info("chatId:{},prompt:{}",chatId,prompt);
    //加入会话列表
    memoryChatHistoryRepository.add(chatId,prompt);

    return chatClient.prompt()
            .user(prompt)
            .advisors(spec-> spec.param(ChatMemory.CONVERSATION_ID,chatId))
            .stream()
            .content();
}

6.2 获取会话列表

请求

GET/POST /chat/getChatIds

参数

响应

会话 ID 列表 List<ChatInfo>

java 复制代码
    //获取会话列表
    @RequestMapping("/getChatIds")
    public List<ChatInfo> getChatIds(){
        return memoryChatHistoryRepository.get();
    }

6.3 获取会话记录

因为消息有四种类型:USER、SYSTEMASSISTANTTOOL ,后端为了区分信息属于角色,需要实现实体类:MessageVO

第一步:实现MessageVO

java 复制代码
@Data
public class MessageVO {
    private String role;
    private String content;
    //判断角色
    public MessageVO(Message message) {
        switch (message.getMessageType()){
            case USER ->{this.role = "user"; break;}
            case ASSISTANT ->{this.role = "assistant"; break;}
            case SYSTEM ->{this.role = "system"; break;}
            case TOOL ->{this.role = "tool"; break;}
        }
        this.content=message.getText();
    }

第二步:调用ChatMemory.get,前端接收

根据会话Id从ChatMemory获取会话信息记录

请求

GET/POST /chat/getChatHistory

参数

chatId (String) : 会话标识 ID

响应

会话历史记录 List<MessageVO>

java 复制代码
    //获取会话记录
    @RequestMapping("/getChatHistory")
    public List<MessageVO> getChatHistory(String chatId){
        log.info("获取会话记录,chatId:{}",chatId);
        List<Message> messages = chatMemory.get(chatId);
        //List<Message>转化成List<MessageVO>
        return messages.stream().map(MessageVO::new).collect(Collectors.toList());
    }

6.4 删除会话记录

用户在前端删除会话列表记录时返回会话Id,后端根据会话id从会话列表中删除和对应Id的所有交互信息。

请求

GET/POST /chat/deleteChat

参数

chatId (String) : 会话标识 ID

响应

true/false

java 复制代码
    @RequestMapping("/deleteChat")
    public Boolean deleteChat(String chatId){
        log.error("删除会话记录,chatId:{}",chatId);
        try {
            memoryChatHistoryRepository.clear(chatId);
            chatMemory.clear(chatId);
        }catch(Exception e){
            log.error("会话记录删除失败,chatId:{}",chatId);
            return false;
        }
        return true;
    }
相关推荐
修炼室1 小时前
Java开发工程师笔试经验贴【高频知识】
java
木井巳1 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠2 小时前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
伊信2 小时前
Spring AI 基础学习与应用
java·后端
夜郎king2 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取
Anastasiozzzz2 小时前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai
careathers2 小时前
【数据结构】队列
java·数据结构
步行cgn2 小时前
Spring 通过 factory-method 实例化 Bean 详解与常见错误排查
java·后端
lv__pf2 小时前
seata【实战 msb】
java·开发语言·后端