提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

1. 项⽬介绍

随着⼈⼯智能技术的快速发展和⼤模型开源趋势的兴起, 智能聊天机器⼈在客服、知识问答、⽣活助⼿等领域得到了⼴泛应⽤. Deepseek作为优秀的开源⼤模型框架, 为开发者提供了强⼤的基础能⼒. 当前市场上已有多个基于Deepseek的AI应⽤案例, 我们接下来模仿这些应⽤来实现⼀个智能聊天机器⼈, 提升⽤⼾交互体验.

产品⽬标:
• 提供流畅, ⾃然的对话体验
• ⽀持多轮对话及上下⽂理解
• 能够回答常⻅问题
• 记录和管理⽤⼾历史对话

核⼼功能

  1. 对话
    • ⽀持⽤⼾与机器⼈进⾏⽂本对话
    • 实时响应⽤⼾输⼊, 输出⾃然语⾔回复
  2. 多轮对话
    • 能够理解和处理多轮对话, 保持上下⽂连续性
    • ⽀持基于上下⽂的智能应答
  3. 历史记录
    • ⾃动保存⽤⼾与机器⼈的对话历史
    • ⽀持⽤⼾查看历史对话内容

界⾯设计
前端代码随资料提供

在这里插入图片描述
在这里插入图片描述
这个就是多轮会话

2. 搭建环境

创建项⽬: spring-chat-bot

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        deepseek-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-M6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
spring:
  application:
    name: spring-chat-bot
  ai:
    openai:
      api-key: sk-af290165703c47a78a451bc62d28688e
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
          temperature: 0.7
logging:
  pattern:
    console: '%d{HH:mm:ss.SSS} %c %M %L [%thread] %m%n'
    file: '%d{HH:mm:ss.SSS} %c %M %L [%thread] %m%n'
  level:
    org.springframework.ai.chat.client.advisor: debug

然后是前端的页面

前端一行都不写—》可以直接复制

3. 简单对话

在这里插入图片描述

@Configuration
public class CommonConfig {
    @Bean
    public ChatClient deepseekChatClient(ChatClient.Builder chatClientBuilder) {
        return chatClientBuilder
                .defaultSystem("你是lyx,是由ck研发的一款智能AI助手,你很擅长医学研究,请以友好的态度来回答问题")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}
@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatClient chatClient;

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

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

}

构造函数中,就是获取的deepseekChatClient的bean

在这里插入图片描述
如果是本地部署的模型—》有思考过程的

在这里插入图片描述
我们发现对话是没有记忆的

4. 对话记忆

原因这两次请求之间没有关系

"⼤模型的对话记忆"这⼀概念, 指的是模型在与⽤⼾进⾏交互式对话过程中, 能够追踪、理解并利⽤先前对话上下⽂的能⼒. 此机制使得⼤模型不仅能够响应即时的输⼊请求, 还能基于之前的交流内容能够在对话中记住先前的对话内容, 并根据这些信息进⾏后续的响应

⼤模型本⾝是不具备记忆能⼒的, 要想让⼤模型记住之前的聊天内容, 需要把之前的聊天内容与新的提⽰词⼀起发给⼤模型.以 阿⾥百炼平台-通义千问Plus 进⾏演⽰

阿⾥百炼平台-通义千问Plus

在这里插入图片描述
点击右边的模型调试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
发现返回值就在assistant中

4.1 Spring AI ⻆⾊消息类型

在对话系统 (尤其是⼤语⾔模型应⽤) 中, SystemMessage、UserMessage 和 AssistantMessage 是三种核⼼⻆⾊消息类型, ⽤于构建上下⽂感知的对话框架.

AssistantMessage 是助手消息,一般是模型返回给用户的消息

SystemMessage: 系统消息, 通常由系统设定或初始化时提供, ⽤于设定对话的背景、⻆⾊、⾏为准则等. 它不是⽤⼾或助⼿发出的, ⽽是系统层⾯的指令或上下⽂信息. 例如, 在对话开始前, 系统消息可以设定助⼿的⻆⾊:"你是⼀个乐于助⼈的助⼿, ⽤中⽂回答问题. "比如defaultSystem
• UserMessage: ⽤⼾消息, 即由⽤⼾输⼊的内容. 在对话中, ⽤⼾的问题或语句都属于此类.
• AssistantMessage: 助⼿消息, 即由助⼿ (通常是AI模型) ⽣成并返回给⽤⼾的响应.

例如, ⼀个对话如下:
SystemMessage: "你是⼀个翻译助⼿, 将⽤⼾的中⽂翻译成英⽂. "
UserMessage: "你好, 今天天⽓真好. "
AssistantMessage: “Hello, the weather is really nice today.”
当模型⽣成回复时, 它会看到整个对话历史 (包括系统消息、之前的⽤⼾消息和助⼿消息) , 从⽽⽣成连贯且符合上下⽂的回复.

在技术实现上, 不同框架或库可能会采⽤不同的命名, 但核⼼思想⼀致. 例如, 在OpenAI的API中, 消息以⻆⾊ (role) 字段区分, 包括"system"、“user”、“assistant”.
在多轮对话中, AssistantMessage 与 UserMessage 交替排列形成时序链, 使模型能通过注意⼒机制理解当前问题的前置语境 (短期记忆)

在这里插入图片描述
发现正常的模型都会把原来模型输出的内容放在assistant中

删除UserMessage 和 AssistantMessage , 发现⼤模型不具备记忆能⼒

4.2 Chat Memory

⼤型语⾔模型 (LLM) 是⽆状态的, 也就是它们不会保留有关以前交互的信息. 当开发⼈员希望在多个交互中维护上下⽂或状态时, 这可能是⼀个限制. 为了解决这个问题, Spring AI 提供了对话内存功能, 定义了ChatMemory接⼝, 允许开发⼈员在与 LLM 的多次交互中存储和检索信息.

在这里插入图片描述
这个就是Chat Memory

add的意思是,第一个参数是每一个会话的id,就是向一个会话添加一个消息

get意思就是向以前的会话id,获取消息记录
clear就是清楚会话记录

add(String conversationId, List<Message> messages)
说明: 将单条或多条对话消息 (如⽤⼾输⼊或AI回复) 添加到指定会话的记忆库中
参数:
◦ conversationId: 区分不同会话的唯⼀标识
◦ Messages: 可包含 UserMessage/AssistantMessage 等类型
. List<Message> get(String conversationId, int lastN)
说明: 根据会话标识, 获取历史消息
参数:
◦ conversationId: 会话唯⼀标识
◦ lastN 参数表⽰从指定会话中获取的最新消息数量
clear(String conversationId)
说明: 清空指定会话的记忆存储
参数:
◦ conversationId: 会话唯⼀标识

Spring AI 会⾃动配置ChatMemory , 开发⼈员可以直接在应⽤程序中使⽤这个 bean, Spring AI 提供了默认实现 InMemoryChatMemory , 不需要开发⼈员显⽰的调⽤记录每⼀轮的对话历史.
默认情况下, ChatMemory 使⽤内存来存储消息, —》服务重启的话就消失了,开发⼈员可以根据⾃⼰的需求, 去配置不同的存储库,⽐如Cassandra、JDBC 或 Neo4j.

关于Chat Memory, Spring AI 和Spring AI alibaba都有实现, 感兴趣可以参考: 聊天记忆, 对话记忆

Spring AI聊天记忆

Spring Alibaba 对话记忆

在这里插入图片描述
在这里插入图片描述

会话的id我们交给前端生成了

4.3 修改接口

在这里插入图片描述

@Configuration
public class CommonConfig {
    @Bean
    public ChatMemory chatMemory(){
        return new InMemoryChatMemory();//创建一个ChatMemory,然后把它添加给defaultAdvisors
    }
    @Bean
    public ChatClient deepseekChatClient(ChatClient.Builder chatClientBuilder,ChatMemory chatMemory) {
        return chatClientBuilder
                .defaultSystem("你是lyx,是由ck研发的一款智能AI助手,你很擅长医学研究,请以友好的态度来回答问题")
                .defaultAdvisors(new SimpleLoggerAdvisor(),new MessageChatMemoryAdvisor(chatMemory))
                .build();
    }
}
    @RequestMapping(value = "/stream",produces = "text/html;chatset=utf-8")
    public Flux<String> stream(String prompt,String chatId){//chatId是会话id,由前端生成
        return this.chatClient.prompt()
                .user(prompt)
                .advisors(spec -> spec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }

在这里插入图片描述

这样就成功了
在这里插入图片描述

5. 对话历史

在这里插入图片描述

可以查询历史消息,历史会话列表,根据会话ID,返回历史消息,然后可以删除历史会话

内存ChatMemory来存储,存储会话ID,会话标题
每个会话有很多个消息

@Data
public class ChatInfo {
    private String chatId;
    private String title;

    public ChatInfo(String chatId, String title) {
        this.chatId = chatId;
        this.title = title==null?"无标题":title.length()>=15?title.substring(0,15):title;
    }
}

在这里插入图片描述
历史会话列表,就是返回List了

内存来存储会话记录,删除会话记录,查询会话记录

public interface ChatHistoryRepository {
//    内存来存储会话记录,删除会话记录,查询会话记录
    void save(String chatId,String title);

    void clearByChatId(String chatId);//清楚历史会话

    List<ChatInfo> getChats();//返回历史会话
}

然后是实现类

还有一点就是我们的stream接口,不管是哪个会话,都是调用的这个接口,所以还要判断这次会话的id是不是最新的,还是以前已经存储过的

@Repository
public class MemoryChatHistoryRepository implements ChatHistoryRepository{
    private Map<String,String> chatInfos=new LinkedHashMap<>();

//    保存会话记录,如果是新会话,则新增,如果会话已经存在,则更新--->Map的put操作,但是Map是无序的----》LinkedHashMap
    @Override
    public void save(String chatId, String title) {
        chatInfos.put(chatId,title);
    }

    @Override
    public void clearByChatId(String chatId) {
        chatInfos.remove(chatId);
    }

    @Override
    public List<ChatInfo> getChats() {
        return chatInfos.entrySet().stream()
                .map(entry->new ChatInfo(entry.getKey(),entry.getValue()))
                .toList();
    }
}

5.1 存储会话

    @Autowired
    private ChatHistoryRepository memoryChatHistoryRepository;

    @RequestMapping(value = "/stream",produces = "text/html;chatset=utf-8")
    public Flux<String> stream(String prompt,String chatId){//chatId是会话id,由前端生成
        log.info("chatId:{},prompt:{}",chatId,prompt);
        memoryChatHistoryRepository.save(chatId,prompt);
        return this.chatClient.prompt()
                .user(prompt)
                .advisors(spec -> spec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }

5.2 获取会话列表

在这里插入图片描述

    @RequestMapping("/getChatIds")
    public List<ChatInfo> getChatIds(){
        return memoryChatHistoryRepository.getChats();
    }

5.3 获取会话记录

就是点击会话列表的时候,要返回会话记录给我

每个会话的详细记录都是在ChatMemory中存储的

直接调用ChatMemory的接口就可以了

在这里插入图片描述

我们发现ChatMemory就有一个get接口

在这里插入图片描述
Message有MessageType这个方法
在这里插入图片描述
就有这几个类型

用户消息,AI生成消息,就这两种

Message继承了Content
在这里插入图片描述
在这里插入图片描述
然后就是对于不同的消息,前端渲染的格式不同

@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();
    }

}

在这里插入图片描述

    @Autowired
    private  ChatMemory chatMemory;
//获取会话记录
    @RequestMapping("/getChatHistory")
    public List<MessageVO> getChatHistory(String chatId){
        log.info("获取会话记录,chatId:{}",chatId);
        List<Message> messages = chatMemory.get(chatId,20);//获取20条
        return messages.stream().map(MessageVO::new).toList();
    }
    

5.4 删除会话记录

会话及其消息都要删除掉

就是要从列表记录,和Chatmemory中同时删除

    @RequestMapping("/deleteChat")
    public Boolean deleteChat(String chatId){
        log.info("删除会话,chatId:{}",chatId);
        try {
            memoryChatHistoryRepository.clearByChatId(chatId);
            chatMemory.clear(chatId);
        }catch (Exception e){
            log.error("删除会话失败,chat:{}",chatId);
            return false;
        }
        return true;
    }

因为我们的数据都是存储在内存中的,所以服务重启,会话消息就会消失掉

先手动弄四个会话
刷新

在这里插入图片描述
会话记录已经存储下来了

在这里插入图片描述

会话记录都在

在这里插入图片描述
可以删除

扩展:这个对话,不区分用户,所有人的会话都在这个地方

总结

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐