From e0aa745ba4e577b262bf9c1f8adbae2e549e198b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 30 Oct 2025 11:27:01 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat=EF=BC=9A=E8=AE=B0=E5=BF=86=E9=81=97?= =?UTF-8?q?=E5=BF=98=E5=92=8C=E5=8A=A8=E6=80=81=E5=90=88=E5=B9=B6=E9=98=88?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/replyer/group_generator.py | 2 +- src/chat/replyer/private_generator.py | 2 +- src/memory_system/Memory_chest.py | 83 ++++++++++++++++++++- src/memory_system/memory_management_task.py | 25 +++++-- 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index b8c68954..457ee9eb 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -772,7 +772,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") diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 4c60be9d..8d5c7d59 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -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") diff --git a/src/memory_system/Memory_chest.py b/src/memory_system/Memory_chest.py index bd1d0a1c..d8efec8f 100644 --- a/src/memory_system/Memory_chest.py +++ b/src/memory_system/Memory_chest.py @@ -41,6 +41,78 @@ 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.2: + return 0.40 + else: + return 0.35 + except Exception: + # 发生异常时使用保守阈值 + return 0.70 + async def build_running_content(self, chat_id: str = None) -> str: """ 构建记忆仓库的运行内容 @@ -446,19 +518,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: diff --git a/src/memory_system/memory_management_task.py b/src/memory_system/memory_management_task.py index d750c1e5..a9212862 100644 --- a/src/memory_system/memory_management_task.py +++ b/src/memory_system/memory_management_task.py @@ -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,14 +55,14 @@ 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 < 0.8: # 大于等于50%,每300秒执行一次 return 1800 - elif percentage < 0.9: - # 大于等于70%,每120秒执行一次 + elif percentage < 1.0: + # 大于等于100%,每120秒执行一次 return 300 elif percentage < 1.2: return 30 @@ -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 > 1.6: + logger.info("记忆过多,开始遗忘记忆") + while True: + if percentage <= 1.6: + 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 From c6dadc2872c2faa0fcb311046fa504b89eef9efa Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 30 Oct 2025 11:30:48 +0800 Subject: [PATCH 02/10] =?UTF-8?q?fix=EF=BC=9A=E6=88=AA=E6=96=ADLog?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/openai_client.py | 58 +++++++++++++------- src/memory_system/Memory_chest.py | 6 +- src/memory_system/memory_management_task.py | 16 +++--- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/llm_models/model_client/openai_client.py b/src/llm_models/model_client/openai_client.py index dd92b9e8..137ca78e 100644 --- a/src/llm_models/model_client/openai_client.py +++ b/src/llm_models/model_client/openai_client.py @@ -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() @@ -402,14 +421,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 配置!" - ) - else: - logger.warning("⚠ OpenAI 响应因达到 max_tokens 限制被截断,\n 请修改模型 max_tokens 配置!") + print(resp) + _model_name = resp.model + # 统一日志格式 + logger.info( + "模型%s因为超过最大max_token限制,可能仅输出部分内容,可视情况调整" + % (_model_name or "") + ) return api_response, _usage_record except Exception as e: logger.debug(f"检查 MAX_TOKENS 截断时异常: {e}") @@ -522,7 +540,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}") diff --git a/src/memory_system/Memory_chest.py b/src/memory_system/Memory_chest.py index d8efec8f..9404cf21 100644 --- a/src/memory_system/Memory_chest.py +++ b/src/memory_system/Memory_chest.py @@ -105,10 +105,12 @@ class MemoryChest: return 0.60 elif percentage < 1.0: return 0.50 - elif percentage < 1.2: + elif percentage < 1.5: return 0.40 + elif percentage < 2: + return 0.30 else: - return 0.35 + return 0.25 except Exception: # 发生异常时使用保守阈值 return 0.70 diff --git a/src/memory_system/memory_management_task.py b/src/memory_system/memory_management_task.py index a9212862..90b6e2ca 100644 --- a/src/memory_system/memory_management_task.py +++ b/src/memory_system/memory_management_task.py @@ -58,16 +58,16 @@ class MemoryManagementTask(AsyncTask): if percentage < 0.6: # 小于50%,每600秒执行一次 return 3600 - elif percentage < 0.8: + elif percentage < 1: # 大于等于50%,每300秒执行一次 return 1800 - elif percentage < 1.0: + elif percentage < 1.5: # 大于等于100%,每120秒执行一次 - return 300 - elif percentage < 1.2: - return 30 + return 600 + elif percentage < 1.8: + return 120 else: - return 10 + return 30 except Exception as e: logger.error(f"[记忆管理] 计算执行间隔时出错: {e}") @@ -93,10 +93,10 @@ class MemoryManagementTask(AsyncTask): logger.info(f"当前记忆数量: {current_count}/{self.max_memory_number} ({percentage:.1%})") # 当占比 > 1.6 时,持续删除直到占比 <= 1.6(越老/越新更易被删) - if percentage > 1.6: + if percentage > 2: logger.info("记忆过多,开始遗忘记忆") while True: - if percentage <= 1.6: + if percentage <= 1.8: break removed = global_memory_chest.remove_one_memory_by_age_weight() if not removed: From cb5962f7394694e3478fcfd1066655a77f54aaac Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Thu, 30 Oct 2025 13:22:29 +0800 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20=E6=92=A4=E5=9B=9E=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E6=97=A5=E5=BF=97=E4=BC=98=E5=8C=96=E5=B9=B6=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E4=BA=8C=E6=AC=A1=E7=A9=BA=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/message_receive/bot.py | 50 ++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index 43d2754a..3b80d525 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -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() From 0d5e393bcb611a74c8b8941f870f2dabdc6ad54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 31 Oct 2025 14:40:27 +0800 Subject: [PATCH 04/10] test docker build --- src/common/database/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/database/database.py b/src/common/database/database.py index 0fa26866..0ebc7cf5 100644 --- a/src/common/database/database.py +++ b/src/common/database/database.py @@ -8,7 +8,7 @@ install(extra_lines=3) # 定义数据库文件路径 ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) _DB_DIR = os.path.join(ROOT_PATH, "data") -_DB_FILE = os.path.join(_DB_DIR, "MaiBot.db") +_DB_FILE = os.path.join(_DB_DIR, "MaiBot.db" ) # 确保数据库目录存在 os.makedirs(_DB_DIR, exist_ok=True) From 2aea5046b003d2f388608ff54657dcef27c187f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 31 Oct 2025 14:50:19 +0800 Subject: [PATCH 05/10] test x2 --- src/common/database/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/database/database.py b/src/common/database/database.py index 0ebc7cf5..0fa26866 100644 --- a/src/common/database/database.py +++ b/src/common/database/database.py @@ -8,7 +8,7 @@ install(extra_lines=3) # 定义数据库文件路径 ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) _DB_DIR = os.path.join(ROOT_PATH, "data") -_DB_FILE = os.path.join(_DB_DIR, "MaiBot.db" ) +_DB_FILE = os.path.join(_DB_DIR, "MaiBot.db") # 确保数据库目录存在 os.makedirs(_DB_DIR, exist_ok=True) From 24884ee3d171dae333b7860d34588cd82014d4ab Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 31 Oct 2025 14:50:20 +0800 Subject: [PATCH 06/10] Update config.py --- src/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.py b/src/config/config.py index cf53228c..e34b02b6 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -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): From 50e0bc651387951a83ed842c736edcca1ab1c460 Mon Sep 17 00:00:00 2001 From: xiaoxi68 <3520824673@qq.com> Date: Sat, 1 Nov 2025 16:40:36 +0800 Subject: [PATCH 07/10] =?UTF-8?q?fix(openai):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=A9=BA=E5=9B=9E=E5=A4=8D=20TypeError=EF=BC=88choices=3DNone?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E5=B9=B6=E5=9C=A8=E6=8A=9B=E9=94=99=E5=89=8D?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/model_client/openai_client.py | 29 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/llm_models/model_client/openai_client.py b/src/llm_models/model_client/openai_client.py index 137ca78e..36af7775 100644 --- a/src/llm_models/model_client/openai_client.py +++ b/src/llm_models/model_client/openai_client.py @@ -370,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 = "" + 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 # 有有效的推理字段 From b63057edeca6e9e9090634aec798df273bc31f6a Mon Sep 17 00:00:00 2001 From: exynos <110159911+exynos967@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:33:58 +0800 Subject: [PATCH 08/10] =?UTF-8?q?fix(model=5Futils):=20HTTP=20400=20?= =?UTF-8?q?=E4=B8=8D=E7=BB=88=E6=AD=A2=E5=85=A8=E5=B1=80=E5=B0=9D=E8=AF=95?= =?UTF-8?q?=EF=BC=8C=E7=BB=A7=E7=BB=AD=E5=88=87=E6=8D=A2=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llm_models/utils_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 4d7865d9..0474b9d7 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -369,8 +369,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: From 3e5058eb0fe201a85e8602b76139e1ff11297a10 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Mon, 3 Nov 2025 22:41:21 +0800 Subject: [PATCH 09/10] =?UTF-8?q?fix=EF=BC=9A=E4=BC=98=E5=8C=96=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E6=8F=90=E5=8F=96=EF=BC=8C=E6=8F=90=E4=BE=9B=E7=BB=86?= =?UTF-8?q?=E8=8A=82prompt=20debug=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/emoji_system/emoji_manager.py | 21 ++++--- src/chat/replyer/group_generator.py | 5 +- src/chat/replyer/private_generator.py | 4 +- src/config/official_configs.py | 6 ++ src/llm_models/utils_model.py | 21 ++++++- src/plugins/built_in/memory/build_memory.py | 68 +++------------------ src/plugins/built_in/memory/plugin.py | 3 +- template/bot_config_template.toml | 4 +- 8 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/chat/emoji_system/emoji_manager.py b/src/chat/emoji_system/emoji_manager.py index b143f0f7..512e7e55 100644 --- a/src/chat/emoji_system/emoji_manager.py +++ b/src/chat/emoji_system/emoji_manager.py @@ -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 ) # 处理情感列表 diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index 457ee9eb..cc6dfee4 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -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 @@ -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") diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 8d5c7d59..2bd48de4 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -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): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index d509ad9c..8c29d066 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -641,6 +641,12 @@ class DebugConfig(ConfigBase): show_prompt: bool = False """是否显示prompt""" + + show_replyer_prompt: bool = True + """是否显示回复器prompt""" + + show_replyer_reasoning: bool = True + """是否显示回复器推理""" @dataclass diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 4d7865d9..fc86566a 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -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: diff --git a/src/plugins/built_in/memory/build_memory.py b/src/plugins/built_in/memory/build_memory.py index 3c1b4dc5..9422e22f 100644 --- a/src/plugins/built_in/memory/build_memory.py +++ b/src/plugins/built_in/memory/build_memory.py @@ -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}") @@ -139,8 +132,13 @@ class GetMemoryTool(BaseTool): content_parts.append(f"在 {time_point} 的时间点,你没有参与聊天") 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}" diff --git a/src/plugins/built_in/memory/plugin.py b/src/plugins/built_in/memory/plugin.py index c3045fdc..5d2ba419 100644 --- a/src/plugins/built_in/memory/plugin.py +++ b/src/plugins/built_in/memory/plugin.py @@ -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 diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 9d7941b4..d2621a35 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -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验证,为空则不启用验证 From 3a6dfbbe060f675eee518184a5a39363e7f9fdc3 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 4 Nov 2025 20:55:42 +0800 Subject: [PATCH 10/10] Update changelog.md --- changelogs/changelog.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 7e413b76..18a2d0f2 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -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 ### 🌟 主要功能更改 - 重构记忆系统,新的记忆系统更可靠,双通道查询,可以查询文本记忆和过去聊天记录 - 主动发言功能,麦麦会自主提出问题(可精细调控频率)