收紧 ai_auto_response 群内问句主动回复策略

- 为疑问句增加 question_detected 形态标记,区分问句形态与真正指向 bot 的提问
- 仅在 @bot、点 bot 名字或明确定向时才把疑问句升级为问答触发
- 阻断普通群问句通过 topic 主动接话路径进入模型,避免 bot 抢答群友互问
- 将 social call 收紧为 名字/别名 + 召唤词 的组合,减少 帮忙看/看看 等泛词误触发
- 在配置中增加 bot_name_keywords 与 social_call_verb_patterns,便于后续按人格扩展
This commit is contained in:
liuwei
2026-04-24 14:26:08 +08:00
parent 571008a04b
commit 058a7aec80
3 changed files with 61 additions and 5 deletions

View File

@@ -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

View File

@@ -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"):
# 策略收敛:
# 问答类回复只在“明确指向机器人”时触发,防止把群友之间的疑问句当作对机器人提问。

View File

@@ -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)