告别“失忆”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陪伴更贴心、更高效