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%概率
]