mirror of https://github.com/Mai-with-u/MaiBot.git
better:优化分割,优化表达使用,优化Planner选择和联动,优化记忆总结,优化回复Log
parent
3ea775af92
commit
1e159213cf
|
|
@ -145,16 +145,33 @@ class ExpressionSelector:
|
|||
for expr in style_query
|
||||
]
|
||||
|
||||
# 要求至少有10个 count > 1 的表达方式才进行选择
|
||||
min_required = 10
|
||||
# 要求至少有一定数量的 count > 1 的表达方式才进行“完整简单模式”选择
|
||||
min_required = 8
|
||||
if len(style_exprs) < min_required:
|
||||
# 高 count 样本不足:如果还有候选,就降级为随机选 3 个;如果一个都没有,则直接返回空
|
||||
if not style_exprs:
|
||||
logger.info(
|
||||
f"聊天流 {chat_id} 没有满足 count > 1 且未被拒绝的表达方式,简单模式不进行选择"
|
||||
)
|
||||
# 完全没有高 count 样本时,退化为全量随机抽样(不进入LLM流程)
|
||||
fallback_num = min(3, max_num) if max_num > 0 else 3
|
||||
fallback_selected = self._random_expressions(chat_id, fallback_num)
|
||||
if fallback_selected:
|
||||
self.update_expressions_last_active_time(fallback_selected)
|
||||
selected_ids = [expr["id"] for expr in fallback_selected]
|
||||
logger.info(
|
||||
f"聊天流 {chat_id} 使用简单模式降级随机抽选 {len(fallback_selected)} 个表达(无 count>1 样本)"
|
||||
)
|
||||
return fallback_selected, selected_ids
|
||||
return [], []
|
||||
logger.info(
|
||||
f"聊天流 {chat_id} count > 1 的表达方式不足 {min_required} 个(实际 {len(style_exprs)} 个),不进行选择"
|
||||
f"聊天流 {chat_id} count > 1 的表达方式不足 {min_required} 个(实际 {len(style_exprs)} 个),"
|
||||
f"简单模式降级为随机选择 3 个"
|
||||
)
|
||||
return [], []
|
||||
|
||||
# 固定选择5个
|
||||
select_count = 5
|
||||
select_count = min(3, len(style_exprs))
|
||||
else:
|
||||
# 高 count 数量达标时,固定选择 5 个
|
||||
select_count = 5
|
||||
import random
|
||||
|
||||
selected_style = random.sample(style_exprs, select_count)
|
||||
|
|
@ -308,20 +325,28 @@ class ExpressionSelector:
|
|||
select_random_count = 5
|
||||
|
||||
# 检查数量要求
|
||||
# 对于高 count 表达:如果数量不足,不再直接停止,而是仅跳过“高 count 优先选择”
|
||||
if len(high_count_exprs) < min_high_count:
|
||||
logger.info(
|
||||
f"聊天流 {chat_id} count > 1 的表达方式不足 {min_high_count} 个(实际 {len(high_count_exprs)} 个),不进行选择"
|
||||
f"聊天流 {chat_id} count > 1 的表达方式不足 {min_high_count} 个(实际 {len(high_count_exprs)} 个),"
|
||||
f"将跳过高 count 优先选择,仅从全部表达中随机抽样"
|
||||
)
|
||||
return [], []
|
||||
high_count_valid = False
|
||||
else:
|
||||
high_count_valid = True
|
||||
|
||||
# 总量不足仍然直接返回,避免样本过少导致选择质量过低
|
||||
if len(all_style_exprs) < min_total_count:
|
||||
logger.info(
|
||||
f"聊天流 {chat_id} 总表达方式不足 {min_total_count} 个(实际 {len(all_style_exprs)} 个),不进行选择"
|
||||
)
|
||||
return [], []
|
||||
|
||||
# 先选取高count的表达方式
|
||||
selected_high = weighted_sample(high_count_exprs, min(len(high_count_exprs), select_high_count))
|
||||
# 先选取高count的表达方式(如果数量达标)
|
||||
if high_count_valid:
|
||||
selected_high = weighted_sample(high_count_exprs, min(len(high_count_exprs), select_high_count))
|
||||
else:
|
||||
selected_high = []
|
||||
|
||||
# 然后从所有表达方式中随机抽样(使用加权抽样)
|
||||
remaining_num = select_random_count
|
||||
|
|
|
|||
|
|
@ -759,8 +759,8 @@ class JargonMiner:
|
|||
content_key = entry["content"]
|
||||
|
||||
# 检查是否包含人物名称
|
||||
logger.info(f"process_extracted_entries 检查是否包含人物名称: {content_key}")
|
||||
logger.info(f"person_name_filter: {person_name_filter}")
|
||||
# logger.info(f"process_extracted_entries 检查是否包含人物名称: {content_key}")
|
||||
# logger.info(f"person_name_filter: {person_name_filter}")
|
||||
if person_name_filter and person_name_filter(content_key):
|
||||
logger.info(f"process_extracted_entries 跳过包含人物名称的黑话: {content_key}")
|
||||
continue
|
||||
|
|
@ -885,7 +885,7 @@ class JargonMiner:
|
|||
logger.info(f"[{self.stream_name}]疑似黑话: {jargon_str}")
|
||||
|
||||
if saved or updated:
|
||||
logger.info(f"jargon写入: 新增 {saved} 条,更新 {updated} 条,chat_id={self.chat_id}")
|
||||
logger.debug(f"jargon写入: 新增 {saved} 条,更新 {updated} 条,chat_id={self.chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"处理已提取的黑话条目失败: {e}")
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class MessageRecorder:
|
|||
self.last_extraction_time = extraction_end_time
|
||||
|
||||
try:
|
||||
logger.info(f"在聊天流 {self.chat_name} 开始统一消息提取和分发")
|
||||
# logger.info(f"在聊天流 {self.chat_name} 开始统一消息提取和分发")
|
||||
|
||||
# 拉取提取窗口内的消息
|
||||
messages = get_raw_msg_by_timestamp_with_chat_inclusive(
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
|||
from src.chat.utils.chat_message_builder import (
|
||||
build_readable_messages_with_id,
|
||||
get_raw_msg_before_timestamp_with_chat,
|
||||
replace_user_references,
|
||||
)
|
||||
from src.chat.utils.utils import get_chat_type_and_target_info
|
||||
from src.chat.planner_actions.action_manager import ActionManager
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.plugin_system.base.component_types import ActionInfo, ComponentType, ActionActivationType
|
||||
from src.plugin_system.core.component_registry import component_registry
|
||||
from src.plugin_system.apis.message_api import translate_pid_to_description
|
||||
from src.person_info.person_info import Person
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.common.data_models.info_data_model import TargetPersonInfo
|
||||
|
|
@ -68,7 +71,8 @@ no_reply
|
|||
{moderation_prompt}
|
||||
|
||||
target_message_id为必填,表示触发消息的id
|
||||
请选择所有符合使用要求的action,动作用json格式输出,用```json包裹,如果输出多个json,每个json都要单独一行放在同一个```json代码块内:
|
||||
请选择所有符合使用要求的action,每个动作最多选择一次,但是可以选择多个动作;
|
||||
动作用json格式输出,用```json包裹,如果输出多个json,每个json都要单独一行放在同一个```json代码块内:
|
||||
**示例**
|
||||
// 理由文本(简短)
|
||||
```json
|
||||
|
|
@ -155,11 +159,41 @@ class ActionPlanner:
|
|||
logger.warning(f"{self.log_prefix}planner理由引用 {msg_id} 未找到对应消息,保持原样")
|
||||
return msg_id
|
||||
|
||||
msg_text = (message.processed_plain_text or message.display_message or "").strip()
|
||||
msg_text = (message.processed_plain_text or "").strip()
|
||||
if not msg_text:
|
||||
logger.warning(f"{self.log_prefix}planner理由引用 {msg_id} 的消息内容为空,保持原样")
|
||||
return msg_id
|
||||
|
||||
# 替换 [picid:xxx] 为 [图片:描述]
|
||||
pic_pattern = r"\[picid:([^\]]+)\]"
|
||||
def replace_pic_id(pic_match: re.Match) -> str:
|
||||
pic_id = pic_match.group(1)
|
||||
description = translate_pid_to_description(pic_id)
|
||||
return f"[图片:{description}]"
|
||||
msg_text = re.sub(pic_pattern, replace_pic_id, msg_text)
|
||||
|
||||
# 替换用户引用格式:回复<aaa:bbb> 和 @<aaa:bbb>
|
||||
platform = getattr(message, "user_info", None) and message.user_info.platform or getattr(message, "chat_info", None) and message.chat_info.platform or "qq"
|
||||
msg_text = replace_user_references(msg_text, platform, replace_bot_name=True)
|
||||
|
||||
# 替换单独的 <用户名:用户ID> 格式(replace_user_references 已处理回复<和@<格式)
|
||||
# 匹配所有 <aaa:bbb> 格式,由于 replace_user_references 已经替换了回复<和@<格式,
|
||||
# 这里匹配到的应该都是单独的格式
|
||||
user_ref_pattern = r"<([^:<>]+):([^:<>]+)>"
|
||||
def replace_user_ref(user_match: re.Match) -> str:
|
||||
user_name = user_match.group(1)
|
||||
user_id = user_match.group(2)
|
||||
try:
|
||||
# 检查是否是机器人自己
|
||||
if user_id == global_config.bot.qq_account:
|
||||
return f"{global_config.bot.nickname}(你)"
|
||||
person = Person(platform=platform, user_id=user_id)
|
||||
return person.person_name or user_name
|
||||
except Exception:
|
||||
# 如果解析失败,使用原始昵称
|
||||
return user_name
|
||||
msg_text = re.sub(user_ref_pattern, replace_user_ref, msg_text)
|
||||
|
||||
preview = msg_text if len(msg_text) <= 100 else f"{msg_text[:97]}..."
|
||||
logger.info(f"{self.log_prefix}planner理由引用 {msg_id} -> 消息({preview})")
|
||||
return f"消息({msg_text})"
|
||||
|
|
|
|||
|
|
@ -98,8 +98,10 @@ class DefaultReplyer:
|
|||
available_actions = {}
|
||||
try:
|
||||
# 3. 构建 Prompt
|
||||
timing_logs = []
|
||||
almost_zero_str = ""
|
||||
with Timer("构建Prompt", {}): # 内部计时器,可选保留
|
||||
prompt, selected_expressions = await self.build_prompt_reply_context(
|
||||
prompt, selected_expressions, timing_logs, almost_zero_str = await self.build_prompt_reply_context(
|
||||
extra_info=extra_info,
|
||||
available_actions=available_actions,
|
||||
chosen_actions=chosen_actions,
|
||||
|
|
@ -136,9 +138,22 @@ class DefaultReplyer:
|
|||
content, reasoning_content, model_name, tool_call = await self.llm_generate_content(prompt)
|
||||
# logger.debug(f"replyer生成内容: {content}")
|
||||
|
||||
logger.info(f"模型: [{model_name}][思考等级:{think_level}]生成内容: {content}")
|
||||
if global_config.debug.show_replyer_reasoning and reasoning_content:
|
||||
logger.info(f"模型: [{model_name}][思考等级:{think_level}]生成推理:\n{reasoning_content}")
|
||||
# 统一输出所有日志信息,使用try-except确保即使某个步骤出错也能输出
|
||||
try:
|
||||
# 1. 输出回复准备日志
|
||||
timing_log_str = f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s" if timing_logs or almost_zero_str else "回复准备: 无计时信息"
|
||||
logger.info(timing_log_str)
|
||||
# 2. 输出Prompt日志
|
||||
if global_config.debug.show_replyer_prompt:
|
||||
logger.info(f"\n{prompt}\n")
|
||||
else:
|
||||
logger.debug(f"\nreplyer_Prompt:{prompt}\n")
|
||||
# 3. 输出模型生成内容和推理日志
|
||||
logger.info(f"模型: [{model_name}][思考等级:{think_level}]生成内容: {content}")
|
||||
if global_config.debug.show_replyer_reasoning and reasoning_content:
|
||||
logger.info(f"模型: [{model_name}][思考等级:{think_level}]生成推理:\n{reasoning_content}")
|
||||
except Exception as e:
|
||||
logger.warning(f"输出日志时出错: {e}")
|
||||
|
||||
llm_response.content = content
|
||||
llm_response.reasoning = reasoning_content
|
||||
|
|
@ -162,6 +177,21 @@ class DefaultReplyer:
|
|||
except Exception as llm_e:
|
||||
# 精简报错信息
|
||||
logger.error(f"LLM 生成失败: {llm_e}")
|
||||
# 即使LLM生成失败,也尝试输出已收集的日志信息
|
||||
try:
|
||||
# 1. 输出回复准备日志
|
||||
timing_log_str = f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s" if timing_logs or almost_zero_str else "回复准备: 无计时信息"
|
||||
logger.info(timing_log_str)
|
||||
# 2. 输出Prompt日志
|
||||
if global_config.debug.show_replyer_prompt:
|
||||
logger.info(f"\n{prompt}\n")
|
||||
else:
|
||||
logger.debug(f"\nreplyer_Prompt:{prompt}\n")
|
||||
# 3. 输出模型生成失败信息
|
||||
logger.info("模型生成失败,无法输出生成内容和推理")
|
||||
except Exception as log_e:
|
||||
logger.warning(f"输出日志时出错: {log_e}")
|
||||
|
||||
return False, llm_response # LLM 调用失败则无法生成回复
|
||||
|
||||
return True, llm_response
|
||||
|
|
@ -705,7 +735,7 @@ class DefaultReplyer:
|
|||
enable_tool: bool = True,
|
||||
reply_time_point: Optional[float] = time.time(),
|
||||
think_level: int = 1,
|
||||
) -> Tuple[str, List[int]]:
|
||||
) -> Tuple[str, List[int], List[str], str]:
|
||||
"""
|
||||
构建回复器上下文
|
||||
|
||||
|
|
@ -838,7 +868,8 @@ class DefaultReplyer:
|
|||
continue
|
||||
|
||||
timing_logs.append(f"{chinese_name}: {duration:.1f}s")
|
||||
logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s")
|
||||
# 不再在这里输出日志,而是返回给调用者统一输出
|
||||
# logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s")
|
||||
|
||||
expression_habits_block, selected_expressions = results_dict["expression_habits"]
|
||||
expression_habits_block: str
|
||||
|
|
@ -915,7 +946,7 @@ class DefaultReplyer:
|
|||
memory_retrieval=memory_retrieval,
|
||||
chat_prompt=chat_prompt_block,
|
||||
planner_reasoning=planner_reasoning,
|
||||
), selected_expressions
|
||||
), selected_expressions, timing_logs, almost_zero_str
|
||||
|
||||
async def build_prompt_rewrite_context(
|
||||
self,
|
||||
|
|
@ -1046,10 +1077,11 @@ class DefaultReplyer:
|
|||
# 直接使用已初始化的模型实例
|
||||
# logger.info(f"\n{prompt}\n")
|
||||
|
||||
if global_config.debug.show_replyer_prompt:
|
||||
logger.info(f"\n{prompt}\n")
|
||||
else:
|
||||
logger.debug(f"\nreplyer_Prompt:{prompt}\n")
|
||||
# 不再在这里输出日志,而是返回给调用者统一输出
|
||||
# if global_config.debug.show_replyer_prompt:
|
||||
# logger.info(f"\n{prompt}\n")
|
||||
# else:
|
||||
# logger.debug(f"\nreplyer_Prompt:{prompt}\n")
|
||||
|
||||
content, (reasoning_content, model_name, tool_calls) = await self.express_model.generate_response_async(
|
||||
prompt
|
||||
|
|
|
|||
|
|
@ -198,21 +198,21 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
|
|||
List[str]: 分割和合并后的句子列表
|
||||
"""
|
||||
# 预处理:处理多余的换行符
|
||||
# 1. 将连续的换行符替换为单个换行符
|
||||
# 1. 将连续的换行符替换为单个换行符(保留换行符用于分割)
|
||||
text = re.sub(r"\n\s*\n+", "\n", text)
|
||||
# 2. 处理换行符和其他分隔符的组合
|
||||
text = re.sub(r"\n\s*([,,。;\s])", r"\1", text)
|
||||
text = re.sub(r"([,,。;\s])\s*\n", r"\1", text)
|
||||
# 2. 处理换行符和其他分隔符的组合(保留换行符,删除其他分隔符)
|
||||
text = re.sub(r"\n\s*([,,。;\s])", r"\n\1", text)
|
||||
text = re.sub(r"([,,。;\s])\s*\n", r"\1\n", text)
|
||||
|
||||
# 处理两个汉字中间的换行符
|
||||
text = re.sub(r"([\u4e00-\u9fff])\n([\u4e00-\u9fff])", r"\1。\2", text)
|
||||
# 处理两个汉字中间的换行符(保留换行符,不替换为句号,让换行符强制分割)
|
||||
# text = re.sub(r"([\u4e00-\u9fff])\n([\u4e00-\u9fff])", r"\1。\2", text) # 注释掉,保留换行符用于分割
|
||||
|
||||
len_text = len(text)
|
||||
if len_text < 3:
|
||||
return list(text) if random.random() < 0.01 else [text]
|
||||
|
||||
# 定义分隔符
|
||||
separators = {",", ",", " ", "。", ";"}
|
||||
# 定义分隔符(包含换行符,换行符必须强制分割)
|
||||
separators = {",", ",", " ", "。", ";", "\n"}
|
||||
segments = []
|
||||
current_segment = ""
|
||||
|
||||
|
|
@ -221,13 +221,27 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
|
|||
while i < len(text):
|
||||
char = text[i]
|
||||
if char in separators:
|
||||
# 检查分割条件:如果空格左右都是英文字母、数字,或数字和英文之间,则不分割(仅对空格应用此规则)
|
||||
can_split = True
|
||||
if 0 < i < len(text) - 1:
|
||||
prev_char = text[i - 1]
|
||||
next_char = text[i + 1]
|
||||
# 只对空格应用"不分割数字和数字、数字和英文、英文和数字、英文和英文之间的空格"规则
|
||||
if char == " ":
|
||||
# 换行符必须强制分割,不受其他规则影响
|
||||
if char == "\n":
|
||||
can_split = True
|
||||
else:
|
||||
# 检查分割条件
|
||||
can_split = True
|
||||
# 检查分隔符左右是否有冒号(中英文),如果有则不分割
|
||||
if i > 0:
|
||||
prev_char = text[i - 1]
|
||||
if prev_char in {":", ":"}:
|
||||
can_split = False
|
||||
if i < len(text) - 1:
|
||||
next_char = text[i + 1]
|
||||
if next_char in {":", ":"}:
|
||||
can_split = False
|
||||
|
||||
# 如果左右没有冒号,再检查空格的特殊情况
|
||||
if can_split and char == " " and i > 0 and i < len(text) - 1:
|
||||
prev_char = text[i - 1]
|
||||
next_char = text[i + 1]
|
||||
# 不分割数字和数字、数字和英文、英文和数字、英文和英文之间的空格
|
||||
prev_is_alnum = prev_char.isdigit() or is_english_letter(prev_char)
|
||||
next_is_alnum = next_char.isdigit() or is_english_letter(next_char)
|
||||
if prev_is_alnum and next_is_alnum:
|
||||
|
|
@ -237,8 +251,8 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
|
|||
# 只有当当前段不为空时才添加
|
||||
if current_segment:
|
||||
segments.append((current_segment, char))
|
||||
# 如果当前段为空,但分隔符是空格,则也添加一个空段(保留空格)
|
||||
elif char == " ":
|
||||
# 如果当前段为空,但分隔符是空格或换行符,则也添加一个空段(保留分隔符)
|
||||
elif char in {" ", "\n"}:
|
||||
segments.append(("", char))
|
||||
current_segment = ""
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import asyncio
|
|||
import json
|
||||
import time
|
||||
import re
|
||||
import difflib
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from dataclasses import dataclass, field
|
||||
|
|
@ -30,16 +31,18 @@ HIPPO_CACHE_DIR = Path(__file__).resolve().parents[2] / "data" / "hippo_memorize
|
|||
def init_prompt():
|
||||
"""初始化提示词模板"""
|
||||
|
||||
topic_analysis_prompt = """
|
||||
【历史话题标题列表】(仅标题,不含具体内容):
|
||||
topic_analysis_prompt = """【历史话题标题列表】(仅标题,不含具体内容):
|
||||
{history_topics_block}
|
||||
【历史话题标题列表结束】
|
||||
|
||||
【本次聊天记录】(每条消息前有编号,用于后续引用):
|
||||
{messages_block}
|
||||
【本次聊天记录结束】
|
||||
|
||||
请完成以下任务:
|
||||
**识别话题**
|
||||
1. 识别【本次聊天记录】中正在进行的一个或多个话题;
|
||||
2. 【本次聊天记录】的中的消息可能与历史话题有关,也可能毫无关联。
|
||||
2. 判断【历史话题标题列表】中的话题是否在【本次聊天记录】中出现,如果出现,则直接使用该历史话题标题字符串;
|
||||
|
||||
**选取消息**
|
||||
|
|
@ -374,10 +377,10 @@ class ChatHistorySummarizer:
|
|||
should_check = True
|
||||
logger.info(f"{self.log_prefix} 触发检查条件: 消息数量达到 {message_count} 条(阈值: 100条)")
|
||||
|
||||
# 条件2: 距离上一次检查 > 3600 秒(1小时),触发一次检查
|
||||
elif time_since_last_check > 2400:
|
||||
# 条件2: 距离上一次检查 > 3600 * 8 秒(8小时)且消息数量 >= 20 条,触发一次检查
|
||||
elif time_since_last_check > 3600 * 8 and message_count >= 20:
|
||||
should_check = True
|
||||
logger.info(f"{self.log_prefix} 触发检查条件: 距上次检查 {time_str}(阈值: 1小时)")
|
||||
logger.info(f"{self.log_prefix} 触发检查条件: 距上次检查 {time_str}(阈值: 8小时)且消息数量达到 {message_count} 条(阈值: 20条)")
|
||||
|
||||
if should_check:
|
||||
await self._run_topic_check_and_update_cache(messages)
|
||||
|
|
@ -459,9 +462,31 @@ class ChatHistorySummarizer:
|
|||
|
||||
if not success or not topic_to_indices:
|
||||
logger.error(f"{self.log_prefix} 话题识别连续 {max_retries} 次失败或始终无有效话题,本次检查放弃")
|
||||
# 即使识别失败,也认为是一次“检查”,但不更新 no_update_checks(保持原状)
|
||||
# 即使识别失败,也认为是一次"检查",但不更新 no_update_checks(保持原状)
|
||||
return
|
||||
|
||||
# 3.5. 检查新话题是否与历史话题相似(相似度>=90%则使用历史标题)
|
||||
topic_mapping = self._build_topic_mapping(topic_to_indices, similarity_threshold=0.9)
|
||||
|
||||
# 应用话题映射:将相似的新话题标题替换为历史话题标题
|
||||
if topic_mapping:
|
||||
new_topic_to_indices: Dict[str, List[int]] = {}
|
||||
for new_topic, indices in topic_to_indices.items():
|
||||
# 如果这个新话题需要映射到历史话题
|
||||
if new_topic in topic_mapping:
|
||||
historical_topic = topic_mapping[new_topic]
|
||||
# 如果历史话题已经存在,合并消息索引
|
||||
if historical_topic in new_topic_to_indices:
|
||||
# 合并索引并去重
|
||||
combined_indices = list(set(new_topic_to_indices[historical_topic] + indices))
|
||||
new_topic_to_indices[historical_topic] = combined_indices
|
||||
else:
|
||||
new_topic_to_indices[historical_topic] = indices
|
||||
else:
|
||||
# 不需要映射,保持原样
|
||||
new_topic_to_indices[new_topic] = indices
|
||||
topic_to_indices = new_topic_to_indices
|
||||
|
||||
# 4. 统计哪些话题在本次检查中有新增内容
|
||||
updated_topics: Set[str] = set()
|
||||
|
||||
|
|
@ -528,6 +553,71 @@ class ChatHistorySummarizer:
|
|||
# 无论成功与否,都从缓存中删除,避免重复
|
||||
self.topic_cache.pop(topic, None)
|
||||
|
||||
def _find_most_similar_topic(
|
||||
self, new_topic: str, existing_topics: List[str], similarity_threshold: float = 0.9
|
||||
) -> Optional[tuple[str, float]]:
|
||||
"""
|
||||
查找与给定新话题最相似的历史话题
|
||||
|
||||
Args:
|
||||
new_topic: 新话题标题
|
||||
existing_topics: 历史话题标题列表
|
||||
similarity_threshold: 相似度阈值,默认0.9(90%)
|
||||
|
||||
Returns:
|
||||
Optional[tuple[str, float]]: 如果找到相似度>=阈值的历史话题,返回(历史话题标题, 相似度),
|
||||
否则返回None
|
||||
"""
|
||||
if not existing_topics:
|
||||
return None
|
||||
|
||||
best_match = None
|
||||
best_similarity = 0.0
|
||||
|
||||
for existing_topic in existing_topics:
|
||||
similarity = difflib.SequenceMatcher(None, new_topic, existing_topic).ratio()
|
||||
if similarity > best_similarity:
|
||||
best_similarity = similarity
|
||||
best_match = existing_topic
|
||||
|
||||
# 如果相似度达到阈值,返回匹配结果
|
||||
if best_match and best_similarity >= similarity_threshold:
|
||||
return (best_match, best_similarity)
|
||||
|
||||
return None
|
||||
|
||||
def _build_topic_mapping(
|
||||
self, topic_to_indices: Dict[str, List[int]], similarity_threshold: float = 0.9
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
构建新话题到历史话题的映射(如果相似度>=阈值)
|
||||
|
||||
Args:
|
||||
topic_to_indices: 新话题到消息索引的映射
|
||||
similarity_threshold: 相似度阈值,默认0.9(90%)
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 新话题 -> 历史话题的映射字典
|
||||
"""
|
||||
existing_topics_list = list(self.topic_cache.keys())
|
||||
topic_mapping: Dict[str, str] = {}
|
||||
|
||||
for new_topic in topic_to_indices.keys():
|
||||
# 如果新话题已经在历史话题中,不需要检查
|
||||
if new_topic in existing_topics_list:
|
||||
continue
|
||||
|
||||
# 查找最相似的历史话题
|
||||
result = self._find_most_similar_topic(new_topic, existing_topics_list, similarity_threshold)
|
||||
if result:
|
||||
historical_topic, similarity = result
|
||||
topic_mapping[new_topic] = historical_topic
|
||||
logger.info(
|
||||
f"{self.log_prefix} 话题相似度检查: '{new_topic}' 与历史话题 '{historical_topic}' 相似度 {similarity:.2%},使用历史标题"
|
||||
)
|
||||
|
||||
return topic_mapping
|
||||
|
||||
def _build_numbered_messages_for_llm(
|
||||
self, messages: List[DatabaseMessages]
|
||||
) -> tuple[List[str], Dict[int, str], Dict[int, str], Dict[int, Set[str]]]:
|
||||
|
|
@ -622,8 +712,7 @@ class ChatHistorySummarizer:
|
|||
try:
|
||||
response, _ = await self.summarizer_llm.generate_response_async(
|
||||
prompt=prompt,
|
||||
temperature=0.2,
|
||||
max_tokens=800,
|
||||
temperature=0.3,
|
||||
)
|
||||
|
||||
logger.info(f"{self.log_prefix} 话题识别LLM Prompt: {prompt}")
|
||||
|
|
|
|||
|
|
@ -108,8 +108,8 @@ async def generate_with_model_with_tools(
|
|||
"""
|
||||
try:
|
||||
model_name_list = model_config.model_list
|
||||
logger.info(f"[LLMAPI] 使用模型集合 {model_name_list} 生成内容")
|
||||
logger.debug(f"[LLMAPI] 完整提示词: {prompt}")
|
||||
logger.info(f"使用模型{model_name_list}生成内容")
|
||||
logger.debug(f"完整提示词: {prompt}")
|
||||
|
||||
llm_request = LLMRequest(model_set=model_config, request_type=request_type)
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ async def generate_with_model_with_tools_by_message_factory(
|
|||
"""
|
||||
try:
|
||||
model_name_list = model_config.model_list
|
||||
logger.info(f"[LLMAPI] 使用模型集合 {model_name_list} 生成内容(消息工厂)")
|
||||
logger.info(f"使用模型 {model_name_list} 生成内容")
|
||||
|
||||
llm_request = LLMRequest(model_set=model_config, request_type=request_type)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[inner]
|
||||
version = "1.9.0"
|
||||
version = "1.9.1"
|
||||
|
||||
# 配置文件版本号迭代规则同bot_config.toml
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ price_out = 0
|
|||
[model_task_config.utils] # 在麦麦的一些组件中使用的模型,例如表情包模块,取名模块,关系模块,麦麦的情绪变化等,是麦麦必须的模型
|
||||
model_list = ["siliconflow-deepseek-v3.2"] # 使用的模型列表,每个子项对应上面的模型名称(name)
|
||||
temperature = 0.2 # 模型温度,新V3建议0.1-0.3
|
||||
max_tokens = 2048 # 最大输出token数
|
||||
max_tokens = 4096 # 最大输出token数
|
||||
slow_threshold = 15.0 # 慢请求阈值(秒),模型等待回复时间超过此值会输出警告日志
|
||||
|
||||
[model_task_config.utils_small] # 在麦麦的一些组件中使用的小模型,消耗量较大,建议使用速度较快的小模型
|
||||
|
|
|
|||
Loading…
Reference in New Issue