feat(proactive_thinking): 重构主动思考为由Planner直接决策

重构了主动思考的触发和决策流程。原有的通过生成特定prompt来启动思考循环的方式被移除,改为直接调用Planner的`PROACTIVE`模式。

- **Planner增强**:
  - 新增`PROACTIVE`聊天模式,用于处理主动思考场景。
  - 为`PROACTIVE`模式设计了专用的prompt模板,整合了长期记忆、当前状态等信息。
  - 引入`do_nothing`动作,允许Planner在分析后决定保持沉默。
  - 增加从海马体(长期记忆)获取上下文的功能,为决策提供更丰富的背景。

- **ProactiveThinker简化**:
  - 移除了原有的prompt生成和调用`observe`的逻辑。
  - 现在直接调用`action_planner.plan(mode=ChatMode.PROACTIVE)`来获取决策。
  - 根据Planner返回的动作(如`do_nothing`或具体行动),决定是保持沉默还是执行计划。

- **CycleProcessor & Tracker调整**:
  - `CycleProcessor`新增`execute_plan`方法,用于执行一个已经由Planner预先制定好的计划。
  - `CycleTracker`能够区分并标记由主动思考发起的循环(例如,cycle_id为 "1.p"),以便于追踪和分析。
This commit is contained in:
minecraft1024a
2025-08-21 19:29:14 +08:00
parent 15e53742f5
commit 09b36585b3
6 changed files with 191 additions and 116 deletions

View File

@@ -130,6 +130,34 @@ class CycleProcessor:
return True
async def execute_plan(self, action_result: Dict[str, Any], target_message: Optional[Dict[str, Any]]):
"""
执行一个已经制定好的计划
"""
action_type = action_result.get("action_type", "error")
# 这里我们需要为执行计划创建一个新的循环追踪
cycle_timers, thinking_id = self.cycle_tracker.start_cycle(is_proactive=True)
loop_start_time = time.time()
if action_type == "reply":
# 主动思考不应该直接触发简单回复但为了逻辑完整性我们假设它会调用response_handler
# 注意:这里的 available_actions 和 plan_result 是缺失的,需要根据实际情况处理
await self._handle_reply_action(target_message, {}, None, loop_start_time, cycle_timers, thinking_id, {"action_result": action_result})
else:
await self._handle_other_actions(
action_type,
action_result.get("reasoning", ""),
action_result.get("action_data", {}),
action_result.get("is_parallel", False),
None,
target_message,
cycle_timers,
thinking_id,
{"action_result": action_result},
loop_start_time
)
async def _handle_reply_action(self, message_data, available_actions, gen_task, loop_start_time, cycle_timers, thinking_id, plan_result):
"""
处理回复类型的动作

View File

@@ -21,10 +21,13 @@ class CycleTracker:
"""
self.context = context
def start_cycle(self) -> Tuple[Dict[str, float], str]:
def start_cycle(self, is_proactive: bool = False) -> Tuple[Dict[str, float], str]:
"""
开始新的思考循环
Args:
is_proactive: 标记这个循环是否由主动思考发起
Returns:
tuple: (循环计时器字典, 思考ID字符串)
@@ -34,8 +37,11 @@ class CycleTracker:
- 生成唯一的思考ID
- 初始化循环计时器
"""
self.context.cycle_counter += 1
self.context.current_cycle_detail = CycleDetail(self.context.cycle_counter)
if not is_proactive:
self.context.cycle_counter += 1
cycle_id = self.context.cycle_counter if not is_proactive else f"{self.context.cycle_counter}.p"
self.context.current_cycle_detail = CycleDetail(cycle_id)
self.context.current_cycle_detail.thinking_id = f"tid{str(round(time.time(), 2))}"
cycle_timers = {}
return cycle_timers, self.context.current_cycle_detail.thinking_id

View File

@@ -1,5 +1,5 @@
import time
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, Union
from src.config.config import global_config
from src.common.logger import get_logger
@@ -23,7 +23,7 @@ class CycleDetail:
- 提供序列化和转换功能
"""
def __init__(self, cycle_id: int):
def __init__(self, cycle_id: Union[int, str]):
"""
初始化循环详情记录

View File

@@ -245,58 +245,23 @@ class ProactiveThinker:
Args:
silence_duration: 沉默持续时间(秒)
功能说明:
- 格式化沉默时间并记录触发日志
- 获取适当的思考提示模板
- 创建主动思考类型的消息数据
- 调用循环处理器执行思考和可能的回复
- 处理执行过程中的异常
"""
formatted_time = self._format_duration(silence_duration)
logger.info(f"{self.context.log_prefix} 触发主动思考,已沉默{formatted_time}")
try:
proactive_prompt = self._get_proactive_prompt(formatted_time)
# 直接调用 planner 的 PROACTIVE 模式
action_result_tuple, target_message = await self.cycle_processor.action_planner.plan(mode=ChatMode.PROACTIVE)
action_result = action_result_tuple.get("action_result")
thinking_message = {
"processed_plain_text": proactive_prompt,
"user_id": "system_proactive_thinking",
"user_platform": "system",
"timestamp": time.time(),
"message_type": "proactive_thinking",
"user_nickname": "系统主动思考",
"chat_info_platform": "system",
"message_id": f"proactive_{int(time.time())}",
}
logger.info(f"{self.context.log_prefix} 开始主动思考...")
await self.cycle_processor.observe(message_data=thinking_message)
logger.info(f"{self.context.log_prefix} 主动思考完成")
# 如果决策不是 do_nothing则执行
if action_result and action_result.get("action_type") != "do_nothing":
logger.info(f"{self.context.log_prefix} 主动思考决策: {action_result.get('action_type')}, 原因: {action_result.get('reasoning')}")
# 将决策结果交给 cycle_processor 的后续流程处理
await self.cycle_processor.execute_plan(action_result, target_message)
else:
logger.info(f"{self.context.log_prefix} 主动思考决策: 保持沉默")
except Exception as e:
logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}")
logger.error(traceback.format_exc())
def _get_proactive_prompt(self, formatted_time: str) -> str:
"""
获取主动思考的提示模板
Args:
formatted_time: 格式化后的沉默时间字符串
Returns:
str: 填充了时间信息的提示模板
功能说明:
- 优先使用自定义的提示模板(如果配置了)
- 根据聊天类型(群聊/私聊)选择默认模板
- 将格式化的时间信息填入模板
- 返回完整的主动思考提示文本
"""
if hasattr(global_config.chat, 'proactive_thinking_prompt_template') and global_config.chat.proactive_thinking_prompt_template.strip():
return global_config.chat.proactive_thinking_prompt_template.format(time=formatted_time)
chat_type = "group" if self.context.chat_stream and self.context.chat_stream.group_info else "private"
prompt_template = self.proactive_thinking_prompts.get(chat_type, self.proactive_thinking_prompts["group"])
return prompt_template.format(time=formatted_time)

View File

@@ -23,15 +23,16 @@ from src.plugin_system.base.component_types import ActionInfo, ChatMode, Compone
from src.plugin_system.core.component_registry import component_registry
from src.manager.schedule_manager import schedule_manager
from src.mood.mood_manager import mood_manager
from src.chat.memory_system.Hippocampus import hippocampus_manager
logger = get_logger("planner")
install(extra_lines=3)
def init_prompt():
Prompt(
"""
{schedule_block}
Prompt(
"""
{schedule_block}
{mood_block}
{time_block}
{identity_block}
@@ -55,6 +56,32 @@ def init_prompt():
"planner_prompt",
)
Prompt(
"""
# 主动思考决策
## 你的内部状态
{time_block}
{identity_block}
{schedule_block}
{mood_block}
## 长期记忆摘要
{long_term_memory_block}
## 任务
基于以上所有信息,分析当前情况,决定是否需要主动做些什么。
如果你认为不需要,就选择 'do_nothing'
## 可用动作
{action_options_text}
你必须从上面列出的可用action中选择一个。
请以严格的 JSON 格式输出,且仅包含 JSON 内容:
""",
"proactive_planner_prompt",
)
Prompt(
"""
动作:{action_name}
@@ -84,6 +111,78 @@ class ActionPlanner:
self.plan_retry_count = 0
self.max_plan_retries = 3
async def _get_long_term_memory_context(self) -> str:
"""
获取长期记忆上下文
"""
try:
# 1. 生成时间相关的关键词
now = datetime.now()
keywords = ["今天", "日程", "计划"]
if 5 <= now.hour < 12:
keywords.append("早上")
elif 12 <= now.hour < 18:
keywords.append("中午")
else:
keywords.append("晚上")
# TODO: 添加与聊天对象相关的关键词
# 2. 调用 hippocampus_manager 检索记忆
retrieved_memories = await hippocampus_manager.get_memory_from_topic(
valid_keywords=keywords,
max_memory_num=5,
max_memory_length=1
)
if not retrieved_memories:
return "最近没有什么特别的记忆。"
# 3. 格式化记忆
memory_statements = []
for topic, memory_item in retrieved_memories:
memory_statements.append(f"关于'{topic}', 你记得'{memory_item}'")
return " ".join(memory_statements)
except Exception as e:
logger.error(f"获取长期记忆时出错: {e}")
return "回忆时出现了一些问题。"
async def _build_action_options(self, current_available_actions: Dict[str, ActionInfo], mode: ChatMode, target_prompt: str = "") -> str:
"""
构建动作选项
"""
action_options_block = ""
if mode == ChatMode.PROACTIVE:
action_options_block += """动作do_nothing
动作描述:保持沉默,不主动发起任何动作或对话。
- 当你分析了所有信息后,觉得当前不是一个发起互动的好时机时
{{
"action": "do_nothing",
"reason":"决定保持沉默的具体原因"
}}
"""
for action_name, action_info in current_available_actions.items():
# TODO: 增加一个字段来判断action是否支持在PROACTIVE模式下使用
param_text = ""
if action_info.action_parameters:
param_text = "\n" + "\n".join(f' "{p_name}":"{p_desc}"' for p_name, p_desc in action_info.action_parameters.items())
require_text = "\n".join(f"- {req}" for req in action_info.action_require)
using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt")
action_options_block += using_action_prompt.format(
action_name=action_name,
action_description=action_info.description,
action_parameters=param_text,
action_require=require_text,
target_prompt=target_prompt,
)
return action_options_block
def find_message_by_id(self, message_id: str, message_id_list: list) -> Optional[Dict[str, Any]]:
# sourcery skip: use-next
"""
@@ -118,7 +217,7 @@ class ActionPlanner:
async def plan(
self, mode: ChatMode = ChatMode.FOCUS
) -> Tuple[Dict[str, Dict[str, Any] | str], Optional[Dict[str, Any]]]:
) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
"""
规划器 (Planner): 使用LLM根据上下文决定做出什么动作。
"""
@@ -267,6 +366,40 @@ class ActionPlanner:
) -> tuple[str, list]: # sourcery skip: use-join
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try:
# --- 通用信息获取 ---
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
bot_name = global_config.bot.nickname
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else ""
bot_core_personality = global_config.personality.personality_core
identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}"
schedule_block = ""
if global_config.schedule.enable:
if current_activity := schedule_manager.get_current_activity():
schedule_block = f"你当前正在:{current_activity}"
mood_block = ""
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(self.chat_id)
mood_block = f"你现在的心情是:{chat_mood.mood_state}"
# --- 根据模式构建不同的Prompt ---
if mode == ChatMode.PROACTIVE:
long_term_memory_block = await self._get_long_term_memory_context()
action_options_text = await self._build_action_options(current_available_actions, mode)
prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt")
prompt = prompt_template.format(
time_block=time_block,
identity_block=identity_block,
schedule_block=schedule_block,
mood_block=mood_block,
long_term_memory_block=long_term_memory_block,
action_options_text=action_options_text,
)
return prompt, []
# --- FOCUS 和 NORMAL 模式的逻辑 ---
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
@@ -288,16 +421,11 @@ class ActionPlanner:
limit=5,
)
actions_before_now_block = build_readable_actions(
actions=actions_before_now,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
# 注意不在这里更新last_obs_time_mark应该在plan成功后再更新避免异常情况下错误更新时间戳
self.last_obs_time_mark = time.time()
if mode == ChatMode.FOCUS:
mentioned_bonus = ""
if global_config.chat.mentioned_bot_inevitable_reply:
@@ -323,7 +451,7 @@ class ActionPlanner:
}}
"""
else:
else: # NORMAL Mode
by_what = "聊天内容和用户的最新消息"
target_prompt = ""
no_action_block = """重要说明:
@@ -331,67 +459,14 @@ class ActionPlanner:
- 其他action表示在普通回复的基础上执行相应的额外动作"""
chat_context_description = "你现在正在一个群聊中"
chat_target_name = None # Only relevant for private
if not is_group_chat and chat_target_info:
chat_target_name = (
chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方"
)
chat_target_name = chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方"
chat_context_description = f"你正在和 {chat_target_name} 私聊"
action_options_block = ""
# 先定义 schedule_block 和 mood_block这些在主模板中需要使用
schedule_block = ""
if global_config.schedule.enable:
current_activity = schedule_manager.get_current_activity()
if current_activity:
schedule_block = f"你当前正在:{current_activity}"
mood_block = ""
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(self.chat_id)
mood_block = f"你现在的心情是:{chat_mood.mood_state}"
for using_actions_name, using_actions_info in current_available_actions.items():
if using_actions_info.action_parameters:
param_text = "\n"
for param_name, param_description in using_actions_info.action_parameters.items():
param_text += f' "{param_name}":"{param_description}"\n'
param_text = param_text.rstrip("\n")
else:
param_text = ""
require_text = ""
for require_item in using_actions_info.action_require:
require_text += f"- {require_item}\n"
require_text = require_text.rstrip("\n")
using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt")
using_action_prompt = using_action_prompt.format(
schedule_block=schedule_block,
mood_block=mood_block,
action_name=using_actions_name,
action_description=using_actions_info.description,
action_parameters=param_text,
action_require=require_text,
target_prompt=target_prompt,
)
action_options_block += using_action_prompt
action_options_block = await self._build_action_options(current_available_actions, mode, target_prompt)
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
bot_name = global_config.bot.nickname
if global_config.bot.alias_names:
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}"
else:
bot_nickname = ""
bot_core_personality = global_config.personality.personality_core
identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}"
# 处理自定义提示词
custom_prompt_block = ""
if global_config.custom_prompt.planner_custom_prompt_enable and global_config.custom_prompt.planner_custom_prompt_content:
custom_prompt_block = global_config.custom_prompt.planner_custom_prompt_content

View File

@@ -40,6 +40,7 @@ class ChatMode(Enum):
FOCUS = "focus" # Focus聊天模式
NORMAL = "normal" # Normal聊天模式
PROACTIVE = "proactive" # 主动思考模式
PRIORITY = "priority" # 优先级聊天模式
ALL = "all" # 所有聊天模式