refactor(proactive_thinker): 优化主动思考的决策与交互,使其更自然且避免打扰
本次提交对主动思考插件进行了多项核心优化,旨在提升其交互的自然度和人性化,并引入了关键的防打扰机制。
主要变更包括:
1. **重构冷启动任务 (`ColdStartTask`)**:
- 任务逻辑从一个长期运行的周期性任务,重构为在机器人启动时执行一次的“唤醒”任务。
- 新逻辑不仅能为白名单中的全新用户发起首次问候,还能智能地“唤醒”那些因机器人重启而“沉睡”的聊天流,确保了主动思考功能的连续性。
2. **增强决策提示词 (`_build_plan_prompt`)**:
- 引入了更精细的决策原则,核心是增加了防打扰机制。现在模型在决策时会检查上一条消息是否为自己发送,如果对方尚未回复,则倾向于不发起新对话,以表现出耐心和体贴。
- 优化了示例,引导模型优先利用上下文信息,并在无切入点时使用简单的问候,避免创造生硬抽象的话题。
3. **改善回复生成逻辑 (`_build_*_reply_prompt`)**:
- 在生成回复的指令中,明确要求模型必须先用一句通用的礼貌问候语(如“在吗?”、“下午好!”)作为开场白,然后再衔接具体话题。这使得主动发起的对话更加自然、流畅,符合人类的沟通习惯。
4. **模型调整**:
- 将决策规划阶段的 LLM 模型从 `utils` 调整为 `replyer`,以更好地适应生成对话策略的任务。
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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}”的话题。
|
||||||
|
- **重要**: 在开始你的话题前,必须先用一句通用的、礼貌的开场白进行问候(例如:“哈喽,大家好呀~”、“下午好!”),然后再自然地衔接你的话题,确保整个回复在一条消息内流畅、自然、像人类的说话方式。
|
||||||
- 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。你的语气应该符合你的人设)。
|
- 你的语气应该更活泼、更具包容性,以吸引更多群成员参与讨论。你的语气应该符合你的人设)。
|
||||||
- 请结合以上所有情境信息,自然地开启对话。
|
- 请结合以上所有情境信息,自然地开启对话。
|
||||||
- 可以分享你的看法、提出相关问题,或者开个合适的玩笑。
|
- 可以分享你的看法、提出相关问题,或者开个合适的玩笑。
|
||||||
|
|||||||
Reference in New Issue
Block a user