refactor(proactive_thinker): 优化主动思考的决策与交互,使其更自然且避免打扰

本次提交对主动思考插件进行了多项核心优化,旨在提升其交互的自然度和人性化,并引入了关键的防打扰机制。

主要变更包括:

1.  **重构冷启动任务 (`ColdStartTask`)**:
    -   任务逻辑从一个长期运行的周期性任务,重构为在机器人启动时执行一次的“唤醒”任务。
    -   新逻辑不仅能为白名单中的全新用户发起首次问候,还能智能地“唤醒”那些因机器人重启而“沉睡”的聊天流,确保了主动思考功能的连续性。

2.  **增强决策提示词 (`_build_plan_prompt`)**:
    -   引入了更精细的决策原则,核心是增加了防打扰机制。现在模型在决策时会检查上一条消息是否为自己发送,如果对方尚未回复,则倾向于不发起新对话,以表现出耐心和体贴。
    -   优化了示例,引导模型优先利用上下文信息,并在无切入点时使用简单的问候,避免创造生硬抽象的话题。

3.  **改善回复生成逻辑 (`_build_*_reply_prompt`)**:
    -   在生成回复的指令中,明确要求模型必须先用一句通用的礼貌问候语(如“在吗?”、“下午好!”)作为开场白,然后再衔接具体话题。这使得主动发起的对话更加自然、流畅,符合人类的沟通习惯。

4.  **模型调整**:
    -   将决策规划阶段的 LLM 模型从 `utils` 调整为 `replyer`,以更好地适应生成对话策略的任务。
This commit is contained in:
tt-P607
2025-10-03 21:44:31 +08:00
parent 135909449c
commit bb4ff48e26
2 changed files with 75 additions and 60 deletions

View File

@@ -21,76 +21,69 @@ logger = get_logger(__name__)
class ColdStartTask(AsyncTask): class ColdStartTask(AsyncTask):
""" """
冷启动任务,专门用于处理那些在白名单里,但从未与机器人发生过交互的用户 冷启动任务,在机器人启动时执行一次
它的核心职责是“破冰”,主动创建聊天流并发起第一次问候 它的核心职责是“唤醒”那些因重启而“沉睡”的聊天流,确保它们能够接收主动思考
对于在白名单中但从未有过记录的全新用户,它也会发起第一次“破冰”问候。
""" """
def __init__(self): def __init__(self, bot_start_time: float):
super().__init__(task_name="ColdStartTask") super().__init__(task_name="ColdStartTask")
self.chat_manager = get_chat_manager() self.chat_manager = get_chat_manager()
self.executor = ProactiveThinkerExecutor() self.executor = ProactiveThinkerExecutor()
self.bot_start_time = bot_start_time
async def run(self): async def run(self):
"""任务主循环,周期性地检查是否有需要“破冰”的新用户""" """任务主逻辑,在启动后执行一次白名单扫描"""
logger.info("冷启动任务已启动,将周期性检查白名单中的新朋友。") logger.info("冷启动任务已启动,将在短暂延迟后开始唤醒沉睡的聊天流...")
# 初始等待一段时间,确保其他服务(如数据库)完全启动 await asyncio.sleep(30) # 延迟以确保所有服务和聊天流已从数据库加载完毕
await asyncio.sleep(100)
while True: try:
try: logger.info("【冷启动】开始扫描白名单,唤醒沉睡的聊天流...")
#开始就先暂停一小时,等bot聊一会再说()
await asyncio.sleep(3600)
logger.info("【冷启动】开始扫描白名单,寻找从未聊过的用户...")
# 从全局配置中获取私聊白名单 enabled_private_chats = global_config.proactive_thinking.enabled_private_chats
enabled_private_chats = global_config.proactive_thinking.enabled_private_chats if not enabled_private_chats:
if not enabled_private_chats: logger.debug("【冷启动】私聊白名单为空,任务结束。")
logger.debug("【冷启动】私聊白名单为空,任务暂停一小时。") return
await asyncio.sleep(3600) # 白名单为空时,没必要频繁检查
continue
# 遍历白名单中的每一个用户 for chat_id in enabled_private_chats:
for chat_id in enabled_private_chats: try:
try: platform, user_id_str = chat_id.split(":")
platform, user_id_str = chat_id.split(":") user_id = int(user_id_str)
user_id = int(user_id_str)
# 【核心逻辑】使用 chat_api 检查该用户是否已经存在聊天流ChatStream should_wake_up = False
# 如果返回了 ChatStream 对象,说明已经聊过天了,不是本次任务的目标 stream = chat_api.get_stream_by_user_id(user_id_str, platform)
if chat_api.get_stream_by_user_id(user_id_str, platform):
continue # 跳过已存在的用户
logger.info(f"【冷启动】发现白名单新用户 {chat_id},准备发起第一次问候。") if not stream:
should_wake_up = True
logger.info(f"【冷启动】发现全新用户 {chat_id},准备发起第一次问候。")
elif stream.last_active_time < self.bot_start_time:
should_wake_up = True
logger.info(f"【冷启动】发现沉睡的聊天流 {chat_id} (最后活跃于 {datetime.fromtimestamp(stream.last_active_time)}),准备唤醒。")
# 【增强体验】尝试从关系数据库中获取该用户的昵称 if should_wake_up:
# 这样打招呼时可以更亲切而不是只知道一个冷冰冰的ID
person_id = person_api.get_person_id(platform, user_id) person_id = person_api.get_person_id(platform, user_id)
nickname = await person_api.get_person_value(person_id, "nickname") nickname = await person_api.get_person_value(person_id, "nickname")
# 如果数据库里有昵称,就用数据库里的;如果没有,就用 "用户+ID" 作为备用
user_nickname = nickname or f"用户{user_id}" user_nickname = nickname or f"用户{user_id}"
# 创建 UserInfo 对象,这是创建聊天流的必要信息
user_info = UserInfo(platform=platform, user_id=str(user_id), user_nickname=user_nickname) user_info = UserInfo(platform=platform, user_id=str(user_id), user_nickname=user_nickname)
# 【关键步骤】主动创建聊天流。 # 使用 get_or_create_stream 来安全地获取或创建流
# 创建后,该用户就进入了机器人的“好友列表”,后续将由 ProactiveThinkingTask 接管
stream = await self.chat_manager.get_or_create_stream(platform, user_info) stream = await self.chat_manager.get_or_create_stream(platform, user_info)
await self.executor.execute(stream_id=stream.stream_id, start_mode="cold_start") formatted_stream_id = f"{stream.user_info.platform}:{stream.user_info.user_id}:private"
logger.info(f"【冷启动】已为新用户 {chat_id} (昵称: {user_nickname}) 创建聊天流并发送问候。") await self.executor.execute(stream_id=formatted_stream_id, start_mode="cold_start")
logger.info(f"【冷启动】已为用户 {chat_id} (昵称: {user_nickname}) 发送唤醒/问候消息。")
except ValueError: except ValueError:
logger.warning(f"【冷启动】白名单条目格式错误或用户ID无效已跳过: {chat_id}") logger.warning(f"【冷启动】白名单条目格式错误或用户ID无效已跳过: {chat_id}")
except Exception as e: except Exception as e:
logger.error(f"【冷启动】处理用户 {chat_id} 时发生未知错误: {e}", exc_info=True) logger.error(f"【冷启动】处理用户 {chat_id} 时发生未知错误: {e}", exc_info=True)
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("冷启动任务被正常取消。") logger.info("冷启动任务被正常取消。")
break except Exception as e:
except Exception as e: logger.error(f"【冷启动】任务出现严重错误: {e}", exc_info=True)
logger.error(f"【冷启动】任务出现严重错误将在5分钟后重试: {e}", exc_info=True) finally:
await asyncio.sleep(300) logger.info("【冷启动】任务执行完毕。")
class ProactiveThinkingTask(AsyncTask): class ProactiveThinkingTask(AsyncTask):
@@ -222,13 +215,15 @@ class ProactiveThinkerEventHandler(BaseEventHandler):
logger.info("检测到插件启动事件,正在初始化【主动思考】") logger.info("检测到插件启动事件,正在初始化【主动思考】")
# 检查总开关 # 检查总开关
if global_config.proactive_thinking.enable: if global_config.proactive_thinking.enable:
bot_start_time = time.time() # 记录“诞生时刻”
# 启动负责“日常唤醒”的核心任务 # 启动负责“日常唤醒”的核心任务
proactive_task = ProactiveThinkingTask() proactive_task = ProactiveThinkingTask()
await async_task_manager.add_task(proactive_task) await async_task_manager.add_task(proactive_task)
# 检查“冷启动”功能的独立开关 # 检查“冷启动”功能的独立开关
if global_config.proactive_thinking.enable_cold_start: if global_config.proactive_thinking.enable_cold_start:
cold_start_task = ColdStartTask() cold_start_task = ColdStartTask(bot_start_time)
await async_task_manager.add_task(cold_start_task) await async_task_manager.add_task(cold_start_task)
else: else:

View File

@@ -80,7 +80,7 @@ class ProactiveThinkerExecutor:
plan_prompt = self._build_plan_prompt(context, start_mode, topic, reason) plan_prompt = self._build_plan_prompt(context, start_mode, topic, reason)
is_success, response, _, _ = await llm_api.generate_with_model( is_success, response, _, _ = await llm_api.generate_with_model(
prompt=plan_prompt, model_config=model_config.model_task_config.utils prompt=plan_prompt, model_config=model_config.model_task_config.replyer
) )
if is_success and response: if is_success and response:
@@ -158,12 +158,12 @@ class ProactiveThinkerExecutor:
) )
# 2. 构建基础上下文 # 2. 构建基础上下文
mood_state = "暂时没有"
if global_config.mood.enable_mood: if global_config.mood.enable_mood:
try: try:
mood_state = mood_manager.get_mood_by_chat_id(stream.stream_id).mood_state mood_state = mood_manager.get_mood_by_chat_id(stream.stream_id).mood_state
except Exception as e: except Exception as e:
logger.error(f"获取情绪失败,原因:{e}") logger.error(f"获取情绪失败,原因:{e}")
mood_state = "暂时没有"
base_context = { base_context = {
"schedule_context": schedule_context, "schedule_context": schedule_context,
"recent_chat_history": recent_chat_history, "recent_chat_history": recent_chat_history,
@@ -281,29 +281,47 @@ class ProactiveThinkerExecutor:
# 构建通用尾部 # 构建通用尾部
prompt += """ prompt += """
# 决策指令 # 决策指令
请综合以上所有信息做出决策。你的决策需要以JSON格式输出包含以下字段 请综合以上所有信息,以稳定、真实、拟人的方式做出决策。你的决策需要以JSON格式输出包含以下字段
- `should_reply`: bool, 是否应该发起对话。 - `should_reply`: bool, 是否应该发起对话。
- `topic`: str, 如果 `should_reply` 为 true你打算聊什么话题(例如:问候一下今天的日程、关心一下昨天的某件事、分享一个你自己的趣事等) - `topic`: str, 如果 `should_reply` 为 true你打算聊什么话题
- `reason`: str, 做出此决策的简要理由。 - `reason`: str, 做出此决策的简要理由。
# 决策原则 # 决策原则
- **避免打扰**: 如果你最近(尤其是在最近的几次决策中)已经主动发起对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情 - **谨慎对待未回复的对话**: 在发起新话题前,请检查【最近的聊天摘要】。如果最后一条消息是你自己发送的,请仔细评估等待的时间和上下文,判断再次主动发起对话是否礼貌和自然。如果等待时间很短(例如几分钟或半小时内),通常应该选择“不回复”
- **优先利用上下文**: 优先从【情境分析】中已有的信息如最近的聊天摘要、你的日程、你对Ta的关系印象寻找自然的话题切入点。
- **简单问候作为备选**: 如果上下文中没有合适的话题,可以生成一个简单、真诚的日常问候(例如“在忙吗?”,“下午好呀~”)。
- **避免抽象**: 避免创造过于复杂、抽象或需要对方思考很久才能明白的话题。目标是轻松、自然地开启对话。
- **避免过于频繁**: 如果你最近(尤其是在最近的几次决策中)已经主动发起过对话,请倾向于选择“不回复”,除非有非常重要和紧急的事情。
--- ---
示例1 (应该回复): 示例1 (基于上下文):
{{ {{
"should_reply": true, "should_reply": true,
"topic": "提醒大家今天下午有'项目会议'的日程", "topic": "关心一下Ta昨天提到的那个项目进展如何了",
"reason": "现在是上午,下午有个重要会议,我觉得应该主动提醒一下大家,这会显得我很贴心" "reason": "用户昨天在聊天中提到了一个重要的项目,现在主动关心一下进展,会显得很体贴,也能自然地开启对话"
}} }}
示例2 (不应回复): 示例2 (简单问候):
{{
"should_reply": true,
"topic": "打个招呼问问Ta现在在忙些什么",
"reason": "最近没有聊天记录,日程也很常规,没有特别的切入点。一个简单的日常问候是最安全和自然的方式来重新连接。"
}}
示例3 (不应回复 - 过于频繁):
{{ {{
"should_reply": false, "should_reply": false,
"topic": null, "topic": null,
"reason": "虽然群里很活跃,但现在是深夜,而且最近的聊天话题我也不熟悉,没有合适的理由去打扰大家。" "reason": "虽然群里很活跃,但现在是深夜,而且最近的聊天话题我也不熟悉,没有合适的理由去打扰大家。"
}} }}
示例4 (不应回复 - 等待回应):
{{
"should_reply": false,
"topic": null,
"reason": "我注意到上一条消息是我几分钟前主动发送的,对方可能正在忙。为了表现出耐心和体贴,我现在最好保持安静,等待对方的回应。"
}}
--- ---
请输出你的决策: 请输出你的决策:
@@ -399,6 +417,7 @@ class ProactiveThinkerExecutor:
# 对话指引 # 对话指引
- 你决定和Ta聊聊关于“{topic}”的话题。 - 你决定和Ta聊聊关于“{topic}”的话题。
- **重要**: 在开始你的话题前,必须先用一句通用的、礼貌的开场白进行问候(例如:“在吗?”、“上午好!”、“晚上好呀~”),然后再自然地衔接你的话题,确保整个回复在一条消息内流畅、自然、像人类的说话方式。
- 请结合以上所有情境信息,自然地开启对话。 - 请结合以上所有情境信息,自然地开启对话。
- 你的语气应该符合你的人设({context["mood_state"]})以及你对Ta的好感度。 - 你的语气应该符合你的人设({context["mood_state"]})以及你对Ta的好感度。
""" """
@@ -436,6 +455,7 @@ class ProactiveThinkerExecutor:
# 对话指引 # 对话指引
- 你决定和大家聊聊关于“{topic}”的话题。 - 你决定和大家聊聊关于“{topic}”的话题。
- **重要**: 在开始你的话题前,必须先用一句通用的、礼貌的开场白进行问候(例如:“哈喽,大家好呀~”、“下午好!”),然后再自然地衔接你的话题,确保整个回复在一条消息内流畅、自然、像人类的说话方式。
- 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。你的语气应该符合你的人设)。 - 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。你的语气应该符合你的人设)。
- 请结合以上所有情境信息,自然地开启对话。 - 请结合以上所有情境信息,自然地开启对话。
- 可以分享你的看法、提出相关问题,或者开个合适的玩笑。 - 可以分享你的看法、提出相关问题,或者开个合适的玩笑。