From 1acead1f9d7bc0746e95e0c4be70cb60b665ccd0 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Wed, 3 Dec 2025 11:42:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(cache):=20=E6=B7=BB=E5=8A=A0=20LRU=20?= =?UTF-8?q?=E6=B7=98=E6=B1=B0=E6=9C=BA=E5=88=B6=E5=92=8C=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E9=99=90=E5=88=B6=E4=BB=A5=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=86=85=E5=AD=98=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/express/style_learner.py | 39 ++++++++++++++- src/chat/utils/utils_image.py | 23 ++++++++- src/plugin_system/core/stream_tool_history.py | 41 +++++++++++++++- .../services/relationship_service.py | 47 ++++++++++++++++++- 4 files changed, 145 insertions(+), 5 deletions(-) diff --git a/src/chat/express/style_learner.py b/src/chat/express/style_learner.py index 1b9975bdb..722c5b0c2 100644 --- a/src/chat/express/style_learner.py +++ b/src/chat/express/style_learner.py @@ -437,7 +437,13 @@ class StyleLearner: class StyleLearnerManager: - """多聊天室表达风格学习管理器""" + """多聊天室表达风格学习管理器 + + 添加 LRU 淘汰机制,限制最大活跃 learner 数量 + """ + + # 🔧 最大活跃 learner 数量 + MAX_ACTIVE_LEARNERS = 50 def __init__(self, model_save_path: str = "data/expression/style_models"): """ @@ -445,6 +451,7 @@ class StyleLearnerManager: model_save_path: 模型保存路径 """ self.learners: dict[str, StyleLearner] = {} + self.learner_last_used: dict[str, float] = {} # 🔧 记录最后使用时间 self.model_save_path = model_save_path # 确保保存目录存在 @@ -452,6 +459,30 @@ class StyleLearnerManager: logger.debug(f"StyleLearnerManager初始化成功, 模型保存路径: {model_save_path}") + def _evict_if_needed(self) -> None: + """🔧 内存优化:如果超过最大数量,淘汰最久未使用的 learner""" + if len(self.learners) < self.MAX_ACTIVE_LEARNERS: + return + + # 按最后使用时间排序,淘汰最旧的 20% + evict_count = max(1, len(self.learners) // 5) + sorted_by_time = sorted( + self.learner_last_used.items(), + key=lambda x: x[1] + ) + + evicted = [] + for chat_id, last_used in sorted_by_time[:evict_count]: + if chat_id in self.learners: + # 先保存再淘汰 + self.learners[chat_id].save(self.model_save_path) + del self.learners[chat_id] + del self.learner_last_used[chat_id] + evicted.append(chat_id) + + if evicted: + logger.info(f"StyleLearner LRU淘汰: 释放了 {len(evicted)} 个不活跃的学习器") + def get_learner(self, chat_id: str, model_config: dict | None = None) -> StyleLearner: """ 获取或创建指定chat_id的学习器 @@ -463,7 +494,13 @@ class StyleLearnerManager: Returns: StyleLearner实例 """ + # 🔧 更新最后使用时间 + self.learner_last_used[chat_id] = time.time() + if chat_id not in self.learners: + # 🔧 检查是否需要淘汰 + self._evict_if_needed() + # 创建新的学习器 learner = StyleLearner(chat_id, model_config) diff --git a/src/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index dd2033122..f51f18b29 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -168,15 +168,22 @@ class ImageManager: image_bytes = base64.b64decode(image_base64) image_hash = hashlib.md5(image_bytes).hexdigest() + # 如果缓存命中,可以提前释放 image_bytes + # 但如果需要保存表情包,则需要保留 image_bytes + # 2. 优先查询已注册表情的缓存(Emoji表) if full_description := await emoji_manager.get_emoji_description_by_hash(image_hash): logger.info("[缓存命中] 使用已注册表情包(Emoji表)的完整描述") + del image_bytes # 缓存命中,不再需要 + del image_base64 refined_part = full_description.split(" Keywords:")[0] return f"[表情包:{refined_part}]" # 3. 查询通用图片描述缓存(ImageDescriptions表) if cached_description := await self._get_description_from_db(image_hash, "emoji"): logger.info("[缓存命中] 使用通用图片缓存(ImageDescriptions表)中的描述") + del image_bytes # 缓存命中,不再需要 + del image_base64 refined_part = cached_description.split(" Keywords:")[0] return f"[表情包:{refined_part}]" @@ -209,7 +216,11 @@ class ImageManager: await self._save_description_to_db(image_hash, full_description, "emoji") logger.info(f"新生成的表情包描述已存入通用缓存 (Hash: {image_hash[:8]}...)") - # 6. 返回新生成的描述中用于显示的“精炼描述”部分 + # 内存优化:处理完成后主动释放大型二进制数据 + del image_bytes + del image_base64 + + # 6. 返回新生成的描述中用于显示的"精炼描述"部分 refined_part = full_description.split(" Keywords:")[0] return f"[表情包:{refined_part}]" @@ -248,11 +259,17 @@ class ImageManager: existing_image = result.scalar() if existing_image and existing_image.description: logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...") + # 缓存命中,释放 base64 和 image_bytes + del image_bytes + del image_base64 return f"[图片:{existing_image.description}]" # 3. 其次查询 ImageDescriptions 表缓存 if cached_description := await self._get_description_from_db(image_hash, "image"): logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...") + # 缓存命中,释放 base64 和 image_bytes + del image_bytes + del image_base64 return f"[图片:{cached_description}]" # 4. 如果都未命中,则同步调用VLM生成新描述 @@ -301,6 +318,10 @@ class ImageManager: logger.info(f"新生成的图片描述已存入缓存 (Hash: {image_hash[:8]}...)") + # 内存优化:处理完成后主动释放大型二进制数据 + del image_bytes + del image_base64 + return f"[图片:{description}]" except Exception as e: diff --git a/src/plugin_system/core/stream_tool_history.py b/src/plugin_system/core/stream_tool_history.py index e589e6fe7..8e498395b 100644 --- a/src/plugin_system/core/stream_tool_history.py +++ b/src/plugin_system/core/stream_tool_history.py @@ -387,8 +387,36 @@ class StreamToolHistoryManager: return result -# 全局管理器字典,按chat_id索引 +# 内存优化:全局管理器字典,按chat_id索引,添加 LRU 淘汰 _stream_managers: dict[str, StreamToolHistoryManager] = {} +_stream_managers_last_used: dict[str, float] = {} # 记录最后使用时间 +_STREAM_MANAGERS_MAX_SIZE = 100 # 最大保留数量 + + +def _evict_old_stream_managers() -> None: + """内存优化:淘汰最久未使用的 stream manager""" + import time + + if len(_stream_managers) < _STREAM_MANAGERS_MAX_SIZE: + return + + # 按最后使用时间排序,淘汰最旧的 20% + evict_count = max(1, len(_stream_managers) // 5) + sorted_by_time = sorted( + _stream_managers_last_used.items(), + key=lambda x: x[1] + ) + + evicted = [] + for chat_id, _ in sorted_by_time[:evict_count]: + if chat_id in _stream_managers: + del _stream_managers[chat_id] + if chat_id in _stream_managers_last_used: + del _stream_managers_last_used[chat_id] + evicted.append(chat_id) + + if evicted: + logger.info(f"🔧 StreamToolHistoryManager LRU淘汰: 释放了 {len(evicted)} 个不活跃的管理器") def get_stream_tool_history_manager(chat_id: str) -> StreamToolHistoryManager: @@ -400,7 +428,14 @@ def get_stream_tool_history_manager(chat_id: str) -> StreamToolHistoryManager: Returns: 工具历史记录管理器实例 """ + import time + + # 🔧 更新最后使用时间 + _stream_managers_last_used[chat_id] = time.time() + if chat_id not in _stream_managers: + # 🔧 检查是否需要淘汰 + _evict_old_stream_managers() _stream_managers[chat_id] = StreamToolHistoryManager(chat_id) return _stream_managers[chat_id] @@ -413,4 +448,6 @@ def cleanup_stream_manager(chat_id: str) -> None: """ if chat_id in _stream_managers: del _stream_managers[chat_id] - logger.info(f"已清理聊天 {chat_id} 的工具历史记录管理器") + if chat_id in _stream_managers_last_used: + del _stream_managers_last_used[chat_id] + logger.info(f"已清理聊天 {chat_id} 的工具历史记录管理器") diff --git a/src/plugin_system/services/relationship_service.py b/src/plugin_system/services/relationship_service.py index 1bb995209..424832c68 100644 --- a/src/plugin_system/services/relationship_service.py +++ b/src/plugin_system/services/relationship_service.py @@ -14,11 +14,19 @@ logger = get_logger("relationship_service") class RelationshipService: - """用户关系分服务 - 独立于插件的数据库直接访问层""" + """用户关系分服务 - 独立于插件的数据库直接访问层 + + 内存优化:添加缓存大小限制和自动过期清理 + """ + + # 🔧 缓存配置 + CACHE_MAX_SIZE = 1000 # 最大缓存用户数 def __init__(self): self._cache: dict[str, dict] = {} # user_id -> {score, text, last_updated} self._cache_ttl = 300 # 缓存5分钟 + self._last_cleanup = time.time() # 上次清理时间 + self._cleanup_interval = 60 # 每60秒清理一次过期条目 async def get_user_relationship_score(self, user_id: str) -> float: """ @@ -162,6 +170,9 @@ class RelationshipService: def _get_from_cache(self, user_id: str) -> dict | None: """从缓存获取数据""" + # 🔧 触发定期清理 + self._maybe_cleanup_expired() + if user_id in self._cache: cached_data = self._cache[user_id] if time.time() - cached_data["last_updated"] < self._cache_ttl: @@ -173,12 +184,46 @@ class RelationshipService: def _update_cache(self, user_id: str, score: float, text: str): """更新缓存""" + # 🔧 内存优化:检查缓存大小限制 + if len(self._cache) >= self.CACHE_MAX_SIZE and user_id not in self._cache: + # 淘汰最旧的 10% 条目 + self._evict_oldest_entries() + self._cache[user_id] = { "score": score, "text": text, "last_updated": time.time() } + def _maybe_cleanup_expired(self): + """🔧 内存优化:定期清理过期条目""" + now = time.time() + if now - self._last_cleanup < self._cleanup_interval: + return + + self._last_cleanup = now + expired_keys = [] + for user_id, data in self._cache.items(): + if now - data["last_updated"] >= self._cache_ttl: + expired_keys.append(user_id) + + for key in expired_keys: + del self._cache[key] + + if expired_keys: + logger.debug(f"🔧 relationship_service 清理了 {len(expired_keys)} 个过期缓存条目") + + def _evict_oldest_entries(self): + """🔧 内存优化:淘汰最旧的条目""" + evict_count = max(1, len(self._cache) // 10) + sorted_entries = sorted( + self._cache.items(), + key=lambda x: x[1]["last_updated"] + ) + for user_id, _ in sorted_entries[:evict_count]: + del self._cache[user_id] + logger.debug(f"🔧 relationship_service LRU淘汰了 {evict_count} 个缓存条目") + async def _fetch_from_database(self, user_id: str) -> UserRelationships | None: """从数据库获取关系数据""" try: