diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 19af8528..a514dd0c 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -1,7 +1,10 @@ from .observation import ChattingObservation +from src.plugins.knowledge.knowledge_lib import qa_manager from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config +from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat,build_readable_messages import time +import re import traceback from src.common.logger_manager import get_logger from src.individuality.individuality import Individuality @@ -24,18 +27,35 @@ logger = get_logger("sub_heartflow") def init_prompt(): # --- Group Chat Prompt --- group_prompt = """ -{extra_info} -{relation_prompt} -你的名字是{bot_name},{prompt_personality} -{last_loop_prompt} -{cycle_info_block} -现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: -{chat_observe_info} + + 你的名字是{bot_name}。 + {prompt_personality} + -你现在{mood_info} -请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复或发言。然后思考你是否需要使用函数工具。 -思考并输出你的内心想法 -输出要求: + + {extra_info} + {relation_prompt} + + + + {last_loop_prompt} + {cycle_info_block} + 你现在{mood_info} + + + + 现在是{time_now}。 + 你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: +{chat_observe_info} + + + +请仔细阅读当前聊天内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复或发言。然后思考你是否需要使用函数工具。 +思考并输出你真实的内心想法。 + + + + 1. 根据聊天内容生成你的想法,{hf_do_next} 2. 不要分点、不要使用表情符号 3. 避免多余符号(冒号、引号、括号等) @@ -43,11 +63,17 @@ def init_prompt(): 5. 如果你刚发言,并且没有人回复你,请谨慎考虑要不要继续发消息 6. 不要把注意力放在别人发的表情包上,它们只是一种辅助表达方式 7. 注意分辨群里谁在跟谁说话,你不一定是当前聊天的主角,消息中的“你”不一定指的是你({bot_name}),也可能是别人 -8. 思考要不要回复或发言,如果要,需要思考一下具体说什么。 -工具使用说明: +8. 思考要不要回复或发言,如果要,必须思考一下具体说什么,怎么说 +9. 默认使用中文 + + + 1. 输出想法后考虑是否需要使用工具 2. 工具可获取信息或执行操作 -3. 如需处理消息或回复,请使用工具。""" +3. 如需处理消息或回复,请使用工具。 + + +""" Prompt(group_prompt, "sub_heartflow_prompt_before") # --- Private Chat Prompt --- @@ -85,6 +111,35 @@ def init_prompt(): Prompt(last_loop_t, "last_loop") +def parse_knowledge_and_get_max_relevance(knowledge_str: str) -> (str, float): + """ + 解析 qa_manager.get_knowledge 返回的字符串,提取所有知识的文本和最高的相关性得分。 + 返回: (原始知识字符串, 最高相关性得分),如果无有效相关性则返回 (原始知识字符串, 0.0) + """ + if not knowledge_str: + return None, 0.0 + + max_relevance = 0.0 + # 正则表达式匹配 "该条知识对于问题的相关性:数字" + # 我们需要捕获数字部分 + relevance_scores = re.findall(r"该条知识对于问题的相关性:([0-9.]+)", knowledge_str) + + if relevance_scores: + try: + max_relevance = max(float(score) for score in relevance_scores) + except ValueError: + logger.warning(f"解析相关性得分时出错: {relevance_scores}") + return knowledge_str, 0.0 # 出错时返回0.0 + else: + # 如果没有找到 "该条知识对于问题的相关性:" 这样的模式, + # 说明可能 qa_manager 返回的格式有变,或者没有有效的知识。 + # 在这种情况下,我们无法确定相关性,保守起见返回0.0 + logger.debug(f"在知识字符串中未找到明确的相关性得分标记: '{knowledge_str[:100]}...'") + return knowledge_str, 0.0 + + return knowledge_str, max_relevance + + def calculate_similarity(text_a: str, text_b: str) -> float: """ 计算两个文本字符串的相似度。 @@ -117,6 +172,8 @@ def calculate_replacement_probability(similarity: float) -> float: # p = s + 0.1 probability = similarity + 0.1 return min(1.0, max(0.0, probability)) + + class SubMind: @@ -127,7 +184,7 @@ class SubMind: self.llm_model = LLMRequest( model=global_config.llm_sub_heartflow, temperature=global_config.llm_sub_heartflow["temp"], - max_tokens=800, + max_tokens=1000, request_type="sub_heart_flow", ) @@ -142,6 +199,14 @@ class SubMind: name = chat_manager.get_stream_name(self.subheartflow_id) self.log_prefix = f"[{name}] " self._update_structured_info_str() + # 阶梯式筛选 + self.knowledge_retrieval_steps = self.knowledge_retrieval_steps = [ + {"name": "latest_1_msg", "limit": 1, "relevance_threshold": 0.75}, # 新增:最新1条,极高阈值 + {"name": "latest_2_msgs", "limit": 2, "relevance_threshold": 0.65}, # 新增:最新2条,较高阈值 + {"name": "short_window_3_msgs", "limit": 3, "relevance_threshold": 0.50}, # 原有的3条,阈值可保持或微调 + {"name": "medium_window_8_msgs", "limit": 8, "relevance_threshold": 0.30}, # 原有的8条,阈值可保持或微调 + # 完整窗口的回退逻辑保持不变 + ] def _update_structured_info_str(self): """根据 structured_info 更新 structured_info_str""" @@ -184,23 +249,26 @@ class SubMind: # ---------- 0. 更新和清理 structured_info ---------- if self.structured_info: logger.debug( - f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + f"{self.log_prefix} 清理前 structured_info 中包含的lpmm_knowledge数量: " + f"{len([item for item in self.structured_info if item.get('type') == 'lpmm_knowledge'])}" ) - updated_info = [] - for item in self.structured_info: + # 筛选出所有不是 lpmm_knowledge 类型的条目,或者其他需要保留的条目 + info_to_keep = [item for item in self.structured_info if item.get("type") != "lpmm_knowledge"] + + # 针对我们仅希望 lpmm_knowledge "用完即弃" 的情况: + processed_info_to_keep = [] + for item in info_to_keep: # info_to_keep 已经不包含 lpmm_knowledge item["ttl"] -= 1 if item["ttl"] > 0: - updated_info.append(item) + processed_info_to_keep.append(item) else: - logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}") - self.structured_info = updated_info + logger.debug(f"{self.log_prefix} 移除过期的非lpmm_knowledge项: {item.get('id', '未知ID')}") + + self.structured_info = processed_info_to_keep logger.debug( - f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + f"{self.log_prefix} 清理后 structured_info (仅保留非lpmm_knowledge且TTL有效项): " + f"{safe_json_dumps(self.structured_info, ensure_ascii=False)}" ) - self._update_structured_info_str() - logger.debug( - f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" - ) # ---------- 1. 准备基础数据 ---------- # 获取现有想法和情绪状态 @@ -270,6 +338,108 @@ class SubMind: logger.error(f"{self.log_prefix} 获取记忆时出错: {e}") logger.error(traceback.format_exc()) + + # ---------- 2.5 阶梯式获取知识库信息 ---------- + final_knowledge_to_add = None + retrieval_source_info = "未进行知识检索" + + # 确保 observation 对象存在且可用 + if not observation: + logger.warning(f"{self.log_prefix} Observation 对象不可用,跳过知识库检索。") + else: + # 阶段1和阶段2的阶梯检索 + for step_config in self.knowledge_retrieval_steps: + step_name = step_config["name"] + limit = step_config["limit"] + threshold = step_config["relevance_threshold"] + + logger.info(f"{self.log_prefix} 尝试阶梯检索 - 阶段: {step_name} (最近{limit}条, 阈值>{threshold})") + + try: + # 1. 获取当前阶段的聊天记录上下文 + # 我们需要从 observation 中获取原始消息列表来构建特定长度的上下文 + # get_raw_msg_before_timestamp_with_chat 在 observation.py 中被导入 + # from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages + + # 需要确保 ChattingObservation 的实例 (self.observations[0]) 能提供 chat_id + # 并且 build_readable_messages 可用 + context_messages_dicts = get_raw_msg_before_timestamp_with_chat( + chat_id=observation.chat_id, + timestamp=time.time(), + limit=limit + ) + + if not context_messages_dicts: + logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 未获取到聊天记录,跳过此阶段。") + continue + + current_context_text = await build_readable_messages( + messages=context_messages_dicts, + timestamp_mode="lite" # 或者您认为适合知识检索的模式 + ) + + if not current_context_text: + logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 构建的上下文为空,跳过此阶段。") + continue + + logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 使用上下文: '{current_context_text[:150]}...'") + + # 2. 调用知识库进行检索 + raw_knowledge_str = qa_manager.get_knowledge(current_context_text) + + if raw_knowledge_str: + # 3. 解析知识并检查相关性 + knowledge_content, max_relevance = parse_knowledge_and_get_max_relevance(raw_knowledge_str) + logger.info(f"{self.log_prefix} 阶段 '{step_name}' 检索到知识,最高相关性: {max_relevance:.4f}") + + if max_relevance >= threshold: + logger.info(f"{self.log_prefix} 阶段 '{step_name}' 满足阈值 ({max_relevance:.4f} >= {threshold}),采纳此知识。") + final_knowledge_to_add = knowledge_content + retrieval_source_info = f"阶段 '{step_name}' (最近{limit}条, 相关性 {max_relevance:.4f})" + break # 找到符合条件的知识,跳出阶梯循环 + else: + logger.info(f"{self.log_prefix} 阶段 '{step_name}' 未满足阈值 ({max_relevance:.4f} < {threshold}),继续下一阶段。") + else: + logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 未从知识库检索到任何内容。") + + except Exception as e_step: + logger.error(f"{self.log_prefix} 阶梯检索阶段 '{step_name}' 发生错误: {e_step}") + logger.error(traceback.format_exc()) + continue # 当前阶段出错,尝试下一阶段 + + # 阶段3: 如果前面的阶梯都没有成功,则使用完整的 chat_observe_info (即您配置的20条) + if not final_knowledge_to_add and chat_observe_info: # 确保 chat_observe_info 可用 + logger.info(f"{self.log_prefix} 前序阶梯均未满足条件,尝试使用完整观察窗口 ('{observation.max_now_obs_len}'条)进行检索。") + try: + raw_knowledge_str = qa_manager.get_knowledge(chat_observe_info) + if raw_knowledge_str: + # 对于完整窗口,我们可能不强制要求阈值,或者使用一个较低的阈值 + # 或者,您可以选择在这里仍然应用一个阈值,例如 self.knowledge_retrieval_steps 中最后一个的阈值,或一个特定值 + knowledge_content, max_relevance = parse_knowledge_and_get_max_relevance(raw_knowledge_str) + logger.info(f"{self.log_prefix} 完整窗口检索到知识,(此处未设阈值,或相关性: {max_relevance:.4f})。") + final_knowledge_to_add = knowledge_content # 默认采纳 + retrieval_source_info = f"完整窗口 (最多{observation.max_now_obs_len}条, 相关性 {max_relevance:.4f})" + else: + logger.debug(f"{self.log_prefix} 完整窗口检索也未找到知识。") + except Exception as e_full: + logger.error(f"{self.log_prefix} 完整窗口知识检索发生错误: {e_full}") + logger.error(traceback.format_exc()) + + # 将最终选定的知识(如果有)添加到 structured_info + if final_knowledge_to_add: + knowledge_item = { + "type": "lpmm_knowledge", + "id": f"lpmm_knowledge_{time.time()}", + "content": final_knowledge_to_add, + "ttl": 1 # 由于是当轮精心选择的,可以让TTL短一些,下次重新评估(或者按照您的意愿设为3) + } + # 我们在方法开头已经清理了旧的 lpmm_knowledge,这里直接添加新的 + self.structured_info.append(knowledge_item) + logger.info(f"{self.log_prefix} 添加了来自 '{retrieval_source_info}' 的知识到 structured_info (ID: {knowledge_item['id']})") + self._update_structured_info_str() # 更新字符串表示 + else: + logger.info(f"{self.log_prefix} 经过所有阶梯检索后,没有最终采纳的知识。") + # ---------- 3. 准备工具和个性化数据 ---------- # 初始化工具 tool_instance = ToolUser() diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 4daa4d3f..f6391386 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -29,15 +29,15 @@ def init_prompt(): {chat_target} {chat_talking_prompt} 现在你想要回复或参与讨论。\n -你是{bot_name},{prompt_personality}。 -你正在{chat_target_2},现在请你读读之前的聊天记录,可以自然随意一些,简短一些,就像群聊里的真人一样,注意把握聊天内容,整体风格可以平和、简短。 +你是{bot_name}。你正在{chat_target_2} + 看到以上聊天记录,你刚刚在想: - {current_mind_info} -因为上述想法,你决定发言,原因是:{reason} +因为上述想法,你决定发言。 -回复尽量简短一些。请注意把握聊天内容,{reply_style2}。请一次只回复一个话题,不要同时回复多个人。{prompt_ger} -{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出回复内容,不要去主动讨论或评价别人发的表情包,它们只是一种辅助表达方式。 +现在请你读读之前的聊天记录,把你的想法组织成合适语言,然后发一条消息,可以自然随意一些,简短一些,就像群聊里的真人一样,注意把握聊天内容,整体风格可以平和、简短,范围避免超出你的内心想法 +这条消息可以尽量简短一些。{reply_style2}。请一次只回复一个话题,不要同时回复多个人。{prompt_ger} +{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出消息内容,不要去主动讨论或评价别人发的表情包,它们只是一种辅助表达方式。 {moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", ) @@ -53,36 +53,40 @@ def init_prompt(): # Planner提示词 - 修改为要求 JSON 输出 Prompt( - """你的名字是{bot_name},{prompt_personality},{chat_context_description}。需要基于以下信息决定如何参与对话: -{structured_info_block} + """现在{bot_name}开始在一个qq群聊中专注聊天。你需要操控{bot_name},并且根据以下消息决定是否,如何参与对话: {nickname_info} {chat_content_block} {current_mind_block} {cycle_info_block} -请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。 +请综合分析聊天内容和你看到的新消息,参考{bot_name}的内心想法,并根据以下原则和可用动作灵活谨慎的做出决策,需要符合正常的群聊社交节奏。 -【发送新消息原则】 -1. 不发送新消息(no_reply)适用: -- 话题无关/无聊/不感兴趣 -- 最后一条消息是你自己发的且无人回应你 -- 讨论你不懂的专业话题 -- 你发送了太多消息,且无人回复 -2. 发送文字消息(text_reply)适用: -- 有实质性内容需要表达 -- 有人提到你,但你还没有回应他 +【决策指导】 +1. 以下情况可以不发送新消息(no_reply): +- {bot_name}的内心想法表达不想发言 +- 话题似乎对{bot_name}来说无关/无聊/不感兴趣 +- 现在说话不太合适了 +- 最后一条消息是{bot_name}自己发的且无人回应{bot_name} +- 讨论不了解的专业话题,或你不知道的梗,且对{bot_name}来说似乎没那么重要。 +- {bot_name}发送了太多消息,且无人回复 + +2. 以下情况可以发送文字消息(text_reply): +- 确认内心想法显示{bot_name}想要发言,且有实质内容想表达 +- 同时确认现在适合发言 - 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合) - 不要追加太多表情 3. 发送纯表情(emoji_reply)适用: +- {bot_name}似乎想加入话题或继续讨论,但是似乎又没什么实质表达内容 - 适合用表情回应的场景 - 需提供明确的emoji_query +- 群聊里的大家都在发表情包 -4. 自我对话处理: -- 如果最后一条消息是你自己发的,而你还想继续发消息,需自然衔接,不要有不自然的内容重叠 -- 避免重复或评价自己的发言 -- 不要自己和自己聊天 +4. 对话处理: +- 如果最后一条消息是{bot_name}发的,而你还想操控{bot_name}继续发消息,请确保这是合适的 +- 注意话题的推进,如果没有必要,不要揪着一个话题不放。 +- 不要让{bot_name}自己和自己聊天 决策任务 {action_options_text} @@ -221,18 +225,18 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s reply_styles1 = [ ("给出日常且口语化的回复,平淡一些", 0.4), ("给出非常简短的回复", 0.4), - ("给出缺失主语的回复,简短", 0.15), - ("给出带有语病的回复,朴实平淡", 0.05), + ("**给出省略主语的回复,简短**", 0.15), + ("给出带有语病的回复,朴实平淡", 0.00), ] reply_style1_chosen = random.choices( [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 )[0] reply_styles2 = [ - ("不要回复的太有条理,可以有个性", 0.6), - ("不要回复的太有条理,可以复读", 0.15), + ("不要回复的太有条理,可以有个性", 0.8), + ("不要回复的太有条理,可以复读", 0.0), ("回复的认真一些", 0.2), - ("可以回复单个表情符号", 0.05), + ("可以回复单个表情符号", 0.00), ] reply_style2_chosen = random.choices( [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 @@ -362,8 +366,8 @@ class PromptBuilder: [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 )[0] reply_styles2 = [ - ("不用回复的太有条理,可以有个性", 0.7), # 60%概率 - ("不用回复的太有条理,可以复读", 0.05), # 15%概率 + ("不用回复的太有条理,可以有个性", 0.75), # 60%概率 + ("不用回复的太有条理,可以复读", 0.0), # 15%概率 ("回复的认真一些", 0.2), # 20%概率 ("可以回复单个表情符号", 0.05), # 5%概率 ]