pull/937/head
114514 2025-05-12 21:10:29 +08:00
parent c412b27507
commit 1e015aed9f
2 changed files with 229 additions and 55 deletions

View File

@ -1,7 +1,10 @@
from .observation import ChattingObservation from .observation import ChattingObservation
from src.plugins.knowledge.knowledge_lib import qa_manager
from src.plugins.models.utils_model import LLMRequest from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config 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 time
import re
import traceback import traceback
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.individuality.individuality import Individuality from src.individuality.individuality import Individuality
@ -24,18 +27,35 @@ logger = get_logger("sub_heartflow")
def init_prompt(): def init_prompt():
# --- Group Chat Prompt --- # --- Group Chat Prompt ---
group_prompt = """ group_prompt = """
{extra_info} <identity>
{relation_prompt} <bot_name>你的名字是{bot_name}</bot_name>
你的名字是{bot_name},{prompt_personality} <personality_profile>{prompt_personality}</personality_profile>
{last_loop_prompt} </identity>
{cycle_info_block}
现在是{time_now}你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
{chat_observe_info}
你现在{mood_info} <knowledge_base>
请仔细阅读当前群聊内容分析讨论话题和群成员关系分析你刚刚发言和别人对你的发言的反应思考你要不要回复或发言然后思考你是否需要使用函数工具 <structured_information>{extra_info}</structured_information>
思考并输出你的内心想法 <social_relationships>{relation_prompt}</social_relationships>
输出要求 </knowledge_base>
<recent_internal_state>
<previous_thoughts_and_actions>{last_loop_prompt}</previous_thoughts_and_actions>
<recent_reply_history>{cycle_info_block}</recent_reply_history>
<current_mood>你现在{mood_info}</current_mood>
</recent_internal_state>
<live_chat_context>
<timestamp>现在是{time_now}</timestamp>
<chat_log>你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
{chat_observe_info}</chat_log>
</live_chat_context>
<thinking_guidance>
请仔细阅读当前聊天内容分析讨论话题和群成员关系分析你刚刚发言和别人对你的发言的反应思考你要不要回复或发言然后思考你是否需要使用函数工具
思考并输出你真实的内心想法
</thinking_guidance>
<output_requirements_for_inner_thought>
1. 根据聊天内容生成你的想法{hf_do_next} 1. 根据聊天内容生成你的想法{hf_do_next}
2. 不要分点不要使用表情符号 2. 不要分点不要使用表情符号
3. 避免多余符号(冒号引号括号等) 3. 避免多余符号(冒号引号括号等)
@ -43,11 +63,17 @@ def init_prompt():
5. 如果你刚发言并且没有人回复你请谨慎考虑要不要继续发消息 5. 如果你刚发言并且没有人回复你请谨慎考虑要不要继续发消息
6. 不要把注意力放在别人发的表情包上它们只是一种辅助表达方式 6. 不要把注意力放在别人发的表情包上它们只是一种辅助表达方式
7. 注意分辨群里谁在跟谁说话你不一定是当前聊天的主角消息中的不一定指的是你{bot_name}也可能是别人 7. 注意分辨群里谁在跟谁说话你不一定是当前聊天的主角消息中的不一定指的是你{bot_name}也可能是别人
8. 思考要不要回复或发言如果要需要思考一下具体说什么 8. 思考要不要回复或发言如果要必须思考一下具体说什么怎么说
工具使用说明 9. 默认使用中文
</output_requirements_for_inner_thought>
<tool_usage_instructions>
1. 输出想法后考虑是否需要使用工具 1. 输出想法后考虑是否需要使用工具
2. 工具可获取信息或执行操作 2. 工具可获取信息或执行操作
3. 如需处理消息或回复请使用工具""" 3. 如需处理消息或回复请使用工具
</tool_usage_instructions>
"""
Prompt(group_prompt, "sub_heartflow_prompt_before") Prompt(group_prompt, "sub_heartflow_prompt_before")
# --- Private Chat Prompt --- # --- Private Chat Prompt ---
@ -85,6 +111,35 @@ def init_prompt():
Prompt(last_loop_t, "last_loop") 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: def calculate_similarity(text_a: str, text_b: str) -> float:
""" """
计算两个文本字符串的相似度 计算两个文本字符串的相似度
@ -119,6 +174,8 @@ def calculate_replacement_probability(similarity: float) -> float:
return min(1.0, max(0.0, probability)) return min(1.0, max(0.0, probability))
class SubMind: class SubMind:
def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: ChattingObservation): def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: ChattingObservation):
self.last_active_time = None self.last_active_time = None
@ -127,7 +184,7 @@ class SubMind:
self.llm_model = LLMRequest( self.llm_model = LLMRequest(
model=global_config.llm_sub_heartflow, model=global_config.llm_sub_heartflow,
temperature=global_config.llm_sub_heartflow["temp"], temperature=global_config.llm_sub_heartflow["temp"],
max_tokens=800, max_tokens=1000,
request_type="sub_heart_flow", request_type="sub_heart_flow",
) )
@ -142,6 +199,14 @@ class SubMind:
name = chat_manager.get_stream_name(self.subheartflow_id) name = chat_manager.get_stream_name(self.subheartflow_id)
self.log_prefix = f"[{name}] " self.log_prefix = f"[{name}] "
self._update_structured_info_str() 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): def _update_structured_info_str(self):
"""根据 structured_info 更新 structured_info_str""" """根据 structured_info 更新 structured_info_str"""
@ -184,23 +249,26 @@ class SubMind:
# ---------- 0. 更新和清理 structured_info ---------- # ---------- 0. 更新和清理 structured_info ----------
if self.structured_info: if self.structured_info:
logger.debug( 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 = [] # 筛选出所有不是 lpmm_knowledge 类型的条目,或者其他需要保留的条目
for item in self.structured_info: 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 item["ttl"] -= 1
if item["ttl"] > 0: if item["ttl"] > 0:
updated_info.append(item) processed_info_to_keep.append(item)
else: else:
logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}") logger.debug(f"{self.log_prefix} 移除过期的非lpmm_knowledge项: {item.get('id', '未知ID')}")
self.structured_info = updated_info
self.structured_info = processed_info_to_keep
logger.debug( 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. 准备基础数据 ---------- # ---------- 1. 准备基础数据 ----------
# 获取现有想法和情绪状态 # 获取现有想法和情绪状态
@ -270,6 +338,108 @@ class SubMind:
logger.error(f"{self.log_prefix} 获取记忆时出错: {e}") logger.error(f"{self.log_prefix} 获取记忆时出错: {e}")
logger.error(traceback.format_exc()) 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. 准备工具和个性化数据 ---------- # ---------- 3. 准备工具和个性化数据 ----------
# 初始化工具 # 初始化工具
tool_instance = ToolUser() tool_instance = ToolUser()

View File

@ -29,15 +29,15 @@ def init_prompt():
{chat_target} {chat_target}
{chat_talking_prompt} {chat_talking_prompt}
现在你想要回复或参与讨论\n 现在你想要回复或参与讨论\n
你是{bot_name}{prompt_personality} 你是{bot_name}你正在{chat_target_2}
你正在{chat_target_2},现在请你读读之前的聊天记录可以自然随意一些简短一些就像群聊里的真人一样注意把握聊天内容整体风格可以平和简短
看到以上聊天记录你刚刚在想 看到以上聊天记录你刚刚在想
{current_mind_info} {current_mind_info}
因为上述想法你决定发言原因是{reason} 因为上述想法你决定发言
回复尽量简短一些请注意把握聊天内容{reply_style2}请一次只回复一个话题不要同时回复多个人{prompt_ger} 现在请你读读之前的聊天记录把你的想法组织成合适语言然后发一条消息可以自然随意一些简短一些就像群聊里的真人一样注意把握聊天内容整体风格可以平和简短范围避免超出你的内心想法
{reply_style1}说中文不要刻意突出自身学科背景注意只输出回复内容不要去主动讨论或评价别人发的表情包它们只是一种辅助表达方式 这条消息可以尽量简短一些{reply_style2}请一次只回复一个话题不要同时回复多个人{prompt_ger}
{reply_style1}说中文不要刻意突出自身学科背景注意只输出消息内容不要去主动讨论或评价别人发的表情包它们只是一种辅助表达方式
{moderation_prompt}注意回复不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )""", {moderation_prompt}注意回复不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )""",
"heart_flow_prompt", "heart_flow_prompt",
) )
@ -53,36 +53,40 @@ def init_prompt():
# Planner提示词 - 修改为要求 JSON 输出 # Planner提示词 - 修改为要求 JSON 输出
Prompt( Prompt(
"""你的名字是{bot_name},{prompt_personality}{chat_context_description}。需要基于以下信息决定如何参与对话: """现在{bot_name}开始在一个qq群聊中专注聊天。你需要操控{bot_name},并且根据以下消息决定是否,如何参与对话:
{structured_info_block}
{nickname_info} {nickname_info}
{chat_content_block} {chat_content_block}
{current_mind_block} {current_mind_block}
{cycle_info_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填写表情包的适用场合也就是当前场合) - 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合也就是当前场合)
- 不要追加太多表情 - 不要追加太多表情
3. 发送纯表情(emoji_reply)适用 3. 发送纯表情(emoji_reply)适用
- {bot_name}似乎想加入话题或继续讨论但是似乎又没什么实质表达内容
- 适合用表情回应的场景 - 适合用表情回应的场景
- 需提供明确的emoji_query - 需提供明确的emoji_query
- 群聊里的大家都在发表情包
4. 自我对话处理 4. 对话处理
- 如果最后一条消息是你自己发的而你还想继续发消息需自然衔接不要有不自然的内容重叠 - 如果最后一条消息是{bot_name}发的而你还想操控{bot_name}继续发消息请确保这是合适的
- 避免重复或评价自己的发言 - 注意话题的推进如果没有必要不要揪着一个话题不放
- 不要自己和自己聊天 - 不要{bot_name}自己和自己聊天
决策任务 决策任务
{action_options_text} {action_options_text}
@ -221,18 +225,18 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s
reply_styles1 = [ reply_styles1 = [
("给出日常且口语化的回复,平淡一些", 0.4), ("给出日常且口语化的回复,平淡一些", 0.4),
("给出非常简短的回复", 0.4), ("给出非常简短的回复", 0.4),
("给出缺失主语的回复,简短", 0.15), ("**给出省略主语的回复,简短**", 0.15),
("给出带有语病的回复,朴实平淡", 0.05), ("给出带有语病的回复,朴实平淡", 0.00),
] ]
reply_style1_chosen = random.choices( reply_style1_chosen = random.choices(
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
)[0] )[0]
reply_styles2 = [ reply_styles2 = [
("不要回复的太有条理,可以有个性", 0.6), ("不要回复的太有条理,可以有个性", 0.8),
("不要回复的太有条理,可以复读", 0.15), ("不要回复的太有条理,可以复读", 0.0),
("回复的认真一些", 0.2), ("回复的认真一些", 0.2),
("可以回复单个表情符号", 0.05), ("可以回复单个表情符号", 0.00),
] ]
reply_style2_chosen = random.choices( reply_style2_chosen = random.choices(
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 [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 [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
)[0] )[0]
reply_styles2 = [ reply_styles2 = [
("不用回复的太有条理,可以有个性", 0.7), # 60%概率 ("不用回复的太有条理,可以有个性", 0.75), # 60%概率
("不用回复的太有条理,可以复读", 0.05), # 15%概率 ("不用回复的太有条理,可以复读", 0.0), # 15%概率
("回复的认真一些", 0.2), # 20%概率 ("回复的认真一些", 0.2), # 20%概率
("可以回复单个表情符号", 0.05), # 5%概率 ("可以回复单个表情符号", 0.05), # 5%概率
] ]