feat: 采用三层内存系统实现统一内存管理器

- 添加了UnifiedMemoryManager,以整合感知层、短期记忆层和长期记忆层。
- 实现了初始化、消息添加和内存搜索功能。
- 引入了记忆从短期存储到长期存储的自动转移机制。
- 开发了用于结构化内存表示的内存格式化工具。
- 增强日志记录功能,以便在内存操作过程中更好地进行追踪。
This commit is contained in:
Windpicker-owo
2025-11-18 16:17:25 +08:00
parent b5cfa41d36
commit dc3ad19809
19 changed files with 1544 additions and 1686 deletions

View File

@@ -0,0 +1,564 @@
"""
感知记忆层管理器 (Perceptual Memory Manager)
负责管理全局记忆堆:
- 消息分块处理
- 向量生成
- TopK 召回
- 激活次数统计
- FIFO 淘汰
"""
import asyncio
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any
import numpy as np
from src.common.logger import get_logger
from src.memory_graph.models import MemoryBlock, PerceptualMemory
from src.memory_graph.utils.embeddings import EmbeddingGenerator
from src.memory_graph.utils.similarity import cosine_similarity
logger = get_logger(__name__)
class PerceptualMemoryManager:
"""
感知记忆层管理器
全局单例,管理所有聊天流的感知记忆块。
"""
def __init__(
self,
data_dir: Path | None = None,
max_blocks: int = 50,
block_size: int = 5,
activation_threshold: int = 3,
recall_top_k: int = 5,
recall_similarity_threshold: float = 0.55,
):
"""
初始化感知记忆层管理器
Args:
data_dir: 数据存储目录
max_blocks: 记忆堆最大容量
block_size: 每个块包含的消息数量
activation_threshold: 激活阈值(召回次数)
recall_top_k: 召回时返回的最大块数
recall_similarity_threshold: 召回的相似度阈值
"""
self.data_dir = data_dir or Path("data/memory_graph/three_tier")
self.data_dir.mkdir(parents=True, exist_ok=True)
# 配置参数
self.max_blocks = max_blocks
self.block_size = block_size
self.activation_threshold = activation_threshold
self.recall_top_k = recall_top_k
self.recall_similarity_threshold = recall_similarity_threshold
# 核心数据
self.perceptual_memory: PerceptualMemory | None = None
self.embedding_generator: EmbeddingGenerator | None = None
# 状态
self._initialized = False
self._save_lock = asyncio.Lock()
logger.info(
f"感知记忆管理器已创建 (max_blocks={max_blocks}, "
f"block_size={block_size}, activation_threshold={activation_threshold})"
)
@property
def memory(self) -> PerceptualMemory:
"""获取感知记忆对象(保证非 None"""
if self.perceptual_memory is None:
raise RuntimeError("感知记忆管理器未初始化")
return self.perceptual_memory
async def initialize(self) -> None:
"""初始化管理器"""
if self._initialized:
logger.warning("感知记忆管理器已经初始化")
return
try:
logger.info("开始初始化感知记忆管理器...")
# 初始化嵌入生成器
self.embedding_generator = EmbeddingGenerator()
# 尝试加载现有数据
await self._load_from_disk()
# 如果没有加载到数据,创建新的
if not self.perceptual_memory:
logger.info("未找到现有数据,创建新的感知记忆堆")
self.perceptual_memory = PerceptualMemory(
max_blocks=self.max_blocks,
block_size=self.block_size,
)
self._initialized = True
logger.info(
f"✅ 感知记忆管理器初始化完成 "
f"(已加载 {len(self.perceptual_memory.blocks)} 个记忆块)"
)
except Exception as e:
logger.error(f"感知记忆管理器初始化失败: {e}", exc_info=True)
raise
async def add_message(self, message: dict[str, Any]) -> MemoryBlock | None:
"""
添加消息到感知记忆层
消息会按 stream_id 组织,同一聊天流的消息才能进入同一个记忆块。
当单个 stream_id 的消息累积到 block_size 条时自动创建记忆块。
Args:
message: 消息字典,需包含以下字段:
- content: str - 消息内容
- sender_id: str - 发送者ID
- sender_name: str - 发送者名称
- timestamp: float - 时间戳
- stream_id: str - 聊天流ID
- 其他可选字段
Returns:
如果创建了新块,返回 MemoryBlock否则返回 None
"""
if not self._initialized:
await self.initialize()
try:
# 添加到待处理消息队列
self.perceptual_memory.pending_messages.append(message)
stream_id = message.get("stream_id", "unknown")
logger.debug(
f"消息已添加到待处理队列 (stream={stream_id[:8]}, "
f"总数={len(self.perceptual_memory.pending_messages)})"
)
# 按 stream_id 检查是否达到创建块的条件
stream_messages = [msg for msg in self.perceptual_memory.pending_messages if msg.get("stream_id") == stream_id]
if len(stream_messages) >= self.block_size:
new_block = await self._create_memory_block(stream_id)
return new_block
return None
except Exception as e:
logger.error(f"添加消息失败: {e}", exc_info=True)
return None
async def _create_memory_block(self, stream_id: str) -> MemoryBlock | None:
"""
从指定 stream_id 的待处理消息创建记忆块
Args:
stream_id: 聊天流ID
Returns:
新创建的记忆块,失败返回 None
"""
try:
# 只取出指定 stream_id 的 block_size 条消息
stream_messages = [msg for msg in self.perceptual_memory.pending_messages if msg.get("stream_id") == stream_id]
if len(stream_messages) < self.block_size:
logger.warning(f"stream {stream_id} 的消息不足 {self.block_size} 条,无法创建块")
return None
# 取前 block_size 条消息
messages = stream_messages[:self.block_size]
# 从 pending_messages 中移除这些消息
for msg in messages:
self.perceptual_memory.pending_messages.remove(msg)
# 合并消息文本
combined_text = self._combine_messages(messages)
# 生成向量
embedding = await self._generate_embedding(combined_text)
# 创建记忆块
block = MemoryBlock(
id=f"block_{uuid.uuid4().hex[:12]}",
messages=messages,
combined_text=combined_text,
embedding=embedding,
metadata={"stream_id": stream_id} # 添加 stream_id 元数据
)
# 添加到记忆堆顶部
self.perceptual_memory.blocks.insert(0, block)
# 更新所有块的位置
for i, b in enumerate(self.perceptual_memory.blocks):
b.position_in_stack = i
# FIFO 淘汰:如果超过最大容量,移除最旧的块
if len(self.perceptual_memory.blocks) > self.max_blocks:
removed_blocks = self.perceptual_memory.blocks[self.max_blocks :]
self.perceptual_memory.blocks = self.perceptual_memory.blocks[: self.max_blocks]
logger.info(f"记忆堆已满,移除 {len(removed_blocks)} 个旧块")
logger.info(
f"✅ 创建新记忆块: {block.id} (stream={stream_id[:8]}, "
f"堆大小={len(self.perceptual_memory.blocks)}/{self.max_blocks})"
)
# 异步保存
asyncio.create_task(self._save_to_disk())
return block
except Exception as e:
logger.error(f"创建记忆块失败: {e}", exc_info=True)
return None
def _combine_messages(self, messages: list[dict[str, Any]]) -> str:
"""
合并多条消息为单一文本
Args:
messages: 消息列表
Returns:
合并后的文本
"""
lines = []
for msg in messages:
# 兼容新旧字段名
sender = msg.get("sender_name") or msg.get("sender") or msg.get("sender_id", "Unknown")
content = msg.get("content", "")
timestamp = msg.get("timestamp", datetime.now())
# 格式化时间
if isinstance(timestamp, (int, float)):
# Unix 时间戳
time_str = datetime.fromtimestamp(timestamp).strftime("%H:%M")
elif isinstance(timestamp, datetime):
time_str = timestamp.strftime("%H:%M")
else:
time_str = str(timestamp)
lines.append(f"[{time_str}] {sender}: {content}")
return "\n".join(lines)
async def _generate_embedding(self, text: str) -> np.ndarray | None:
"""
生成文本向量
Args:
text: 文本内容
Returns:
向量数组,失败返回 None
"""
try:
if not self.embedding_generator:
logger.error("嵌入生成器未初始化")
return None
embedding = await self.embedding_generator.generate(text)
return embedding
except Exception as e:
logger.error(f"生成向量失败: {e}", exc_info=True)
return None
async def recall_blocks(
self,
query_text: str,
top_k: int | None = None,
similarity_threshold: float | None = None,
) -> list[MemoryBlock]:
"""
根据查询召回相关记忆块
Args:
query_text: 查询文本
top_k: 返回的最大块数None 则使用默认值)
similarity_threshold: 相似度阈值None 则使用默认值)
Returns:
召回的记忆块列表(按相似度降序)
"""
if not self._initialized:
await self.initialize()
top_k = top_k or self.recall_top_k
similarity_threshold = similarity_threshold or self.recall_similarity_threshold
try:
# 生成查询向量
query_embedding = await self._generate_embedding(query_text)
if query_embedding is None:
logger.warning("查询向量生成失败,返回空列表")
return []
# 计算所有块的相似度
scored_blocks = []
for block in self.perceptual_memory.blocks:
if block.embedding is None:
continue
similarity = cosine_similarity(query_embedding, block.embedding)
# 过滤低于阈值的块
if similarity >= similarity_threshold:
scored_blocks.append((block, similarity))
# 按相似度降序排序
scored_blocks.sort(key=lambda x: x[1], reverse=True)
# 取 TopK
top_blocks = scored_blocks[:top_k]
# 更新召回计数和位置
recalled_blocks = []
for block, similarity in top_blocks:
block.increment_recall()
recalled_blocks.append(block)
# 检查是否达到激活阈值
if block.recall_count >= self.activation_threshold:
logger.info(
f"🔥 记忆块 {block.id} 被激活!"
f"(召回次数={block.recall_count}, 阈值={self.activation_threshold})"
)
# 将召回的块移到堆顶(保持顺序)
if recalled_blocks:
await self._promote_blocks(recalled_blocks)
# 检查是否有块达到激活阈值(需要转移到短期记忆)
activated_blocks = [
block for block in recalled_blocks
if block.recall_count >= self.activation_threshold
]
if activated_blocks:
logger.info(
f"检测到 {len(activated_blocks)} 个记忆块达到激活阈值 "
f"(recall_count >= {self.activation_threshold}),需要转移到短期记忆"
)
# 设置标记供 unified_manager 处理
for block in activated_blocks:
block.metadata["needs_transfer"] = True
logger.info(
f"召回 {len(recalled_blocks)} 个记忆块 "
f"(top_k={top_k}, threshold={similarity_threshold:.2f})"
)
# 异步保存
asyncio.create_task(self._save_to_disk())
return recalled_blocks
except Exception as e:
logger.error(f"召回记忆块失败: {e}", exc_info=True)
return []
async def _promote_blocks(self, blocks_to_promote: list[MemoryBlock]) -> None:
"""
将召回的块提升到堆顶
Args:
blocks_to_promote: 需要提升的块列表
"""
try:
# 从原位置移除这些块
for block in blocks_to_promote:
if block in self.perceptual_memory.blocks:
self.perceptual_memory.blocks.remove(block)
# 将它们插入到堆顶(保持原有的相对顺序)
for block in reversed(blocks_to_promote):
self.perceptual_memory.blocks.insert(0, block)
# 更新所有块的位置
for i, block in enumerate(self.perceptual_memory.blocks):
block.position_in_stack = i
logger.debug(f"提升 {len(blocks_to_promote)} 个块到堆顶")
except Exception as e:
logger.error(f"提升块失败: {e}", exc_info=True)
def get_activated_blocks(self) -> list[MemoryBlock]:
"""
获取已激活的记忆块(召回次数 >= 激活阈值)
Returns:
激活的记忆块列表
"""
if not self._initialized or not self.perceptual_memory:
return []
activated = [
block
for block in self.perceptual_memory.blocks
if block.recall_count >= self.activation_threshold
]
return activated
async def remove_block(self, block_id: str) -> bool:
"""
移除指定的记忆块(通常在转为短期记忆后调用)
Args:
block_id: 记忆块ID
Returns:
是否成功移除
"""
if not self._initialized:
await self.initialize()
try:
# 查找并移除块
for i, block in enumerate(self.perceptual_memory.blocks):
if block.id == block_id:
self.perceptual_memory.blocks.pop(i)
# 更新剩余块的位置
for j, b in enumerate(self.perceptual_memory.blocks):
b.position_in_stack = j
logger.info(f"移除记忆块: {block_id}")
# 异步保存
asyncio.create_task(self._save_to_disk())
return True
logger.warning(f"记忆块不存在: {block_id}")
return False
except Exception as e:
logger.error(f"移除记忆块失败: {e}", exc_info=True)
return False
def get_statistics(self) -> dict[str, Any]:
"""
获取感知记忆层统计信息
Returns:
统计信息字典
"""
if not self._initialized or not self.perceptual_memory:
return {}
total_messages = sum(len(block.messages) for block in self.perceptual_memory.blocks)
total_recalls = sum(block.recall_count for block in self.perceptual_memory.blocks)
activated_count = len(self.get_activated_blocks())
return {
"total_blocks": len(self.perceptual_memory.blocks),
"max_blocks": self.max_blocks,
"pending_messages": len(self.perceptual_memory.pending_messages),
"total_messages": total_messages,
"total_recalls": total_recalls,
"activated_blocks": activated_count,
"block_size": self.block_size,
"activation_threshold": self.activation_threshold,
}
async def _save_to_disk(self) -> None:
"""保存感知记忆到磁盘"""
async with self._save_lock:
try:
if not self.perceptual_memory:
return
# 保存到 JSON 文件
import orjson
save_path = self.data_dir / "perceptual_memory.json"
data = self.perceptual_memory.to_dict()
save_path.write_bytes(orjson.dumps(data, option=orjson.OPT_INDENT_2))
logger.debug(f"感知记忆已保存到 {save_path}")
except Exception as e:
logger.error(f"保存感知记忆失败: {e}", exc_info=True)
async def _load_from_disk(self) -> None:
"""从磁盘加载感知记忆"""
try:
import orjson
load_path = self.data_dir / "perceptual_memory.json"
if not load_path.exists():
logger.info("未找到感知记忆数据文件")
return
data = orjson.loads(load_path.read_bytes())
self.perceptual_memory = PerceptualMemory.from_dict(data)
# 重新加载向量数据
await self._reload_embeddings()
logger.info(f"感知记忆已从 {load_path} 加载")
except Exception as e:
logger.error(f"加载感知记忆失败: {e}", exc_info=True)
async def _reload_embeddings(self) -> None:
"""重新生成记忆块的向量"""
if not self.perceptual_memory:
return
logger.info("重新生成记忆块向量...")
for block in self.perceptual_memory.blocks:
if block.embedding is None and block.combined_text:
block.embedding = await self._generate_embedding(block.combined_text)
logger.info(f"✅ 向量重新生成完成({len(self.perceptual_memory.blocks)} 个块)")
async def shutdown(self) -> None:
"""关闭管理器"""
if not self._initialized:
return
try:
logger.info("正在关闭感知记忆管理器...")
# 最后一次保存
await self._save_to_disk()
self._initialized = False
logger.info("✅ 感知记忆管理器已关闭")
except Exception as e:
logger.error(f"关闭感知记忆管理器失败: {e}", exc_info=True)
# 全局单例
_perceptual_manager_instance: PerceptualMemoryManager | None = None
def get_perceptual_manager() -> PerceptualMemoryManager:
"""获取感知记忆管理器单例"""
global _perceptual_manager_instance
if _perceptual_manager_instance is None:
_perceptual_manager_instance = PerceptualMemoryManager()
return _perceptual_manager_instance