【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 (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
更多推荐



所有评论(0)