From e2f988745ab32f05db24d7f936e5dd8012478a5a Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Fri, 31 Oct 2025 21:56:21 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(config):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E8=81=8A=E5=A4=A9=E9=85=8D=E7=BD=AE=E5=8F=8A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=AD=97=E6=AE=B5=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=A8=A1=E6=9D=BF=E4=BB=A5=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 2 -- src/config/official_configs.py | 48 +------------------------------ template/bot_config_template.toml | 16 ++--------- 3 files changed, 3 insertions(+), 63 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index b22674893..9ba1b32ad 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -31,7 +31,6 @@ from src.config.official_configs import ( MemoryConfig, MessageReceiveConfig, MoodConfig, - NormalChatConfig, NoticeConfig, PermissionConfig, PersonalityConfig, @@ -379,7 +378,6 @@ class Config(ValidatedConfigBase): chat: ChatConfig = Field(..., description="聊天配置") message_receive: MessageReceiveConfig = Field(..., description="消息接收配置") notice: NoticeConfig = Field(..., description="Notice消息配置") - normal_chat: NormalChatConfig = Field(..., description="普通聊天配置") emoji: EmojiConfig = Field(..., description="表情配置") expression: ExpressionConfig = Field(..., description="表达配置") memory: MemoryConfig = Field(..., description="记忆配置") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 24957cd30..cc0799a83 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -103,19 +103,10 @@ class ChatConfig(ValidatedConfigBase): """聊天配置类""" max_context_size: int = Field(default=18, description="最大上下文大小") - replyer_random_probability: float = Field(default=0.5, description="回复者随机概率") thinking_timeout: int = Field(default=40, description="思考超时时间") - talk_frequency: float = Field(default=1.0, description="聊天频率") mentioned_bot_inevitable_reply: bool = Field(default=False, description="提到机器人的必然回复") at_bot_inevitable_reply: bool = Field(default=False, description="@机器人的必然回复") allow_reply_self: bool = Field(default=False, description="是否允许回复自己说的话") - focus_value: float = Field(default=1.0, description="专注值") - focus_mode_quiet_groups: list[str] = Field( - default_factory=list, - description='专注模式下需要保持安静的群组列表, 格式: ["platform:group_id1", "platform:group_id2"]', - ) - force_reply_private: bool = Field(default=False, description="强制回复私聊") - group_chat_mode: Literal["auto", "normal", "focus"] = Field(default="auto", description="群聊模式") timestamp_display_mode: Literal["normal", "normal_no_YMD", "relative"] = Field( default="normal_no_YMD", description="时间戳显示模式" ) @@ -129,13 +120,6 @@ class ChatConfig(ValidatedConfigBase): default=0.1, ge=0.0, le=1.0, description="最低打断概率(即使达到较高打断次数,也保证有此概率的打断机会)" ) - # DEPRECATED: interruption_probability_factor (已废弃的配置项) - # 新的线性概率模型不再需要复杂的概率因子 - # 保留此字段是为了向后兼容,现有配置文件不会报错 - interruption_probability_factor: float = Field( - default=0.8, ge=0.0, le=1.0, description="[已废弃] 打断概率因子,新线性概率模型不再使用此参数" - ) - # 动态消息分发系统配置 dynamic_distribution_enabled: bool = Field(default=True, description="是否启用动态消息分发周期调整") dynamic_distribution_base_interval: float = Field(default=5.0, ge=1.0, le=60.0, description="基础分发间隔(秒)") @@ -170,10 +154,6 @@ class NoticeConfig(ValidatedConfigBase): notice_retention_time: int = Field(default=86400, ge=3600, le=604800, description="notice保留时间(秒)") -class NormalChatConfig(ValidatedConfigBase): - """普通聊天配置类""" - - class ExpressionRule(ValidatedConfigBase): """表达学习规则""" @@ -736,33 +716,7 @@ class ProactiveThinkingConfig(ValidatedConfigBase): # --- 总开关 --- enable: bool = Field(default=False, description="是否启用主动发起对话功能") - # --- 触发时机 --- - interval: int = Field(default=1500, description="基础触发间隔(秒),AI会围绕这个时间点主动发起对话") - interval_sigma: int = Field( - default=120, description="间隔随机化标准差(秒),让触发时间更自然。设为0则为固定间隔。" - ) - talk_frequency_adjust: list[list[str]] = Field( - default_factory=lambda: [["", "8:00,1", "12:00,1.2", "18:00,1.5", "01:00,0.6"]], - description='每日活跃度调整,格式:[["", "HH:MM,factor", ...], ["stream_id", ...]]', - ) - - # --- 作用范围 --- - enable_in_private: bool = Field(default=True, description="是否允许在私聊中主动发起对话") - enable_in_group: bool = Field(default=True, description="是否允许在群聊中主动发起对话") - enabled_private_chats: list[str] = Field( - default_factory=list, description='私聊白名单,为空则对所有私聊生效。格式: ["platform:user_id", ...]' - ) - enabled_group_chats: list[str] = Field( - default_factory=list, description='群聊白名单,为空则对所有群聊生效。格式: ["platform:group_id", ...]' - ) - - # --- 冷启动配置 (针对私聊) --- - enable_cold_start: bool = Field(default=True, description='对于白名单中不活跃的私聊,是否允许进行一次"冷启动"问候') - cold_start_cooldown: int = Field( - default=86400, description="冷启动后,该私聊的下一次主动思考需要等待的最小时间(秒)" - ) - - # --- 新增:间隔配置 --- + # --- 间隔配置 --- base_interval: int = Field(default=1800, ge=60, description="基础触发间隔(秒),默认30分钟") min_interval: int = Field(default=600, ge=60, description="最小触发间隔(秒),默认10分钟。兴趣分数高时会接近此值") max_interval: int = Field(default=7200, ge=60, description="最大触发间隔(秒),默认2小时。兴趣分数低时会接近此值") diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 2329fe56b..1b195d588 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.5.3" +version = "7.5.4" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -195,9 +195,6 @@ auto_ban_duration_hours = 2 # 封禁持续时间(小时) shield_prefix = "🛡️ " # 加盾消息前缀 shield_suffix = " 🛡️" # 加盾消息后缀 -[normal_chat] #普通聊天 -willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical,mxp模式:mxp,自定义模式:custom(需要你自己实现) - [tool] enable_tool = true # 是否在普通聊天中启用工具 @@ -622,13 +619,4 @@ throw_topic_weight = 0.3 # throw_topic动作的基础权重 # --- 调试与监控 --- enable_statistics = true # 是否启用统计功能(记录触发次数、决策分布等) -log_decisions = false # 是否记录每次决策的详细日志(用于调试) - -# --- 兼容旧配置(已废弃,建议删除) --- -interval = 1800 # [已废弃] 请使用 base_interval -interval_sigma = 120 # [已废弃] 随机化功能已移除 -talk_frequency_adjust = [] # [已废弃] 请使用 enable_time_strategy 和相关配置 -enabled_private_chats = [] # [已废弃] 请使用 whitelist_private -enabled_group_chats = [] # [已废弃] 请使用 whitelist_group -enable_cold_start = false # [已废弃] 冷启动功能已移除 -cold_start_cooldown = 86400 # [已废弃] 冷启动功能已移除 \ No newline at end of file +log_decisions = false # 是否记录每次决策的详细日志(用于调试) \ No newline at end of file From 320e686df60313e94ba8436532423a1fd1dd6efa Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Fri, 31 Oct 2025 22:06:19 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor(chatter):=20=E5=B0=86=E4=B8=BB?= =?UTF-8?q?=E5=8A=A8=E6=80=9D=E8=80=83=E7=9A=84Prompt=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 `proactive_thinking_executor.py` 中的硬编码Prompt字符串重构为独立的 `Prompt` 对象。 这次重构主要有以下几个好处: - **提高可读性**:将大段的文本从业务逻辑中分离,让代码结构更清晰。 - **便于维护**:以后要调整 Prompt 的时候,直接修改模板对象就行,不用在函数里大海捞针了。 - **提升复用性**:虽然现在还没复用,但以后有类似需求时,这种模式也更容易扩展。 --- .../proactive_thinking_executor.py | 328 ++++++++++-------- 1 file changed, 185 insertions(+), 143 deletions(-) diff --git a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_executor.py b/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_executor.py index cea22211c..e172c4600 100644 --- a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_executor.py +++ b/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_executor.py @@ -10,6 +10,7 @@ from typing import Any, Literal from sqlalchemy import select from src.chat.express.expression_selector import expression_selector +from src.chat.utils.prompt import Prompt from src.common.database.sqlalchemy_database_api import get_db_session from src.common.database.sqlalchemy_models import ChatStreams from src.common.logger import get_logger @@ -20,6 +21,133 @@ from src.plugin_system.apis import message_api, send_api logger = get_logger("proactive_thinking_executor") +# ================================================================================================== +# == Prompt Templates +# ================================================================================================== + +# 决策 Prompt +decision_prompt_template = Prompt( + """你的人设是: +{bot_personality} + +现在是 {current_time},你正在考虑是否要在与 "{stream_name}" 的对话中主动说些什么。 + +【你当前的心情】 +{current_mood} + +【聊天环境信息】 +- 整体印象: {stream_impression} +- 聊天风格: {chat_style} +- 常见话题: {topic_keywords} +- 你的兴趣程度: {interest_score:.2f}/1.0 +{last_decision_text} + +【最近的聊天记录】 +{recent_chat_history} + +请根据以上信息,决定你现在应该做什么: + +**选项1:什么都不做 (do_nothing)** +- 适用场景:气氛不适合说话、最近对话很活跃、没什么特别想说的、或者此时说话会显得突兀。 +- 心情影响:如果心情不好(如生气、难过),可能更倾向于保持沉默。 + +**选项2:简单冒个泡 (simple_bubble)** +- 适用场景:对话有些冷清,你想缓和气氛或开启新的互动。 +- 方式:说一句轻松随意的话,旨在建立或维持连接。 +- 心情影响:心情会影响你冒泡的方式和内容。 + +**选项3:发起一次有目的的互动 (throw_topic)** +- 适用场景:你想延续对话、表达关心、或深入讨论某个具体话题。 +- **【互动类型1:延续约定或提醒】(最高优先级)**:检查最近的聊天记录,是否存在可以延续的互动。例如,如果昨晚的最后一条消息是“晚安”,现在是早上,一个“早安”的回应是绝佳的选择。如果之前提到过某个约定(如“待会聊”),现在可以主动跟进。 +- **【互动类型2:展现真诚的关心】(次高优先级)**:如果不存在可延续的约定,请仔细阅读聊天记录,寻找对方提及的个人状况(如天气、出行、身体、情绪、工作学习等),并主动表达关心。 +- **【互动类型3:开启新话题】**:当以上两点都不适用时,可以考虑开启一个你感兴趣的新话题。 +- 心情影响:心情会影响你想发起互动的方式和内容。 + +请以JSON格式回复你的决策: +{{ + "action": "do_nothing" | "simple_bubble" | "throw_topic", + "reasoning": "你的决策理由(请结合你的心情、聊天环境和对话历史进行分析)", + "topic": "(仅当action=throw_topic时填写)你的互动意图(如:回应晚安并说早安、关心对方的考试情况、讨论新游戏)" +}} + +注意: +1. 兴趣度较低(<0.4)时或者最近聊天很活跃(不到1小时),倾向于 `do_nothing` 或 `simple_bubble`。 +2. 你的心情会影响你的行动倾向和表达方式。 +3. 参考上次决策,避免重复,并可根据上次的互动效果调整策略。 +4. 只有在真的有感而发时才选择 `throw_topic`。 +5. 保持你的人设,确保行为一致性。 +""", + name="proactive_thinking_decision", +) + +# 冒泡回复 Prompt +simple_bubble_reply_prompt_template = Prompt( + """你的人设是: +{bot_personality} + +距离上次对话已经有一段时间了,你决定主动说些什么,轻松地开启新的互动。 + +【你当前的心情】 +{current_mood} + +【聊天环境】 +- 整体印象: {stream_impression} +- 聊天风格: {chat_style} + +【最近的聊天记录】 +{recent_chat_history} +{expression_habits} +请生成一条简短的消息,用于水群。 +【要求】 +1. 风格简短随意(5-20字) +2. 不要提出明确的话题或问题,可以是问候、表达心情或一句随口的话。 +3. 符合你的人设和当前聊天风格。 +4. **你的心情应该影响消息的内容和语气**。 +5. 如果有表达方式参考,在合适时自然使用。 +6. 合理参考历史记录。 +直接输出消息内容,不要解释:""", + name="proactive_thinking_simple_bubble", +) + +# 抛出话题回复 Prompt +throw_topic_reply_prompt_template = Prompt( + """你的人设是: +{bot_personality} + +现在是 {current_time},你决定在与 "{stream_name}" 的对话中主动发起一次互动。 + +【你当前的心情】 +{current_mood} + +【聊天环境】 +- 整体印象: {stream_impression} +- 聊天风格: {chat_style} +- 常见话题: {topic_keywords} + +【最近的聊天记录】 +{recent_chat_history} + +【你的互动意图】 +{topic} +{expression_habits} +【构思指南】 +请根据你的互动意图,生成一条有温度的消息。 +- 如果意图是**延续约定**(如回应“晚安”),请直接生成对应的问候。 +- 如果意图是**表达关心**(如跟进对方提到的事),请生成自然、真诚的关心话语。 +- 如果意图是**开启新话题**,请自然地引入话题。 + +请根据这个意图,生成一条消息,要求: +1. 自然地引入话题或表达关心。 +2. 长度适中(20-50字)。 +3. 可以结合最近的聊天记录,使对话更连贯。 +4. 符合你的人设和当前聊天风格。 +5. **你的心情会影响你的表达方式**。 +6. 如果有表达方式参考,在合适时自然使用。 + +直接输出消息内容,不要解释:""", + name="proactive_thinking_throw_topic", +) + class ProactiveThinkingPlanner: """主动思考规划器 @@ -167,7 +295,35 @@ class ProactiveThinkingPlanner: response = None try: - decision_prompt = self._build_decision_prompt(context) + # 构建上次决策信息 + last_decision_text = "" + if context.get("last_decision"): + last_dec = context["last_decision"] + last_action = last_dec.get("action", "未知") + last_reasoning = last_dec.get("reasoning", "无") + last_topic = last_dec.get("topic") + last_time = last_dec.get("timestamp", "未知") + + last_decision_text = f""" +【上次主动思考的决策】 +- 时间: {last_time} +- 决策: {last_action} +- 理由: {last_reasoning}""" + if last_topic: + last_decision_text += f"\n- 话题: {last_topic}" + + decision_prompt = decision_prompt_template.format( + bot_personality=context["bot_personality"], + current_time=context["current_time"], + stream_name=context["stream_name"], + current_mood=context.get("current_mood", "感觉很平静"), + stream_impression=context["stream_impression"], + chat_style=context["chat_style"], + topic_keywords=context["topic_keywords"] or "暂无", + interest_score=context["interest_score"], + last_decision_text=last_decision_text, + recent_chat_history=context["recent_chat_history"], + ) if global_config.debug.show_prompt: logger.info(f"决策提示词:\n{decision_prompt}") @@ -195,76 +351,6 @@ class ProactiveThinkingPlanner: logger.error(f"决策过程失败: {e}", exc_info=True) return None - def _build_decision_prompt(self, context: dict[str, Any]) -> str: - """构建决策提示词""" - # 构建上次决策信息 - last_decision_text = "" - if context.get("last_decision"): - last_dec = context["last_decision"] - last_action = last_dec.get("action", "未知") - last_reasoning = last_dec.get("reasoning", "无") - last_topic = last_dec.get("topic") - last_time = last_dec.get("timestamp", "未知") - - last_decision_text = f""" -【上次主动思考的决策】 -- 时间: {last_time} -- 决策: {last_action} -- 理由: {last_reasoning}""" - if last_topic: - last_decision_text += f"\n- 话题: {last_topic}" - - return f"""你的人设是: -{context['bot_personality']} - -现在是 {context['current_time']},你正在考虑是否要在与 "{context['stream_name']}" 的对话中主动说些什么。 - -【你当前的心情】 -{context.get("current_mood", "感觉很平静")} - -【聊天环境信息】 -- 整体印象: {context["stream_impression"]} -- 聊天风格: {context["chat_style"]} -- 常见话题: {context["topic_keywords"] or "暂无"} -- 你的兴趣程度: {context["interest_score"]:.2f}/1.0 -{last_decision_text} - -【最近的聊天记录】 -{context["recent_chat_history"]} - -请根据以上信息,决定你现在应该做什么: - -**选项1:什么都不做 (do_nothing)** -- 适用场景:气氛不适合说话、最近对话很活跃、没什么特别想说的、或者此时说话会显得突兀。 -- 心情影响:如果心情不好(如生气、难过),可能更倾向于保持沉默。 - -**选项2:简单冒个泡 (simple_bubble)** -- 适用场景:对话有些冷清,你想缓和气氛或开启新的互动。 -- 方式:说一句轻松随意的话,旨在建立或维持连接。 -- 心情影响:心情会影响你冒泡的方式和内容。 - -**选项3:发起一次有目的的互动 (throw_topic)** -- 适用场景:你想延续对话、表达关心、或深入讨论某个具体话题。 -- **【互动类型1:延续约定或提醒】(最高优先级)**:检查最近的聊天记录,是否存在可以延续的互动。例如,如果昨晚的最后一条消息是“晚安”,现在是早上,一个“早安”的回应是绝佳的选择。如果之前提到过某个约定(如“待会聊”),现在可以主动跟进。 -- **【互动类型2:展现真诚的关心】(次高优先级)**:如果不存在可延续的约定,请仔细阅读聊天记录,寻找对方提及的个人状况(如天气、出行、身体、情绪、工作学习等),并主动表达关心。 -- **【互动类型3:开启新话题】**:当以上两点都不适用时,可以考虑开启一个你感兴趣的新话题。 -- 心情影响:心情会影响你想发起互动的方式和内容。 - -请以JSON格式回复你的决策: -{{ - "action": "do_nothing" | "simple_bubble" | "throw_topic", - "reasoning": "你的决策理由(请结合你的心情、聊天环境和对话历史进行分析)", - "topic": "(仅当action=throw_topic时填写)你的互动意图(如:回应晚安并说早安、关心对方的考试情况、讨论新游戏)" -}} - -注意: -1. 兴趣度较低(<0.4)时或者最近聊天很活跃(不到1小时),倾向于 `do_nothing` 或 `simple_bubble`。 -2. 你的心情会影响你的行动倾向和表达方式。 -3. 参考上次决策,避免重复,并可根据上次的互动效果调整策略。 -4. 只有在真的有感而发时才选择 `throw_topic`。 -5. 保持你的人设,确保行为一致性。 -""" - async def generate_reply( self, context: dict[str, Any], action: Literal["simple_bubble", "throw_topic"], topic: str | None = None ) -> str | None: @@ -283,7 +369,34 @@ class ProactiveThinkingPlanner: return None try: - reply_prompt = await self._build_reply_prompt(context, action, topic) + # 获取表达方式参考 + expression_habits = await self._get_expression_habits( + stream_id=context.get("stream_id", ""), chat_history=context.get("recent_chat_history", "") + ) + + if action == "simple_bubble": + reply_prompt = simple_bubble_reply_prompt_template.format( + bot_personality=context["bot_personality"], + current_mood=context.get("current_mood", "感觉很平静"), + stream_impression=context["stream_impression"], + chat_style=context["chat_style"], + recent_chat_history=context["recent_chat_history"], + expression_habits=expression_habits, + ) + else: # throw_topic + reply_prompt = throw_topic_reply_prompt_template.format( + bot_personality=context["bot_personality"], + current_time=context["current_time"], + stream_name=context["stream_name"], + current_mood=context.get("current_mood", "感觉很平静"), + stream_impression=context["stream_impression"], + chat_style=context["chat_style"], + topic_keywords=context["topic_keywords"] or "暂无", + recent_chat_history=context["recent_chat_history"], + topic=topic, + expression_habits=expression_habits, + ) + if global_config.debug.show_prompt: logger.info(f"回复提示词:\n{reply_prompt}") @@ -350,77 +463,6 @@ class ProactiveThinkingPlanner: logger.warning(f"获取表达方式失败: {e}") return "" - async def _build_reply_prompt( - self, context: dict[str, Any], action: Literal["simple_bubble", "throw_topic"], topic: str | None - ) -> str: - """构建回复提示词""" - # 获取表达方式参考 - expression_habits = await self._get_expression_habits( - stream_id=context.get("stream_id", ""), chat_history=context.get("recent_chat_history", "") - ) - - if action == "simple_bubble": - return f"""你的人设是: -{context['bot_personality']} - -距离上次对话已经有一段时间了,你决定主动说些什么,轻松地开启新的互动。 - -【你当前的心情】 -{context.get("current_mood", "感觉很平静")} - -【聊天环境】 -- 整体印象: {context["stream_impression"]} -- 聊天风格: {context["chat_style"]} - -【最近的聊天记录】 -{context["recent_chat_history"]} -{expression_habits} -请生成一条简短的消息,用于水群。 -【要求】 -1. 风格简短随意(5-20字) -2. 不要提出明确的话题或问题,可以是问候、表达心情或一句随口的话。 -3. 符合你的人设和当前聊天风格。 -4. **你的心情应该影响消息的内容和语气**。 -5. 如果有表达方式参考,在合适时自然使用。 -6. 合理参考历史记录。 -直接输出消息内容,不要解释:""" - - else: # throw_topic - return f"""你的人设是: -{context['bot_personality']} - -现在是 {context['current_time']},你决定在与 "{context['stream_name']}" 的对话中主动发起一次互动。 - -【你当前的心情】 -{context.get("current_mood", "感觉很平静")} - -【聊天环境】 -- 整体印象: {context["stream_impression"]} -- 聊天风格: {context["chat_style"]} -- 常见话题: {context["topic_keywords"] or "暂无"} - -【最近的聊天记录】 -{context["recent_chat_history"]} - -【你的互动意图】 -{topic} -{expression_habits} -【构思指南】 -请根据你的互动意图,生成一条有温度的消息。 -- 如果意图是**延续约定**(如回应“晚安”),请直接生成对应的问候。 -- 如果意图是**表达关心**(如跟进对方提到的事),请生成自然、真诚的关心话语。 -- 如果意图是**开启新话题**,请自然地引入话题。 - -请根据这个意图,生成一条消息,要求: -1. 自然地引入话题或表达关心。 -2. 长度适中(20-50字)。 -3. 可以结合最近的聊天记录,使对话更连贯。 -4. 符合你的人设和当前聊天风格。 -5. **你的心情会影响你的表达方式**。 -6. 如果有表达方式参考,在合适时自然使用。 - -直接输出消息内容,不要解释:""" - def _clean_json_response(self, response: str) -> str: """清理LLM响应中的JSON格式标记""" import re From ac4c92b620d9abc1d3942190da31305ac88fc724 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Fri, 31 Oct 2025 22:36:53 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(chatter):=20=E6=B7=BB=E5=8A=A0should?= =?UTF-8?q?=5Fquote=5Freply=E5=8F=82=E6=95=B0=E4=BB=A5=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=9B=9E=E5=A4=8D=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/planner_actions/action_manager.py | 38 +++++++++++++++---- src/common/data_models/info_data_model.py | 1 + .../affinity_flow_chatter/plan_executor.py | 13 ++++++- .../affinity_flow_chatter/plan_filter.py | 9 +++++ .../built_in/affinity_flow_chatter/planner.py | 1 + .../affinity_flow_chatter/planner_prompts.py | 30 ++++++++++++--- 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index f52e40657..502ee396a 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -277,6 +277,11 @@ class ChatterActionManager: logger.debug(f"{log_prefix} 并行执行:回复生成任务已被取消") return {"action_type": "reply", "success": False, "reply_text": "", "loop_info": None} + # 从action_data中提取should_quote_reply参数 + should_quote_reply = None + if action_data and isinstance(action_data, dict): + should_quote_reply = action_data.get("should_quote_reply", None) + # 发送并存储回复 loop_info, reply_text, cycle_timers_reply = await self._send_and_store_reply( chat_stream, @@ -286,6 +291,7 @@ class ChatterActionManager: {}, # cycle_timers thinking_id, [], # actions + should_quote_reply, # 传递should_quote_reply参数 ) # 记录回复动作到目标消息 @@ -474,6 +480,7 @@ class ChatterActionManager: cycle_timers: dict[str, float], thinking_id, actions, + should_quote_reply: bool | None = None, ) -> tuple[dict[str, Any], str, dict[str, float]]: """ 发送并存储回复信息 @@ -486,13 +493,16 @@ class ChatterActionManager: cycle_timers: 循环计时器 thinking_id: 思考ID actions: 动作列表 + should_quote_reply: 是否应该引用回复原消息,None表示自动决定 Returns: Tuple[Dict[str, Any], str, Dict[str, float]]: 循环信息, 回复文本, 循环计时器 """ # 发送回复 with Timer("回复发送", cycle_timers): - reply_text = await self.send_response(chat_stream, response_set, loop_start_time, action_message) + reply_text = await self.send_response( + chat_stream, response_set, loop_start_time, action_message, should_quote_reply + ) # 存储reply action信息 person_info_manager = get_person_info_manager() @@ -551,16 +561,18 @@ class ChatterActionManager: return loop_info, reply_text, cycle_timers - async def send_response(self, chat_stream, reply_set, thinking_start_time, message_data) -> str: + async def send_response( + self, chat_stream, reply_set, thinking_start_time, message_data, should_quote_reply: bool | None = None + ) -> str: """ 发送回复内容的具体实现 Args: chat_stream: ChatStream实例 reply_set: 回复内容集合,包含多个回复段 - reply_to: 回复目标 thinking_start_time: 思考开始时间 message_data: 消息数据 + should_quote_reply: 是否应该引用回复原消息,None表示自动决定 Returns: str: 完整的回复文本 @@ -614,12 +626,24 @@ class ChatterActionManager: # 发送第一段回复 if not first_replied: - # 私聊场景不使用引用回复(因为只有两个人对话,引用是多余的) - # 群聊场景使用引用回复(帮助定位回复的目标消息) + # 决定是否引用回复 is_private_chat = not bool(chat_stream.group_info) - set_reply_flag = bool(message_data) and not is_private_chat + + # 如果明确指定了should_quote_reply,则使用指定值 + if should_quote_reply is not None: + set_reply_flag = should_quote_reply and bool(message_data) + logger.debug( + f"📤 [ActionManager] 使用planner指定的引用设置: should_quote_reply={should_quote_reply}" + ) + else: + # 否则使用默认逻辑:默认不引用,让对话更流畅自然 + set_reply_flag = False + logger.debug( + f"📤 [ActionManager] 使用默认引用逻辑: 默认不引用(is_private={is_private_chat})" + ) + logger.debug( - f"📤 [ActionManager] 准备发送第一段回复。message_data: {message_data}, is_private: {is_private_chat}, set_reply: {set_reply_flag}" + f"📤 [ActionManager] 准备发送第一段回复。message_data: {message_data}, set_reply: {set_reply_flag}" ) await send_api.text_to_stream( text=data, diff --git a/src/common/data_models/info_data_model.py b/src/common/data_models/info_data_model.py index e9ed04162..d53921c4b 100644 --- a/src/common/data_models/info_data_model.py +++ b/src/common/data_models/info_data_model.py @@ -27,6 +27,7 @@ class ActionPlannerInfo(BaseDataModel): action_data: dict | None = None action_message: Optional["DatabaseMessages"] = None available_actions: dict[str, "ActionInfo"] | None = None + should_quote_reply: bool | None = None # 是否应该引用回复原消息,None表示由系统自动决定 @dataclass diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py b/src/plugins/built_in/affinity_flow_chatter/plan_executor.py index 59bb098e6..2eacae777 100644 --- a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py +++ b/src/plugins/built_in/affinity_flow_chatter/plan_executor.py @@ -185,15 +185,24 @@ class ChatterPlanExecutor: "reply_content": "", } # 构建回复动作参数 + action_data = action_info.action_data or {} + + # 如果action_info中有should_quote_reply且action_data中没有,则添加到action_data中 + if action_info.should_quote_reply is not None and "should_quote_reply" not in action_data: + action_data["should_quote_reply"] = action_info.should_quote_reply + action_params = { "chat_id": plan.chat_id, "target_message": action_info.action_message, "reasoning": action_info.reasoning, - "action_data": action_info.action_data or {}, + "action_data": action_data, "clear_unread_messages": clear_unread, } - logger.debug(f"📬 [PlanExecutor] 准备调用 ActionManager,target_message: {action_info.action_message}") + logger.debug( + f"📬 [PlanExecutor] 准备调用 ActionManager,target_message: {action_info.action_message}, " + f"should_quote_reply: {action_info.should_quote_reply}" + ) # 通过动作管理器执行回复 execution_result = await self.action_manager.execute_action( diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_filter.py b/src/plugins/built_in/affinity_flow_chatter/plan_filter.py index c15f5d244..ca389e6ea 100644 --- a/src/plugins/built_in/affinity_flow_chatter/plan_filter.py +++ b/src/plugins/built_in/affinity_flow_chatter/plan_filter.py @@ -599,6 +599,14 @@ class ChatterPlanFilter: reasoning = f"LLM 返回了当前不可用的动作 '{action}'。原始理由: {reasoning}" action = "no_action" + # 从action_data中提取should_quote_reply参数 + should_quote_reply = action_data.get("should_quote_reply", None) + # 将should_quote_reply转换为布尔值(如果是字符串的话) + if isinstance(should_quote_reply, str): + should_quote_reply = should_quote_reply.lower() in ["true", "1", "yes"] + elif not isinstance(should_quote_reply, bool): + should_quote_reply = None + parsed_actions.append( ActionPlannerInfo( action_type=action, @@ -606,6 +614,7 @@ class ChatterPlanFilter: action_data=action_data, action_message=action_message_obj, # 使用转换后的 DatabaseMessages 对象 available_actions=plan.available_actions, + should_quote_reply=should_quote_reply, # 传递should_quote_reply参数 ) ) except Exception as e: diff --git a/src/plugins/built_in/affinity_flow_chatter/planner.py b/src/plugins/built_in/affinity_flow_chatter/planner.py index 8fc75b4ef..128b309eb 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner.py @@ -319,6 +319,7 @@ class ChatterActionPlanner: reasoning="Normal模式: 兴趣度达到阈值,直接回复", action_data={"target_message_id": target_message.message_id}, action_message=target_message, + should_quote_reply=False, # Normal模式默认不引用回复,保持对话流畅 ) # Normal模式下直接构建最小化的Plan,跳过generator和action_modifier diff --git a/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py b/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py index 0e760fc15..212c15f9d 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py @@ -81,38 +81,56 @@ def init_prompts(): "reasoning": "选择该动作的理由", "action_data": {{ "target_message_id": "m123", - "content": "你的回复内容" + "content": "你的回复内容", + "should_quote_reply": false }} }} ] }} ``` -示例(多重回复,并行): +示例(多重回复,并行 - 需要区分回复对象时才引用): ```json {{ "thinking": "在这里写下你的思绪流...", "actions": [ {{ "action_type": "reply", - "reasoning": "理由A", + "reasoning": "理由A - 这个消息较早且需要明确回复对象", "action_data": {{ "target_message_id": "m124", - "content": "对A的回复" + "content": "对A的回复", + "should_quote_reply": true }} }}, {{ "action_type": "reply", - "reasoning": "理由B", + "reasoning": "理由B - 这是对最新消息的自然接续", "action_data": {{ "target_message_id": "m125", - "content": "对B的回复" + "content": "对B的回复", + "should_quote_reply": false }} }} ] }} ``` +# 引用回复控制(should_quote_reply) +在群聊中回复消息时,你可以通过 `should_quote_reply` 参数控制是否引用原消息: +- **true**: 明确引用原消息(适用于需要明确指向特定消息时,如回答问题、回应多人之一、回复较早的消息) +- **false**: 不引用原消息(适用于自然对话流、接续最新话题、轻松闲聊等场景) +- **不填写**: 系统将自动决定(默认不引用,让对话更流畅) + +**【重要】默认策略:大多数情况下应该使用 `false` 以保持对话自然流畅** + +**使用建议**: +- 当对话自然流畅、你的回复是接续最新话题时,**建议明确设为 `false`** 以避免打断对话节奏 +- 当需要明确回复某个特定用户或特定问题时,设为 `true` 以帮助定位 +- 当群聊中多人同时发言,你要回复其中一个较早的消息(非最新消息)时,设为 `true` +- 当有人直接@你或明确向你提问时,可以考虑设为 `true` 表明你在回复他 +- 私聊场景**必须**设为 `false` 或不填(因为只有两个人,引用是多余的) + # 强制规则 - 需要目标消息的动作(reply/poke_user/set_emoji_like 等),必须提供准确的 target_message_id(来自未读历史里的 标签)。 - 当动作需要额外参数时,必须在 action_data 中补全。