feat: 重构聊天系统的内存处理与格式化功能

- 更新了DefaultReplyer,采用新的内存格式化工具以提供更优质的内存描述。
- 已移除 Config 类中已弃用的内存配置。
- 在主系统中增强内存系统初始化检查,确保配置正确。
- 优化了MemoryManager,使其可直接使用全局配置进行内存设置。
- 新增了一个内存格式化工具,用于将内存对象转换为自然语言描述。
- 更新了内存工具,提供了更清晰的内存创建与管理指南。
- 精炼插件工具与使用提示,提升用户交互体验与记忆准确性。
- 根据内存系统结构的变化调整了机器人配置模板。
This commit is contained in:
Windpicker-owo
2025-11-06 08:47:18 +08:00
parent 7bb0248ca2
commit b6a693895b
13 changed files with 691 additions and 118 deletions

View File

@@ -61,6 +61,34 @@ class ExpressorModel:
if cid not in self.nb.token_counts:
self.nb.token_counts[cid] = defaultdict(float)
def remove_candidate(self, cid: str) -> bool:
"""
删除候选文本
Args:
cid: 候选ID
Returns:
是否删除成功
"""
removed = False
if cid in self._candidates:
del self._candidates[cid]
removed = True
if cid in self._situations:
del self._situations[cid]
# 从nb模型中删除
if cid in self.nb.cls_counts:
del self.nb.cls_counts[cid]
if cid in self.nb.token_counts:
del self.nb.token_counts[cid]
return removed
def predict(self, text: str, k: int | None = None) -> tuple[str | None, dict[str, float]]:
"""
直接对所有候选进行朴素贝叶斯评分

View File

@@ -36,6 +36,8 @@ class StyleLearner:
# 动态风格管理
self.max_styles = 2000 # 每个chat_id最多2000个风格
self.cleanup_threshold = 0.9 # 达到90%容量时触发清理
self.cleanup_ratio = 0.2 # 每次清理20%的风格
self.style_to_id: dict[str, str] = {} # style文本 -> style_id
self.id_to_style: dict[str, str] = {} # style_id -> style文本
self.id_to_situation: dict[str, str] = {} # style_id -> situation文本
@@ -45,6 +47,7 @@ class StyleLearner:
self.learning_stats = {
"total_samples": 0,
"style_counts": {},
"style_last_used": {}, # 记录每个风格最后使用时间
"last_update": time.time(),
}
@@ -66,10 +69,19 @@ class StyleLearner:
if style in self.style_to_id:
return True
# 检查是否超过最大限制
if len(self.style_to_id) >= self.max_styles:
logger.warning(f"已达到最大风格数量限制 ({self.max_styles})")
return False
# 检查是否需要清理
current_count = len(self.style_to_id)
cleanup_trigger = int(self.max_styles * self.cleanup_threshold)
if current_count >= cleanup_trigger:
if current_count >= self.max_styles:
# 已经达到最大限制,必须清理
logger.warning(f"已达到最大风格数量限制 ({self.max_styles}),开始清理")
self._cleanup_styles()
elif current_count >= cleanup_trigger:
# 接近限制,提前清理
logger.info(f"风格数量达到 {current_count}/{self.max_styles},触发预防性清理")
self._cleanup_styles()
# 生成新的style_id
style_id = f"style_{self.next_style_id}"
@@ -94,6 +106,80 @@ class StyleLearner:
logger.error(f"添加风格失败: {e}")
return False
def _cleanup_styles(self):
"""
清理低价值的风格,为新风格腾出空间
清理策略:
1. 综合考虑使用次数和最后使用时间
2. 删除得分最低的风格
3. 默认清理 cleanup_ratio (20%) 的风格
"""
try:
current_time = time.time()
cleanup_count = max(1, int(len(self.style_to_id) * self.cleanup_ratio))
# 计算每个风格的价值分数
style_scores = []
for style_id in self.style_to_id.values():
# 使用次数
usage_count = self.learning_stats["style_counts"].get(style_id, 0)
# 最后使用时间(越近越好)
last_used = self.learning_stats["style_last_used"].get(style_id, 0)
time_since_used = current_time - last_used if last_used > 0 else float('inf')
# 综合分数:使用次数越多越好,距离上次使用时间越短越好
# 使用对数来平滑使用次数的影响
import math
usage_score = math.log1p(usage_count) # log(1 + count)
# 时间分数:转换为天数,使用指数衰减
days_unused = time_since_used / 86400 # 转换为天
time_score = math.exp(-days_unused / 30) # 30天衰减因子
# 综合分数80%使用频率 + 20%时间新鲜度
total_score = 0.8 * usage_score + 0.2 * time_score
style_scores.append((style_id, total_score, usage_count, days_unused))
# 按分数排序,分数低的先删除
style_scores.sort(key=lambda x: x[1])
# 删除分数最低的风格
deleted_styles = []
for style_id, score, usage, days in style_scores[:cleanup_count]:
style_text = self.id_to_style.get(style_id)
if style_text:
# 从映射中删除
del self.style_to_id[style_text]
del self.id_to_style[style_id]
if style_id in self.id_to_situation:
del self.id_to_situation[style_id]
# 从统计中删除
if style_id in self.learning_stats["style_counts"]:
del self.learning_stats["style_counts"][style_id]
if style_id in self.learning_stats["style_last_used"]:
del self.learning_stats["style_last_used"][style_id]
# 从expressor模型中删除
self.expressor.remove_candidate(style_id)
deleted_styles.append((style_text[:30], usage, f"{days:.1f}"))
logger.info(
f"风格清理完成: 删除了 {len(deleted_styles)}/{len(style_scores)} 个风格,"
f"剩余 {len(self.style_to_id)} 个风格"
)
# 记录前5个被删除的风格用于调试
if deleted_styles:
logger.debug(f"被删除的风格样例(前5): {deleted_styles[:5]}")
except Exception as e:
logger.error(f"清理风格失败: {e}", exc_info=True)
def learn_mapping(self, up_content: str, style: str) -> bool:
"""
学习一个up_content到style的映射
@@ -118,9 +204,11 @@ class StyleLearner:
self.expressor.update_positive(up_content, style_id)
# 更新统计
current_time = time.time()
self.learning_stats["total_samples"] += 1
self.learning_stats["style_counts"][style_id] += 1
self.learning_stats["last_update"] = time.time()
self.learning_stats["style_last_used"][style_id] = current_time # 更新最后使用时间
self.learning_stats["last_update"] = current_time
logger.debug(f"学习映射成功: {up_content[:20]}... -> {style}")
return True
@@ -171,6 +259,10 @@ class StyleLearner:
else:
logger.warning(f"跳过无法转换的style_id: {sid}")
# 更新最后使用时间(仅针对最佳风格)
if best_style_id:
self.learning_stats["style_last_used"][best_style_id] = time.time()
logger.debug(
f"预测成功: up_content={up_content[:30]}..., "
f"best_style={best_style}, top3_scores={list(style_scores.items())[:3]}"
@@ -208,6 +300,30 @@ class StyleLearner:
"""
return list(self.style_to_id.keys())
def cleanup_old_styles(self, ratio: float | None = None) -> int:
"""
手动清理旧风格
Args:
ratio: 清理比例如果为None则使用默认的cleanup_ratio
Returns:
清理的风格数量
"""
old_count = len(self.style_to_id)
if ratio is not None:
old_cleanup_ratio = self.cleanup_ratio
self.cleanup_ratio = ratio
self._cleanup_styles()
self.cleanup_ratio = old_cleanup_ratio
else:
self._cleanup_styles()
new_count = len(self.style_to_id)
cleaned = old_count - new_count
logger.info(f"手动清理完成: chat_id={self.chat_id}, 清理了 {cleaned} 个风格")
return cleaned
def apply_decay(self, factor: float | None = None):
"""
应用知识衰减
@@ -241,6 +357,11 @@ class StyleLearner:
import pickle
meta_path = os.path.join(save_dir, "meta.pkl")
# 确保 learning_stats 包含所有必要字段
if "style_last_used" not in self.learning_stats:
self.learning_stats["style_last_used"] = {}
meta_data = {
"style_to_id": self.style_to_id,
"id_to_style": self.id_to_style,
@@ -295,6 +416,10 @@ class StyleLearner:
self.id_to_situation = meta_data["id_to_situation"]
self.next_style_id = meta_data["next_style_id"]
self.learning_stats = meta_data["learning_stats"]
# 确保旧数据兼容:如果没有 style_last_used 字段,添加它
if "style_last_used" not in self.learning_stats:
self.learning_stats["style_last_used"] = {}
logger.info(f"StyleLearner加载成功: {save_dir}")
return True
@@ -398,6 +523,26 @@ class StyleLearnerManager:
logger.info(f"保存所有StyleLearner {'成功' if success else '部分失败'}")
return success
def cleanup_all_old_styles(self, ratio: float | None = None) -> dict[str, int]:
"""
对所有学习器清理旧风格
Args:
ratio: 清理比例
Returns:
{chat_id: 清理数量}
"""
cleanup_results = {}
for chat_id, learner in self.learners.items():
cleaned = learner.cleanup_old_styles(ratio)
if cleaned > 0:
cleanup_results[chat_id] = cleaned
total_cleaned = sum(cleanup_results.values())
logger.info(f"清理所有StyleLearner完成: 总共清理了 {total_cleaned} 个风格")
return cleanup_results
def apply_decay_all(self, factor: float | None = None):
"""
对所有学习器应用知识衰减

View File

@@ -590,17 +590,29 @@ class DefaultReplyer:
if memories:
logger.info(f"[记忆图] 检索到 {len(memories)} 条相关记忆")
# 使用新的格式化工具构建完整的记忆描述
from src.memory_graph.utils.memory_formatter import (
format_memory_for_prompt,
get_memory_type_label,
)
for memory in memories:
topic = memory.metadata.get("topic", "")
mem_type = memory.metadata.get("memory_type", "未知")
if topic:
# 使用格式化工具生成完整的主谓宾描述
content = format_memory_for_prompt(memory, include_metadata=False)
# 获取记忆类型
mem_type = memory.memory_type.value if memory.memory_type else "未知"
if content:
all_memories.append({
"content": topic,
"content": content,
"memory_type": mem_type,
"importance": memory.importance,
"relevance": 0.7,
"source": "memory_graph",
})
logger.debug(f"[记忆构建] 格式化记忆: [{mem_type}] {content[:50]}...")
else:
logger.debug("[记忆图] 未找到相关记忆")
except Exception as e:
@@ -634,8 +646,13 @@ class DefaultReplyer:
logger.debug(f"[记忆构建] 空记忆详情: {running_memory}")
continue
# 使用全局记忆类型映射表
chinese_type = get_memory_type_chinese_label(memory_type)
# 使用记忆图的类型映射(优先)或全局映射
try:
from src.memory_graph.utils.memory_formatter import get_memory_type_label
chinese_type = get_memory_type_label(memory_type)
except ImportError:
# 回退到全局映射
chinese_type = get_memory_type_chinese_label(memory_type)
# 提取纯净内容(如果包含旧格式的元数据)
clean_content = content

View File

@@ -396,7 +396,7 @@ class Config(ValidatedConfigBase):
notice: NoticeConfig = Field(..., description="Notice消息配置")
emoji: EmojiConfig = Field(..., description="表情配置")
expression: ExpressionConfig = Field(..., description="表达配置")
memory: Optional[MemoryConfig] = Field(default=None, description="记忆配置(旧版,已废弃)")
memory: Optional[MemoryConfig] = Field(default=None, description="记忆配置")
mood: MoodConfig = Field(..., description="情绪配置")
reaction: ReactionConfig = Field(default_factory=ReactionConfig, description="反应规则配置")
chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置")

View File

@@ -251,7 +251,7 @@ class MainSystem:
# 停止增强记忆系统
try:
if global_config.memory.enable_memory:
if global_config.memory and getattr(global_config.memory, 'enable', False):
cleanup_tasks.append(("增强记忆系统", self.memory_manager.shutdown()))
except Exception as e:
logger.error(f"准备停止增强记忆系统时出错: {e}")
@@ -469,7 +469,7 @@ MoFox_Bot(第三方修改版)
task.add_done_callback(_background_tasks.discard)
# 初始化增强记忆系统
if global_config.memory.enable_memory:
if global_config.memory and getattr(global_config.memory, 'enable', False):
from src.chat.memory_system.memory_system import initialize_memory_system
await self._safe_init("增强记忆系统", initialize_memory_system)()
await self._safe_init("记忆管理器", self.memory_manager.initialize)()

View File

@@ -133,8 +133,10 @@ class MemoryGraphConfig:
def from_bot_config(cls, bot_config) -> MemoryGraphConfig:
"""从bot_config加载配置"""
try:
# 尝试获取配置
if hasattr(bot_config, 'memory_graph'):
# 尝试获取配置优先使用memory兼容memory_graph
if hasattr(bot_config, 'memory') and bot_config.memory is not None:
mg_config = bot_config.memory
elif hasattr(bot_config, 'memory_graph'):
mg_config = bot_config.memory_graph
config = cls(

View File

@@ -14,7 +14,7 @@ from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple
from src.memory_graph.config import MemoryGraphConfig
from src.config.config import global_config
from src.memory_graph.core.builder import MemoryBuilder
from src.memory_graph.core.extractor import MemoryExtractor
from src.memory_graph.models import Memory, MemoryEdge, MemoryNode, MemoryType, NodeType, EdgeType
@@ -41,18 +41,20 @@ class MemoryManager:
def __init__(
self,
config: Optional[MemoryGraphConfig] = None,
data_dir: Optional[Path] = None,
):
"""
初始化记忆管理器
Args:
config: 记忆图配置
data_dir: 数据目录
data_dir: 数据目录可选默认从global_config读取
"""
self.config = config or MemoryGraphConfig()
self.data_dir = data_dir or Path("data/memory_graph")
# 直接使用 global_config.memory
if not global_config.memory or not getattr(global_config.memory, 'enable', False):
raise ValueError("记忆系统未启用,请在配置文件中启用 [memory] enable = true")
self.config = global_config.memory
self.data_dir = data_dir or Path(getattr(self.config, 'data_dir', 'data/memory_graph'))
# 存储组件
self.vector_store: Optional[VectorStore] = None
@@ -69,10 +71,10 @@ class MemoryManager:
self._initialized = False
self._last_maintenance = datetime.now()
self._maintenance_task: Optional[asyncio.Task] = None
self._maintenance_interval_hours = self.config.consolidation_interval_hours # 从配置读取
self._maintenance_interval_hours = getattr(self.config, 'consolidation_interval_hours', 1.0)
self._maintenance_schedule_id: Optional[str] = None # 调度任务ID
logger.info(f"记忆管理器已创建 (data_dir={data_dir}, enable={self.config.enable})")
logger.info(f"记忆管理器已创建 (data_dir={self.data_dir}, enable={getattr(self.config, 'enable', False)})")
async def initialize(self) -> None:
"""
@@ -93,8 +95,12 @@ class MemoryManager:
# 1. 初始化存储层
self.data_dir.mkdir(parents=True, exist_ok=True)
# 获取存储配置
storage_config = getattr(self.config, 'storage', None)
vector_collection_name = getattr(storage_config, 'vector_collection_name', 'memory_graph') if storage_config else 'memory_graph'
self.vector_store = VectorStore(
collection_name=self.config.storage.vector_collection_name,
collection_name=vector_collection_name,
data_dir=self.data_dir,
)
await self.vector_store.initialize()
@@ -557,7 +563,8 @@ class MemoryManager:
# 计算时间衰减
last_access_dt = datetime.fromisoformat(last_access)
hours_passed = (now - last_access_dt).total_seconds() / 3600
decay_factor = self.config.activation_decay_rate ** (hours_passed / 24)
decay_rate = getattr(self.config, 'activation_decay_rate', 0.95)
decay_factor = decay_rate ** (hours_passed / 24)
current_activation = activation_info.get("level", 0.0) * decay_factor
else:
current_activation = 0.0
@@ -576,13 +583,16 @@ class MemoryManager:
# 激活传播:激活相关记忆
if strength > 0.1: # 只有足够强的激活才传播
propagation_depth = getattr(self.config, 'activation_propagation_depth', 2)
related_memories = self._get_related_memories(
memory_id,
max_depth=self.config.activation_propagation_depth
max_depth=propagation_depth
)
propagation_strength = strength * self.config.activation_propagation_strength
propagation_strength_factor = getattr(self.config, 'activation_propagation_strength', 0.5)
propagation_strength = strength * propagation_strength_factor
for related_id in related_memories[:self.config.max_related_memories]:
max_related = getattr(self.config, 'max_related_memories', 5)
for related_id in related_memories[:max_related]:
await self.activate_memory(related_id, propagation_strength)
# 保存更新
@@ -681,7 +691,8 @@ class MemoryManager:
continue
# 跳过高重要性记忆
if memory.importance >= self.config.forgetting_min_importance:
min_importance = getattr(self.config, 'forgetting_min_importance', 7.0)
if memory.importance >= min_importance:
continue
# 计算当前激活度
@@ -876,8 +887,8 @@ class MemoryManager:
# 使用配置值或参数覆盖
time_window_hours = time_window_hours if time_window_hours is not None else 24
max_candidates = max_candidates if max_candidates is not None else self.config.auto_link_max_candidates
min_confidence = min_confidence if min_confidence is not None else self.config.auto_link_min_confidence
max_candidates = max_candidates if max_candidates is not None else getattr(self.config, 'auto_link_max_candidates', 10)
min_confidence = min_confidence if min_confidence is not None else getattr(self.config, 'auto_link_min_confidence', 0.7)
try:
logger.info(f"开始自动关联记忆 (时间窗口={time_window_hours}h)...")
@@ -1249,22 +1260,22 @@ class MemoryManager:
}
# 1. 记忆整理(合并相似记忆)
if self.config.consolidation_enabled:
if getattr(self.config, 'consolidation_enabled', False):
consolidate_result = await self.consolidate_memories(
similarity_threshold=self.config.consolidation_similarity_threshold,
time_window_hours=self.config.consolidation_time_window_hours
similarity_threshold=getattr(self.config, 'consolidation_similarity_threshold', 0.9),
time_window_hours=getattr(self.config, 'consolidation_time_window_hours', 24.0)
)
result["consolidated"] = consolidate_result.get("merged_count", 0)
# 2. 自动关联记忆(发现和建立关系)
if self.config.auto_link_enabled:
if getattr(self.config, 'auto_link_enabled', True):
link_result = await self.auto_link_memories()
result["linked"] = link_result.get("linked_count", 0)
# 3. 自动遗忘
if self.config.forgetting_enabled:
if getattr(self.config, 'forgetting_enabled', True):
forgotten_count = await self.auto_forget_memories(
threshold=self.config.forgetting_activation_threshold
threshold=getattr(self.config, 'forgetting_activation_threshold', 0.1)
)
result["forgotten"] = forgotten_count

View File

@@ -21,17 +21,17 @@ _initialized: bool = False
async def initialize_memory_manager(
data_dir: Optional[Path | str] = None,
config = None,
) -> Optional[MemoryManager]:
"""
初始化全局 MemoryManager
直接从 global_config.memory 读取配置
Args:
data_dir: 数据目录,默认使用 data/memory_graph
config: MemoryGraphConfig 或 bot_config 实例
data_dir: 数据目录(可选,默认从配置读取)
Returns:
MemoryManager 实例
MemoryManager 实例,如果禁用则返回 None
"""
global _memory_manager, _initialized
@@ -40,26 +40,10 @@ async def initialize_memory_manager(
return _memory_manager
try:
from src.memory_graph.config import MemoryGraphConfig
# 处理配置
if config is None:
# 尝试从全局配置加载
try:
from src.config.config import global_config
memory_config = MemoryGraphConfig.from_bot_config(global_config)
logger.info("从 bot_config 加载 memory_graph 配置")
except Exception as e:
logger.warning(f"无法从 bot_config 加载配置,使用默认配置: {e}")
memory_config = MemoryGraphConfig()
elif isinstance(config, MemoryGraphConfig):
memory_config = config
else:
# 假设是 bot_config
memory_config = MemoryGraphConfig.from_bot_config(config)
from src.config.config import global_config
# 检查是否启用
if not memory_config.enable:
if not global_config.memory or not getattr(global_config.memory, 'enable', False):
logger.info("记忆图系统已在配置中禁用")
_initialized = False
_memory_manager = None
@@ -67,13 +51,13 @@ async def initialize_memory_manager(
# 处理数据目录
if data_dir is None:
data_dir = memory_config.data_dir
elif isinstance(data_dir, str):
data_dir = getattr(global_config.memory, 'data_dir', 'data/memory_graph')
if isinstance(data_dir, str):
data_dir = Path(data_dir)
logger.info(f"正在初始化全局 MemoryManager (data_dir={data_dir})...")
_memory_manager = MemoryManager(config=memory_config, data_dir=data_dir)
_memory_manager = MemoryManager(data_dir=data_dir)
await _memory_manager.initialize()
_initialized = True

View File

@@ -19,15 +19,47 @@ class CreateMemoryTool(BaseTool):
"""创建记忆工具"""
name = "create_memory"
description = "创建一个新的记忆。记忆由主体、类型、主题、客体(可选)和属性组成。用于记录重要的信息、事件、想法等"
description = """记录对话中有价值的信息,构建长期记忆
## 应该记录的内容类型:
### 高优先级记录importance 0.7-1.0
- 个人核心信息:姓名、年龄、职业、学历、联系方式
- 重要关系:家人、亲密朋友、恋人关系
- 核心目标:人生规划、职业目标、重要决定
- 关键事件:毕业、入职、搬家、重要成就
### 中等优先级importance 0.5-0.7
- 生活状态:工作内容、学习情况、日常习惯
- 兴趣偏好:喜欢/不喜欢的事物、消费偏好
- 观点态度:价值观、对事物的看法
- 技能知识:掌握的技能、专业领域
- 一般事件:日常活动、例行任务
### 低优先级importance 0.3-0.5
- 临时状态:今天心情、当前活动
- 一般评价:对产品/服务的简单评价
- 琐碎事件:买东西、看电影等常规活动
### ❌ 不应记录
- 单纯招呼语:"你好""再见""谢谢"
- 无意义语气词:"""""好的"
- 纯粹回复确认:没有信息量的回应
## 记忆拆分原则
一句话多个信息点 → 多次调用创建多条记忆
示例:"我最近在学Python想找数据分析的工作"
→ 调用1{{subject:"[从历史提取真实名字]", memory_type:"事实", topic:"学习", object:"Python", attributes:{{时间:"最近", 状态:"进行中"}}, importance:0.7}}
→ 调用2{{subject:"[从历史提取真实名字]", memory_type:"目标", topic:"求职", object:"数据分析岗位", attributes:{{状态:"计划中"}}, importance:0.8}}"""
parameters: ClassVar[list[tuple[str, ToolParamType, str, bool, list[str] | None]]] = [
("subject", ToolParamType.STRING, "记忆主体,通常是'''用户'或具体的人名", True, None),
("memory_type", ToolParamType.STRING, "记忆类型", True, ["事件", "事实", "关系", "观点"]),
("topic", ToolParamType.STRING, "记忆的主题,即发生的事情或状态", True, None),
("object", ToolParamType.STRING, "记忆的客体,即主题作用的对象(可选)", False, None),
("attributes", ToolParamType.STRING, "记忆的属性JSON格式字符串),如 {\"时间\":\"今天\",\"地点\":\"家里\"}", False, None),
("importance", ToolParamType.FLOAT, "记忆的重要性0.0-1.0),默认0.5", False, None),
("subject", ToolParamType.STRING, "记忆主体(重要!)。从对话历史中提取真实发送人名字。示例:如果看到'Prou(12345678): 我喜欢...'subject应填'Prou';如果看到'张三: 我在...'subject应填'张三'。❌禁止使用'用户'这种泛指,必须用具体名字!", True, None),
("memory_type", ToolParamType.STRING, "记忆类型。【事件】=有明确时间点的动作(昨天吃饭、明天开会)【事实】=稳定状态(职业是程序员、住在北京)【观点】=主观看法(喜欢/讨厌/认为)【关系】=人际关系(朋友、同事)", True, ["事件", "事实", "关系", "观点"]),
("topic", ToolParamType.STRING, "记忆的核心内容(做什么/是什么状态/什么关系)。必须明确、具体,包含关键动词或状态", True, None),
("object", ToolParamType.STRING, "记忆涉及的对象或目标。如果topic已经很完整可以不填如果有明确对象建议填写", False, None),
("attributes", ToolParamType.STRING, "详细属性JSON格式字符串。强烈建议包含:时间(具体到日期)、地点、状态、原因等上下文信息。例:{\"时间\":\"2025-11-06\",\"地点\":\"公司\",\"状态\":\"进行中\",\"原因\":\"项目需要\"}", False, None),
("importance", ToolParamType.FLOAT, "重要性评分 0.0-1.0。参考日常琐事0.3-0.4一般对话0.5-0.6重要信息0.7-0.8核心记忆0.9-1.0。不确定时用0.5", False, None),
]
available_for_llm = True

View File

@@ -73,83 +73,92 @@ class MemoryTools:
"""
return {
"name": "create_memory",
"description": """创建一个新的记忆节点。
"description": """创建一个新的记忆节点,记录对话中有价值的信息
⚠️ 记忆创建原则(必须遵守):
1. **价值判断**:只创建具有长期价值的关键信息,避免记录日常闲聊、礼貌用语、重复信息
2. **细粒度原则**:每条记忆只包含一个明确的事实/事件/观点,避免泛化
3. **原子性**:如果一句话包含多个重要信息点,拆分成多条独立记忆
4. **具体性**:记录具体的人、事、物、时间、地点,避免模糊描述
🎯 **核心原则**:主动记录、积极构建、丰富细节
❌ 不应创建记忆的情况
- 普通问候、感谢、确认等礼貌性对话
- 已存在的重复信息
- 临时性、一次性的琐碎信息
- 纯粹的功能操作指令(如"帮我查一下"
- 缺乏上下文的碎片化信息
✅ **优先创建记忆的场景**(鼓励记录)
1. **个人信息**:姓名、昵称、年龄、职业、身份、所在地、联系方式等
2. **兴趣爱好**:喜欢/不喜欢的事物、娱乐偏好、运动爱好、饮食口味等
3. **生活状态**:工作学习状态、生活习惯、作息时间、日常安排等
4. **经历事件**:正在做的事、完成的任务、参与的活动、遇到的问题等
5. **观点态度**:对事物的看法、价值观、情绪表达、评价意见等
6. **计划目标**:未来打算、学习计划、工作目标、待办事项等
7. **人际关系**:提到的朋友、家人、同事、认识的人等
8. **技能知识**:掌握的技能、学习的知识、专业领域、使用的工具等
9. **物品资源**:拥有的物品、使用的设备、喜欢的品牌等
10. **时间地点**:重要时间节点、常去的地点、活动场所等
✅ 应该创建记忆的情况
- 用户的个人信息(姓名、职业、兴趣、联系方式等
- 重要事件(项目进展、重大决定、关键行动等
- 长期偏好/观点(喜好、价值观、习惯等
- 人际关系变化(新朋友、合作关系等)
- 具体计划/目标(明确的待办事项、长期目标等)
⚠️ **暂不创建的情况**(仅限以下)
- 纯粹的招呼语(单纯的"你好""再见"
- 完全无意义的语气词(单纯的""""
- 明确的系统指令(如"切换模式""重启"
📝 拆分示例
- "用户喜欢编程最近在学Python和机器学习" → 过于泛化
- ✅ 拆分为3条
1. "用户喜欢编程"(观点)
2. "用户正在学习Python"(事件)
3. "用户正在学习机器学习"(事件)
<EFBFBD> **记忆拆分建议**
- 一句话包含多个信息点 → 拆成多条记忆(更利于后续检索)
- 例如:"我最近在学Python和机器学习想找工作"
→ 拆成3条
1. "用户正在学习Python"(事件)
2. "用户正在学习机器学习"(事件)
3. "用户想找工作"(事件/目标)
记忆结构:主体 + 类型 + 主题 + 客体(可选)+ 属性""",
📌 **记忆质量建议**
- 记录时尽量补充时间("今天""最近""昨天"等)
- 包含具体细节(越具体越好)
- 主体明确(优先使用"用户"或具体人名,避免""
记忆结构:主体 + 类型 + 主题 + 客体(可选)+ 属性(越详细越好)""",
"parameters": {
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "记忆的主体,通常是'用户'或具体的人名(避免使用''",
"description": "记忆的主体(谁的信息):\n- 对话中的用户统一使用'用户'\n- 提到的具体人物使用其名字(如'小明''张三'\n- 避免使用''''等代词",
},
"memory_type": {
"type": "string",
"enum": ["事件", "事实", "关系", "观点"],
"description": "记忆类型:\n- 事件时间绑定的具体动作(如'完成项目''学习课程'\n- 事实:稳定的客观状态(如'职业是工程师''住在北京'\n- 关系:人际关系(如'识了朋友''同事关系'\n- 观点:主观评价/偏好(如'喜欢Python''认为AI很重要'",
"description": "选择最合适的记忆类型:\n\n事件时间相关的动作或发生的事(用'正在''完成''参加'等动词\n正在学习Python、完成了项目、参加会议、去旅行\n\n【事实】相对稳定的客观信息(用''''''等描述状态\n 例:职业是工程师、住在北京、有一只猫、会说英语\n\n【观点】主观看法、喜好、态度(用'喜欢''''觉得'\n喜欢Python、认为AI很重要、觉得累、讨厌加班\n\n【关系】人与人之间的关系\n 例:认识了朋友、是同事、家人关系",
},
"topic": {
"type": "string",
"description": "记忆的核心主题,必须具体明确('学习PyTorch框架'而非'学习编程'",
"description": "记忆的核心内容(做什么/是什么/关于什么):\n- 尽量具体明确('学习Python编程' 优于 '学习'\n- 包含关键动词或核心概念\n- 可以包含时间状态('正在学习''已完成''计划做'",
},
"object": {
"type": "string",
"description": "记忆的客体/对象,作为主题的补充说明(如主题是'学习',客体可以是'PyTorch框架'",
"description": "可选:记忆涉及的对象或目标:\n- 事件的对象(学习的是什么、购买的是什么)\n- 观点的对象(喜欢的是什么、讨厌的是什么)\n- 可以留空如果topic已经足够完整",
},
"attributes": {
"type": "object",
"description": "记忆的具体属性(尽量填写以增加记忆的信息密度)",
"description": "记忆的详细属性(建议尽量填写,越详细越好):",
"properties": {
"时间": {
"type": "string",
"description": "具体时间表达式,如'2025-11-05''今天下午''最近一周''3天前'",
"description": "时间信息(强烈建议填写):\n- 具体日期:'2025-11-05''2025年11月'\n- 相对时间:'今天''昨天''上周''最近''3天前'\n- 时间段:'今天下午''上个月''这学期'",
},
"地点": {
"type": "string",
"description": "具体地点(如果相关)"
"description": "地点信息(如涉及):\n- 具体地址、城市名、国家\n- 场所类型:'在家''公司''学校''咖啡店'"
},
"原因": {
"type": "string",
"description": "事件发生的原因或动机(如明确)"
"description": "为什么这样做/这样想(如明确提到"
},
"方式": {
"type": "string",
"description": "完成的方式或途径(如果相关"
"description": "怎么做的/通过什么方式(如明确提到"
},
"结果": {
"type": "string",
"description": "事件的结果或影响(如果已知"
"description": "结果如何/产生什么影响(如明确提到"
},
"状态": {
"type": "string",
"description": "当前状态(如'进行中''已完成''计划中'"
"description": "当前进展:'进行中''已完成''计划中''暂停'"
},
"程度": {
"type": "string",
"description": "程度描述(如'非常''比较''有点''不太'"
},
},
"additionalProperties": True,
@@ -158,7 +167,7 @@ class MemoryTools:
"type": "number",
"minimum": 0.0,
"maximum": 1.0,
"description": "记忆的重要性评分(0.0-1.0\n- 0.3-0.4: 次要信息\n- 0.5-0.6: 一般信息\n- 0.7-0.8: 重要信息(用户明确表达的偏好、重要事件\n- 0.9-1.0: 关键信息(核心个人信息、重大决定、强烈偏好\n默认0.5",
"description": "重要性评分(默认0.5日常对话建议0.5-0.7\n\n0.3-0.4: 次要细节(偶然提及的琐事)\n0.5-0.6: 日常信息(一般性的分享、普通爱好)← 推荐默认值\n0.7-0.8: 重要信息(明确的偏好、重要计划、核心爱好\n0.9-1.0: 关键信息(身份信息、重大决定、强烈情感\n\n💡 建议日常对话中大部分记忆使用0.5-0.6,除非用户特别强调",
},
},
"required": ["subject", "memory_type", "topic"],

View File

@@ -0,0 +1,323 @@
"""
记忆格式化工具
用于将记忆图系统的Memory对象转换为适合提示词的自然语言描述
"""
import logging
from typing import Optional, List, Dict, Any
from datetime import datetime
from src.memory_graph.models import Memory, MemoryNode, NodeType, EdgeType, MemoryType
logger = logging.getLogger(__name__)
def format_memory_for_prompt(memory: Memory, include_metadata: bool = False) -> str:
"""
将记忆对象格式化为适合提示词的自然语言描述
根据记忆的图结构,构建完整的主谓宾描述,包含:
- 主语subject node
- 谓语/动作topic node
- 宾语/对象object node如果存在
- 属性信息attributes如时间、地点等
- 关系信息(记忆之间的关系)
Args:
memory: 记忆对象
include_metadata: 是否包含元数据(时间、重要性等)
Returns:
格式化后的自然语言描述
"""
try:
# 1. 获取主体节点(主语)
subject_node = memory.get_subject_node()
if not subject_node:
logger.warning(f"记忆 {memory.id} 缺少主体节点")
return "(记忆格式错误:缺少主体)"
subject_text = subject_node.content
# 2. 查找主题节点(谓语/动作)
topic_node = None
memory_type_relation = None
for edge in memory.edges:
if edge.edge_type == EdgeType.MEMORY_TYPE and edge.source_id == memory.subject_id:
topic_node = memory.get_node_by_id(edge.target_id)
memory_type_relation = edge.relation
break
if not topic_node:
logger.warning(f"记忆 {memory.id} 缺少主题节点")
return f"{subject_text}(记忆格式错误:缺少主题)"
topic_text = topic_node.content
# 3. 查找客体节点(宾语)和核心关系
object_node = None
core_relation = None
for edge in memory.edges:
if edge.edge_type == EdgeType.CORE_RELATION and edge.source_id == topic_node.id:
object_node = memory.get_node_by_id(edge.target_id)
core_relation = edge.relation if edge.relation else ""
break
# 4. 收集属性节点
attributes: Dict[str, str] = {}
for edge in memory.edges:
if edge.edge_type == EdgeType.ATTRIBUTE:
# 查找属性节点和值节点
attr_node = memory.get_node_by_id(edge.target_id)
if attr_node and attr_node.node_type == NodeType.ATTRIBUTE:
# 查找这个属性的值
for value_edge in memory.edges:
if (value_edge.edge_type == EdgeType.ATTRIBUTE
and value_edge.source_id == attr_node.id):
value_node = memory.get_node_by_id(value_edge.target_id)
if value_node and value_node.node_type == NodeType.VALUE:
attributes[attr_node.content] = value_node.content
break
# 5. 构建自然语言描述
parts = []
# 主谓宾结构
if object_node is not None:
# 有完整的主谓宾
if core_relation:
parts.append(f"{subject_text}{topic_text}{core_relation}{object_node.content}")
else:
parts.append(f"{subject_text}{topic_text}{object_node.content}")
else:
# 只有主谓
parts.append(f"{subject_text}{topic_text}")
# 添加属性信息
if attributes:
attr_parts = []
# 优先显示时间和地点
if "时间" in attributes:
attr_parts.append(f"{attributes['时间']}")
if "地点" in attributes:
attr_parts.append(f"{attributes['地点']}")
# 其他属性
for key, value in attributes.items():
if key not in ["时间", "地点"]:
attr_parts.append(f"{key}{value}")
if attr_parts:
parts.append(f"{' '.join(attr_parts)}")
description = "".join(parts)
# 6. 添加元数据(可选)
if include_metadata:
metadata_parts = []
# 记忆类型
if memory.memory_type:
metadata_parts.append(f"类型:{memory.memory_type.value}")
# 重要性
if memory.importance >= 0.8:
metadata_parts.append("重要")
elif memory.importance >= 0.6:
metadata_parts.append("一般")
# 时间(如果没有在属性中)
if "时间" not in attributes:
time_str = _format_relative_time(memory.created_at)
if time_str:
metadata_parts.append(time_str)
if metadata_parts:
description += f" [{', '.join(metadata_parts)}]"
return description
except Exception as e:
logger.error(f"格式化记忆失败: {e}", exc_info=True)
return f"(记忆格式化错误: {str(e)[:50]}"
def format_memories_for_prompt(
memories: List[Memory],
max_count: Optional[int] = None,
include_metadata: bool = False,
group_by_type: bool = False
) -> str:
"""
批量格式化多条记忆为提示词文本
Args:
memories: 记忆列表
max_count: 最大记忆数量(可选)
include_metadata: 是否包含元数据
group_by_type: 是否按类型分组
Returns:
格式化后的文本,包含标题和列表
"""
if not memories:
return ""
# 限制数量
if max_count:
memories = memories[:max_count]
# 按类型分组
if group_by_type:
type_groups: Dict[MemoryType, List[Memory]] = {}
for memory in memories:
if memory.memory_type not in type_groups:
type_groups[memory.memory_type] = []
type_groups[memory.memory_type].append(memory)
# 构建分组文本
parts = ["### 🧠 相关记忆 (Relevant Memories)", ""]
type_order = [MemoryType.FACT, MemoryType.EVENT, MemoryType.RELATION, MemoryType.OPINION]
for mem_type in type_order:
if mem_type in type_groups:
parts.append(f"#### {mem_type.value}")
for memory in type_groups[mem_type]:
desc = format_memory_for_prompt(memory, include_metadata)
parts.append(f"- {desc}")
parts.append("")
return "\n".join(parts)
else:
# 不分组,直接列出
parts = ["### 🧠 相关记忆 (Relevant Memories)", ""]
for memory in memories:
# 获取类型标签
type_label = memory.memory_type.value if memory.memory_type else "未知"
# 格式化记忆内容
desc = format_memory_for_prompt(memory, include_metadata)
# 添加类型标签
parts.append(f"- **[{type_label}]** {desc}")
return "\n".join(parts)
def get_memory_type_label(memory_type: str) -> str:
"""
获取记忆类型的中文标签
Args:
memory_type: 记忆类型(可能是英文或中文)
Returns:
中文标签
"""
# 映射表
type_mapping = {
# 英文到中文
"event": "事件",
"fact": "事实",
"relation": "关系",
"opinion": "观点",
"preference": "偏好",
"emotion": "情绪",
"knowledge": "知识",
"skill": "技能",
"goal": "目标",
"experience": "经历",
"contextual": "情境",
# 中文(保持不变)
"事件": "事件",
"事实": "事实",
"关系": "关系",
"观点": "观点",
"偏好": "偏好",
"情绪": "情绪",
"知识": "知识",
"技能": "技能",
"目标": "目标",
"经历": "经历",
"情境": "情境",
}
# 转换为小写进行匹配
memory_type_lower = memory_type.lower() if memory_type else ""
return type_mapping.get(memory_type_lower, "未知")
def _format_relative_time(timestamp: datetime) -> Optional[str]:
"""
格式化相对时间(如"2天前""刚才"
Args:
timestamp: 时间戳
Returns:
相对时间描述如果太久远则返回None
"""
try:
now = datetime.now()
delta = now - timestamp
if delta.total_seconds() < 60:
return "刚才"
elif delta.total_seconds() < 3600:
minutes = int(delta.total_seconds() / 60)
return f"{minutes}分钟前"
elif delta.total_seconds() < 86400:
hours = int(delta.total_seconds() / 3600)
return f"{hours}小时前"
elif delta.days < 7:
return f"{delta.days}天前"
elif delta.days < 30:
weeks = delta.days // 7
return f"{weeks}周前"
elif delta.days < 365:
months = delta.days // 30
return f"{months}个月前"
else:
# 超过一年不显示相对时间
return None
except Exception:
return None
def format_memory_summary(memory: Memory) -> str:
"""
生成记忆的简短摘要(用于日志和调试)
Args:
memory: 记忆对象
Returns:
简短摘要
"""
try:
subject_node = memory.get_subject_node()
subject_text = subject_node.content if subject_node else "?"
topic_text = "?"
for edge in memory.edges:
if edge.edge_type == EdgeType.MEMORY_TYPE and edge.source_id == memory.subject_id:
topic_node = memory.get_node_by_id(edge.target_id)
if topic_node:
topic_text = topic_node.content
break
return f"{subject_text} - {memory.memory_type.value if memory.memory_type else '?'}: {topic_text}"
except Exception:
return f"记忆 {memory.id[:8]}"
# 导出主要函数
__all__ = [
'format_memory_for_prompt',
'format_memories_for_prompt',
'get_memory_type_label',
'format_memory_summary',
]

View File

@@ -18,20 +18,36 @@ logger = get_logger("tool_use")
def init_tool_executor_prompt():
"""初始化工具执行器的提示词"""
tool_executor_prompt = """
你是一个专门执行工具的助手。你的名字是{bot_name}。现在是{time_now}
群里正在进行的聊天内容:
# 工具调用系统
## 📋 你的身份
- **名字**: {bot_name}
- **核心人设**: {personality_core}
- **人格特质**: {personality_side}
- **当前时间**: {time_now}
## 💬 上下文信息
### 对话历史
{chat_history}
现在,{sender}发送了内容:{target_message},你想要回复ta。
请仔细分析聊天内容,考虑以下几点:
1. 内容中是否包含需要查询信息的问题
2. 是否有明确的工具使用指令
3. 之前的工具调用是否提供了有用的信息
4. 是否需要基于之前的工具结果进行进一步的查询
### 当前消息
**{sender}** 说: {target_message}
{tool_history}
If you need to use a tool, please directly call the corresponding tool function. If you do not need to use any tool, simply output "No tool needed".
## 🔧 工具使用
根据上下文判断是否需要使用工具。每个工具都有详细的description说明其用途和参数请根据工具定义决定是否调用。
**⚠️ 记忆创建特别提醒:**
创建记忆时subject主体必须使用对话历史中显示的**真实发送人名字**
- ✅ 正确:从"Prou(12345678): ..."中提取"Prou"作为subject
- ❌ 错误:使用"用户""对方"等泛指词
**执行指令:**
- 需要使用工具 → 直接调用相应的工具函数
- 不需要工具 → 输出 "No tool needed"
"""
Prompt(tool_executor_prompt, "tool_executor_prompt")
@@ -110,6 +126,10 @@ class ToolExecutor:
# 构建工具调用历史文本
tool_history = self._format_tool_history()
# 获取人设信息
personality_core = global_config.personality.personality_core
personality_side = global_config.personality.personality_side
# 构建工具调用提示词
prompt = await global_prompt_manager.format_prompt(
@@ -120,6 +140,8 @@ class ToolExecutor:
bot_name=bot_name,
time_now=time_now,
tool_history=tool_history,
personality_core=personality_core,
personality_side=personality_side,
)
logger.debug(f"{self.log_prefix}开始LLM工具调用分析")

View File

@@ -239,9 +239,9 @@ max_context_emojis = 30 # 每次随机传递给LLM的表情包详细描述的最
# 新一代记忆系统:基于知识图谱 + 语义向量的混合记忆架构
# 替代旧的 enhanced memory 系统
[memory_graph]
[memory]
# === 基础配置 ===
enable = true # 是否启用记忆系统
enable = true # 是否启用记忆系统
data_dir = "data/memory_graph" # 记忆数据存储目录
# === 向量存储配置 ===