feat(reply): 引入 reply 和 respond 动作,优化消息回复机制

- 增加 reply 动作,针对单条消息进行深度回复,使用 s4u 模板。
- 增加 respond 动作,统一回应未读消息,使用 normal 模板。
- 更新核心动作插件以支持新动作,确保配置选项可用。
- 优化动作执行逻辑,提升对话流畅性和响应准确性。
This commit is contained in:
Windpicker-owo
2025-11-10 13:24:45 +08:00
parent b27b2e4811
commit 90ed6964b3
9 changed files with 341 additions and 129 deletions

View File

@@ -69,14 +69,7 @@ def init_prompt():
{keywords_reaction_prompt}
{moderation_prompt}
不要复读你前面发过的内容,意思相近也不行。
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 ),只输出一条回复就好。
**【!!!绝对禁止!!!】在回复中输出任何格式化标记**
- **核心原则**: 你的回复**只能**包含纯粹的口语化文本。任何看起来像程序指令、系统提示或格式标签的内容都**绝对不允许**出现在你的回复里。
- **禁止模仿系统消息**: 绝对禁止输出任何类似 `[回复<xxx>xxx]`、`[表情包xxx]`、`[图片xxx]` 的格式。这些都是系统用于展示消息的方式,不是你应该说的话。
- **禁止模仿动作指令**: 绝对禁止输出 `[戳了戳]` 或 `[poke]`。这类互动由名为 `poke_user` 的特殊动作处理,不是文本消息。
- **正确提及用户**: 如果想提到某人,直接说“你”或他/她的名字,绝对禁止使用 `[回复<某人>]` 或 `@某人` 的格式。
- **正确表达情绪**: 如果想表达笑的情绪,直接说“哈哈”、“嘻嘻”等,绝对禁止使用 `[表情包:笑哭]` 这样的文字。
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀冒号和引号括号表情包at[xxxxx]系统格式化文字或 @等 ),只输出一条回复就好。
*你叫{bot_name},也有人叫你{bot_nickname}*
@@ -138,19 +131,13 @@ def init_prompt():
## 规则
{safety_guidelines_block}
注意:在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。
你的回复应该是一条简短、完整且口语化的回复。
--------------------------------
{time_block}
请注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容。
**【!!!绝对禁止!!!】在回复中输出任何格式化标记**
- **核心原则**: 你的回复**只能**包含纯粹的口语化文本。任何看起来像程序指令、系统提示或格式标签的内容都**绝对不允许**出现在你的回复里。
- **禁止模仿系统消息**: 绝对禁止输出任何类似 `[回复<xxx>xxx]`、`[表情包xxx]`、`[图片xxx]` 的格式。这些都是系统用于展示消息的方式,不是你应该说的话。
- **禁止模仿动作指令**: 绝对禁止输出 `[戳了戳]` 或 `[poke]`。这类互动由名为 `poke_user` 的特殊动作处理,不是文本消息。
- **正确提及用户**: 如果想提到某人,直接说“你”或他/她的名字,绝对禁止使用 `[回复<某人>]` 或 `@某人` 的格式。
- **正确表达情绪**: 如果想表达笑的情绪,直接说“哈哈”、“嘻嘻”等,绝对禁止使用 `[表情包:笑哭]` 这样的文字。
请注意不要输出多余内容(包括前后缀冒号和引号at[xxxxx]系统格式化文字或 @等 )。只输出回复内容。
{moderation_prompt}
@@ -178,60 +165,66 @@ If you need to use the search tool, please directly call the function "lpmm_sear
name="lpmm_get_knowledge_prompt",
)
# normal 版 prompt 模板(0.9之前的简化模式
# normal 版 prompt 模板(参考 s4u 格式,用于统一回应未读消息
logger.debug("[Prompt模式调试] 正在注册normal_style_prompt模板")
Prompt(
"""
{chat_scene}
# 人设:{identity}
**重要:消息针对性判断**
在回应之前,首先分析消息的针对性:
1. **直接针对你**@你、回复你、明确询问你 → 必须回应
2. **间接相关**:涉及你感兴趣的话题但未直接问你 → 谨慎参与
3. **他人对话**:与你无关的私人交流 → 通常不参与
4. **重复内容**:他人已充分回答的问题 → 避免重复
## 当前状态
- 你现在的心情是:{mood_state}
{schedule_block}
{expression_habits_block}
{tool_info_block}
{knowledge_prompt}
{memory_block}
{relation_info_block}
{extra_info_block}
## 历史记录
### 📜 已读历史消息
{read_history_prompt}
{cross_context_block}
{identity}
如果有人说你是人机,你可以用一种阴阳怪气的口吻来回应
{schedule_block}
### 📬 未读历史消息
{unread_history_prompt}
{notice_block}
## 表达方式
- *你需要参考你的回复风格:*
{reply_style}
{keywords_reaction_prompt}
{expression_habits_block}
{tool_info_block}
{knowledge_prompt}
## 其他信息
{memory_block}
{relation_info_block}
{extra_info_block}
{auth_role_prompt_block}
{action_descriptions}
下面是群里最近的聊天内容:
--------------------------------
## 任务
*{chat_scene}*
### 核心任务
- 你需要对以上未读历史消息进行统一回应。这些消息可能来自不同的参与者,你需要理解整体对话动态,生成一段自然、连贯的回复。
- 你的回复应该能够推动对话继续,可以回应其中一个或多个话题,也可以提出新的观点。
## 规则
{safety_guidelines_block}
注意:在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。
你的回复应该是一条简短、完整且口语化的回复。
--------------------------------
{time_block}
{chat_info}
--------------------------------
{reply_target_block}
你现在的心情是:{mood_state}
{config_expression_style}
注意不要复读你前面发过的内容,意思相近也不行。
{keywords_reaction_prompt}
请注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容。
**【!!!绝对禁止!!!】在回复中输出任何格式化标记**
- **核心原则**: 你的回复**只能**包含纯粹的口语化文本。任何看起来像程序指令、系统提示或格式标签的内容都**绝对不允许**出现在你的回复里。
- **禁止模仿系统消息**: 绝对禁止输出任何类似 `[回复<xxx>xxx]`、`[表情包xxx]`、`[图片xxx]` 的格式。这些都是系统用于展示消息的方式,不是你应该说的话。
- **禁止模仿动作指令**: 绝对禁止输出 `[戳了戳]` 或 `[poke]`。这类互动由名为 `poke_user` 的特殊动作处理,不是文本消息。
- **正确提及用户**: 如果想提到某人,直接说“你”或他/她的名字,绝对禁止使用 `[回复<某人>]` 或 `@某人` 的格式。
- **正确表达情绪**: 如果想表达笑的情绪,直接说“哈哈”、“嘻嘻”等,绝对禁止使用 `[表情包:笑哭]` 这样的文字。
请注意不要输出多余内容(包括前后缀冒号和引号at[xxxxx]系统格式化文字或 @等 )。只输出回复内容。
{moderation_prompt}
你的核心任务是针对 {reply_target_block} 中提到的内容,{relation_info_block}生成一段紧密相关且能推动对话的回复。你的回复应该:
1. 明确回应目标消息,而不是宽泛地评论。
2. 可以分享你的看法、提出相关问题,或者开个合适的玩笑。
3. 目的是让对话更有趣、更深入。
最终请输出一条简短、完整且口语化的回复。
*你叫{bot_name},也有人叫你{bot_nickname}*
@@ -371,6 +364,15 @@ class DefaultReplyer:
return False, None, None
llm_response = None
try:
# 从available_actions中提取prompt_mode由action_manager传递
# 如果没有指定默认使用s4u模式
prompt_mode_value: str = "s4u"
if available_actions and "_prompt_mode" in available_actions:
mode = available_actions.get("_prompt_mode", "s4u")
# 确保类型安全
if isinstance(mode, str):
prompt_mode_value = mode
# 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_reply_context(
@@ -380,6 +382,7 @@ class DefaultReplyer:
choosen_actions=choosen_actions,
enable_tool=enable_tool,
reply_message=reply_message,
prompt_mode=prompt_mode_value, # 传递prompt_mode
)
if not prompt:
@@ -1121,6 +1124,7 @@ class DefaultReplyer:
available_actions: dict[str, ActionInfo] | None = None,
enable_tool: bool = True,
reply_message: DatabaseMessages | None = None,
prompt_mode: str = "s4u", # 新增参数s4u 或 normal
) -> str:
"""
构建回复器上下文
@@ -1133,6 +1137,7 @@ class DefaultReplyer:
enable_timeout: 是否启用超时处理
enable_tool: 是否启用工具调用
reply_message: 回复的原始消息
prompt_mode: 提示词模式,"s4u"(针对单条消息回复)或"normal"(统一回应未读消息)
Returns:
str: 构建好的上下文
@@ -1213,16 +1218,41 @@ class DefaultReplyer:
target = await replace_user_references_async(target, chat_stream.platform, replace_bot_name=True)
# (简化)不再对自消息做额外任务段落清理,只通过前置选择逻辑避免自目标
# 构建action描述 (如果启用planner)
# 构建action描述告诉回复器已选取的动作
action_descriptions = ""
if available_actions:
action_descriptions = "以下是系统中可用的动作列表。**【重要】**这些动作将由一个独立的决策模型决定是否执行,**并非你的职责**。你只需要了解这些能力的存在,以便更好地理解对话情景,**严禁**在你的回复中模仿、调用或提及这些动作本身。\n"
for action_name, action_info in available_actions.items():
action_description = action_info.description
action_descriptions += f"- {action_name}: {action_description}\n"
action_descriptions += "\n"
# 过滤掉特殊键以_开头
action_items = {k: v for k, v in available_actions.items() if not k.startswith("_")}
# 提取目标消息信息(如果存在)
target_msg_info = available_actions.get("_target_message") # type: ignore
if action_items:
if len(action_items) == 1:
# 单个动作
action_name, action_info = list(action_items.items())[0]
action_desc = action_info.description
# 构建基础决策信息
action_descriptions = f"## 决策信息\n\n你已经决定要执行 **{action_name}** 动作({action_desc})。\n\n"
# 如果有目标消息信息,添加目标消息详情
if target_msg_info and isinstance(target_msg_info, dict):
import time as time_module
sender = target_msg_info.get("sender", "未知用户")
content = target_msg_info.get("content", "")
msg_time = target_msg_info.get("time", 0)
time_str = time_module.strftime("%H:%M:%S", time_module.localtime(msg_time)) if msg_time else "未知时间"
action_descriptions += f"**目标消息**: {time_str} {sender} 说: {content}\n\n"
else:
# 多个动作
action_descriptions = "## 决策信息\n\n你已经决定同时执行以下动作:\n\n"
for action_name, action_info in action_items.items():
action_desc = action_info.description
action_descriptions += f"- **{action_name}**: {action_desc}\n"
action_descriptions += "\n"
# 从内存获取历史消息,避免重复查询数据库
from src.plugin_system.apis.chat_api import get_chat_manager
@@ -1423,7 +1453,7 @@ class DefaultReplyer:
duration_minutes = (now - start_time).total_seconds() / 60
remaining_minutes = (end_time - now).total_seconds() / 60
schedule_block = (
f"你当前正在进行“{activity}”,"
f"- 你当前正在进行“{activity}”,"
f"计划时间从{start_time.strftime('%H:%M')}{end_time.strftime('%H:%M')}"
f"这项活动已经开始了{duration_minutes:.0f}分钟,"
f"预计还有{remaining_minutes:.0f}分钟结束。"
@@ -1431,9 +1461,9 @@ class DefaultReplyer:
)
except (ValueError, AttributeError):
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
schedule_block = f"- 你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
else:
schedule_block = f"你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
schedule_block = f"- 你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
moderation_prompt_block = (
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
@@ -1483,11 +1513,21 @@ class DefaultReplyer:
if sender and target:
if is_group_chat:
reply_target_block = (
f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。原因是{reply_reason}"
)
if sender:
reply_target_block = (
f"现在{sender}的消息:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。"
)
elif target:
reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。"
else:
reply_target_block = "现在,你想要在群里发言或者回复消息。"
else: # private chat
reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,针对这条消息回复。原因是{reply_reason}"
if sender:
reply_target_block = f"现在{sender}的消息:{target}。引起了你的注意,针对这条消息回复。"
elif target:
reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。"
else:
reply_target_block = "现在,你想要回复。"
else:
reply_target_block = ""
@@ -1511,7 +1551,7 @@ class DefaultReplyer:
available_actions=available_actions,
enable_tool=enable_tool,
chat_target_info=self.chat_target_info,
prompt_mode="s4u",
prompt_mode=prompt_mode, # 使用传入的prompt_mode参数
message_list_before_now_long=message_list_before_now_long,
message_list_before_short=message_list_before_short,
chat_talking_prompt_short=chat_talking_prompt_short,
@@ -1539,8 +1579,10 @@ class DefaultReplyer:
bot_nickname=",".join(global_config.bot.alias_names) if global_config.bot.alias_names else "",
)
# 使用新的统一Prompt系统 - 使用正确的模板名称
template_name = "s4u_style_prompt"
# 使用新的统一Prompt系统 - 根据prompt_mode选择模板
# s4u: 针对单条消息的深度回复
# normal: 对未读消息的统一回应
template_name = "s4u_style_prompt" if prompt_mode == "s4u" else "normal_style_prompt"
# 获取模板内容
template_prompt = await global_prompt_manager.get_prompt_async(template_name)