mirror of https://github.com/Mai-with-u/MaiBot.git
commit
b9b8c9632f
|
|
@ -1,6 +1,14 @@
|
|||
# Changelog
|
||||
|
||||
## [0.11.0] - 2025-9-22
|
||||
## [0.11.1] - 2025-11-4
|
||||
### 功能更改和修复
|
||||
- 记忆现在能够被遗忘,并且拥有更好的合并
|
||||
- 修复部分llm请求问题
|
||||
- 优化记忆提取
|
||||
- 提供replyer的细节debug配置
|
||||
|
||||
|
||||
## [0.11.0] - 2025-10-27
|
||||
### 🌟 主要功能更改
|
||||
- 重构记忆系统,新的记忆系统更可靠,双通道查询,可以查询文本记忆和过去聊天记录
|
||||
- 主动发言功能,麦麦会自主提出问题(可精细调控频率)
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ class EmojiManager:
|
|||
|
||||
self._scan_task = None
|
||||
|
||||
self.vlm = LLMRequest(model_set=model_config.model_task_config.vlm, request_type="emoji")
|
||||
self.vlm = LLMRequest(model_set=model_config.model_task_config.vlm, request_type="emoji.see")
|
||||
self.llm_emotion_judge = LLMRequest(
|
||||
model_set=model_config.model_task_config.utils, request_type="emoji"
|
||||
) # 更高的温度,更少的token(后续可以根据情绪来调整温度)
|
||||
|
|
@ -940,16 +940,16 @@ class EmojiManager:
|
|||
image_base64 = get_image_manager().transform_gif(image_base64) # type: ignore
|
||||
if not image_base64:
|
||||
raise RuntimeError("GIF表情包转换失败")
|
||||
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
|
||||
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,简短描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
|
||||
description, _ = await self.vlm.generate_response_for_image(
|
||||
prompt, image_base64, "jpg", temperature=0.3, max_tokens=1000
|
||||
prompt, image_base64, "jpg", temperature=0.5
|
||||
)
|
||||
else:
|
||||
prompt = (
|
||||
"这是一个表情包,请详细描述一下表情包所表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
|
||||
"这是一个表情包,请详细描述一下表情包所表达的情感和内容,简短描述细节,从互联网梗,meme的角度去分析"
|
||||
)
|
||||
description, _ = await self.vlm.generate_response_for_image(
|
||||
prompt, image_base64, image_format, temperature=0.3, max_tokens=1000
|
||||
prompt, image_base64, image_format, temperature=0.5
|
||||
)
|
||||
|
||||
# 审核表情包
|
||||
|
|
@ -970,13 +970,14 @@ class EmojiManager:
|
|||
|
||||
# 第二步:LLM情感分析 - 基于详细描述生成情感标签列表
|
||||
emotion_prompt = f"""
|
||||
请你识别这个表情包的含义和适用场景,给我简短的描述,每个描述不要超过15个字
|
||||
这是一个基于这个表情包的描述:'{description}'
|
||||
你可以关注其幽默和讽刺意味,动用贴吧,微博,小红书的知识,必须从互联网梗,meme的角度去分析
|
||||
请直接输出描述,不要出现任何其他内容,如果有多个描述,可以用逗号分隔
|
||||
这是一个聊天场景中的表情包描述:'{description}'
|
||||
|
||||
请你识别这个表情包的含义和适用场景,给我简短的描述,每个描述不要超过15个字
|
||||
你可以关注其幽默和讽刺意味,动用贴吧,微博,小红书的知识,必须从互联网梗,meme的角度去分析
|
||||
请直接输出描述,不要出现任何其他内容,如果有多个描述,可以用逗号分隔
|
||||
"""
|
||||
emotions_text, _ = await self.llm_emotion_judge.generate_response_async(
|
||||
emotion_prompt, temperature=0.7, max_tokens=600
|
||||
emotion_prompt, temperature=0.7, max_tokens=256
|
||||
)
|
||||
|
||||
# 处理情感列表
|
||||
|
|
|
|||
|
|
@ -149,8 +149,51 @@ class ChatBot:
|
|||
async def handle_notice_message(self, message: MessageRecv):
|
||||
if message.message_info.message_id == "notice":
|
||||
message.is_notify = True
|
||||
logger.info("notice消息")
|
||||
print(message)
|
||||
logger.debug("notice消息")
|
||||
try:
|
||||
seg = message.message_segment
|
||||
mi = message.message_info
|
||||
sub_type = None
|
||||
scene = None
|
||||
msg_id = None
|
||||
recalled_id = None
|
||||
|
||||
if getattr(seg, "type", None) == "notify" and isinstance(getattr(seg, "data", None), dict):
|
||||
sub_type = seg.data.get("sub_type")
|
||||
scene = seg.data.get("scene")
|
||||
msg_id = seg.data.get("message_id")
|
||||
recalled = seg.data.get("recalled_user_info") or {}
|
||||
if isinstance(recalled, dict):
|
||||
recalled_id = recalled.get("user_id")
|
||||
|
||||
op = mi.user_info
|
||||
gid = mi.group_info.group_id if mi.group_info else None
|
||||
|
||||
# 撤回事件打印;无法获取被撤回者则省略
|
||||
if sub_type == "recall":
|
||||
op_name = getattr(op, "user_cardname", None) or getattr(op, "user_nickname", None) or str(getattr(op, "user_id", None))
|
||||
recalled_name = None
|
||||
try:
|
||||
if isinstance(recalled, dict):
|
||||
recalled_name = (
|
||||
recalled.get("user_cardname")
|
||||
or recalled.get("user_nickname")
|
||||
or str(recalled.get("user_id"))
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if recalled_name and str(recalled_id) != str(getattr(op, "user_id", None)):
|
||||
logger.info(f"{op_name} 撤回了 {recalled_name} 的消息")
|
||||
else:
|
||||
logger.info(f"{op_name} 撤回了消息")
|
||||
else:
|
||||
logger.debug(
|
||||
f"[notice] sub_type={sub_type} scene={scene} op={getattr(op,'user_nickname',None)}({getattr(op,'user_id',None)}) "
|
||||
f"gid={gid} msg_id={msg_id} recalled={recalled_id}"
|
||||
)
|
||||
except Exception:
|
||||
logger.info("[notice] (简略) 收到一条通知事件")
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -215,8 +258,7 @@ class ChatBot:
|
|||
message.message_segment = Seg(type="seglist", data=modified_message.message_segments)
|
||||
|
||||
if await self.handle_notice_message(message):
|
||||
# return
|
||||
pass
|
||||
return
|
||||
|
||||
# 处理消息内容,生成纯文本
|
||||
await message.process()
|
||||
|
|
|
|||
|
|
@ -136,7 +136,8 @@ class DefaultReplyer:
|
|||
# logger.debug(f"replyer生成内容: {content}")
|
||||
|
||||
logger.info(f"replyer生成内容: {content}")
|
||||
logger.info(f"replyer生成推理: {reasoning_content}")
|
||||
if global_config.debug.show_replyer_reasoning:
|
||||
logger.info(f"replyer生成推理:\n{reasoning_content}")
|
||||
logger.info(f"replyer生成模型: {model_name}")
|
||||
|
||||
llm_response.content = content
|
||||
|
|
@ -772,7 +773,7 @@ class DefaultReplyer:
|
|||
continue
|
||||
|
||||
timing_logs.append(f"{chinese_name}: {duration:.1f}s")
|
||||
if duration > 8:
|
||||
if duration > 12:
|
||||
logger.warning(f"回复生成前信息获取耗时过长: {chinese_name} 耗时: {duration:.1f}s,请使用更快的模型")
|
||||
logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s")
|
||||
|
||||
|
|
@ -1000,7 +1001,7 @@ class DefaultReplyer:
|
|||
# 直接使用已初始化的模型实例
|
||||
# logger.info(f"\n{prompt}\n")
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
if global_config.debug.show_replyer_prompt:
|
||||
logger.info(f"\n{prompt}\n")
|
||||
else:
|
||||
logger.debug(f"\nreplyer_Prompt:{prompt}\n")
|
||||
|
|
|
|||
|
|
@ -679,7 +679,7 @@ class PrivateReplyer:
|
|||
continue
|
||||
|
||||
timing_logs.append(f"{chinese_name}: {duration:.1f}s")
|
||||
if duration > 8:
|
||||
if duration > 12:
|
||||
logger.warning(f"回复生成前信息获取耗时过长: {chinese_name} 耗时: {duration:.1f}s,请使用更快的模型")
|
||||
logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s")
|
||||
|
||||
|
|
@ -922,7 +922,7 @@ class PrivateReplyer:
|
|||
# 直接使用已初始化的模型实例
|
||||
logger.info(f"\n{prompt}\n")
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
if global_config.debug.show_replyer_prompt:
|
||||
logger.info(f"\n{prompt}\n")
|
||||
else:
|
||||
logger.debug(f"\n{prompt}\n")
|
||||
|
|
@ -934,6 +934,8 @@ class PrivateReplyer:
|
|||
content = content.strip()
|
||||
|
||||
logger.info(f"使用 {model_name} 生成回复内容: {content}")
|
||||
if global_config.debug.show_replyer_reasoning:
|
||||
logger.info(f"使用 {model_name} 生成回复推理:\n{reasoning_content}")
|
||||
return content, reasoning_content, model_name, tool_calls
|
||||
|
||||
async def get_prompt_info(self, message: str, sender: str, target: str):
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
|
|||
|
||||
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
||||
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
|
||||
MMC_VERSION = "0.11.0"
|
||||
MMC_VERSION = "0.11.1-snapshot.1"
|
||||
|
||||
|
||||
def get_key_comment(toml_table, key):
|
||||
|
|
|
|||
|
|
@ -642,6 +642,12 @@ class DebugConfig(ConfigBase):
|
|||
show_prompt: bool = False
|
||||
"""是否显示prompt"""
|
||||
|
||||
show_replyer_prompt: bool = True
|
||||
"""是否显示回复器prompt"""
|
||||
|
||||
show_replyer_reasoning: bool = True
|
||||
"""是否显示回复器推理"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExperimentalConfig(ConfigBase):
|
||||
|
|
|
|||
|
|
@ -237,15 +237,8 @@ def _build_stream_api_resp(
|
|||
|
||||
resp.tool_calls.append(ToolCall(call_id, function_name, arguments))
|
||||
|
||||
# 检查 max_tokens 截断
|
||||
if finish_reason == "length":
|
||||
if resp.content and resp.content.strip():
|
||||
logger.warning(
|
||||
"⚠ OpenAI 响应因达到 max_tokens 限制被部分截断,\n"
|
||||
" 可能会对回复内容造成影响,建议修改模型 max_tokens 配置!"
|
||||
)
|
||||
else:
|
||||
logger.warning("⚠ OpenAI 响应因达到 max_tokens 限制被截断,\n 请修改模型 max_tokens 配置!")
|
||||
# 检查 max_tokens 截断(流式的告警改由处理函数统一输出,这里不再输出)
|
||||
# 保留 finish_reason 仅用于上层判断
|
||||
|
||||
if not resp.content and not resp.tool_calls:
|
||||
raise EmptyResponseException()
|
||||
|
|
@ -270,6 +263,7 @@ async def _default_stream_response_handler(
|
|||
_tool_calls_buffer: list[tuple[str, str, io.StringIO]] = [] # 工具调用缓冲区,用于存储接收到的工具调用
|
||||
_usage_record = None # 使用情况记录
|
||||
finish_reason: str | None = None # 记录最后的 finish_reason
|
||||
_model_name: str | None = None # 记录模型名
|
||||
|
||||
def _insure_buffer_closed():
|
||||
# 确保缓冲区被关闭
|
||||
|
|
@ -300,6 +294,9 @@ async def _default_stream_response_handler(
|
|||
if hasattr(event.choices[0], "finish_reason") and event.choices[0].finish_reason:
|
||||
finish_reason = event.choices[0].finish_reason
|
||||
|
||||
if hasattr(event, "model") and event.model and not _model_name:
|
||||
_model_name = event.model # 记录模型名
|
||||
|
||||
if hasattr(delta, "reasoning_content") and delta.reasoning_content: # type: ignore
|
||||
# 标记:有独立的推理内容块
|
||||
_has_rc_attr_flag = True
|
||||
|
|
@ -322,12 +319,34 @@ async def _default_stream_response_handler(
|
|||
)
|
||||
|
||||
try:
|
||||
return _build_stream_api_resp(
|
||||
resp = _build_stream_api_resp(
|
||||
_fc_delta_buffer,
|
||||
_rc_delta_buffer,
|
||||
_tool_calls_buffer,
|
||||
finish_reason=finish_reason,
|
||||
), _usage_record
|
||||
)
|
||||
# 统一在这里输出 max_tokens 截断的警告,并从 resp 中读取
|
||||
if finish_reason == "length":
|
||||
# 把模型名塞到 resp.raw_data,后续严格“从 resp 提取”
|
||||
try:
|
||||
if _model_name:
|
||||
resp.raw_data = {"model": _model_name}
|
||||
except Exception:
|
||||
pass
|
||||
model_dbg = None
|
||||
try:
|
||||
if isinstance(resp.raw_data, dict):
|
||||
model_dbg = resp.raw_data.get("model")
|
||||
except Exception:
|
||||
model_dbg = None
|
||||
|
||||
# 统一日志格式
|
||||
logger.info(
|
||||
"模型%s因为超过最大max_token限制,可能仅输出部分内容,可视情况调整"
|
||||
% (model_dbg or "")
|
||||
)
|
||||
|
||||
return resp, _usage_record
|
||||
except Exception:
|
||||
# 确保缓冲区被关闭
|
||||
_insure_buffer_closed()
|
||||
|
|
@ -351,9 +370,32 @@ def _default_normal_response_parser(
|
|||
"""
|
||||
api_response = APIResponse()
|
||||
|
||||
if not hasattr(resp, "choices") or len(resp.choices) == 0:
|
||||
raise EmptyResponseException("响应解析失败,缺失choices字段或choices列表为空")
|
||||
message_part = resp.choices[0].message
|
||||
# 兼容部分 OpenAI 兼容服务在空回复时返回 choices=None 的情况
|
||||
choices = getattr(resp, "choices", None)
|
||||
if not choices:
|
||||
try:
|
||||
model_dbg = getattr(resp, "model", None)
|
||||
id_dbg = getattr(resp, "id", None)
|
||||
usage_dbg = None
|
||||
if hasattr(resp, "usage") and resp.usage:
|
||||
usage_dbg = {
|
||||
"prompt": getattr(resp.usage, "prompt_tokens", None),
|
||||
"completion": getattr(resp.usage, "completion_tokens", None),
|
||||
"total": getattr(resp.usage, "total_tokens", None),
|
||||
}
|
||||
try:
|
||||
raw_snippet = str(resp)[:300]
|
||||
except Exception:
|
||||
raw_snippet = "<unserializable>"
|
||||
logger.debug(
|
||||
f"empty choices: model={model_dbg} id={id_dbg} usage={usage_dbg} raw≈{raw_snippet}"
|
||||
)
|
||||
except Exception:
|
||||
# 日志采集失败不应影响控制流
|
||||
pass
|
||||
# 统一抛出可重试的 EmptyResponseException,触发上层重试逻辑
|
||||
raise EmptyResponseException("响应解析失败,choices 为空或缺失")
|
||||
message_part = choices[0].message
|
||||
|
||||
if hasattr(message_part, "reasoning_content") and message_part.reasoning_content: # type: ignore
|
||||
# 有有效的推理字段
|
||||
|
|
@ -402,14 +444,13 @@ def _default_normal_response_parser(
|
|||
choice0 = resp.choices[0]
|
||||
reason = getattr(choice0, "finish_reason", None)
|
||||
if reason and reason == "length":
|
||||
has_real_output = bool(api_response.content and api_response.content.strip())
|
||||
if has_real_output:
|
||||
logger.warning(
|
||||
"⚠ OpenAI 响应因达到 max_tokens 限制被部分截断,\n"
|
||||
" 可能会对回复内容造成影响,建议修改模型 max_tokens 配置!"
|
||||
print(resp)
|
||||
_model_name = resp.model
|
||||
# 统一日志格式
|
||||
logger.info(
|
||||
"模型%s因为超过最大max_token限制,可能仅输出部分内容,可视情况调整"
|
||||
% (_model_name or "")
|
||||
)
|
||||
else:
|
||||
logger.warning("⚠ OpenAI 响应因达到 max_tokens 限制被截断,\n 请修改模型 max_tokens 配置!")
|
||||
return api_response, _usage_record
|
||||
except Exception as e:
|
||||
logger.debug(f"检查 MAX_TOKENS 截断时异常: {e}")
|
||||
|
|
@ -522,7 +563,7 @@ class OpenaiClient(BaseClient):
|
|||
await asyncio.sleep(0.1) # 等待0.5秒后再次检查任务&中断信号量状态
|
||||
|
||||
# logger.
|
||||
logger.debug(f"OpenAI API响应(非流式): {req_task.result()}")
|
||||
# logger.debug(f"OpenAI API响应(非流式): {req_task.result()}")
|
||||
|
||||
# logger.info(f"OpenAI请求时间: {model_info.model_identifier} {time.time() - start_time} \n{messages}")
|
||||
|
||||
|
|
|
|||
|
|
@ -270,13 +270,28 @@ class LLMRequest:
|
|||
audio_base64=audio_base64,
|
||||
extra_params=model_info.extra_params,
|
||||
)
|
||||
except (EmptyResponseException, NetworkConnectionError) as e:
|
||||
except EmptyResponseException as e:
|
||||
# 空回复:通常为临时问题,单独记录并重试
|
||||
retry_remain -= 1
|
||||
if retry_remain <= 0:
|
||||
logger.error(f"模型 '{model_info.name}' 在用尽对临时错误的重试次数后仍然失败。")
|
||||
logger.error(f"模型 '{model_info.name}' 在多次出现空回复后仍然失败。")
|
||||
raise ModelAttemptFailed(f"模型 '{model_info.name}' 重试耗尽", original_exception=e) from e
|
||||
|
||||
logger.warning(f"模型 '{model_info.name}' 遇到可重试错误: {str(e)}。剩余重试次数: {retry_remain}")
|
||||
logger.warning(
|
||||
f"模型 '{model_info.name}' 返回空回复(可重试)。剩余重试次数: {retry_remain}"
|
||||
)
|
||||
await asyncio.sleep(api_provider.retry_interval)
|
||||
|
||||
except NetworkConnectionError as e:
|
||||
# 网络错误:单独记录并重试
|
||||
retry_remain -= 1
|
||||
if retry_remain <= 0:
|
||||
logger.error(f"模型 '{model_info.name}' 在网络错误重试用尽后仍然失败。")
|
||||
raise ModelAttemptFailed(f"模型 '{model_info.name}' 重试耗尽", original_exception=e) from e
|
||||
|
||||
logger.warning(
|
||||
f"模型 '{model_info.name}' 遇到网络错误(可重试): {str(e)}。剩余重试次数: {retry_remain}"
|
||||
)
|
||||
await asyncio.sleep(api_provider.retry_interval)
|
||||
|
||||
except RespNotOkException as e:
|
||||
|
|
@ -369,8 +384,8 @@ class LLMRequest:
|
|||
failed_models_this_request.add(model_info.name)
|
||||
|
||||
if isinstance(last_exception, RespNotOkException) and last_exception.status_code == 400:
|
||||
logger.error("收到不可恢复的客户端错误 (400),中止所有尝试。")
|
||||
raise last_exception from e
|
||||
logger.warning("收到客户端错误 (400),跳过当前模型并继续尝试其他模型。")
|
||||
continue
|
||||
|
||||
logger.error(f"所有 {max_attempts} 个模型均尝试失败。")
|
||||
if last_exception:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,80 @@ class MemoryChest:
|
|||
self.running_content_list = {} # {chat_id: {"content": running_content, "last_update_time": timestamp, "create_time": timestamp}}
|
||||
self.fetched_memory_list = [] # [(chat_id, (question, answer, timestamp)), ...]
|
||||
|
||||
def remove_one_memory_by_age_weight(self) -> bool:
|
||||
"""
|
||||
删除一条记忆:按“越老/越新更易被删”的权重随机选择(老=较小id,新=较大id)。
|
||||
|
||||
返回:是否删除成功
|
||||
"""
|
||||
try:
|
||||
memories = list(MemoryChestModel.select())
|
||||
if not memories:
|
||||
return False
|
||||
|
||||
# 排除锁定项
|
||||
candidates = [m for m in memories if not getattr(m, "locked", False)]
|
||||
if not candidates:
|
||||
return False
|
||||
|
||||
# 按 id 排序,使用 id 近似时间顺序(小 -> 老,大 -> 新)
|
||||
candidates.sort(key=lambda m: m.id)
|
||||
n = len(candidates)
|
||||
if n == 1:
|
||||
MemoryChestModel.delete().where(MemoryChestModel.id == candidates[0].id).execute()
|
||||
logger.info(f"[记忆管理] 已删除一条记忆(权重抽样):{candidates[0].title}")
|
||||
return True
|
||||
|
||||
# 计算U型权重:中间最低,两端最高
|
||||
# r ∈ [0,1] 为位置归一化,w = 0.1 + 0.9 * (abs(r-0.5)*2)**1.5
|
||||
weights = []
|
||||
for idx, _m in enumerate(candidates):
|
||||
r = idx / (n - 1)
|
||||
w = 0.1 + 0.9 * (abs(r - 0.5) * 2) ** 1.5
|
||||
weights.append(w)
|
||||
|
||||
import random as _random
|
||||
selected = _random.choices(candidates, weights=weights, k=1)[0]
|
||||
|
||||
MemoryChestModel.delete().where(MemoryChestModel.id == selected.id).execute()
|
||||
logger.info(f"[记忆管理] 已删除一条记忆(权重抽样):{selected.title}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"[记忆管理] 按年龄权重删除记忆时出错: {e}")
|
||||
return False
|
||||
|
||||
def _compute_merge_similarity_threshold(self) -> float:
|
||||
"""
|
||||
根据当前记忆数量占比动态计算合并相似度阈值。
|
||||
|
||||
规则:占比越高,阈值越低。
|
||||
- < 60%: 0.80(更严格,避免早期误合并)
|
||||
- < 80%: 0.70
|
||||
- < 100%: 0.60
|
||||
- < 120%: 0.50
|
||||
- >= 120%: 0.45(最宽松,加速收敛)
|
||||
"""
|
||||
try:
|
||||
current_count = MemoryChestModel.select().count()
|
||||
max_count = max(1, int(global_config.memory.max_memory_number))
|
||||
percentage = current_count / max_count
|
||||
|
||||
if percentage < 0.6:
|
||||
return 0.70
|
||||
elif percentage < 0.8:
|
||||
return 0.60
|
||||
elif percentage < 1.0:
|
||||
return 0.50
|
||||
elif percentage < 1.5:
|
||||
return 0.40
|
||||
elif percentage < 2:
|
||||
return 0.30
|
||||
else:
|
||||
return 0.25
|
||||
except Exception:
|
||||
# 发生异常时使用保守阈值
|
||||
return 0.70
|
||||
|
||||
async def build_running_content(self, chat_id: str = None) -> str:
|
||||
"""
|
||||
构建记忆仓库的运行内容
|
||||
|
|
@ -446,19 +520,22 @@ class MemoryChest:
|
|||
logger.warning("未提供chat_id,无法进行记忆匹配")
|
||||
return [], []
|
||||
|
||||
# 使用相似度匹配查找最相似的记忆
|
||||
# 动态计算相似度阈值(占比越高阈值越低)
|
||||
dynamic_threshold = self._compute_merge_similarity_threshold()
|
||||
|
||||
# 使用相似度匹配查找最相似的记忆(基于动态阈值)
|
||||
similar_memory = find_most_similar_memory_by_chat_id(
|
||||
target_title=memory_title,
|
||||
target_chat_id=chat_id,
|
||||
similarity_threshold=0.5 # 相似度阈值
|
||||
similarity_threshold=dynamic_threshold
|
||||
)
|
||||
|
||||
if similar_memory:
|
||||
selected_title, selected_content, similarity = similar_memory
|
||||
logger.info(f"为 '{memory_title}' 找到相似记忆: '{selected_title}' (相似度: {similarity:.3f})")
|
||||
logger.info(f"为 '{memory_title}' 找到相似记忆: '{selected_title}' (相似度: {similarity:.3f} 阈值: {dynamic_threshold:.2f})")
|
||||
return [selected_title], [selected_content]
|
||||
else:
|
||||
logger.info(f"为 '{memory_title}' 未找到相似度 >= 0.7 的记忆")
|
||||
logger.info(f"为 '{memory_title}' 未找到相似度 >= {dynamic_threshold:.2f} 的记忆")
|
||||
return [], []
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from src.memory_system.Memory_chest import global_memory_chest
|
|||
from src.common.logger import get_logger
|
||||
from src.common.database.database_model import MemoryChest as MemoryChestModel
|
||||
from src.config.config import global_config
|
||||
from src.memory_system.memory_utils import get_all_titles
|
||||
|
||||
logger = get_logger("memory")
|
||||
|
||||
|
|
@ -56,19 +55,19 @@ class MemoryManagementTask(AsyncTask):
|
|||
current_count = self._get_memory_count()
|
||||
percentage = current_count / self.max_memory_number
|
||||
|
||||
if percentage < 0.5:
|
||||
if percentage < 0.6:
|
||||
# 小于50%,每600秒执行一次
|
||||
return 3600
|
||||
elif percentage < 0.7:
|
||||
elif percentage < 1:
|
||||
# 大于等于50%,每300秒执行一次
|
||||
return 1800
|
||||
elif percentage < 0.9:
|
||||
# 大于等于70%,每120秒执行一次
|
||||
return 300
|
||||
elif percentage < 1.2:
|
||||
return 30
|
||||
elif percentage < 1.5:
|
||||
# 大于等于100%,每120秒执行一次
|
||||
return 600
|
||||
elif percentage < 1.8:
|
||||
return 120
|
||||
else:
|
||||
return 10
|
||||
return 30
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[记忆管理] 计算执行间隔时出错: {e}")
|
||||
|
|
@ -93,6 +92,22 @@ class MemoryManagementTask(AsyncTask):
|
|||
percentage = current_count / self.max_memory_number
|
||||
logger.info(f"当前记忆数量: {current_count}/{self.max_memory_number} ({percentage:.1%})")
|
||||
|
||||
# 当占比 > 1.6 时,持续删除直到占比 <= 1.6(越老/越新更易被删)
|
||||
if percentage > 2:
|
||||
logger.info("记忆过多,开始遗忘记忆")
|
||||
while True:
|
||||
if percentage <= 1.8:
|
||||
break
|
||||
removed = global_memory_chest.remove_one_memory_by_age_weight()
|
||||
if not removed:
|
||||
logger.warning("没有可删除的记忆,停止连续删除")
|
||||
break
|
||||
# 重新计算占比
|
||||
current_count = self._get_memory_count()
|
||||
percentage = current_count / self.max_memory_number
|
||||
logger.info(f"遗忘进度: 当前 {current_count}/{self.max_memory_number} ({percentage:.1%})")
|
||||
logger.info("遗忘记忆结束")
|
||||
|
||||
# 如果记忆数量为0,跳过执行
|
||||
if current_count < 10:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
from typing import Tuple
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.chat.utils.prompt_builder import Prompt
|
||||
from src.llm_models.payload_content.tool_option import ToolParamType
|
||||
from src.plugin_system import BaseAction, ActionActivationType
|
||||
from src.chat.utils.utils import cut_key_words
|
||||
from src.memory_system.Memory_chest import global_memory_chest
|
||||
from src.plugin_system.base.base_tool import BaseTool
|
||||
from src.plugin_system.apis.message_api import get_messages_by_time_in_chat, build_readable_messages
|
||||
|
|
@ -125,12 +120,10 @@ class GetMemoryTool(BaseTool):
|
|||
chat_answer = results.get("chat")
|
||||
|
||||
# 构建返回内容
|
||||
content_parts = [f"问题:{question}"]
|
||||
content_parts = []
|
||||
|
||||
if memory_answer:
|
||||
content_parts.append(f"对问题'{question}',你回忆的信息是:{memory_answer}")
|
||||
else:
|
||||
content_parts.append(f"对问题'{question}',没有什么印象")
|
||||
|
||||
if chat_answer:
|
||||
content_parts.append(f"对问题'{question}',基于聊天记录的回答:{chat_answer}")
|
||||
|
|
@ -140,7 +133,12 @@ class GetMemoryTool(BaseTool):
|
|||
elif time_range:
|
||||
content_parts.append(f"在 {time_range} 的时间范围内,你没有参与聊天")
|
||||
|
||||
return {"content": "\n".join(content_parts)}
|
||||
if content_parts:
|
||||
retrieval_content = f"问题:{question}" + "\n".join(content_parts)
|
||||
return {"content": retrieval_content}
|
||||
else:
|
||||
return {"content": ""}
|
||||
|
||||
|
||||
async def _get_answer_from_chat_history(self, question: str, time_point: str = None, time_range: str = None) -> str:
|
||||
"""从聊天记录中获取问题的答案"""
|
||||
|
|
@ -245,53 +243,3 @@ class GetMemoryTool(BaseTool):
|
|||
except Exception as e:
|
||||
logger.error(f"从聊天记录获取答案失败: {e}")
|
||||
return ""
|
||||
|
||||
class GetMemoryAction(BaseAction):
|
||||
"""关系动作 - 获取记忆"""
|
||||
|
||||
activation_type = ActionActivationType.LLM_JUDGE
|
||||
parallel_action = True
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "get_memory"
|
||||
action_description = (
|
||||
"在记忆中搜寻某个问题的答案"
|
||||
)
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters = {
|
||||
"question": "需要搜寻或回答的问题",
|
||||
}
|
||||
|
||||
# 动作使用场景
|
||||
action_require = [
|
||||
"在记忆中搜寻某个问题的答案",
|
||||
"有你不了解的概念",
|
||||
"有人提问关于过去的事情",
|
||||
"你需要根据记忆回答某个问题",
|
||||
]
|
||||
|
||||
# 关联类型
|
||||
associated_types = ["text"]
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
"""执行关系动作"""
|
||||
|
||||
question = self.action_data.get("question", "")
|
||||
answer = await global_memory_chest.get_answer_by_question(self.chat_id, question)
|
||||
if not answer:
|
||||
await self.store_action_info(
|
||||
action_build_into_prompt=True,
|
||||
action_prompt_display=f"你回忆了有关问题:{question}的记忆,但是没有找到相关记忆",
|
||||
action_done=True,
|
||||
)
|
||||
|
||||
return False, f"问题:{question},没有找到相关记忆"
|
||||
|
||||
await self.store_action_info(
|
||||
action_build_into_prompt=True,
|
||||
action_prompt_display=f"你回忆了有关问题:{question}的记忆,答案是:{answer}",
|
||||
action_done=True,
|
||||
)
|
||||
|
||||
return True, f"成功获取记忆: {answer}"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from src.plugin_system.base.config_types import ConfigField
|
|||
# 导入依赖的系统组件
|
||||
from src.common.logger import get_logger
|
||||
|
||||
from src.plugins.built_in.memory.build_memory import GetMemoryAction, GetMemoryTool
|
||||
from src.plugins.built_in.memory.build_memory import GetMemoryTool
|
||||
|
||||
logger = get_logger("memory_build")
|
||||
|
||||
|
|
@ -48,7 +48,6 @@ class MemoryBuildPlugin(BasePlugin):
|
|||
|
||||
# --- 根据配置注册组件 ---
|
||||
components = []
|
||||
# components.append((GetMemoryAction.get_action_info(), GetMemoryAction))
|
||||
components.append((GetMemoryTool.get_tool_info(), GetMemoryTool))
|
||||
|
||||
return components
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[inner]
|
||||
version = "6.19.1"
|
||||
version = "6.19.2"
|
||||
|
||||
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
|
||||
#如果你想要修改配置文件,请递增version的值
|
||||
|
|
@ -221,6 +221,8 @@ library_log_levels = { aiohttp = "WARNING"} # 设置特定库的日志级别
|
|||
|
||||
[debug]
|
||||
show_prompt = false # 是否显示prompt
|
||||
show_replyer_prompt = false # 是否显示回复器prompt
|
||||
show_replyer_reasoning = false # 是否显示回复器推理
|
||||
|
||||
[maim_message]
|
||||
auth_token = [] # 认证令牌,用于API验证,为空则不启用验证
|
||||
|
|
|
|||
Loading…
Reference in New Issue