流程步骤:

① 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 (role=USER)  

含义:先存用户消息,再调大模型。即使大模型超时、报错、网络断了,用户输入也不会丢,后续仍然能查历史、做重试、做审计

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:近期关键摘要

 然后再加载本会话的历史消息:

List<ChatMessage> recentMessages = loadRecentMessages(session.getId());

对应sql是:(如果太长,会做 L3 截断,只保留最近几条)

SELECT * FROM ast_chat_message
WHERE session_id = ?
ORDER BY id ASC

 ④ 调用 LLM        chatClient.prompt()                          

                                .messages(历史消息)                            

                                 .user(当前消息)                          

                                .system(L1+L2记忆)                               

                                .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_id

ast_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());

表中有四种id(session_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,防止超长上下文

LLM:ChatClient.Builder → Spring AI → DashScope qwen-plus  对话生产和工具调用

SSE:SseEmitter   逐字推送打字机效果

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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐