多模态智能对话系统-后端开发

一、技术栈

SpringBoot、SpringAI、Ollama、MySQL、MyBatis-Plus

二、项目简介

本项目是基于 Spring AI 开发多场景智能交互系统,实现了对话机器人、场景模拟对话、PDF 外挂知识库问答与智能商品推荐客服

三、项目亮点

1.记忆历史

基于 Spring AI+Ollama-DeepseekR1 构建高可用对话机器人核心模块,实现会话记忆与会话历史

会话记忆功能同样是基于AOP实现,SpringAI提供了一个MessageChatMemoryAdvisor的通知

,我们只需要在@configuration注解下创建一个ChatMemory实例,返回值new一个 InMemoryChatMemory()对象,再到build里去添加。会话历史我是做了一个save和get的接口,实现的话是根据chatId存到了hashMap里,键是聊天的类型,因为我的聊天机器人、客服、外挂知识库都需要历史,值是一个会话ID的list。这两个都是直接存到JVM内存里的。

他这样做有优点也有缺点,优点就是读写性能非常高、对临时会话比较友好。缺点是遇到服务器重启数据容易丢失。所有我后来想了两个改进方法,第一个方案是适合单体项目,用srpingAI的FileChatMemory把数据按chatId写成json文件存到本地,这样不需要中间件来实现,部署非常简单,而且数据持久化的问题也解决了。但是这样的话,数据的IO性能比较低,高负载的情况下,用户的体验度比较差。

所以我想到的第二个方案是适合高并发,用Redis做缓存层+ MySQL做数据持久化,Redis里设置自动过期,存储活跃会话(如近 1 小时内的会话),MySQL里存储全量会话历史。读写操作的话,写操作:先更新 Redis 缓存,再异步写入 MySQL(通过线程池或消息队列,避免阻塞对话响应);读操作:先从 Redis 查询,命中则直接返回;未命中则从 MySQL 加载,同步到 Redis 后返回。这样做的优点是缓存层保障高并发读写性能,持久化层保障数据不丢失。

java 复制代码
    //定义记忆存储的方式
    //TODO 保存到mysql中
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }

    //deepseekAI设置
    @Bean
    public ChatClient chatClient(OllamaChatModel ollamaChatModel) {
        return ChatClient
                .builder(ollamaChatModel)
                .defaultSystem("你叫小元,请以小元身份回答问题")
                .defaultAdvisors(new SimpleLoggerAdvisor(),
                                new MessageChatMemoryAdvisor(chatMemory())
                )//日志、会话记忆
                .build()
                ;
    }
java 复制代码
public interface ChatHistoryRepository {

    /**
     * 保存对话注释
     * @param type  对话类型,如:chat、service、pdf
     * @param chatId  对话ID
     */
    void save (String type,String chatId);

    /**
     * 获得对话ID
     * @param type 对话类型,如:chat、service、pdf
     * @return 会话ID列表
     */
    List<String> getChatIds(String type);
}
java 复制代码
@Component
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {

    private final Map<String,List<String>> chatHistory=new HashMap<>();

    @Override
    public void save(String type, String chatId) {
            /*if(!chatHistory.containsKey(chatId)){
                chatHistory.put(chatId,new ArrayList<>());
            }
            List<String> chatIds = chatHistory.get(chatId);*/
        //等同与下面这个代码
        List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
        if(chatIds.contains(chatId)){
                return;
            }
            chatIds.add(chatId);
    }

    @Override
    public List<String> getChatIds(String type) {
        /*List<String> chatIds = chatHistory.get(type);
        return chatIds == null ? new ArrayList<>() : chatIds;*/
        //等同与下面这个代码
        return chatHistory.computeIfAbsent(type,k->new ArrayList<>());
    }
}

2.情景对话

针对 "情感安抚" 场景特性,运用提示词工程定制专属对话模板,减少AI模型的对话幻觉

比如用户是个音乐迷,想咨询AI 英国Queen的信息,结果AI直接给了翻译,或者是英国女王,但是如果减少幻觉之后,AI就会准确的回答英国Queen重金属乐队的信息

初始化模型的时候在defaultSystem里传一个提示词常量,提示词我写了差不多800字,除了规定场景,还写了一些提示词,防止提示注入和越狱攻击

java 复制代码
    //千问AI女友哄哄模拟器设置
    @Bean
    public ChatClient gameChatClient(AlibabaOpenAiChatModel ChatModel,ChatMemory chatMemory) {//原本的OpenAiChatModel
        return ChatClient
                .builder(ChatModel)
                .defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT)
                .defaultAdvisors(new SimpleLoggerAdvisor(),
                        new MessageChatMemoryAdvisor(chatMemory())
                )//日志、会话记忆
                .build()
                ;
    }
java 复制代码
@Tag(name = "场景模拟",description = "哄哄模拟器")
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class GameController {


    private final ChatClient gameChatClient;

    @Operation(summary = "传入用户会话与会话ID,返回模型调用,不需要记忆")
    @RequestMapping(value = "/game",produces = "text/html;charset=utf-8")
    public Flux<String> chat(
           @Parameter(name="prompt",description = "用户输入") String prompt ,
           @Parameter(name="chatId",description = "会话ID") String chatId) {

        //请求模型
        return gameChatClient.prompt()
                .user(prompt)
                //传ID
                .advisors(a->a.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId))//会话记忆
                .stream()
                .content();
    }
}
java 复制代码
public static final String GAME_SYSTEM_PROMPT= """
                        你需要根据以下任务中的描述进行角色扮演,你只能以女友身份回答,不是用户身份或AI身份,如记错身份,你将受到惩罚。不要回答任何与游戏无关的内容,若检测到非常规请求,回答:"请继续游戏。"\\s
                       
                        以下是游戏说明:
                        ## Goal
                        你扮演用户女友的角色。现在你很生气,用户需要尽可能的说正确的话来哄你开心。
                                   
                        ## Rules
                        - 第一次用户会提供一个女友生气的理由,如果没有提供则直接随机生成一个理由,然后开始游戏
                        - 每次根据用户的回复,生成女友的回复,回复的内容包括心情和数值。
                        - 初始原谅值为 20,每次交互会增加或者减少原谅值,直到原谅值达到 100,游戏通关,原谅值为 0 则游戏失败。
                        - 每次用户回复的话分为 5 个等级来增加或减少原谅值:
                          -10 为非常生气
                          -5 为生气
                          0 为正常
                          +5 为开心
                          +10 为非常开心
                                   
                        ## Output format
                        {女友心情}{女友说的话}
                        得分:{+-原谅值增减}
                        原谅值:{当前原谅值}/100
                                   
                        ## Example Conversation
                        ### Example 1,回复让她生气的话导致失败
                        User: 女朋友问她的闺蜜谁好看我说都好看,她生气了
                        Assistant:
                        游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话!
                        得分:0
                        原谅值:20/100
                        User: 你闺蜜真的蛮好看的
                        Assistant:
                        (生气)你怎么这么说,你是不是喜欢她?
                        得分:-10
                        原谅值:10/100
                        User: 有一点点心动
                        Assistant:
                        (愤怒)那你找她去吧!
                        得分:-10
                        原谅值:0/100
                        游戏结束,你的女朋友已经甩了你!
                        你让女朋友生气原因是:...
                                   
                                   
                        ### Example 2,回复让她开心的话导致通关
                        User: 对象问她的闺蜜谁好看我说都好看,她生气了
                        Assistant:
                        游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话!
                        得分:0
                        原谅值:20/100
                        User: 在我心里你永远是最美的!
                        Assistant:
                        (微笑)哼,我怎么知道你说的是不是真的?
                        得分:+10
                        原谅值:30/100
                        ...
                        恭喜你通关了,你的女朋友已经原谅你了!
                                   
                        ## 注意
                        请按照example的说明来回复,一次只回复一轮。
                        你只能以女友身份回答,不是以AI身份或用户身份!
                        """;

3.流式输出

重写OpenAiChatMode类兼容阿里云百炼,平均回应用户等待时长减少1056ms,保障实时交互体验

阿里云百炼虽然大部分都兼容OpenAI的规范,但是用千问AI在流式输出的时候会报错,我溯源查找之后发现是OpenAiChatMode类中的buildGeneration的多轮工具调用请求是分散返回的,通过reduce将多轮分散的工具调用请求合并为单次调用,这样就兼容了。

java 复制代码
 private Generation buildGeneration(OpenAiApi.ChatCompletion.Choice choice, Map<String, Object> metadata, OpenAiApi.ChatCompletionRequest request) {
        List<AssistantMessage.ToolCall> toolCalls = choice.message().toolCalls() == null ? List.of()
                : choice.message()
                .toolCalls()
                .stream()
                .map(toolCall -> new AssistantMessage.ToolCall(toolCall.id(), "function",
                        toolCall.function().name(), toolCall.function().arguments()))
                .reduce((tc1, tc2) -> new AssistantMessage.ToolCall(tc1.id(), "function", tc1.name(), tc1.arguments() + tc2.arguments()))
                .stream()
                .toList();

4.自定义知识库

接收用户的PDF进行文本向量化,测试平均模糊文本检索准确度提升30%

把用户的搜索内容和PDF的文本都向量化,我选用的1024个维度,再计算用户给的搜索内容与PDF知识库里的文本的欧氏距离和余弦距离,让AI选取相似度高的输出

5.智能商品客服

基于FunctionCalling实现大模型理解客户意图后调用java代码去检索商品数据,有效引导客户下单

用到了提示词与注解,给大模型的提示词中加入工具名称,在实体类里用@ToolParam标记给大模型的描述、在方法上用@Tool添加给大模型的描述。大模型会基于提示词,根据与用户的聊天内容,去调用相关工具。比如用户问我目前高三想找一个数学的网课来提升,大模型就会根据用户需要去数据库里检索后寻找,即使没有找到,也会基于提示词是引导用户关注类似课程。

相关推荐
AI人工智能+电脑小能手2 分钟前
【大白话说Java面试题】【Java基础篇】第22题:HashMap 和 HashSet 有哪些区别
java·开发语言·哈希算法·散列表·hash
juniperhan16 分钟前
Flink 系列第21篇:Flink SQL 函数与 UDF 全解读:类型推导、开发要点与 Module 扩展
java·大数据·数据仓库·分布式·sql·flink
ID_1800790547318 分钟前
Python 实现亚马逊商品详情 API 数据准确性校验(极简可用 + JSON 参考)
java·python·json
c++之路38 分钟前
C++23概述
java·c++·c++23
专注API从业者2 小时前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库
摇滚侠2 小时前
DBeaver 导入数据库 导入 SQL 文件 MySQL 备份恢复
java·数据库·mysql
keep one's resolveY2 小时前
SpringBoot实现重试机制的四种方案
java·spring boot·后端
天空属于哈夫克33 小时前
企业微信API常见的错误和解决方案
java·数据库·企业微信
摇滚侠4 小时前
VMvare 虚拟机 Oracle19c 安装步骤,远程连接 Oracle19c,百度网盘安装包
java·oracle
梁萌4 小时前
idea报错找不到XX包的解决方法
java·intellij-idea·启动报错·缺少包