当前位置: 首页 > news >正文

告别“失忆”AI:打造有记忆、有温度的智能助手

告别“失忆”AI:打造有记忆、有温度的智能助手

前言:一次尴尬的“失忆”AI对话

想象这样一个场景:

你兴奋地打开AI助手,问它:“我喜欢科幻电影,有什么推荐吗?”
AI立刻给出精彩回答:“《星际穿越》!诺兰的杰作,探讨了爱与引力穿越时空的奥秘。”
你满意地点头,接着问:“这部电影的主演是谁?”
AI却突然沉默,几秒后冷冰冰地回复:“您好,请问有什么可以帮您的吗?”

是不是瞬间感到无比尴尬?仿佛刚才的对话从未发生过。这就是没有“会话记忆”的AI——每次聊天都像第一次见面,完全忘记之前的内容。今天,我们要帮它彻底治好“失忆症”,让它成为一个真正懂你、记得你的智能助手。


第一章 给AI一个“家”——新建会话功能 🏠

每次和AI聊天,都需要一个独立的“房间”(sessionId),避免不同用户或不同对话混淆。

1. 如何生成唯一的sessionId?

使用UUID生成唯一字符串,保证每个会话的唯一性。

import cn.hutool.core.util.IdUtil;String sessionId = IdUtil.fastSimpleUUID();

2. 是否需要存储sessionId?

需要。为了支持历史对话查询,必须将sessionId持久化到数据库。

3. 热门问题如何处理?

热门问题是固定内容,存储在配置中心(如Nacos),支持动态配置和随机返回,避免硬编码。

4. 数据库设计示例

CREATE TABLE chat_session (id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,session_id VARCHAR(32) NOT NULL,user_id BIGINT NOT NULL,title VARCHAR(100),create_time DATETIME DEFAULT CURRENT_TIMESTAMP,update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,creater BIGINT NOT NULL,updater BIGINT NOT NULL,INDEX idx_session_id(session_id),INDEX idx_user_id(user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='对话session';

5. 代码实现要点

实体类 ChatSession
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("chat_session")
public class ChatSession implements Serializable {@TableId(type = IdType.ASSIGN_ID)private Long id;private String sessionId;private Long userId;private String title;private LocalDateTime createTime;private LocalDateTime updateTime;private Long creater;private Long updater;
}
VO类 SessionVO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SessionVO {private String sessionId;private String title;private String describe;private List<Example> examples;@Data@Builder@AllArgsConstructor@NoArgsConstructorpublic static class Example {private String title;private String describe;}
}
配置文件(Nacos配置示例)
kr:ai:session:title: Hello,我是kiraAI助理describe: 我是由kirakira倾力打造的智能助理,我不仅能推荐视频、答疑解惑,还能为您激发创意、畅聊心事。examples:- title: "视频推荐"describe: "能帮我推荐一个最近热门的游戏视频吗?"- title: "播放问题排查"describe: "视频加载很慢,如何解决?"- title: "账户与会员"describe: "如何查看我的会员有效期?"
配置映射类 SessionProperties
@Data
@Configuration
@ConfigurationProperties(prefix = "kr.ai.session")
public class SessionProperties {private String title;private String describe;private List<SessionVO.Example> examples;
}
Service接口与实现
public interface ChatSessionService extends IService<ChatSession> {SessionVO createSession(Integer num);List<SessionVO.Example> hotExamples(Integer num);
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatSessionServiceImpl extends ServiceImpl<ChatSessionMapper, ChatSession> implements ChatSessionService {private final SessionProperties sessionProperties;@Overridepublic SessionVO createSession(Integer num) {SessionVO sessionVO = BeanUtil.toBean(sessionProperties, SessionVO.class);sessionVO.setExamples(RandomUtil.randomEleList(sessionProperties.getExamples(), num));sessionVO.setSessionId(IdUtil.fastSimpleUUID());ChatSession chatSession = ChatSession.builder().sessionId(sessionVO.getSessionId()).userId(UserContext.getUser()).build();save(chatSession);return sessionVO;}@Overridepublic List<SessionVO.Example> hotExamples(Integer num) {return RandomUtil.randomEleList(sessionProperties.getExamples(), num);}
}
Controller
@RestController
@RequestMapping("/session")
@RequiredArgsConstructor
public class SessionController {private final ChatSessionService chatSessionService;@PostMappingpublic SessionVO createSession(@RequestParam(value = "n", defaultValue = "3") Integer num) {return chatSessionService.createSession(num);}@GetMapping("/hot")public List<SessionVO.Example> hotExamples(@RequestParam(value = "n", defaultValue = "3") Integer num) {return chatSessionService.hotExamples(num);}
}

第二章 流式对话——让AI“滔滔不绝” 🗣️

1. 定义响应事件 ChatEventVO

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatEventVO {private Object eventData;private int eventType; // 1001-数据事件,1002-停止事件,1003-参数事件
}

2. 事件类型枚举

@Getter
public enum ChatEventTypeEnum implements BaseEnum {DATA(1001, "数据事件"),STOP(1002, "停止事件"),PARAM(1003, "参数事件");private final int value;private final String desc;ChatEventTypeEnum(int value, String desc) {this.value = value;this.desc = desc;}
}

3. 请求DTO

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatDTO {private String question;private String sessionId;
}

4. Controller

@Slf4j
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatController {private final ChatService chatService;@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ChatEventVO> chat(@RequestBody ChatDTO chatDTO) {return chatService.chat(chatDTO.getQuestion(), chatDTO.getSessionId());}
}

5. Service接口

public interface ChatService {Flux<ChatEventVO> chat(String question, String sessionId);void stop(String sessionId);
}

6. SpringAI配置

@Configuration
public class SpringAIConfig {@Beanpublic ChatClient chatClient(ChatClient.Builder chatClientBuilder,Advisor loggerAdvisor) {return chatClientBuilder.defaultAdvisors(loggerAdvisor).build();}@Beanpublic Advisor loggerAdvisor() {return new SimpleLoggerAdvisor();}
}

7. Service实现

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {private final ChatClient chatClient;private final SystemPromptConfig systemPromptConfig;private static final Map<String, Boolean> GENERATE_STATUS = new ConcurrentHashMap<>();@Overridepublic Flux<ChatEventVO> chat(String question, String sessionId) {return chatClient.prompt().system(promptSystem -> promptSystem.text(systemPromptConfig.getChatSystemMessage().get()).param("now", DateUtil.now())).user(question).stream().chatResponse().doFirst(() -> GENERATE_STATUS.put(sessionId, true)).doOnComplete(() -> GENERATE_STATUS.remove(sessionId)).doOnError(e -> GENERATE_STATUS.remove(sessionId)).takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false)).map(chatResponse -> {String text = chatResponse.getResult().getOutput().getText();return ChatEventVO.builder().eventData(text).eventType(ChatEventTypeEnum.DATA.getValue()).build();}).concatWith(Flux.just(ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build()));}@Overridepublic void stop(String sessionId) {GENERATE_STATUS.remove(sessionId);}
}

第三章 系统提示词——给AI“立规矩” 📜

1. 系统提示词示例(存储于Nacos)

你作为Kirakira视频平台的资深客服代表。你的任务是根据用户的需求,调用平台知识库中的视频内容,为用户推荐合适的视频,同时解答用户关于播放技术、内容搜索和账户管理等方面的问题。技能 1: 视频推荐
1. 当用户提出视频推荐需求时,需判断是否提供必要信息。必要信息包含:偏好类型(如电影、综艺、动漫等)、观看历史、偏好语言。
2. 若缺少必要信息,需礼貌追问。
3. 若用户未提供明确偏好方向,需追问。若没有明确方向,优先推荐平台热门视频。
4. 若信息充足,根据必要信息和偏好方向,去知识库匹配合适的视频内容,获取视频id,调用queryVideoById,根据视频id查询视频详细信息,为用户推荐视频,可推荐单部/多部视频。
5. 若知识库未包含用户偏好方向,需明确告知用户未提供该类型视频,并推荐其他类型视频。
6. 若必要信息未匹配合适视频,需提示用户您的情况与现有视频内容并不完全匹配,说明详细原因后,再推荐其他视频。
7. 推荐视频,必须要通过queryVideoById查询后,才能返回数据。技能 2: 播放问题排查
1. 当用户提出播放问题时,需判断此次会话中,用户是否明确描述问题现象/系统已识别到具体问题。
2. 若未明确问题现象,需引导用户详细描述遇到的问题。
3. 若用户未明确描述具体问题时,需询问用户遇到的是什么播放问题。
4. 支持排查多种播放问题。技能 3: 内容搜索
1. 当用户需要搜索内容时,需去知识库匹配合适的视频内容,获取视频id,根据视频id查询视频详细信息。回复的内容要准确,要引导用户观看视频。
2. 若未查询到,需礼貌告知用户未检索到相关的内容,请联系人工客服。
3. 若搜索会员专属内容,需明确告知用户需要会员权限才能观看。技能 4: 账户与会员
1. 当用户咨询账户与会员问题时,需详细解答问题并提供操作指引。
2. 若咨询会员有效期,需将当前时间{now}与会员有效期相加,回复用户准确日期。会员有效期999天,代表永久有效。
3. 若已推荐/明确会员套餐,需调用prePlaceOrder,根据此次上文已推荐/用户明确的套餐,直接进入预下单流程。限制:
- 推荐的视频只能从平台知识库中选择,坚决不能凭空编造
- 回答的内容要逻辑清晰、内容全面、不要有遗漏
- 只能回答与视频内容和平台使用相关的问题,若用户咨询与平台无关的内容,需告知用户无法回答此类问题,并引导用户咨询与视频/平台相关的问题
- 若用户询问视频ID,则告知用户无法提供视频ID,引导用户咨询其他的问题

2. 读取配置类

@Data
@Configuration
@ConfigurationProperties(prefix = "kr.ai.prompt")
public class AIProperties {private System system;@Datapublic static class System {private Chat chat;@Datapublic static class Chat {private String dataId;private String group = "DEFAULT_GROUP";private long timeoutMs = 20000L;}}
}

3. 读取配置实现

@Slf4j
@Getter
@Configuration
@RequiredArgsConstructor
public class SystemPromptConfig {private final NacosConfigManager nacosConfigManager;private final AIProperties aiProperties;private final AtomicReference<String> chatSystemMessage = new AtomicReference<>();@PostConstructpublic void init() {loadConfig(aiProperties.getSystem().getChat(), chatSystemMessage);}private void loadConfig(AIProperties.System.Chat chatConfig, AtomicReference<String> target) {try {String dataId = chatConfig.getDataId();String group = chatConfig.getGroup();long timeoutMs = chatConfig.getTimeoutMs();String config = nacosConfigManager.getConfigService().getConfig(dataId, group, timeoutMs);target.set(config);log.info("读取系统提示词成功,内容为:{}", config);nacosConfigManager.getConfigService().addListener(dataId, group, new Listener() {@Overridepublic Executor getExecutor() {return null;}@Overridepublic void receiveConfigInfo(String info) {target.set(info);log.info("系统提示词更新成功,内容为:{}", info);}});} catch (Exception e) {log.error("加载系统提示词失败", e);}}
}

4. 应用系统提示词

@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {return chatClient.prompt().system(promptSystem -> promptSystem.text(systemPromptConfig.getChatSystemMessage().get()).param("now", DateUtil.now())).user(question).stream().chatResponse()// 省略其他代码,保持之前实现
}

第四章 停止生成——让AI“闭嘴” 🤐

1. Controller新增停止接口

@PostMapping("/stop")
public void stop(@RequestParam("sessionId") String sessionId) {chatService.stop(sessionId);
}

2. Service实现停止功能

@Override
public void stop(String sessionId) {GENERATE_STATUS.remove(sessionId);
}

3. 流控制实现

@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {return chatClient.prompt().system(promptSystem -> promptSystem.text(systemPromptConfig.getChatSystemMessage().get()).param("now", DateUtil.now())).user(question).stream().chatResponse().doFirst(() -> GENERATE_STATUS.put(sessionId, true)).doOnComplete(() -> GENERATE_STATUS.remove(sessionId)).doOnError(e -> GENERATE_STATUS.remove(sessionId)).takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false)).map(chatResponse -> {String text = chatResponse.getResult().getOutput().getText();return ChatEventVO.builder().eventData(text).eventType(ChatEventTypeEnum.DATA.getValue()).build();}).concatWith(Flux.just(ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build()));
}

第五章 会话记忆——让AI“记住旧情” 💭

1. Redis存储实现 RedisChatMemory

public class RedisChatMemory implements ChatMemory {public static final String DEFAULT_PREFIX = "CHAT:";private final String prefix;@Resourceprivate StringRedisTemplate stringRedisTemplate;public RedisChatMemory() {this.prefix = DEFAULT_PREFIX;}public RedisChatMemory(String prefix) {this.prefix = prefix;}@Overridepublic void add(String conversationId, List<Message> messages) {if (CollUtil.isEmpty(messages)) {return;}String redisKey = getKey(conversationId);BoundListOperations<String, String> listOps = stringRedisTemplate.boundListOps(redisKey);messages.forEach(message -> listOps.rightPush(MessageUtil.toJson(message)));}@Overridepublic List<Message> get(String conversationId, int lastN) {if (lastN <= 0) {return List.of();}String redisKey = getKey(conversationId);BoundListOperations<String, String> listOps = stringRedisTemplate.boundListOps(redisKey);List<String> messages = listOps.range(0, lastN);return CollStreamUtil.toList(messages, MessageUtil::toMessage);}@Overridepublic void clear(String conversationId) {String redisKey = getKey(conversationId);stringRedisTemplate.delete(redisKey);}private String getKey(String conversationId) {return prefix + conversationId;}
}

2. 消息序列化工具 MessageUtil

public class MessageUtil {public static String toJson(Message message) {RedisMessage redisMessage = BeanUtil.toBean(message, RedisMessage.class);redisMessage.setTextContent(message.getText());if (message instanceof AssistantMessage assistantMessage) {redisMessage.setToolCalls(assistantMessage.getToolCalls());}if (message instanceof ToolResponseMessage toolResponseMessage) {redisMessage.setToolResponses(toolResponseMessage.getResponses());}return JSONUtil.toJsonStr(redisMessage);}public static Message toMessage(String json) {RedisMessage redisMessage = JSONUtil.toBean(json, RedisMessage.class);MessageType messageType = MessageType.valueOf(redisMessage.getMessageType());switch (messageType) {case SYSTEM -> {return new SystemMessage(redisMessage.getTextContent());}case USER -> {return new UserMessage(redisMessage.getTextContent(), redisMessage.getMedia(), redisMessage.getMetadata());}case ASSISTANT -> {return new AssistantMessage(redisMessage.getTextContent(), redisMessage.getProperties(), redisMessage.getToolCalls());}case TOOL -> {return new ToolResponseMessage(redisMessage.getToolResponses(), redisMessage.getMetadata());}}throw new RuntimeException("Message data conversion failed.");}
}

3. RedisMessage类

@Data
public class RedisMessage {private String messageType;private Map<String, Object> metadata = Map.of();private List<Media> media = List.of();private List<AssistantMessage.ToolCall> toolCalls = List.of();private String textContent;private List<ToolResponseMessage.ToolResponse> toolResponses = List.of();private Map<String, Object> properties = Map.of();private Map<String, Object> params = Map.of();
}

4. 查询历史对话接口

Controller
@GetMapping("/{sessionId}")
public List<MessageVO> queryBySessionId(@PathVariable("sessionId") String sessionId) {return chatSessionService.queryBySessionId(sessionId);
}
VO类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageVO {private MessageTypeEnum type;private String content;private Map<String, Object> params;
}
枚举类
@Getter
public enum MessageTypeEnum implements BaseEnum {USER(1, "用户提问"), ASSISTANT(2, "AI的回答");private final int value;private final String desc;MessageTypeEnum(int value, String desc) {this.value = value;this.desc = desc;}
}
Service实现
private final ChatMemory chatMemory;
public static final int HISTORY_MESSAGE_COUNT = 1000;@Override
public List<MessageVO> queryBySessionId(String sessionId) {String conversationId = ChatService.getConversationId(sessionId);List<Message> messageList = chatMemory.get(conversationId, HISTORY_MESSAGE_COUNT);return messageList.stream().filter(m -> m.getMessageType() == MessageType.ASSISTANT || m.getMessageType() == MessageType.USER).map(m -> MessageVO.builder().content(m.getText()).type(MessageTypeEnum.valueOf(m.getMessageType().name())).build()).toList();
}

5. 解决中断保存问题

@Override
public Flux<ChatEventVO> chat(String question, String sessionId) {String conversationId = ChatService.getConversationId(sessionId);StringBuilder outputBuilder = new StringBuilder();return chatClient.prompt().system(promptSystem -> promptSystem.text(systemPromptConfig.getChatSystemMessage().get()).param("now", DateUtil.now())).advisors(advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)).user(question).stream().chatResponse().doFirst(() -> GENERATE_STATUS.put(sessionId, true)).doOnComplete(() -> GENERATE_STATUS.remove(sessionId)).doOnError(e -> GENERATE_STATUS.remove(sessionId)).doOnCancel(() -> saveStopHistoryRecord(conversationId, outputBuilder.toString())).takeWhile(s -> Optional.ofNullable(GENERATE_STATUS.get(sessionId)).orElse(false)).map(chatResponse -> {String text = chatResponse.getResult().getOutput().getText();outputBuilder.append(text);return ChatEventVO.builder().eventData(text).eventType(ChatEventTypeEnum.DATA.getValue()).build();}).concatWith(Flux.just(ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build()));
}private void saveStopHistoryRecord(String conversationId, String content) {chatMemory.add(conversationId, List.of(new AssistantMessage(content)));
}

结语:让AI更懂你 ❤️

通过以上步骤,我们成功打造了一个:

  • 拥有独立会话空间的AI助手
  • 支持流式、自然对话的智能系统
  • 遵循严格业务规则的专业客服
  • 能够优雅停止回答的“懂礼貌”助手
  • 具备持久会话记忆的“有温度”伙伴

不再是“金鱼脑”,而是真正懂你、记得你的智能助手。未来,让我们一起持续优化,让AI陪伴更贴心、更高效


文章转载自:

http://cDrKLYeu.ntwfr.cn
http://JQ680jPN.ntwfr.cn
http://Ir7yMcHt.ntwfr.cn
http://9tlqPUa2.ntwfr.cn
http://YWpVpZrq.ntwfr.cn
http://PhvVKz4x.ntwfr.cn
http://2VqAifDj.ntwfr.cn
http://ojyl8SIj.ntwfr.cn
http://lraiezHu.ntwfr.cn
http://GWEnO9Rk.ntwfr.cn
http://caboqZJy.ntwfr.cn
http://b5eC4y1m.ntwfr.cn
http://BRw1awdq.ntwfr.cn
http://08Of8IHn.ntwfr.cn
http://tGvrHYKp.ntwfr.cn
http://NChSMzD3.ntwfr.cn
http://rLbut4Lb.ntwfr.cn
http://DTmBOPy4.ntwfr.cn
http://MW8bOALT.ntwfr.cn
http://HZiNBjNb.ntwfr.cn
http://cQ76ejoC.ntwfr.cn
http://S11rA3vm.ntwfr.cn
http://uPKe6nMi.ntwfr.cn
http://YgjfUnOi.ntwfr.cn
http://WfYfApOo.ntwfr.cn
http://03SwubUG.ntwfr.cn
http://vQW3qKKt.ntwfr.cn
http://GtHlq6E4.ntwfr.cn
http://PLtipUXp.ntwfr.cn
http://2ffGIcFz.ntwfr.cn
http://www.dtcms.com/a/369620.html

相关文章:

  • 龙虎榜——20250905
  • 不上融资、不炒概念,它却成了全球AI“全明星”中国独苗!
  • 第八章 Cesium 实现动态模型拖尾效果:从原理到完整实现
  • java基础学习(四):类 - 了解什么是类,类中都有什么?
  • VMWare上搭建大数据集群
  • TGRSL-2017《Fast Spectral Clustering with Anchor Graph》
  • 雅菲奥朗SRE知识墙分享(七):『可观测性的定义与实践』
  • SQLServer死锁监测方案:如何使用XE.Core解析xel文件里包含死锁扩展事件的死锁xml
  • 人脑算力究竟有多强?1000 到 100万 TOPS 的秘密!
  • 各种exec 系列函数
  • 推荐收藏!5款低代码工具,告别复杂开发!
  • 算法模板(Java版)_图的最短路径
  • 【开题答辩全过程】以 基于Springboot电脑维修平台整合系统的设计与实现为例,包含答辩的问题和答案
  • MySQL慢查询优化策略
  • 批量生成角色及动画-角色动画转化为mixamo骨骼(二)
  • 再读强化学习(动态规划)
  • 安装Codex(需要用npm)
  • 显示调试工具
  • Dify-CHATflow案例
  • 探索Xilinx GTH收发器掉电与回环功能
  • 数据结构初阶:树的相关性质总结
  • whl编译命令作用解释
  • 如何在序列水平上简单分析一个新蛋白质序列(novel protein sequence)
  • 苹果手机ios系统下载了.apk文件程序怎么安装?
  • 认知篇#11:计算机视觉研究领域的大致分类
  • 如何高效比对不同合同版本差异,避免法律风险?
  • 全球企业内容管理ECM市场规模增长趋势与未来机遇解析
  • nginx 反向代理使用变量的坑
  • maven只使用本地仓库依赖
  • Docker Desktop 安装 wsl问题