diff --git a/plugins/ai_auto_response/context/context_builder.py b/plugins/ai_auto_response/context/context_builder.py index 230f2c4..9316d31 100644 --- a/plugins/ai_auto_response/context/context_builder.py +++ b/plugins/ai_auto_response/context/context_builder.py @@ -53,6 +53,8 @@ class ContextBuilder: "speaker_name_clean": self._clean_display_name(sender_name), "is_at": bool(trigger.get("is_at", False)), "is_directed": bool(trigger.get("is_directed", False)), + # 这类标记会被后面的 prompt 策略层消费,用来决定要不要放开群级记忆。 + "is_group_memory_query": bool(trigger.get("is_group_memory_query", False)), "recent_message_items": self._build_recent_message_items(selected_messages), "recent_messages": recent_lines, "recent_summary": "", diff --git a/plugins/ai_auto_response/core/response_planner.py b/plugins/ai_auto_response/core/response_planner.py index cfe44dd..46ca1b4 100644 --- a/plugins/ai_auto_response/core/response_planner.py +++ b/plugins/ai_auto_response/core/response_planner.py @@ -9,6 +9,12 @@ class ResponsePlanner: # 而是一句短短的回怼或挡回去,所以这里强制走 social_short。 if trigger.get("is_directed_abuse"): return "social_short" + # “群里最近都聊什么”这类问题,本质是在问群记忆摘要: + # 1. 如果继续走 qa_fast,就只会优先依赖最近现场消息; + # 2. 这里直接抬到 qa_with_context,后面才会打开群事实/向量记忆等补充层; + # 3. 不依赖 flow_state,是因为这类问题和当前场子热不热关系不大。 + if trigger.get("is_question") and trigger.get("is_group_memory_query"): + return "qa_with_context" if trigger.get("is_question"): return "qa_with_context" if flow_state in {"engaged", "deep_engaged"} else "qa_fast" if trigger.get("is_followup"): diff --git a/plugins/ai_auto_response/core/triggers.py b/plugins/ai_auto_response/core/triggers.py index 896bff0..6845a64 100644 --- a/plugins/ai_auto_response/core/triggers.py +++ b/plugins/ai_auto_response/core/triggers.py @@ -11,6 +11,16 @@ QUESTION_PATTERNS = [ r"\?$", r"?$", r"怎么", r"如何", r"咋弄", r"为啥", r"为什么", r"有人知道", r"谁知道", r"能不能", r"可以吗", r"报错", r"怎么解决", ] +GROUP_MEMORY_QUERY_PATTERNS = [ + # 这组模式专门识别“回顾群里最近在聊什么”的提问: + # 1. 这类问题本质上不是要 bot 现场接一句,而是要它调动群级记忆来做概括; + # 2. 之前它会被当成普通问句,落进 qa_fast,结果只看最近 30 条现场消息; + # 3. 单独打标后,后续策略层才能有依据切到 qa_with_context。 + r"(最近|这两天|这几天|近期).*(聊|讨论|在说|在聊|话题|主题|重点|近况)", + r"(都|最近).*(聊什么|说什么|讨论什么|在聊啥|在说啥|啥话题)", + r"(群里|大家|他们).*(最近|这两天|这几天).*(聊|讨论|话题|重点)", + r"(总结|概括).*(最近|这两天|这几天|近期).*(聊天|讨论|话题|内容)", +] SOCIAL_PATTERNS = [r"小牛", r"在吗", r"出来", r"帮忙看", r"看看"] LIGHT_SOCIAL_PATTERNS = [ r"哈哈", r"笑死", r"绷不住", r"离谱", r"逆天", r"牛逼", r"卧槽", r"我去", @@ -34,6 +44,9 @@ class TriggerResult: is_directed_abuse: bool = False is_followup: bool = False is_social_call: bool = False + # 这类问题是在问“群里最近都在聊啥”, + # 需要优先动用群事实/长期摘要/向量召回,而不是只看短期现场窗口。 + is_group_memory_query: bool = False is_returning_member: bool = False should_respond: bool = False topic: str = "" @@ -94,6 +107,11 @@ class TriggerRouter: result.is_directed = True result.reasons.append("question_named_bot") result.reasons.append("question") + if self._is_group_memory_query(content): + # “最近都聊什么”这类提问单独记下来,供后面的回复模式和记忆策略升级。 + # 这里不直接改 should_respond,是为了继续遵守“必须是定向提问才回答”的总原则。 + result.is_group_memory_query = True + result.reasons.append("group_memory_query") if is_directed_abuse( content, directed=bool(message.get("is_at")) or named_to_bot or conversation_hints.get("quote_targets_bot", False), @@ -186,6 +204,12 @@ class TriggerRouter: return keyword return "" + def _is_group_memory_query(self, content: str) -> bool: + text = str(content or "").strip() + if not text: + return False + return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in GROUP_MEMORY_QUERY_PATTERNS) + def _has_bot_name(self, content_lower: str) -> bool: text = str(content_lower or "").strip().lower() if not text: diff --git a/plugins/ai_auto_response/main.py b/plugins/ai_auto_response/main.py index f710c1c..73142e5 100644 --- a/plugins/ai_auto_response/main.py +++ b/plugins/ai_auto_response/main.py @@ -1196,6 +1196,7 @@ class AIAutoResponsePlugin(MessagePluginInterface): trigger_type = str(context.get("trigger_type", "none") or "none") is_at = bool(context.get("is_at", False)) is_directed = bool(context.get("is_directed", False)) + is_group_memory_query = bool(context.get("is_group_memory_query", False)) is_followup = bool(memory_hints.get("is_followup", False)) returning_state = str(memory_hints.get("returning_member_state", "") or "").strip() strong_directed = is_at or is_directed or trigger_type in {"at_trigger", "quote_followup_trigger"} @@ -1231,9 +1232,15 @@ class AIAutoResponsePlugin(MessagePluginInterface): # 1. 用户希望回答能带上群里的长期背景和互动关系; # 2. 关系记忆仍会经过相关性过滤,所以放宽入口不会直接把无关关系灌进去; # 3. 这样技术问答里也更容易利用“谁经常和谁接话、谁常问哪类问题”的弱背景。 - allow_social_memory = is_question_like - allow_group_facts = reply_mode == "qa_with_context" - allow_vector_memory = reply_mode == "qa_with_context" or returning_state == "long_absent_member" + allow_social_memory = is_question_like or is_group_memory_query + # “最近都聊什么”这类问题,本身就是在问群级记忆, + # 所以哪怕当前只是普通问答入口,也要把群事实和向量层放开。 + allow_group_facts = reply_mode == "qa_with_context" or is_group_memory_query + allow_vector_memory = ( + reply_mode == "qa_with_context" + or returning_state == "long_absent_member" + or is_group_memory_query + ) return { "target_reply_chars": target_reply_chars_map.get(reply_mode, 10), @@ -1292,6 +1299,13 @@ class AIAutoResponsePlugin(MessagePluginInterface): @staticmethod def _is_text_relevant(content: str, memory_text: str) -> bool: + # 对“最近都聊什么”这类群聊回顾型问题做一个显式兜底: + # 1. 这类问题天然缺少技术关键词,严格按词重叠时经常会得到 0 命中; + # 2. 但它问的恰恰就是“群级记忆摘要”,不应该再被相关性门槛二次挡掉; + # 3. 因此只要当前问题像群聊话题回顾,而记忆文本也明显是群摘要/群事实,就直接放行。 + if AIAutoResponsePlugin._looks_like_group_memory_query(content): + if AIAutoResponsePlugin._looks_like_group_memory_text(memory_text): + return True content_tokens = AIAutoResponsePlugin._extract_relevance_tokens(content) memory_tokens = AIAutoResponsePlugin._extract_relevance_tokens(memory_text) if not content_tokens or not memory_tokens: @@ -1299,6 +1313,30 @@ class AIAutoResponsePlugin(MessagePluginInterface): overlap = content_tokens & memory_tokens return len(overlap) >= 1 + @staticmethod + def _looks_like_group_memory_query(content: str) -> bool: + text = str(content or "").strip() + if not text: + return False + patterns = [ + r"(最近|这两天|这几天|近期).*(聊|讨论|在说|在聊|话题|主题|重点|近况)", + r"(都|最近).*(聊什么|说什么|讨论什么|在聊啥|在说啥|啥话题)", + r"(群里|大家|他们).*(最近|这两天|这几天).*(聊|讨论|话题|重点)", + r"(总结|概括).*(最近|这两天|这几天|近期).*(聊天|讨论|话题|内容)", + ] + return any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in patterns) + + @staticmethod + def _looks_like_group_memory_text(memory_text: str) -> bool: + text = str(memory_text or "").strip() + if not text: + return False + markers = [ + "群长期背景", "长期摘要", "稳定主题", "近期重点", "未决问题", + "群事实", "最近沉淀", "最近长期反复出现", "下面是群", "相关话题", + ] + return any(marker in text for marker in markers) + @staticmethod def _extract_relevance_tokens(text: str) -> set[str]: raw = str(text or "").lower()