From 058a7aec803368ae90767a8553808b2b15fde745 Mon Sep 17 00:00:00 2001 From: liuwei Date: Fri, 24 Apr 2026 14:26:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=20ai=5Fauto=5Fresponse=20?= =?UTF-8?q?=E7=BE=A4=E5=86=85=E9=97=AE=E5=8F=A5=E4=B8=BB=E5=8A=A8=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为疑问句增加 question_detected 形态标记,区分问句形态与真正指向 bot 的提问 - 仅在 @bot、点 bot 名字或明确定向时才把疑问句升级为问答触发 - 阻断普通群问句通过 topic 主动接话路径进入模型,避免 bot 抢答群友互问 - 将 social call 收紧为 名字/别名 + 召唤词 的组合,减少 帮忙看/看看 等泛词误触发 - 在配置中增加 bot_name_keywords 与 social_call_verb_patterns,便于后续按人格扩展 --- plugins/ai_auto_response/config.toml | 5 ++ .../ai_auto_response/core/response_planner.py | 7 +++ plugins/ai_auto_response/core/triggers.py | 54 +++++++++++++++++-- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/plugins/ai_auto_response/config.toml b/plugins/ai_auto_response/config.toml index 7a7fb06..5ad3106 100644 --- a/plugins/ai_auto_response/config.toml +++ b/plugins/ai_auto_response/config.toml @@ -88,6 +88,11 @@ recent_followup_window_minutes = 5 at_bot = 1.0 explicit_question = 0.95 question_requires_at = true +# 这里允许“点 bot 名字”作为弱定向信号,但不会把普通群问句当成 bot 提问。 +# 只有出现这些名字/别名时,问句或社交召唤才会更像在对 bot 说话。 +bot_name_keywords = ["小牛", "xiaoniu", "于谦", "谦哥", "林志玲", "志玲"] +# 这些词本身太泛,必须和 bot 名字一起出现,才算社交召唤。 +social_call_verb_patterns = ["在吗", "出来", "帮忙看", "看看", "说句话", "回一句", "救一下"] followup = 0.90 social_call = 0.65 light_social = 0.45 diff --git a/plugins/ai_auto_response/core/response_planner.py b/plugins/ai_auto_response/core/response_planner.py index feb2e39..6186b5f 100644 --- a/plugins/ai_auto_response/core/response_planner.py +++ b/plugins/ai_auto_response/core/response_planner.py @@ -28,12 +28,19 @@ class ResponsePlanner: conversation_hints = conversation_hints or {} trigger_type = str(trigger.get("trigger_type", "") or "") directed = bool(trigger.get("is_directed")) + question_detected = bool(trigger.get("question_detected")) if trigger.get("is_at") or trigger_type == "at_trigger": return True if trigger_type == "quote_followup_trigger" and directed: return True if trigger.get("is_question") and conversation_hints.get("has_recent_human_solver") and flow_state == "idle": return False + # 关键收敛: + # 1. 群里的普通问句,哪怕命中了 topic,也不应该因为“当前气氛热”就被 bot 主动接住; + # 2. 只要它有明显问句形态,但又没有明确指向 bot,就整体禁止进入模型, + # 从根上阻断“别人互相问一句,bot 突然抢答”的尴尬感。 + if question_detected and not directed and not trigger.get("is_followup"): + return False if trigger.get("is_question"): # 策略收敛: # 问答类回复只在“明确指向机器人”时触发,防止把群友之间的疑问句当作对机器人提问。 diff --git a/plugins/ai_auto_response/core/triggers.py b/plugins/ai_auto_response/core/triggers.py index 3af0ce0..f825ac8 100644 --- a/plugins/ai_auto_response/core/triggers.py +++ b/plugins/ai_auto_response/core/triggers.py @@ -21,6 +21,11 @@ class TriggerResult: trigger_type: str = "none" priority: float = 0.0 is_question: bool = False + # 这里单独保留“像疑问句”的形态标记: + # 1. 群里很多问句并不是问 bot; + # 2. 但我们仍然希望后续决策层知道“这是一条问句形态消息”, + # 以便阻止 topic/主动接话路径误把它当成 bot 可抢答的机会。 + question_detected: bool = False is_directed: bool = False is_followup: bool = False is_social_call: bool = False @@ -34,6 +39,20 @@ class TriggerRouter: def __init__(self, config: Dict): self.config = config or {} self.topic_keywords = [str(item).lower() for item in self.config.get("focus", [])] + # 这里把“bot 名称/别名”做成可配置项,便于不同人格共享同一套触发逻辑。 + # 这样“普通群友之间的问句”和“明确点 bot 名字的问句”可以分开处理。 + self.bot_name_keywords = [ + str(item).strip().lower() + for item in (self.config.get("bot_name_keywords", []) or []) + if str(item).strip() + ] or ["小牛", "xiaoniu", "于谦", "谦哥", "林志玲", "志玲"] + # 这些是明显带有“朝某个对象发话”味道的词。 + # 只有名字 + 这些动词/语气组合在一起,才更像是在叫 bot 接话。 + self.social_call_verb_patterns = [ + str(item).strip() + for item in (self.config.get("social_call_verb_patterns", []) or []) + if str(item).strip() + ] or [r"在吗", r"出来", r"帮忙看", r"看看", r"说句话", r"回一句", r"救一下"] # 业务约束说明: # 1) 群内大量“疑问句”其实是群友之间的随口沟通,不是向机器人求助; # 2) 如果把所有疑问句都当成问答触发,会显著提升误触发率,导致“高频答非所问”; @@ -44,6 +63,7 @@ class TriggerRouter: conversation_hints = conversation_hints or {} content = str(message.get("content", "")).strip() content_lower = content.lower() + named_to_bot = self._has_bot_name(content_lower) result = TriggerResult() if message.get("is_at"): result.trigger_type = "at_trigger" @@ -52,16 +72,22 @@ class TriggerRouter: result.is_directed = True result.reasons.append("is_at") if self._is_question(content): + result.question_detected = True + result.reasons.append("question_shape") # 这里把“是否是疑问句”和“是否应该按咨询触发”拆开处理: # - 疑问句形态用于日志观察; - # - 触发升级只在满足定向条件时才生效,避免群聊里普通问句频繁触发机器人。 - if self.question_requires_at and not message.get("is_at"): - result.reasons.append("question_detected_but_not_at") + # - 触发升级只在满足“明确指向 bot”条件时才生效,避免群聊里普通问句频繁触发机器人。 + directed_question = bool(message.get("is_at")) or named_to_bot or not self.question_requires_at + if not directed_question: + result.reasons.append("question_detected_but_not_directed") else: if result.priority < float(self.config.get("explicit_question", 0.95)): result.trigger_type = "question_trigger" result.priority = float(self.config.get("explicit_question", 0.95)) result.is_question = True + if named_to_bot and not result.is_directed: + result.is_directed = True + result.reasons.append("question_named_bot") result.reasons.append("question") if memory_hints.get("is_followup"): if result.priority < float(self.config.get("followup", 0.90)): @@ -80,7 +106,7 @@ class TriggerRouter: if result.priority < float(self.config.get("casual_topic", 0.35)): result.trigger_type = result.trigger_type if result.trigger_type != "none" else "topic_trigger" result.priority = max(result.priority, float(self.config.get("casual_topic", 0.35))) - if self._is_social_call(content_lower): + if self._is_social_call(content): if result.priority < float(self.config.get("social_call", 0.65)): result.trigger_type = result.trigger_type if result.trigger_type != "none" else "social_trigger" result.priority = max(result.priority, float(self.config.get("social_call", 0.65))) @@ -119,7 +145,19 @@ class TriggerRouter: return any(re.search(pattern, content, flags=re.IGNORECASE) for pattern in QUESTION_PATTERNS) def _is_social_call(self, content: str) -> bool: - return any(re.search(pattern, content, flags=re.IGNORECASE) for pattern in SOCIAL_PATTERNS) + text = str(content or "").strip() + lower = text.lower() + # 社交召唤要尽量保守: + # 1. 必须先出现 bot 名称/别名; + # 2. 再配合“在吗/帮忙看/看看”这类召唤词,或明显疑问语气; + # 这样像“帮忙看一下这个报错”这种群友互问,就不会误触发 bot 接话。 + if not self._has_bot_name(lower): + return False + if any(re.search(pattern, text, flags=re.IGNORECASE) for pattern in self.social_call_verb_patterns): + return True + if self._is_question(text): + return True + return text.endswith(("?", "?")) def _is_light_social_moment(self, content: str) -> bool: if len(content) > 24: @@ -131,3 +169,9 @@ class TriggerRouter: if keyword and keyword in content_lower: return keyword return "" + + def _has_bot_name(self, content_lower: str) -> bool: + text = str(content_lower or "").strip().lower() + if not text: + return False + return any(keyword and keyword in text for keyword in self.bot_name_keywords)