【Springboot升级AI2】项目添加智能助手 流程步骤① getOrCreateSession() - 新建会话 → INSERT ast_chat_session 同步创建 → INSERT ast_chat_session_context含义情况A前端带了 conversationId就按主键查 ast_chat_session 如果查到了说明这是老会话继续聊直接返回该会话情况B前端没带或者带了但数据库里没有。新建一条 ast_chat_session同时新建一条 ast_chat_session_context再返回这个新会话注意这里的“同时创建 context”很重要因为后续记忆压缩是按 session_id 去找上下文的。提前建好后续逻辑就不需要每次判空做大量分支。② 持久化用户消息 → INSERT ast_chat_message (roleUSER)含义先存用户消息再调大模型。即使大模型超时、报错、网络断了用户输入也不会丢后续仍然能查历史、做重试、做审计INSERT INTO ast_chat_message(session_id, role, content, create_time) VALUES (?, USER, ?, NOW())③ 构建记忆上下文 ChatMemoryService.buildContext()- L1会话级记忆摘要 (LLM 压缩, 4条消息触发)- L2全局紧凑摘要 (LLM 压缩, 6条消息触发)- L3运行时截断 (token 50000 → 保留最近3条)含义上下文来源有两部分数据库里的历史对话ast_chat_message表按 session_id 查某个会话全部消息按 id 升序取出再转成 Spring AI 的 UserMessage / AssistantMessage、消息记忆压缩后的摘要ast_chat_session_context表session_memory L1 近期会话记忆摘要compact_summary L2 更长周期的紧凑摘要 记忆摘要String memoryContext memoryService.buildContext(session);ChatMemoryService.buildContext()方法ChatSessionContext ctx contextMapper.selectById(session.getId()); if (ctx null) { return ; } StringBuilder sb new StringBuilder(); if (ctx.getCompactSummary() ! null !ctx.getCompactSummary().isBlank()) { sb.append(【历史对话精要】\n) .append(ctx.getCompactSummary()) .append(\n\n); } if (ctx.getSessionMemory() ! null !ctx.getSessionMemory().isBlank()) { sb.append(【当前会话关键信息】\n) .append(ctx.getSessionMemory()) .append(\n\n); } return sb.toString();上述代码中包含两层记忆 compact_summary长期压缩摘要session_memory近期关键摘要然后再加载本会话的历史消息ListChatMessage recentMessages loadRecentMessages(session.getId());对应sql是如果太长会做 L3 截断只保留最近几条SELECT * FROM ast_chat_message WHERE session_id ? ORDER BY id ASC④ 调用 LLM chatClient.prompt().messages(历史消息).user(当前消息).system(L1L2记忆).tools(ChatTools - 8个Tool方法).call() → 文本回答var prompt chatClient.prompt() .messages(historyMessages)//给模型喂历史聊天记录 .user(request.getMessage())//当前这一轮用户问题 .tools(chatTools);//把 ChatTools 里所有 Tool 方法注册成可调用工具不是纯聊天它能查业务数据 if (memoryContext ! null !memoryContext.isBlank()) { prompt prompt.system(s - s.text(memoryContext));//系统层注入的压缩记忆 } answer prompt.call().content();⑤ SSE 异步推送 emitter.send(event:token, 逐字符) //token 事件不断往气泡里追加字emitter.send(event:meta, {conversationId}) //meta 事件告诉前端这次会话的conversationId含义插入助手消息到 ast_chat_message表更新 ast_chat_session.message_count调 memoryService.afterMessage() 触发记忆维护new Thread(() - { SecurityContextHolder.getContext().setAuthentication(auth); try { for (int i 0; i finalAnswer.length(); i) {//把答案拆成一个字一个字推 String ch finalAnswer.substring(i, i 1); emitter.send(SseEmitter.event().name(token).data(ch)); if (i % 5 0) Thread.sleep(5); } emitter.send(SseEmitter.event().name(meta) .data(Map.of(conversationId, sessionId))); emitter.complete(); } catch (Exception e) { log.error(SSE send failed, e); emitter.completeWithError(e); } finally { SecurityContextHolder.clearContext(); } }).start();⑥ 持久化助手消息 更新统计 触发记忆压缩 txTemplate.executeWithoutResult()持久化助手消息ChatMessage assistantMsg new ChatMessage() .setSessionId(sessionId) .setRole(ASSISTANT) .setContent(finalAnswer) .setCreateTime(LocalDateTime.now()); messageMapper.insert(assistantMsg);含义给数据库ast_chat_message表里落第二条消息用同一个session_idast_chat_message ----------------------------------------- id session_id role content 1 12 USER 查询我的考勤 2 12 ASSISTANT 你本月出勤...更新会话统计ChatSession s sessionMapper.selectById(sessionId); if (s ! null) { s.setMessageCount(s.getMessageCount() 2); sessionMapper.updateById(s); }含义message_count 加 2触发记忆维护memoryService.afterMessage( sessionMapper.selectById(sessionId), toolMode, assistantMsg.getId());表中有四种idsession_memory_base_message_id、session_memory_range_end_message_id、compact_summary_base_message_id、compact_summary_range_end_message_id记录哪些消息已经被摘要覆盖了哪些消息是下次要增量处理的这样下次压缩时不用把整场历史重新送给 LLM只需要送新增部分。为什么选用乐观锁因为这里更适合低冲突、轻量更新场景。大部分时间一个会话不会同时高频并发压缩、乐观锁比 SELECT FOR UPDATE 更轻、避免锁等待和死锁、更适合这种偶发冲突、允许重试的摘要写入。结构分四个实体类 ChatSession员工的会话 / ChatMessage会话的消息 / ChatSessionContext会话的上下文内容/ ChatLlmUsage会话的LLM调用记录服务ChatService 对话生命周期编排记忆ChatMemoryService ChatMemorySummarizer 三级压缩L1、L2 用LLM摘要L3硬截断消息流: [m1] [m2] [m3] [m4] [m5] [m6] [m7] ... │ │ L1触发(4条) L2触发(6条) ▼ ▼ 用户问了请假流程 用户已了解考勤规则 和考勤规则 和请假流程当前 询问加班计算方式L1 会话记忆每 4 条消息触发LLM 将一段对话压缩成一句话L2 紧凑摘要每 6 条消息触发LLM 将全局对话压缩成一段话L3 运行时截断token 50000 时只保留最近 N 条消息给 LLM防止超长上下文LLMChatClient.Builder → Spring AI → DashScope qwen-plus 对话生产和工具调用SSESseEmitter 逐字推送打字机效果SSE 消息格式event: token data: {单个字符} event: meta data: {conversationId: 1}完整业务流程1、助手先为每个员工建立私有会话空间2、每次问答都被保存形成完整对话历史3、助手在回答前会取回当前会话历史和摘要记忆4、对于需要业务数据的问题助手会调用后端工具查询真实数据5、回答完成后系统把本轮消息归档并按阈值更新对话记忆6、这样多轮对话越聊越长时系统仍能保持上下文连续且成本可控完整数据流程前端输入问题 - AssistantChat.vue sendQuestion() - assistant.js chatStream() - POST /assistant/chat - ChatController.chat() - ChatService.chat() - getOrCreateSession() - 可能 INSERT ast_chat_session - 可能 INSERT ast_chat_session_context - INSERT ast_chat_message (USER) - buildContext() - SELECT ast_chat_session_context - loadRecentMessages() - SELECT ast_chat_message - chatClient.prompt() - tools(chatTools) - DashScope 生成回答 - SSE token 推送前端 - INSERT ast_chat_message (ASSISTANT) - UPDATE ast_chat_session(message_count, update_time) - afterMessage() - SELECT 全量 message - 计算增量消息 - 调摘要器 - UPDATE ast_chat_session_context