feat: 优化主动思考器和提示词构建逻辑,新增用户关系和等待思考提示词模板

This commit is contained in:
Windpicker-owo
2025-11-30 20:20:23 +08:00
parent c45f0e9cea
commit b148463f66
4 changed files with 251 additions and 17 deletions

View File

@@ -22,7 +22,7 @@ from src.plugin_system.apis.unified_scheduler import TriggerType, unified_schedu
from .models import EventType, SessionStatus
from .planner import generate_plan
from .replyer import generate_reply_text
from .replyer import _clean_reply_text, generate_reply_text
from .session import KokoroSession, get_session_manager
if TYPE_CHECKING:
@@ -211,13 +211,16 @@ class ProactiveThinker:
"""处理连续思考"""
self._stats["continuous_thinking_triggered"] += 1
# 生成等待中的想法
thought = self._generate_waiting_thought(session, progress)
# 获取用户名
user_name = await self._get_user_name(session.user_id, session.stream_id)
# 调用 LLM 生成等待中的想法
thought = await self._generate_waiting_thought(session, user_name, progress)
# 记录到 mental_log
session.add_waiting_update(
waiting_thought=thought,
mood="", # 可以根据进度设置心情
mood="", # 心情已融入 thought 中
)
# 更新思考计数
@@ -232,10 +235,104 @@ class ProactiveThinker:
f"progress={progress:.1%}, thought={thought[:30]}..."
)
def _generate_waiting_thought(self, session: KokoroSession, progress: float) -> str:
"""生成等待中的想法"""
elapsed_minutes = session.waiting_config.get_elapsed_minutes()
async def _generate_waiting_thought(
self,
session: KokoroSession,
user_name: str,
progress: float,
) -> str:
"""调用 LLM 生成等待中的想法"""
try:
from src.chat.utils.prompt import global_prompt_manager
from src.plugin_system.apis import llm_api
from .prompt.builder import get_prompt_builder
from .prompt.prompts import PROMPT_NAMES
# 使用 PromptBuilder 构建人设块
prompt_builder = get_prompt_builder()
persona_block = prompt_builder._build_persona_block()
# 获取关系信息
relation_block = f"你与 {user_name} 还不太熟悉。"
try:
from src.person_info.relationship_manager import relationship_manager
person_info_manager = await self._get_person_info_manager()
if person_info_manager:
platform = global_config.bot.platform if global_config else "qq"
person_id = person_info_manager.get_person_id(platform, session.user_id)
relationship = await relationship_manager.get_relationship(person_id)
if relationship:
relation_block = f"你与 {user_name} 的亲密度是 {relationship.intimacy}{relationship.description or ''}"
except Exception as e:
logger.debug(f"获取关系信息失败: {e}")
# 获取上次发送的消息
last_bot_message = "(未知)"
for entry in reversed(session.mental_log):
if entry.event_type == EventType.BOT_PLANNING and entry.actions:
for action in entry.actions:
if action.get("type") == "kfc_reply":
content = action.get("content", "")
if content:
last_bot_message = content[:100] + ("..." if len(content) > 100 else "")
break
if last_bot_message != "(未知)":
break
# 构建提示词
elapsed_minutes = session.waiting_config.get_elapsed_minutes()
max_wait_minutes = session.waiting_config.max_wait_seconds / 60
expected_reaction = session.waiting_config.expected_reaction or "对方能回复点什么"
prompt = await global_prompt_manager.format_prompt(
PROMPT_NAMES["waiting_thought"],
persona_block=persona_block,
user_name=user_name,
relation_block=relation_block,
last_bot_message=last_bot_message,
expected_reaction=expected_reaction,
elapsed_minutes=elapsed_minutes,
max_wait_minutes=max_wait_minutes,
progress_percent=int(progress * 100),
)
# 调用情绪模型
models = llm_api.get_available_models()
emotion_config = models.get("emotion") or models.get("replyer")
if not emotion_config:
logger.warning("[ProactiveThinker] 未找到 emotion/replyer 模型配置,使用默认想法")
return self._get_fallback_thought(elapsed_minutes, progress)
success, raw_response, _, model_name = await llm_api.generate_with_model(
prompt=prompt,
model_config=emotion_config,
request_type="kokoro_flow_chatter.waiting_thought",
)
if not success or not raw_response:
logger.warning(f"[ProactiveThinker] LLM 调用失败: {raw_response}")
return self._get_fallback_thought(elapsed_minutes, progress)
# 使用统一的文本清理函数
thought = _clean_reply_text(raw_response)
logger.debug(f"[ProactiveThinker] LLM 生成等待想法 (model={model_name}): {thought[:50]}...")
return thought
except Exception as e:
logger.error(f"[ProactiveThinker] 生成等待想法失败: {e}")
import traceback
traceback.print_exc()
return self._get_fallback_thought(
session.waiting_config.get_elapsed_minutes(),
progress
)
def _get_fallback_thought(self, elapsed_minutes: float, progress: float) -> str:
"""获取备用的等待想法(当 LLM 调用失败时使用)"""
if progress < 0.4:
thoughts = [
f"已经等了 {elapsed_minutes:.0f} 分钟了,对方可能在忙吧...",
@@ -254,9 +351,16 @@ class ProactiveThinker:
"要不要主动说点什么呢...",
"快到时间了,对方还是没回",
]
return random.choice(thoughts)
async def _get_person_info_manager(self):
"""获取 person_info_manager"""
try:
from src.person_info.person_info import get_person_info_manager
return get_person_info_manager()
except Exception:
return None
async def _handle_timeout(self, session: KokoroSession) -> None:
"""处理等待超时"""
self._stats["timeout_decisions"] += 1
@@ -577,10 +681,7 @@ class ProactiveThinker:
from src.person_info.person_info import get_person_info_manager
person_info_manager = get_person_info_manager()
# 从 stream_id 提取 platform格式通常是 platform_xxx
platform = "qq" # 默认平台
if "_" in stream_id:
platform = stream_id.split("_")[0]
platform = global_config.bot.platform if global_config else "qq"
person_id = person_info_manager.get_person_id(platform, user_id)
person_name = await person_info_manager.get_value(person_id, "person_name")

View File

@@ -149,8 +149,8 @@ class PromptBuilder:
# 3. 构建活动流
activity_stream = await self._build_activity_stream(session, user_name)
# 4. 构建当前情况(简化版,不需要那么详细
current_situation = await self._build_current_situation(
# 4. 构建当前情况(回复器专用,简化版,不包含决策语言
current_situation = await self._build_replyer_situation(
session, user_name, situation_type, extra_context
)
@@ -315,9 +315,15 @@ class PromptBuilder:
if not history_messages:
return "(暂无聊天记录)"
# 过滤非文本消息(如戳一戳、禁言等系统通知)
text_messages = self._filter_text_messages(history_messages)
if not text_messages:
return "(暂无聊天记录)"
# 构建可读消息
chat_content, _ = await build_readable_messages_with_id(
messages=[msg.flatten() for msg in history_messages[-30:]], # 最多30条
messages=[msg.flatten() for msg in text_messages[-30:]], # 最多30条
timestamp_mode="normal_no_YMD",
truncate=False,
show_actions=False,
@@ -329,6 +335,33 @@ class PromptBuilder:
logger.warning(f"构建聊天历史块失败: {e}")
return "(获取聊天记录失败)"
def _filter_text_messages(self, messages: list) -> list:
"""
过滤非文本消息
移除系统通知消息(如戳一戳、禁言等),只保留正常的文本聊天消息
Args:
messages: 消息列表DatabaseMessages 对象)
Returns:
过滤后的消息列表
"""
filtered = []
for msg in messages:
# 跳过系统通知消息(戳一戳、禁言等)
if getattr(msg, "is_notify", False):
continue
# 跳过没有实际文本内容的消息
content = getattr(msg, "processed_plain_text", "") or getattr(msg, "display_message", "")
if not content or not content.strip():
continue
filtered.append(msg)
return filtered
async def _build_activity_stream(
self,
session: KokoroSession,
@@ -668,6 +701,66 @@ class PromptBuilder:
注意kfc_reply 动作不需要填写 content 字段,回复内容会单独生成。"""
async def _build_replyer_situation(
self,
session: KokoroSession,
user_name: str,
situation_type: str,
extra_context: dict,
) -> str:
"""
构建回复器专用的当前情况描述
与 Planner 的 _build_current_situation 不同,这里不包含决策性语言,
只描述当前的情景背景
"""
from datetime import datetime
current_time = datetime.now().strftime("%Y年%m月%d%H:%M")
if situation_type == "new_message":
return f"现在是 {current_time}{user_name} 刚给你发了消息。"
elif situation_type == "reply_in_time":
elapsed = session.waiting_config.get_elapsed_seconds()
max_wait = session.waiting_config.max_wait_seconds
return (
f"现在是 {current_time}\n"
f"你之前发了消息后在等 {user_name} 的回复。"
f"等了大约 {elapsed / 60:.1f} 分钟(你原本打算最多等 {max_wait / 60:.1f} 分钟)。"
f"现在 {user_name} 回复了!"
)
elif situation_type == "reply_late":
elapsed = session.waiting_config.get_elapsed_seconds()
max_wait = session.waiting_config.max_wait_seconds
return (
f"现在是 {current_time}\n"
f"你之前发了消息后在等 {user_name} 的回复。"
f"你原本打算最多等 {max_wait / 60:.1f} 分钟,但实际等了 {elapsed / 60:.1f} 分钟才收到回复。"
f"虽然有点迟,但 {user_name} 终于回复了。"
)
elif situation_type == "timeout":
elapsed = session.waiting_config.get_elapsed_seconds()
max_wait = session.waiting_config.max_wait_seconds
return (
f"现在是 {current_time}\n"
f"你之前发了消息后一直在等 {user_name} 的回复。"
f"你原本打算最多等 {max_wait / 60:.1f} 分钟,现在已经等了 {elapsed / 60:.1f} 分钟了,对方还是没回。"
f"你决定主动说点什么。"
)
elif situation_type == "proactive":
silence = extra_context.get("silence_duration", "一段时间")
return (
f"现在是 {current_time}\n"
f"你和 {user_name} 已经有一段时间没聊天了(沉默了 {silence})。"
f"你决定主动找 {user_name} 聊点什么。"
)
# 默认
return f"现在是 {current_time}"
async def _build_reply_context(
self,
session: KokoroSession,

View File

@@ -337,6 +337,45 @@ kfc_REPLYER_CONTEXT_PROACTIVE = Prompt(
你决定主动打破沉默,找 {user_name} 聊点什么。想一个自然的开场白,不要太突兀。""",
)
# =================================================================================================
# 等待思考提示词模板(用于生成等待中的心理活动)
# =================================================================================================
kfc_WAITING_THOUGHT = Prompt(
name="kfc_waiting_thought",
template="""# 等待中的心理活动
## 你是谁
{persona_block}
## 你与 {user_name} 的关系
{relation_block}
## 当前情况
你刚才给 {user_name} 发了消息,现在正在等待对方回复。
**你发的消息**{last_bot_message}
**你期待的反应**{expected_reaction}
**已等待时间**{elapsed_minutes:.1f} 分钟
**计划最多等待**{max_wait_minutes:.1f} 分钟
**等待进度**{progress_percent}%
## 任务
请描述你此刻等待时的内心想法。这是你私下的心理活动,不是要发送的消息。
**要求**
- 用第一人称描述你的感受和想法
- 要符合你的性格和你们的关系
- 根据等待进度自然表达情绪变化:
- 初期0-40%):可能比较平静,稍微期待
- 中期40-70%):可能开始有点在意,但还好
- 后期70-100%):可能有点焦虑、担心,或者想主动做点什么
- 不要太长1-2句话即可
- 不要输出 JSON直接输出你的想法
现在,请直接输出你等待时的内心想法:""",
)
# 导出所有模板名称,方便外部引用
PROMPT_NAMES = {
"main": "kfc_main",
@@ -347,6 +386,7 @@ PROMPT_NAMES = {
"replyer_context_in_time": "kfc_replyer_context_in_time",
"replyer_context_late": "kfc_replyer_context_late",
"replyer_context_proactive": "kfc_replyer_context_proactive",
"waiting_thought": "kfc_waiting_thought",
"situation_new_message": "kfc_situation_new_message",
"situation_reply_in_time": "kfc_situation_reply_in_time",
"situation_reply_late": "kfc_situation_reply_late",

View File

@@ -195,7 +195,7 @@ class PokeAction(BaseAction):
for i in range(times):
logger.info(f"正在向 {display_name} ({user_id}) 发送第 {i + 1}/{times} 次戳一戳...")
await self.send_command(
"SEND_POKE", args=poke_args, display_message=f"戳了戳 {display_name} ({i + 1}/{times})"
"SEND_POKE", args=poke_args, display_message=f"(系统动作)戳了戳 {display_name} ({i + 1}/{times})"
)
# 添加一个延迟,避免因发送过快导致后续戳一戳失败
await asyncio.sleep(1.5)