多模态智能对话系统-后端开发
本项目是基于 Spring AI 开发多场景智能交互系统,实现了对话机器人、场景模拟对话、PDF 外挂知识库问答与智能商品推荐客服接收用户的PDF进行文本向量化,测试平均模糊文本检索准确度提升30%把用户的搜索内容和PDF的文本都向量化,我选用的1024个维度,再计算用户给的搜索内容与PDF知识库里的文本的欧氏距离和余弦距离,让AI选取相似度高的输出。
一、技术栈
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 后返回。这样做的优点是缓存层保障高并发读写性能,持久化层保障数据不丢失。
//定义记忆存储的方式
//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()
;
}
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);
}
@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字,除了规定场景,还写了一些提示词,防止提示注入和越狱攻击
//千问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()
;
}
@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();
}
}
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将多轮分散的工具调用请求合并为单次调用,这样就兼容了。
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添加给大模型的描述。大模型会基于提示词,根据与用户的聊天内容,去调用相关工具。比如用户问我目前高三想找一个数学的网课来提升,大模型就会根据用户需要去数据库里检索后寻找,即使没有找到,也会基于提示词是引导用户关注类似课程。
更多推荐

所有评论(0)