refactor(chat): 重构关系系统并优化消息打断处理机制

- 移除独立的RelationshipConfig,将关系追踪参数整合到AffinityFlowConfig
- 实现消息打断后立即重新处理流程,提升交互响应性
- 优化关系追踪系统,添加概率筛选和超时保护机制
- 改进机器人自引用处理,确保消息内容正确显示
- 增强用户信息提取逻辑,兼容多种消息格式
- 添加异步后台任务处理,避免阻塞主回复流程
- 调整兴趣评分阈值和权重参数,优化消息匹配精度
This commit is contained in:
Windpicker-owo
2025-10-08 22:33:10 +08:00
parent d9cc109401
commit bdf0035034
13 changed files with 370 additions and 106 deletions

View File

@@ -228,10 +228,18 @@ class AffinityInterestCalculator(BaseInterestCalculator):
is_at = getattr(message, "is_at", False)
processed_plain_text = getattr(message, "processed_plain_text", "")
# 判断是否为私聊
chat_info_group_id = getattr(message, "chat_info_group_id", None)
is_private_chat = not chat_info_group_id # 如果没有group_id则是私聊
logger.debug(f"[提及分计算] is_mentioned={is_mentioned}, is_at={is_at}, is_private_chat={is_private_chat}")
if is_mentioned:
if is_at:
logger.debug(f"[提及分计算] 直接@机器人返回1.0")
return 1.0 # 直接@机器人,最高分
else:
logger.debug(f"[提及分计算] 提及机器人返回0.8")
return 0.8 # 提及机器人名字,高分
else:
# 检查是否被提及(文本匹配)
@@ -239,9 +247,14 @@ class AffinityInterestCalculator(BaseInterestCalculator):
is_text_mentioned = any(alias in processed_plain_text for alias in bot_aliases if alias)
# 如果被提及或是私聊都视为提及了bot
if is_text_mentioned or not hasattr(message, "chat_info_group_id"):
if is_text_mentioned:
logger.debug(f"[提及分计算] 文本提及机器人,返回提及分")
return global_config.affinity_flow.mention_bot_interest_score
elif is_private_chat:
logger.debug(f"[提及分计算] 私聊消息,返回提及分")
return global_config.affinity_flow.mention_bot_interest_score
else:
logger.debug(f"[提及分计算] 未提及机器人返回0.0")
return 0.0 # 未提及机器人
def _apply_no_reply_boost(self, base_score: float) -> float:

View File

@@ -184,9 +184,16 @@ class ChatterPlanExecutor:
# 获取用户ID - 兼容对象和字典
if hasattr(action_info.action_message, "user_info"):
# DatabaseMessages对象情况
user_id = action_info.action_message.user_info.user_id
else:
user_id = action_info.action_message.get("user_info", {}).get("user_id")
# 字典情况(向后兼容)- 适配扁平化消息字典结构
# 首先尝试从扁平化结构直接获取用户信息
user_id = action_info.action_message.get("user_id")
# 如果扁平化结构中没有用户信息再尝试从嵌套的user_info获取
if not user_id:
user_id = action_info.action_message.get("user_info", {}).get("user_id")
if user_id == str(global_config.bot.qq_account):
logger.warning("尝试回复自己,跳过此动作以防止死循环。")
@@ -231,11 +238,19 @@ class ChatterPlanExecutor:
except Exception as e:
error_message = str(e)
logger.error(f"执行回复动作失败: {action_info.action_type}, 错误: {error_message}")
"""
# 记录用户关系追踪
# 记录用户关系追踪 - 使用后台异步执行,防止阻塞主流程
if success and action_info.action_message:
await self._track_user_interaction(action_info, plan, reply_content)
"""
logger.debug(f"准备执行关系追踪: success={success}, action_message存在={bool(action_info.action_message)}")
logger.debug(f"关系追踪器状态: {self.relationship_tracker is not None}")
# 直接使用后台异步任务执行关系追踪,避免阻塞主回复流程
import asyncio
asyncio.create_task(self._track_user_interaction(action_info, plan, reply_content))
logger.debug("关系追踪已启动为后台异步任务")
else:
logger.debug(f"跳过关系追踪: success={success}, action_message存在={bool(action_info.action_message)}")
# 将机器人回复添加到已读消息中
await self._add_bot_reply_to_read_messages(action_info, plan, reply_content)
execution_time = time.time() - start_time
self.execution_stats["execution_times"].append(execution_time)
@@ -344,29 +359,54 @@ class ChatterPlanExecutor:
async def _track_user_interaction(self, action_info: ActionPlannerInfo, plan: Plan, reply_content: str):
"""追踪用户交互 - 集成回复后关系追踪"""
try:
logger.debug("🔍 开始执行用户交互追踪")
if not action_info.action_message:
logger.debug("❌ 跳过追踪action_message为空")
return
# 获取用户信息 - 处理对象和字典两种情况
if hasattr(action_info.action_message, "user_info"):
# 对象情况
user_info = action_info.action_message.user_info
user_id = user_info.user_id
user_name = user_info.user_nickname or user_id
user_message = action_info.action_message.content
# 获取用户信息 - 处理DatabaseMessages对象
if hasattr(action_info.action_message, "user_id"):
# DatabaseMessages对象情况
user_id = action_info.action_message.user_id
user_name = action_info.action_message.user_nickname or user_id
# 使用processed_plain_text作为消息内容如果没有则使用display_message
user_message = (
action_info.action_message.processed_plain_text
or action_info.action_message.display_message
or ""
)
logger.debug(f"📝 从DatabaseMessages获取用户信息: user_id={user_id}, user_name={user_name}")
else:
# 字典情况
user_info = action_info.action_message.get("user_info", {})
user_id = user_info.get("user_id")
user_name = user_info.get("user_nickname") or user_id
user_message = action_info.action_message.get("content", "")
# 字典情况(向后兼容)- 适配扁平化消息字典结构
# 首先尝试从扁平化结构直接获取用户信息
user_id = action_info.action_message.get("user_id")
user_name = action_info.action_message.get("user_nickname") or user_id
# 如果扁平化结构中没有用户信息再尝试从嵌套的user_info获取
if not user_id:
user_info = action_info.action_message.get("user_info", {})
user_id = user_info.get("user_id")
user_name = user_info.get("user_nickname") or user_id
logger.debug(f"📝 从嵌套user_info获取用户信息: user_id={user_id}, user_name={user_name}")
else:
logger.debug(f"📝 从扁平化结构获取用户信息: user_id={user_id}, user_name={user_name}")
# 获取消息内容优先使用processed_plain_text
user_message = (
action_info.action_message.get("processed_plain_text", "")
or action_info.action_message.get("display_message", "")
or action_info.action_message.get("content", "")
)
if not user_id:
logger.debug("跳过追踪缺少用户ID")
logger.debug("跳过追踪缺少用户ID")
return
# 如果有设置关系追踪器,执行回复后关系追踪
if self.relationship_tracker:
logger.debug(f"✅ 关系追踪器存在,开始为用户 {user_id} 执行追踪")
# 记录基础交互信息(保持向后兼容)
self.relationship_tracker.add_interaction(
user_id=user_id,
@@ -375,19 +415,102 @@ class ChatterPlanExecutor:
bot_reply=reply_content,
reply_timestamp=time.time(),
)
logger.debug(f"📊 已添加基础交互信息: {user_name}({user_id})")
# 执行新的回复后关系追踪
await self.relationship_tracker.track_reply_relationship(
user_id=user_id, user_name=user_name, bot_reply_content=reply_content, reply_timestamp=time.time()
)
logger.debug(f"🎯 已执行回复后关系追踪: {user_id}")
logger.debug(f"已执行用户交互追踪: {user_id}")
else:
logger.debug("❌ 关系追踪器不存在,跳过追踪")
except Exception as e:
logger.error(f"追踪用户交互时出错: {e}")
logger.debug(f"action_message类型: {type(action_info.action_message)}")
logger.debug(f"action_message内容: {action_info.action_message}")
async def _add_bot_reply_to_read_messages(self, action_info: ActionPlannerInfo, plan: Plan, reply_content: str):
"""将机器人回复添加到已读消息中"""
try:
if not reply_content or not plan.chat_id:
logger.debug("跳过添加已读消息回复内容为空或缺少chat_id")
return
# 获取chat_stream对象
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager()
chat_stream = await chat_manager.get_stream(plan.chat_id)
if not chat_stream:
logger.warning(f"无法获取chat_stream: {plan.chat_id}")
return
# 构建机器人回复的DatabaseMessages对象
from src.common.data_models.database_data_model import DatabaseMessages
current_time = time.time()
# 构建用户信息
bot_user_id = str(global_config.bot.qq_account)
bot_nickname = global_config.bot.nickname
# 创建机器人回复消息
bot_message = DatabaseMessages(
message_id=f"bot_reply_{int(current_time * 1000)}", # 生成唯一ID
time=current_time,
chat_id=plan.chat_id,
reply_to=None, # 不是回复消息
interest_value=None, # 机器人回复不需要兴趣值
processed_plain_text=reply_content,
display_message=reply_content,
is_read=True, # 标记为已读
is_emoji=False,
is_picid=False,
is_command=False,
is_notify=False,
# 用户信息
user_id=bot_user_id,
user_nickname=bot_nickname,
user_cardname=bot_nickname,
user_platform="qq",
# 聊天上下文信息
chat_info_user_id=chat_stream.user_info.user_id if chat_stream.user_info else bot_user_id,
chat_info_user_nickname=chat_stream.user_info.user_nickname if chat_stream.user_info else bot_nickname,
chat_info_user_cardname=chat_stream.user_info.user_cardname if chat_stream.user_info else bot_nickname,
chat_info_user_platform=chat_stream.platform,
chat_info_stream_id=chat_stream.stream_id,
chat_info_platform=chat_stream.platform,
chat_info_create_time=chat_stream.create_time,
chat_info_last_active_time=chat_stream.last_active_time,
# 群组信息(如果是群聊)
chat_info_group_id=chat_stream.group_info.group_id if chat_stream.group_info else None,
chat_info_group_name=chat_stream.group_info.group_name if chat_stream.group_info else None,
chat_info_group_platform=chat_stream.group_info.group_platform if chat_stream.group_info else None,
# 动作信息
actions=["bot_reply"],
should_reply=False,
should_act=False
)
# 添加到chat_stream的已读消息中
if hasattr(chat_stream, 'stream_context') and chat_stream.stream_context:
chat_stream.stream_context.history_messages.append(bot_message)
logger.debug(f"机器人回复已添加到已读消息: {reply_content[:50]}...")
else:
logger.warning("chat_stream没有stream_context无法添加已读消息")
except Exception as e:
logger.error(f"添加机器人回复到已读消息时出错: {e}")
logger.debug(f"plan.chat_id: {plan.chat_id}")
logger.debug(f"reply_content: {reply_content[:100] if reply_content else 'None'}")
def get_execution_stats(self) -> dict[str, Any]:
"""获取执行统计信息"""
stats = self.execution_stats.copy()

View File

@@ -50,6 +50,16 @@ class ChatterActionPlanner:
self.generator = ChatterPlanGenerator(chat_id)
self.executor = ChatterPlanExecutor(action_manager)
# 初始化关系追踪器
if global_config.affinity_flow.enable_relationship_tracking:
from .relationship_tracker import ChatterRelationshipTracker
self.relationship_tracker = ChatterRelationshipTracker()
self.executor.set_relationship_tracker(self.relationship_tracker)
logger.info(f"关系追踪器已初始化 (chat_id: {chat_id})")
else:
self.relationship_tracker = None
logger.info(f"关系系统已禁用,跳过关系追踪器初始化 (chat_id: {chat_id})")
# 使用新的统一兴趣度管理系统
# 规划器统计

View File

@@ -4,6 +4,7 @@
支持数据库持久化存储和回复后自动关系更新
"""
import random
import time
from sqlalchemy import desc, select
@@ -142,26 +143,40 @@ class ChatterRelationshipTracker:
1. 加分必须符合现实关系发展逻辑 - 不能因为对方态度好就盲目加分到不符合当前关系档次的分数
2. 关系提升需要足够的互动积累和时间验证
3. 即使是朋友关系单次互动加分通常不超过0.05-0.1
4. 关系描述要详细具体,包括
- 用户性格特点观察
- 印象深刻的互动记忆
- 你们关系的体状态描述
4. 人物印象描述应该是泛化的、整体的理解,从你的视角对用户整体性格特质的描述
- 描述用户的整体性格特点(如:温柔、幽默、理性、感性等)
- 用户给你的整体感觉和印象
- 你们关系的体状态和氛围
- 避免描述具体事件或对话内容,而是基于这些事件形成的整体认知
根据你的人设性格,思考:
1. 你的性格,你会如何看待这次互动
2. 用户的行为是否符合你性格的喜好?
3. 这次互动是否真的让你们的关系提升了一个档次?为什么
4. 有什么特别值得记住的互动细节
1. 你的性格视角,这个用户给你什么样的整体印象
2. 用户的性格特质和行为模式是否符合你的喜好?
3. 基于这次互动,你对用户的整体认知有什么变化
4. 这个用户在你心中的整体形象是怎样的
请以JSON格式返回更新结果
{{
"new_relationship_score": 0.0~1.0的数值(必须符合现实逻辑),
"reasoning": "从你的性格角度说明更新理由,重点说明是否符合现实关系发展逻辑",
"interaction_summary": "基于你性格的交互总结,包含印象深刻的互动记忆"
"interaction_summary": "基于你性格的用户整体印象描述,包含用户的整体性格特质、给你的整体感觉,避免具体事件描述"
}}
"""
llm_response, _ = await self.relationship_llm.generate_response_async(prompt=prompt)
# 调用LLM进行分析 - 添加超时保护
import asyncio
try:
llm_response, _ = await asyncio.wait_for(
self.relationship_llm.generate_response_async(prompt=prompt),
timeout=30.0 # 30秒超时
)
except asyncio.TimeoutError:
logger.warning(f"初次见面LLM调用超时: user_id={user_id}, 跳过此次追踪")
return
except Exception as e:
logger.error(f"初次见面LLM调用失败: user_id={user_id}, 错误: {e}")
return
if llm_response:
import json
@@ -382,15 +397,35 @@ class ChatterRelationshipTracker:
):
"""回复后关系追踪 - 主要入口点"""
try:
logger.info(f"🔄 [RelationshipTracker] 开始回复后关系追踪: {user_id}")
# 首先检查是否启用关系追踪
if not global_config.affinity_flow.enable_relationship_tracking:
logger.debug(f"🚫 [RelationshipTracker] 关系追踪系统已禁用,跳过用户 {user_id}")
return
# 检查上次追踪时间
last_tracked_time = self._get_last_tracked_time(user_id)
# 概率筛选 - 减少API调用压力
tracking_probability = global_config.affinity_flow.relationship_tracking_probability
if random.random() > tracking_probability:
logger.debug(
f"🎲 [RelationshipTracker] 概率筛选未通过 ({tracking_probability:.2f}),跳过用户 {user_id} 的关系追踪"
)
return
logger.info(f"🔄 [RelationshipTracker] 开始回复后关系追踪: {user_id} (概率通过: {tracking_probability:.2f})")
# 检查上次追踪时间 - 使用配置的冷却时间
last_tracked_time = await self._get_last_tracked_time(user_id)
cooldown_hours = global_config.affinity_flow.relationship_tracking_cooldown_hours
cooldown_seconds = cooldown_hours * 3600
time_diff = reply_timestamp - last_tracked_time
if time_diff < 5 * 60: # 5分钟内不重复追踪
# 使用配置的最小间隔时间
min_interval = global_config.affinity_flow.relationship_tracking_interval_min
required_interval = max(min_interval, cooldown_seconds)
if time_diff < required_interval:
logger.debug(
f"⏱️ [RelationshipTracker] 用户 {user_id} 距离上次追踪时间不足5分钟 ({time_diff:.2f}s),跳过"
f"⏱️ [RelationshipTracker] 用户 {user_id} 距离上次追踪时间不足 {required_interval/60:.1f} 分钟 "
f"(实际: {time_diff/60:.1f} 分钟),跳过"
)
return
@@ -563,30 +598,42 @@ class ChatterRelationshipTracker:
1. 加分必须符合现实关系发展逻辑 - 不能因为用户反应好就盲目加分
2. 关系提升需要足够的互动积累和时间验证单次互动加分通常不超过0.05-0.1
3. 必须考虑当前关系档次不能跳跃式提升比如从0.3直接到0.7
4. 关系印象描述要详细具体100-200字包括
- 用户性格特点和交流风格观察
- 印象深刻的互动记忆和对话片段
- 你们关系的体状态描述和发展阶段
- 根据你的性格,你对用户的真实感受
4. 人物印象描述应该是泛化的、整体的理解100-200字从你的视角对用户整体性格特质的描述
- 描述用户的整体性格特点和行为模式(如:温柔体贴、幽默风趣、理性稳重等)
- 用户给你的整体感觉和印象氛围
- 你们关系的体状态和发展阶段
- 基于所有互动形成的用户整体形象认知
- 避免提及具体事件或对话内容,而是总结形成的整体印象
性格视角深度分析:
1. 你的性格特点,用户这次的反应给你什么感受
2. 用户的情绪和行为符合你性格的喜好吗?具体哪些方面?
1. 你的性格视角,基于这次互动,你对用户的整体印象有什么新的认识
2. 用户的整体性格特质和行为模式符合你的喜好吗?
3. 从现实角度看,这次互动是否足以让关系提升到下一个档次?为什么?
4. 有什么特别值得记住的互动细节或对话内容
5. 基于你们的互动历史,用户给你留下了哪些深刻印象
4. 基于你们的互动历史,用户在你心中的整体形象是怎样的
5. 这个用户给你带来的整体感受和情绪体验是怎样的
请以JSON格式返回更新结果:
{{
"relationship_text": "详细的关系印象描述(100-200字),包含用户性格观察、印象深刻记忆、关系状态描述",
"relationship_text": "泛化的用户整体印象描述(100-200字),包含用户的整体性格特质、给你的整体感觉和印象氛围,避免具体事件描述",
"relationship_score": 0.0~1.0的新分数(必须严格符合现实逻辑),
"analysis_reasoning": "从你性格角度的深度分析,重点说明分数调整的现实合理性",
"interaction_quality": "high/medium/low"
}}
"""
# 调用LLM进行分析
llm_response, _ = await self.relationship_llm.generate_response_async(prompt=prompt)
# 调用LLM进行分析 - 添加超时保护
import asyncio
try:
llm_response, _ = await asyncio.wait_for(
self.relationship_llm.generate_response_async(prompt=prompt),
timeout=30.0 # 30秒超时
)
except asyncio.TimeoutError:
logger.warning(f"关系追踪LLM调用超时: user_id={user_id}, 跳过此次追踪")
return
except Exception as e:
logger.error(f"关系追踪LLM调用失败: user_id={user_id}, 错误: {e}")
return
if llm_response:
import json
@@ -650,7 +697,15 @@ class ChatterRelationshipTracker:
async def _handle_first_interaction(self, user_id: str, user_name: str, bot_reply_content: str):
"""处理与用户的初次交互"""
try:
logger.info(f"✨ [RelationshipTracker] 正在处理与用户 {user_id} 的初次交互")
# 初次交互也进行概率检查,但使用更高的通过率
first_interaction_probability = min(1.0, global_config.affinity_flow.relationship_tracking_probability * 1.5)
if random.random() > first_interaction_probability:
logger.debug(
f"🎲 [RelationshipTracker] 初次交互概率筛选未通过 ({first_interaction_probability:.2f}),跳过用户 {user_id}"
)
return
logger.info(f"✨ [RelationshipTracker] 正在处理与用户 {user_id} 的初次交互 (概率通过: {first_interaction_probability:.2f})")
# 获取bot人设信息
from src.individuality.individuality import Individuality
@@ -671,14 +726,15 @@ class ChatterRelationshipTracker:
【严格要求】:
1. 建立一个初始关系分数通常在0.2-0.4之间(普通网友)。
2. 关系印象描述要简洁地记录你对用户的初步看法50-100字
- 用户名给你的感觉
- 你的回复是基于什么考虑?
- 你对接下来与TA的互动有什么期待
2. 初始关系印象描述要简洁地记录你对用户的整体初步看法50-100字
- 基于用户名和初次互动,用户给你的整体感觉
- 你感受到的用户整体性格特质倾向
- 你对与这个用户建立关系的整体期待和感觉
- 避免描述具体的事件细节,而是整体的直觉印象
请以JSON格式返回结果:
{{
"relationship_text": "简洁的初始关系印象描述(50-100字)",
"relationship_text": "简洁的用户整体初始印象描述(50-100字),基于第一印象的整体性格感觉",
"relationship_score": 0.2~0.4的新分数,
"analysis_reasoning": "从你性格角度说明建立此初始印象的理由"
}}