Skip to content
Open

--- #299

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions ruoyi-admin/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,38 @@ vector-store:
collectionname: LocalKnowledge
api-key:
use-tls: false

--- # 聊天内存配置
chat:
memory:
# 是否启用长期记忆
enabled: true

# ========== 内存管理策略 ==========
# message: 固定消息数量(使用 LangChain4j 原生 MessageWindowChatMemory,不支持 Token 管理和摘要)
# token: 基于 Token 数量(仅截断,不摘要,Token 超限时直接截断旧消息)
# hybrid: 混合策略(摘要 + 截断,Token 达到阈值时先摘要压缩,超限时再截断)
strategy: message

# ========== 通用配置(所有策略生效)==========
# 预留给回复的 Token 数
reserved-for-reply: 2000
# 是否保护系统消息不被截断
preserve-system-messages: true

# ========== message 策略配置 ==========
# 仅当 strategy=message 时生效,控制保留的消息数量
max-messages: 20

# ========== token/hybrid 策略配置 ==========
# 最大 Token 数(null 则根据模型自动获取)
max-tokens: null

# ========== hybrid 策略专属配置 ==========
# 以下配置仅当 strategy=hybrid 时生效,token 策略不支持摘要
# 触发摘要的 Token 使用比例(0.7 = 70%)
summarize-token-ratio: 0.7
# 触发摘要的最小消息数
summarize-threshold: 10
# 摘要模型策略: current(当前模型) / smart(智能映射) / custom(自定义)
summarizer-strategy: current
843 changes: 843 additions & 0 deletions ruoyi-modules/ruoyi-chat/docs/聊天上下文管理实现文档.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
Expand Down Expand Up @@ -57,6 +56,7 @@
import org.ruoyi.observability.*;
import org.ruoyi.service.chat.AbstractChatService;
import org.ruoyi.service.chat.IChatMessageService;
import org.ruoyi.service.chat.impl.memory.ChatMemoryFactory;
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
Expand Down Expand Up @@ -86,8 +86,6 @@
@RequiredArgsConstructor
public class ChatServiceFacade implements IChatService {

private static final Integer DEFAULT_MAX_MESSAGES = 20;

private final IChatModelService chatModelService;

private final ChatServiceFactory chatServiceFactory;
Expand All @@ -106,11 +104,13 @@ public class ChatServiceFacade implements IChatService {

private final ToolProviderFactory toolProviderFactory;

private final ChatMemoryFactory chatMemoryFactory;

/**
* 内存实例缓存,避免同一会话重复创建
* Key: sessionId, Value: MessageWindowChatMemory实例
* Key: sessionId, Value: ChatMemory实例
*/
private static final Map<Object, MessageWindowChatMemory> memoryCache = new ConcurrentHashMap<>();
private static final Map<Object, ChatMemory> memoryCache = new ConcurrentHashMap<>();



Expand All @@ -133,13 +133,15 @@ public SseEmitter sseChat(ChatRequest chatRequest) {
throw new IllegalArgumentException("模型不存在: " + chatRequest.getModel());
}

// 先设置 chatModelVo,以便 buildContextMessages 中创建摘要模型时能获取到配置
chatRequest.setChatModelVo(chatModelVo);

// 2. 构建上下文消息列表
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);

chatRequest.setEmitter(emitter);
chatRequest.setUserId(userId);
chatRequest.setTokenValue(tokenValue);
chatRequest.setChatModelVo(chatModelVo);
chatRequest.setContextMessages(contextMessages);

// 保存用户消息
Expand Down Expand Up @@ -396,18 +398,13 @@ public SseEmitter chat(ChatRequest chatRequest) {
* 同一个会话ID会返回同一个内存实例,避免重复创建和消息丢失
*
* @param memoryId 内存ID(会话ID)
* @return MessageWindowChatMemory实例
* @param model 模型配置
* @return ChatMemory实例
*/
private MessageWindowChatMemory createChatMemory(Object memoryId) {
// 先从缓存中获取
private ChatMemory createChatMemory(Object memoryId, ChatModelVo model) {
return memoryCache.computeIfAbsent(memoryId, key -> {
try {
PersistentChatMemoryStore store = new PersistentChatMemoryStore(chatMessageService);
return MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(DEFAULT_MAX_MESSAGES)
.chatMemoryStore(store)
.build();
return chatMemoryFactory.create(memoryId, model);
} catch (Exception e) {
log.warn("创建聊天内存失败: {}", e.getMessage());
return null;
Expand Down Expand Up @@ -462,7 +459,7 @@ private List<ChatMessage> buildContextMessages(ChatRequest chatRequest) {

// 3. 从数据库查询历史对话消息(放在前面)
if (chatRequest.getSessionId() != null) {
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
ChatMemory memory = createChatMemory(chatRequest.getSessionId(), chatRequest.getChatModelVo());
if (memory != null) {
List<ChatMessage> historicalMessages = memory.messages();
if (historicalMessages != null && !historicalMessages.isEmpty()) {
Expand Down Expand Up @@ -516,12 +513,18 @@ protected StreamingChatResponseHandler createResponseHandler(Long userId, String
@SneakyThrows
@Override
public void onPartialResponse(String partialResponse) {
// 过滤 null 值,避免拼接成 "nullnullnull..."
if (partialResponse == null) {
log.debug("收到 null 消息片段,已忽略");
return;
}

// 将消息片段追加到缓冲区
messageBuffer.append(partialResponse);

// 实时发送内容事件到客户端
SseMessageUtils.sendContent(userId, partialResponse);
log.debug("收到消息片段: {}", partialResponse);
log.debug("收到消息片段: {}", partialResponse);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.ruoyi.service.chat.impl.memory;

import lombok.RequiredArgsConstructor;
import org.ruoyi.service.chat.IChatMessageService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* ChatMemory 配置类
*
* @author yang
* @date 2026-04-27
*/
@Configuration
@EnableConfigurationProperties(ChatMemoryProperties.class)
@RequiredArgsConstructor
public class ChatMemoryConfig {

private final IChatMessageService chatMessageService;

/**
* 持久化存储 Bean
*/
@Bean
public PersistentChatMemoryStore persistentChatMemoryStore() {
return new PersistentChatMemoryStore(chatMessageService);
}

/**
* Token 计数器 Bean
*/
@Bean
public TokenCounter tokenCounter() {
return new TokenCounter();
}
}
Loading