feat: 添加选项必须检索长期记忆
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 3m16s

This commit is contained in:
2025-12-09 23:57:58 +08:00
parent eb877f402c
commit f3a4e9e0aa
4 changed files with 105 additions and 103 deletions

View File

@@ -615,7 +615,7 @@ class DefaultReplyer:
# 使用统一管理器的智能检索Judge模型决策
search_result = await unified_manager.search_memories(
query_text=query_text,
use_judge=True,
use_judge=global_config.memory.use_judge,
recent_chat_history=chat_history, # 传递最近聊天历史
)

View File

@@ -491,6 +491,7 @@ class MemoryConfig(ValidatedConfigBase):
short_term_decay_factor: float = Field(default=0.98, description="衰减因子")
# 长期记忆层配置
use_judge: bool = Field(default=True, description="使用评判模型决定是否检索长期记忆")
long_term_batch_size: int = Field(default=10, description="批量转移大小")
long_term_decay_factor: float = Field(default=0.95, description="衰减因子")
long_term_auto_transfer_interval: int = Field(default=60, description="自动转移间隔(秒)")
@@ -876,30 +877,30 @@ class ProactiveThinkingConfig(ValidatedConfigBase):
class KokoroFlowChatterProactiveConfig(ValidatedConfigBase):
"""
Kokoro Flow Chatter 主动思考子配置
设计哲学:主动行为源于内部状态和外部环境的自然反应,而非机械的限制。
她的主动是因为挂念、因为关心、因为想问候,而不是因为"任务"
"""
enabled: bool = Field(default=True, description="是否启用KFC的私聊主动思考")
# 1. 沉默触发器:当感到长久的沉默时,她可能会想说些什么
silence_threshold_seconds: int = Field(
default=7200, ge=60, le=86400,
description="用户沉默超过此时长可能触发主动思考默认2小时"
)
# 2. 关系门槛:她不会对不熟悉的人过于主动
min_affinity_for_proactive: float = Field(
default=0.3, ge=0.0, le=1.0,
description="需要达到最低好感度,她才会开始主动关心"
)
# 3. 频率呼吸:为了避免打扰,她的关心总是有间隔的
min_interval_between_proactive: int = Field(
default=1800, ge=0,
description="两次主动思考之间的最小间隔默认30分钟"
)
# 4. 自然问候:在特定的时间,她会像朋友一样送上问候
enable_morning_greeting: bool = Field(
default=True, description="是否启用早安问候 (例如: 8:00 - 9:00)"
@@ -907,7 +908,7 @@ class KokoroFlowChatterProactiveConfig(ValidatedConfigBase):
enable_night_greeting: bool = Field(
default=True, description="是否启用晚安问候 (例如: 22:00 - 23:00)"
)
# 5. 勿扰时段:在这段时间内不会主动发起对话
quiet_hours_start: str = Field(
default="23:00", description="勿扰时段开始时间,格式: HH:MM"
@@ -915,7 +916,7 @@ class KokoroFlowChatterProactiveConfig(ValidatedConfigBase):
quiet_hours_end: str = Field(
default="07:00", description="勿扰时段结束时间,格式: HH:MM"
)
# 6. 触发概率:每次检查时主动发起的概率
trigger_probability: float = Field(
default=0.3, ge=0.0, le=1.0,
@@ -961,14 +962,14 @@ class KokoroFlowChatterWaitingConfig(ValidatedConfigBase):
class KokoroFlowChatterConfig(ValidatedConfigBase):
"""
Kokoro Flow Chatter 配置类 - 私聊专用心流对话系统
设计理念KFC不是独立人格它复用全局的人设、情感框架和回复模型
只作为Bot核心人格在私聊中的一种特殊表现模式。
"""
# --- 总开关 ---
enable: bool = Field(
default=True,
default=True,
description="开启后KFC将接管所有私聊消息关闭后私聊消息将由AFC处理"
)
@@ -978,7 +979,7 @@ class KokoroFlowChatterConfig(ValidatedConfigBase):
description="默认的最大等待秒数AI发送消息后愿意等待用户回复的时间"
)
enable_continuous_thinking: bool = Field(
default=True,
default=True,
description="是否在等待期间启用心理活动更新"
)

View File

@@ -36,16 +36,16 @@ def _get_config():
class KFCContextBuilder:
"""
KFC V2 上下文构建器
为提示词提供完整的情境感知数据。
"""
def __init__(self, chat_stream: "ChatStream"):
self.chat_stream = chat_stream
self.chat_id = chat_stream.stream_id
self.platform = chat_stream.platform
self.is_group_chat = bool(chat_stream.group_info)
async def build_all_context(
self,
sender_name: str,
@@ -56,21 +56,21 @@ class KFCContextBuilder:
) -> dict[str, str]:
"""
并行构建所有上下文模块
Args:
sender_name: 发送者名称
target_message: 目标消息内容
context: 聊天流上下文(可选)
user_id: 用户ID可选用于精确查找关系信息
enable_tool: 是否启用工具调用
Returns:
dict: 包含所有上下文块的字典
"""
logger.debug(f"[KFC上下文] 开始构建上下文: sender={sender_name}, target={target_message[:50] if target_message else '(空)'}...")
chat_history = await self._get_chat_history_text(context)
tasks = {
"relation_info": self._build_relation_info(sender_name, target_message, user_id),
"memory_block": self._build_memory_block(chat_history, target_message, context),
@@ -79,10 +79,10 @@ class KFCContextBuilder:
"schedule": self._build_schedule_block(),
"time": self._build_time_block(),
}
results = {}
timing_logs = []
# 任务名称中英文映射
task_name_mapping = {
"relation_info": "感受关系",
@@ -92,13 +92,13 @@ class KFCContextBuilder:
"schedule": "日程",
"time": "时间",
}
try:
task_results = await asyncio.gather(
*[self._wrap_task_with_timing(name, coro) for name, coro in tasks.items()],
return_exceptions=True
)
for result in task_results:
if isinstance(result, tuple) and len(result) == 3:
name, value, duration = result
@@ -111,13 +111,13 @@ class KFCContextBuilder:
logger.warning(f"上下文构建任务异常: {result}")
except Exception as e:
logger.error(f"并行构建上下文失败: {e}")
# 输出耗时日志
if timing_logs:
logger.info(f"在回复前的步骤耗时: {'; '.join(timing_logs)}")
return results
async def _wrap_task_with_timing(self, name: str, coro) -> tuple[str, str, float]:
"""包装任务以返回名称、结果和耗时"""
start_time = time.time()
@@ -129,7 +129,7 @@ class KFCContextBuilder:
duration = time.time() - start_time
logger.error(f"构建 {name} 失败: {e}")
return (name, "", duration)
async def _get_chat_history_text(
self,
context: Optional["StreamContext"] = None,
@@ -138,16 +138,16 @@ class KFCContextBuilder:
"""获取聊天历史文本"""
if context is None:
return ""
try:
from src.chat.utils.chat_message_builder import build_readable_messages
messages = context.get_messages(limit=limit, include_unread=True)
if not messages:
return ""
msg_dicts = [msg.flatten() for msg in messages]
return await build_readable_messages(
msg_dicts,
replace_bot_name=True,
@@ -157,54 +157,54 @@ class KFCContextBuilder:
except Exception as e:
logger.error(f"获取聊天历史失败: {e}")
return ""
async def _build_relation_info(self, sender_name: str, target_message: str, user_id: Optional[str] = None) -> str:
"""构建关系信息块"""
config = _get_config()
if sender_name == f"{config.bot.nickname}(你)":
return "你将要回复的是你自己发送的消息。"
person_info_manager = get_person_info_manager()
# 优先使用 user_id + platform 获取 person_id
person_id = None
if user_id and self.platform:
person_id = person_info_manager.get_person_id(self.platform, user_id)
logger.debug(f"通过 platform={self.platform}, user_id={user_id} 获取 person_id={person_id}")
# 如果没有找到,尝试通过 person_name 查找
if not person_id:
person_id = await person_info_manager.get_person_id_by_person_name(sender_name)
if not person_id:
logger.debug(f"未找到用户 {sender_name} 的 person_id")
return f"你与{sender_name}还没有建立深厚的关系,这是早期的互动阶段。"
try:
from src.person_info.relationship_fetcher import relationship_fetcher_manager
relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_id)
user_relation_info = await relationship_fetcher.build_relation_info(person_id, points_num=5)
stream_impression = await relationship_fetcher.build_chat_stream_impression(self.chat_id)
parts = []
if user_relation_info:
parts.append(f"### 你与 {sender_name} 的关系\n{user_relation_info}")
if stream_impression:
scene_type = "这个群" if self.is_group_chat else "你们的私聊"
parts.append(f"### 你对{scene_type}的印象\n{stream_impression}")
if parts:
return "\n\n".join(parts)
else:
return f"你与{sender_name}还没有建立深厚的关系,这是早期的互动阶段。"
except Exception as e:
logger.error(f"获取关系信息失败: {e}")
return f"你与{sender_name}是普通朋友关系。"
async def _build_memory_block(
self,
chat_history: str,
@@ -213,44 +213,44 @@ class KFCContextBuilder:
) -> str:
"""构建记忆块(使用三层记忆系统)"""
config = _get_config()
if not (config.memory and config.memory.enable):
logger.debug("[KFC记忆] 记忆系统未启用")
return ""
try:
from src.memory_graph.manager_singleton import get_unified_memory_manager
from src.memory_graph.utils.three_tier_formatter import memory_formatter
unified_manager = get_unified_memory_manager()
if not unified_manager:
logger.warning("[KFC记忆] 管理器未初始化,跳过记忆检索")
return ""
# 构建查询文本(使用最近多条消息的组合块)
query_text = self._build_memory_query_text(target_message, context)
logger.debug(f"[KFC记忆] 开始检索,查询文本: {query_text[:100]}...")
search_result = await unified_manager.search_memories(
query_text=query_text,
use_judge=True,
use_judge=config.memory.use_judge,
recent_chat_history=chat_history,
)
if not search_result:
logger.debug("[KFC记忆] 未找到相关记忆")
return ""
perceptual_blocks = search_result.get("perceptual_blocks", [])
short_term_memories = search_result.get("short_term_memories", [])
long_term_memories = search_result.get("long_term_memories", [])
formatted_memories = await memory_formatter.format_all_tiers(
perceptual_blocks=perceptual_blocks,
short_term_memories=short_term_memories,
long_term_memories=long_term_memories
)
total_count = len(perceptual_blocks) + len(short_term_memories) + len(long_term_memories)
if total_count > 0 and formatted_memories.strip():
logger.info(
@@ -258,16 +258,16 @@ class KFCContextBuilder:
f"(感知:{len(perceptual_blocks)}, 短期:{len(short_term_memories)}, 长期:{len(long_term_memories)})"
)
return f"### 🧠 相关记忆\n\n{formatted_memories}"
logger.debug("[KFC记忆] 记忆为空")
return ""
except Exception as e:
logger.error(f"[KFC记忆] 检索失败: {e}")
import traceback
traceback.print_exc()
return ""
def _build_memory_query_text(
self,
fallback_text: str,
@@ -276,23 +276,23 @@ class KFCContextBuilder:
) -> str:
"""
将最近若干条消息拼接为一个查询块,用于生成语义向量。
Args:
fallback_text: 如果无法拼接消息块时使用的后备文本
context: 聊天流上下文
block_size: 组合的消息数量
Returns:
str: 用于检索的查询文本
"""
if not context:
return fallback_text
try:
messages = context.get_messages(limit=block_size, include_unread=True)
if not messages:
return fallback_text
lines = []
for msg in messages:
sender = ""
@@ -303,11 +303,11 @@ class KFCContextBuilder:
lines.append(f"{sender}: {content}")
elif content:
lines.append(content)
return "\n".join(lines) if lines else fallback_text
except Exception:
return fallback_text
async def _build_tool_info(
self,
chat_history: str,
@@ -316,30 +316,30 @@ class KFCContextBuilder:
enable_tool: bool = True,
) -> str:
"""构建工具信息块
Args:
chat_history: 聊天历史记录
sender_name: 发送者名称
target_message: 目标消息内容
enable_tool: 是否启用工具调用
Returns:
str: 工具信息字符串
"""
if not enable_tool:
return ""
try:
from src.plugin_system.core.tool_use import ToolExecutor
tool_executor = ToolExecutor(chat_id=self.chat_id)
info_parts = []
# ========== 1. 主动召回联网搜索缓存 ==========
try:
from src.common.cache_manager import tool_cache
# 使用聊天历史作为语义查询
query_text = chat_history if chat_history else target_message
recalled_caches = await tool_cache.recall_relevant_cache(
@@ -348,7 +348,7 @@ class KFCContextBuilder:
top_k=2,
similarity_threshold=0.65, # 相似度阈值
)
if recalled_caches:
recall_parts = ["### 🔍 相关的历史搜索结果"]
for item in recalled_caches:
@@ -360,19 +360,19 @@ class KFCContextBuilder:
if len(content) > 500:
content = content[:500] + "..."
recall_parts.append(f"**搜索「{original_query}」** (相关度:{similarity:.0%})\n{content}")
info_parts.append("\n\n".join(recall_parts))
logger.info(f"[缓存召回] 召回了 {len(recalled_caches)} 条相关搜索缓存")
except Exception as e:
logger.debug(f"[缓存召回] 召回失败(非关键): {e}")
# ========== 2. 获取工具调用历史 ==========
tool_history_str = tool_executor.history_manager.format_for_prompt(
max_records=3, include_results=True
)
if tool_history_str:
info_parts.append(tool_history_str)
# ========== 3. 执行工具调用 ==========
tool_results, _, _ = await tool_executor.execute_from_chat_message(
sender=sender_name,
@@ -380,7 +380,7 @@ class KFCContextBuilder:
chat_history=chat_history,
return_details=False,
)
# 显示当前工具调用的结果(简要信息)
if tool_results:
current_results_parts = ["### 🔧 刚获取的工具信息"]
@@ -389,35 +389,35 @@ class KFCContextBuilder:
content = tool_result.get("content", "")
# 不进行截断,让工具自己处理结果长度
current_results_parts.append(f"- **{tool_name}**: {content}")
info_parts.append("\n".join(current_results_parts))
logger.info(f"[工具调用] 获取到 {len(tool_results)} 个工具结果")
# 如果没有任何信息,返回空字符串
if not info_parts:
logger.debug("[工具调用] 未获取到任何工具结果或历史记录")
return ""
return "\n\n".join(info_parts)
except Exception as e:
logger.error(f"[工具调用] 工具信息获取失败: {e}")
return ""
async def _build_expression_habits(self, chat_history: str, target_message: str) -> str:
"""构建表达习惯块"""
config = _get_config()
use_expression, _, _ = config.expression.get_expression_config_for_chat(self.chat_id)
if not use_expression:
return ""
try:
from src.chat.express.expression_selector import expression_selector
style_habits = []
grammar_habits = []
selected_expressions = await expression_selector.select_suitable_expressions(
chat_id=self.chat_id,
chat_history=chat_history,
@@ -425,7 +425,7 @@ class KFCContextBuilder:
max_num=8,
min_num=2
)
if selected_expressions:
for expr in selected_expressions:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
@@ -435,40 +435,40 @@ class KFCContextBuilder:
grammar_habits.append(habit_str)
else:
style_habits.append(habit_str)
parts = []
if style_habits:
parts.append("**语言风格习惯**\n" + "\n".join(f"- {h}" for h in style_habits))
if grammar_habits:
parts.append("**句法习惯**\n" + "\n".join(f"- {h}" for h in grammar_habits))
if parts:
return "### 💬 你的表达习惯\n\n" + "\n\n".join(parts)
return ""
except Exception as e:
logger.error(f"构建表达习惯失败: {e}")
return ""
async def _build_schedule_block(self) -> str:
"""构建日程信息块"""
config = _get_config()
if not config.planning_system.schedule_enable:
return ""
try:
from src.schedule.schedule_manager import schedule_manager
activity_info = schedule_manager.get_current_activity()
if not activity_info:
return ""
activity = activity_info.get("activity")
time_range = activity_info.get("time_range")
now = datetime.now()
if time_range:
try:
start_str, end_str = time_range.split("-")
@@ -478,15 +478,15 @@ class KFCContextBuilder:
end_time = datetime.strptime(end_str.strip(), "%H:%M").replace(
year=now.year, month=now.month, day=now.day
)
if end_time < start_time:
end_time += timedelta(days=1)
if now < start_time:
now += timedelta(days=1)
duration_minutes = (now - start_time).total_seconds() / 60
remaining_minutes = (end_time - now).total_seconds() / 60
return (
f"你当前正在「{activity}」,"
f"{start_time.strftime('%H:%M')}开始,预计{end_time.strftime('%H:%M')}结束,"
@@ -494,13 +494,13 @@ class KFCContextBuilder:
)
except (ValueError, AttributeError):
pass
return f"你当前正在「{activity}"
except Exception as e:
logger.error(f"构建日程块失败: {e}")
return ""
async def _build_time_block(self) -> str:
"""构建时间信息块"""
now = datetime.now()

View File

@@ -1,5 +1,5 @@
[inner]
version = "7.9.8"
version = "7.9.9"
#----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读----
#如果你想要修改配置文件请递增version的值
@@ -89,7 +89,7 @@ command_prefixes = ['/']
[personality]
# 建议50字以内描述人格的核心特质
personality_core = "是一个积极向上的女大学生"
personality_core = "是一个积极向上的女大学生"
# 人格的细节,描述人格的一些侧面
personality_side = "用一句话或几句话描述人格的侧面特质"
#アイデンティティがない 生まれないらららら
@@ -297,6 +297,7 @@ short_term_search_top_k = 5 # 搜索时返回的最大数量
short_term_decay_factor = 0.98 # 衰减因子
# 长期记忆层配置
use_judge = true # 使用评判模型决定是否检索长期记忆
long_term_batch_size = 10 # 批量转移大小
long_term_decay_factor = 0.95 # 衰减因子
long_term_auto_transfer_interval = 180 # 自动转移间隔(秒)
@@ -411,7 +412,7 @@ auto_install = true #it can work now!
auto_install_timeout = 300
# 是否使用PyPI镜像源推荐可加速下载
use_mirror = true
mirror_url = "https://pypi.tuna.tsinghua.edu.cn/simple" # PyPI镜像源URL如: "https://pypi.tuna.tsinghua.edu.cn/simple"
mirror_url = "https://pypi.tuna.tsinghua.edu.cn/simple" # PyPI镜像源URL如: "https://pypi.tuna.tsinghua.edu.cn/simple"
# 依赖安装日志级别
install_log_level = "INFO"