From c2a1d7b00bedf3c72bf57b8ac01a3b42237702d4 Mon Sep 17 00:00:00 2001 From: LuiKlee Date: Tue, 16 Dec 2025 16:10:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=BA=A2=E5=87=BA=E7=AD=96?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/check_memory_transfer.py | 303 +++++++++++++++++++++++++ scripts/clear_short_term_memory.py | 74 ++++++ src/memory_graph/README.md | 34 +++ src/memory_graph/manager_singleton.py | 1 + src/memory_graph/short_term_manager.py | 95 +++++++- src/memory_graph/unified_manager.py | 2 + template/bot_config_template.toml | 3 + 7 files changed, 503 insertions(+), 9 deletions(-) create mode 100644 scripts/check_memory_transfer.py create mode 100644 scripts/clear_short_term_memory.py diff --git a/scripts/check_memory_transfer.py b/scripts/check_memory_transfer.py new file mode 100644 index 000000000..01082f242 --- /dev/null +++ b/scripts/check_memory_transfer.py @@ -0,0 +1,303 @@ +import asyncio +import sys +from pathlib import Path + +# 添加项目根目录到 Python 路径 +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from src.memory_graph.manager_singleton import get_unified_memory_manager +from src.common.logger import get_logger + +logger = get_logger("memory_transfer_check") + + +def print_section(title: str): + """打印分节标题""" + print(f"\n{'=' * 60}") + print(f" {title}") + print(f"{'=' * 60}\n") + + +async def check_short_term_status(): + """检查短期记忆状态""" + print_section("1. 短期记忆状态检查") + + manager = get_unified_memory_manager() + short_term = manager.short_term_manager + + # 获取统计信息 + stats = short_term.get_statistics() + + print(f"📊 当前记忆数量: {stats['total_memories']}/{stats['max_memories']}") + + # 计算占用率 + if stats['max_memories'] > 0: + occupancy = stats['total_memories'] / stats['max_memories'] + print(f"📈 容量占用率: {occupancy:.1%}") + + # 根据占用率给出建议 + if occupancy >= 1.0: + print("⚠️ 警告:已达到容量上限!应该触发紧急转移") + elif occupancy >= 0.5: + print("✅ 占用率超过50%,符合自动转移条件") + else: + print(f"ℹ️ 占用率未达到50%阈值,当前 {occupancy:.1%}") + + print(f"🎯 可转移记忆数: {stats['transferable_count']}") + print(f"📏 转移重要性阈值: {stats['transfer_threshold']}") + + return stats + + +async def check_transfer_candidates(): + """检查当前可转移的候选记忆""" + print_section("2. 转移候选记忆分析") + + manager = get_unified_memory_manager() + short_term = manager.short_term_manager + + # 获取转移候选 + candidates = short_term.get_memories_for_transfer() + + print(f"🎫 当前转移候选: {len(candidates)} 条\n") + + if not candidates: + print("❌ 没有记忆符合转移条件!") + print("\n可能原因:") + print(" 1. 所有记忆的重要性都低于阈值") + print(" 2. 短期记忆数量未超过容量限制") + print(" 3. 短期记忆列表为空") + return [] + + # 显示前5条候选的详细信息 + print("前 5 条候选记忆:\n") + for i, mem in enumerate(candidates[:5], 1): + print(f"{i}. 记忆ID: {mem.id[:8]}...") + print(f" 重要性: {mem.importance:.3f}") + print(f" 内容: {mem.content[:50]}...") + print(f" 创建时间: {mem.created_at}") + print() + + if len(candidates) > 5: + print(f"... 还有 {len(candidates) - 5} 条候选记忆\n") + + # 分析重要性分布 + importance_levels = { + "高 (>=0.8)": sum(1 for m in candidates if m.importance >= 0.8), + "中 (0.6-0.8)": sum(1 for m in candidates if 0.6 <= m.importance < 0.8), + "低 (<0.6)": sum(1 for m in candidates if m.importance < 0.6), + } + + print("📊 重要性分布:") + for level, count in importance_levels.items(): + print(f" {level}: {count} 条") + + return candidates + + +async def check_auto_transfer_task(): + """检查自动转移任务状态""" + print_section("3. 自动转移任务状态") + + manager = get_unified_memory_manager() + + # 检查任务是否存在 + if not hasattr(manager, '_auto_transfer_task') or manager._auto_transfer_task is None: + print("❌ 自动转移任务未创建!") + print("\n建议:调用 manager.initialize() 初始化系统") + return False + + task = manager._auto_transfer_task + + # 检查任务状态 + if task.done(): + print("❌ 自动转移任务已结束!") + try: + exception = task.exception() + if exception: + print(f"\n任务异常: {exception}") + except: + pass + print("\n建议:重启系统或手动重启任务") + return False + + print("✅ 自动转移任务正在运行") + + # 检查转移缓存 + if hasattr(manager, '_transfer_cache'): + cache_size = len(manager._transfer_cache) if manager._transfer_cache else 0 + print(f"📦 转移缓存: {cache_size} 条记忆") + + # 检查上次转移时间 + if hasattr(manager, '_last_transfer_time'): + from datetime import datetime + last_time = manager._last_transfer_time + if last_time: + time_diff = (datetime.now() - last_time).total_seconds() + print(f"⏱️ 距上次转移: {time_diff:.1f} 秒前") + + return True + + +async def check_long_term_status(): + """检查长期记忆状态""" + print_section("4. 长期记忆图谱状态") + + manager = get_unified_memory_manager() + long_term = manager.long_term_manager + + # 获取图谱统计 + stats = long_term.get_statistics() + + print(f"👥 人物节点数: {stats.get('person_count', 0)}") + print(f"📅 事件节点数: {stats.get('event_count', 0)}") + print(f"🔗 关系边数: {stats.get('edge_count', 0)}") + print(f"💾 向量存储数: {stats.get('vector_count', 0)}") + + return stats + + +async def manual_transfer_test(): + """手动触发转移测试""" + print_section("5. 手动转移测试") + + manager = get_unified_memory_manager() + + # 询问用户是否执行 + print("⚠️ 即将手动触发一次记忆转移") + print("这将把当前符合条件的短期记忆转移到长期记忆") + response = input("\n是否继续? (y/n): ").strip().lower() + + if response != 'y': + print("❌ 已取消手动转移") + return None + + print("\n🚀 开始手动转移...") + + try: + # 执行手动转移 + result = await manager.manual_transfer() + + print("\n✅ 转移完成!") + print(f"\n转移结果:") + print(f" 已处理: {result.get('processed_count', 0)} 条") + print(f" 成功转移: {len(result.get('transferred_memory_ids', []))} 条") + print(f" 失败: {result.get('failed_count', 0)} 条") + print(f" 跳过: {result.get('skipped_count', 0)} 条") + + if result.get('errors'): + print(f"\n错误信息:") + for error in result['errors'][:3]: # 只显示前3个错误 + print(f" - {error}") + + return result + + except Exception as e: + print(f"\n❌ 转移失败: {e}") + logger.exception("手动转移失败") + return None + + +async def check_configuration(): + """检查相关配置""" + print_section("6. 配置参数检查") + + from src.config.config import global_config + + config = global_config.memory + + print("📋 当前配置:") + print(f" 短期记忆容量: {config.short_term_max_memories}") + print(f" 转移重要性阈值: {config.short_term_transfer_threshold}") + print(f" 批量转移大小: {config.long_term_batch_size}") + print(f" 自动转移间隔: {config.long_term_auto_transfer_interval} 秒") + print(f" 启用泄压清理: {config.short_term_enable_force_cleanup}") + + # 给出配置建议 + print("\n💡 配置建议:") + + if config.short_term_transfer_threshold > 0.6: + print(" ⚠️ 转移阈值较高(>0.6),可能导致记忆难以转移") + print(" 建议:降低到 0.4-0.5") + + if config.long_term_batch_size > 10: + print(" ⚠️ 批量大小较大(>10),可能延迟转移触发") + print(" 建议:设置为 5-10") + + if config.long_term_auto_transfer_interval > 300: + print(" ⚠️ 转移间隔较长(>5分钟),可能导致转移不及时") + print(" 建议:设置为 60-180 秒") + + +async def main(): + """主函数""" + print("\n" + "=" * 60) + print(" MoFox-Bot 记忆转移诊断工具") + print("=" * 60) + + try: + # 初始化管理器 + print("\n⚙️ 正在初始化记忆管理器...") + manager = get_unified_memory_manager() + await manager.initialize() + print("✅ 初始化完成\n") + + # 执行各项检查 + await check_short_term_status() + candidates = await check_transfer_candidates() + task_running = await check_auto_transfer_task() + await check_long_term_status() + await check_configuration() + + # 综合诊断 + print_section("7. 综合诊断结果") + + issues = [] + + if not candidates: + issues.append("❌ 没有符合条件的转移候选") + + if not task_running: + issues.append("❌ 自动转移任务未运行") + + if issues: + print("🚨 发现以下问题:\n") + for issue in issues: + print(f" {issue}") + + print("\n建议操作:") + print(" 1. 检查短期记忆的重要性评分是否合理") + print(" 2. 降低配置中的转移阈值") + print(" 3. 查看日志文件排查错误") + print(" 4. 尝试手动触发转移测试") + else: + print("✅ 系统运行正常,转移机制已就绪") + + if candidates: + print(f"\n当前有 {len(candidates)} 条记忆等待转移") + print("转移将在满足以下任一条件时自动触发:") + print(" • 转移缓存达到批量大小") + print(" • 短期记忆占用率超过 50%") + print(" • 距上次转移超过最大延迟") + print(" • 短期记忆达到容量上限") + + # 询问是否手动触发转移 + if candidates: + print() + await manual_transfer_test() + + print_section("检查完成") + print("详细诊断报告: docs/memory_transfer_diagnostic_report.md") + + except Exception as e: + print(f"\n❌ 检查过程出错: {e}") + logger.exception("检查脚本执行失败") + return 1 + + return 0 + + +if __name__ == "__main__": + exit_code = asyncio.run(main()) + sys.exit(exit_code) diff --git a/scripts/clear_short_term_memory.py b/scripts/clear_short_term_memory.py new file mode 100644 index 000000000..5ef95608c --- /dev/null +++ b/scripts/clear_short_term_memory.py @@ -0,0 +1,74 @@ +"""工具:清空短期记忆存储。 + +用法: + python scripts/clear_short_term_memory.py [--remove-file] + +- 按配置的数据目录加载短期记忆管理器 +- 清空内存缓存并写入空的 short_term_memory.json +- 可选:直接删除存储文件而不是写入空文件 +""" + +import argparse +import asyncio +import sys +from pathlib import Path + +# 让从仓库根目录运行时能够正确导入模块 +PROJECT_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.config.config import global_config # noqa: E402 +from src.memory_graph.short_term_manager import ShortTermMemoryManager # noqa: E402 + + +def resolve_data_dir() -> Path: + """从配置解析记忆数据目录,带安全默认值。""" + memory_cfg = getattr(global_config, "memory", None) + base_dir = getattr(memory_cfg, "data_dir", "data/memory_graph") if memory_cfg else "data/memory_graph" + return PROJECT_ROOT / base_dir + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="清空短期记忆 (示例: python scripts/clear_short_term_memory.py --remove-file)" + ) + parser.add_argument( + "--remove-file", + action="store_true", + help="删除 short_term_memory.json 文件(默认写入空文件)", + ) + return parser.parse_args() + + +async def clear_short_term_memories(remove_file: bool = False) -> None: + data_dir = resolve_data_dir() + storage_file = data_dir / "short_term_memory.json" + + manager = ShortTermMemoryManager(data_dir=data_dir) + await manager.initialize() + + removed_count = len(manager.memories) + + # 清空内存状态 + manager.memories.clear() + manager._memory_id_index.clear() # 内部索引缓存 + manager._similarity_cache.clear() # 相似度缓存 + + if remove_file and storage_file.exists(): + storage_file.unlink() + print(f"Removed storage file: {storage_file}") + else: + # 写入空文件,保留结构 + await manager._save_to_disk() + print(f"Wrote empty short-term memory file: {storage_file}") + + print(f"Cleared {removed_count} short-term memories") + + +async def main() -> None: + args = parse_args() + await clear_short_term_memories(remove_file=args.remove_file) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/memory_graph/README.md b/src/memory_graph/README.md index 6ea034682..6bb3943d2 100644 --- a/src/memory_graph/README.md +++ b/src/memory_graph/README.md @@ -114,6 +114,39 @@ to_transfer = short_term_manager.get_memories_for_transfer() - 🛡️ **泄压机制**:容量 100% 时删除低优先级记忆 - ⚙️ **配置**:`short_term_max_memories = 30` +**溢出策略(新增)**: + +当短期记忆达到容量上限时,支持两种处理策略,可通过配置选择: + +| 策略 | 说明 | 适用场景 | 配置值 | +|------|------|----------|--------| +| **一次性转移** | 容量满时,将**所有记忆**转移到长期存储,然后删除低重要性记忆(importance < 0.6) | 希望保留更多历史信息,适合记忆密集型应用 | `transfer_all`(默认) | +| **选择性清理** | 仅转移高重要性记忆,直接删除低重要性记忆 | 希望快速释放空间,适合性能优先场景 | `selective_cleanup` | + +配置方式: +```toml +[memory] +# 短期记忆溢出策略 +short_term_overflow_strategy = "transfer_all" # 或 "selective_cleanup" +``` + +**行为差异示例**: +```python +# 假设短期记忆已满(30条),其中: +# - 20条高重要性(≥0.6) +# - 10条低重要性(<0.6) + +# 策略1: transfer_all(默认) +# 1. 转移全部30条到长期记忆 +# 2. 删除10条低重要性记忆 +# 结果:短期剩余20条,长期增加30条 + +# 策略2: selective_cleanup +# 1. 仅转移20条高重要性到长期记忆 +# 2. 直接删除10条低重要性记忆 +# 结果:短期剩余20条,长期增加20条 +``` + ### 第3层:长期记忆 (Long-term Memory) **特点**: @@ -176,6 +209,7 @@ perceptual_activation_threshold = 3 # 转移激活阈值 # 短期记忆 short_term_max_memories = 30 # 容量上限 short_term_transfer_threshold = 0.6 # 转移重要性阈值 +short_term_overflow_strategy = "transfer_all" # 溢出策略(transfer_all/selective_cleanup) short_term_enable_force_cleanup = true # 启用泄压 short_term_cleanup_keep_ratio = 0.9 # 泄压保留比例 diff --git a/src/memory_graph/manager_singleton.py b/src/memory_graph/manager_singleton.py index a1d554eae..64b1f0c82 100644 --- a/src/memory_graph/manager_singleton.py +++ b/src/memory_graph/manager_singleton.py @@ -166,6 +166,7 @@ async def initialize_unified_memory_manager(): # 短期记忆配置 short_term_max_memories=getattr(config, "short_term_max_memories", 30), short_term_transfer_threshold=getattr(config, "short_term_transfer_threshold", 0.6), + short_term_overflow_strategy=getattr(config, "short_term_overflow_strategy", "transfer_all"), short_term_enable_force_cleanup=getattr(config, "short_term_enable_force_cleanup", True), short_term_cleanup_keep_ratio=getattr(config, "short_term_cleanup_keep_ratio", 0.9), # 长期记忆配置 diff --git a/src/memory_graph/short_term_manager.py b/src/memory_graph/short_term_manager.py index 45911547c..3a5711430 100644 --- a/src/memory_graph/short_term_manager.py +++ b/src/memory_graph/short_term_manager.py @@ -45,6 +45,7 @@ class ShortTermMemoryManager: llm_temperature: float = 0.2, enable_force_cleanup: bool = False, cleanup_keep_ratio: float = 0.9, + overflow_strategy: str = "transfer_all", ): """ 初始化短期记忆层管理器 @@ -56,6 +57,9 @@ class ShortTermMemoryManager: llm_temperature: LLM 决策的温度参数 enable_force_cleanup: 是否启用泄压功能 cleanup_keep_ratio: 泄压时保留容量的比例(默认0.9表示保留90%) + overflow_strategy: 短期记忆溢出策略 + - "transfer_all": 一次性转移所有记忆到长期记忆,并删除不重要的短期记忆(默认) + - "selective_cleanup": 选择性清理,仅转移重要记忆,直接删除低重要性记忆 """ self.data_dir = data_dir or Path("data/memory_graph") self.data_dir.mkdir(parents=True, exist_ok=True) @@ -66,6 +70,7 @@ class ShortTermMemoryManager: self.llm_temperature = llm_temperature self.enable_force_cleanup = enable_force_cleanup self.cleanup_keep_ratio = cleanup_keep_ratio + self.overflow_strategy = overflow_strategy # 新增:溢出策略 # 核心数据 self.memories: list[ShortTermMemory] = [] @@ -82,6 +87,7 @@ class ShortTermMemoryManager: logger.info( f"短期记忆管理器已创建 (max_memories={max_memories}, " f"transfer_threshold={transfer_importance_threshold:.2f}, " + f"overflow_strategy={overflow_strategy}, " f"force_cleanup={'on' if enable_force_cleanup else 'off'})" ) @@ -639,15 +645,58 @@ class ShortTermMemoryManager: def get_memories_for_transfer(self) -> list[ShortTermMemory]: """ - 获取需要转移到长期记忆的记忆(改进版:转移优先于删除) + 获取需要转移到长期记忆的记忆 - 优化的转移策略: - 1. 优先选择重要性 >= 阈值的记忆进行转移 - 2. 如果高重要性记忆已清空但仍超过容量,则考虑转移低重要性记忆 - 3. 仅当转移不能解决容量问题时,才进行强制删除(由 force_cleanup_overflow 处理) + 根据 overflow_strategy 选择不同的转移策略: + - "transfer_all": 一次性转移所有记忆(满容量时),然后删除低重要性记忆 + - "selective_cleanup": 仅转移高重要性记忆,低重要性记忆直接删除 返回: - 需要转移的记忆列表(优先返回高重要性,次选低重要性) + 需要转移的记忆列表 + """ + if self.overflow_strategy == "transfer_all": + return self._get_transfer_all_strategy() + else: # "selective_cleanup" 或其他值默认使用选择性清理 + return self._get_selective_cleanup_strategy() + + def _get_transfer_all_strategy(self) -> list[ShortTermMemory]: + """ + "一次性转移所有"策略:当短期记忆满了以后,将所有记忆转移到长期记忆 + + 返回: + 需要转移的记忆列表(满容量时返回所有记忆) + """ + # 如果短期记忆已满或接近满,一次性转移所有记忆 + if len(self.memories) >= self.max_memories: + logger.info( + f"转移策略(transfer_all): 短期记忆已满 ({len(self.memories)}/{self.max_memories})," + f"将转移所有 {len(self.memories)} 条记忆到长期记忆" + ) + return self.memories.copy() + + # 如果还没满,检查是否有高重要性记忆需要转移 + high_importance_memories = [ + mem for mem in self.memories + if mem.importance >= self.transfer_importance_threshold + ] + + if high_importance_memories: + logger.debug( + f"转移策略(transfer_all): 发现 {len(high_importance_memories)} 条高重要性记忆待转移" + ) + return high_importance_memories + + logger.debug( + f"转移策略(transfer_all): 无需转移 (当前容量 {len(self.memories)}/{self.max_memories})" + ) + return [] + + def _get_selective_cleanup_strategy(self) -> list[ShortTermMemory]: + """ + "选择性清理"策略(原有策略):优先转移重要记忆,低重要性记忆考虑直接删除 + + 返回: + 需要转移的记忆列表 """ # 单次遍历:同时分类高重要性和低重要性记忆 high_importance_memories = [] @@ -662,7 +711,7 @@ class ShortTermMemoryManager: # 策略1:优先返回高重要性记忆进行转移 if high_importance_memories: logger.debug( - f"转移候选: 发现 {len(high_importance_memories)} 条高重要性记忆待转移" + f"转移策略(selective): 发现 {len(high_importance_memories)} 条高重要性记忆待转移" ) return high_importance_memories @@ -678,14 +727,14 @@ class ShortTermMemoryManager: if to_transfer: logger.debug( - f"转移候选: 发现 {len(to_transfer)} 条低重要性记忆待转移 " + f"转移策略(selective): 发现 {len(to_transfer)} 条低重要性记忆待转移 " f"(当前容量 {len(self.memories)}/{self.max_memories})" ) return to_transfer # 策略3:容量充足,无需转移 logger.debug( - f"转移检查: 无需转移 (当前容量 {len(self.memories)}/{self.max_memories})" + f"转移策略(selective): 无需转移 (当前容量 {len(self.memories)}/{self.max_memories})" ) return [] @@ -739,6 +788,8 @@ class ShortTermMemoryManager: async def clear_transferred_memories(self, memory_ids: list[str]) -> None: """ 清除已转移到长期记忆的记忆 + + 在 "transfer_all" 策略下,还会删除不重要的短期记忆以释放空间 Args: memory_ids: 已转移的记忆ID列表 @@ -753,6 +804,32 @@ class ShortTermMemoryManager: self._similarity_cache.pop(mem_id, None) logger.info(f"清除 {len(memory_ids)} 条已转移的短期记忆") + + # 在 "transfer_all" 策略下,进一步删除不重要的短期记忆 + if self.overflow_strategy == "transfer_all": + # 计算需要删除的低重要性记忆数量 + low_importance_memories = [ + mem for mem in self.memories + if mem.importance < self.transfer_importance_threshold + ] + + if low_importance_memories: + # 按重要性和创建时间排序,删除最不重要的 + low_importance_memories.sort(key=lambda m: (m.importance, m.created_at)) + + # 删除所有低重要性记忆 + to_delete = {mem.id for mem in low_importance_memories} + self.memories = [mem for mem in self.memories if mem.id not in to_delete] + + # 更新索引 + for mem_id in to_delete: + self._memory_id_index.pop(mem_id, None) + self._similarity_cache.pop(mem_id, None) + + logger.info( + f"transfer_all 策略: 额外删除了 {len(to_delete)} 条低重要性记忆 " + f"(重要性 < {self.transfer_importance_threshold:.2f})" + ) # 异步保存 asyncio.create_task(self._save_to_disk()) diff --git a/src/memory_graph/unified_manager.py b/src/memory_graph/unified_manager.py index 42d64a659..2103c083f 100644 --- a/src/memory_graph/unified_manager.py +++ b/src/memory_graph/unified_manager.py @@ -44,6 +44,7 @@ class UnifiedMemoryManager: # 短期记忆配置 short_term_max_memories: int = 30, short_term_transfer_threshold: float = 0.6, + short_term_overflow_strategy: str = "transfer_all", short_term_enable_force_cleanup: bool = False, short_term_cleanup_keep_ratio: float = 0.9, # 长期记忆配置 @@ -98,6 +99,7 @@ class UnifiedMemoryManager: "short_term": { "max_memories": short_term_max_memories, "transfer_importance_threshold": short_term_transfer_threshold, + "overflow_strategy": short_term_overflow_strategy, "enable_force_cleanup": short_term_enable_force_cleanup, "cleanup_keep_ratio": short_term_cleanup_keep_ratio, }, diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 3a759a911..3db0cd02c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -312,6 +312,9 @@ short_term_transfer_threshold = 0.6 # 转移到长期记忆的重要性阈值 short_term_enable_force_cleanup = true # 开启压力泄压(建议高频场景开启) short_term_search_top_k = 5 # 搜索时返回的最大数量 short_term_decay_factor = 0.98 # 衰减因子 +short_term_overflow_strategy = "transfer_all" # 短期记忆溢出策略 +# "transfer_all": 一次性转移所有记忆到长期记忆,然后删除低重要性记忆(默认推荐) +# "selective_cleanup": 选择性清理,仅转移高重要性记忆,直接删除低重要性记忆 # 长期记忆层配置 use_judge = true # 使用评判模型决定是否检索长期记忆