improve xiaoniu group awareness and solver suppression
This commit is contained in:
@@ -436,6 +436,16 @@
|
|||||||
|
|
||||||
当前 `bot_ai.py` 里的“体力值 + 参与度”可以保留,但应降级为“主动聊天限流器”,而不是总入口。
|
当前 `bot_ai.py` 里的“体力值 + 参与度”可以保留,但应降级为“主动聊天限流器”,而不是总入口。
|
||||||
|
|
||||||
|
这里建议再补两层非常关键的拟人化约束:
|
||||||
|
|
||||||
|
- `group_acceptance`
|
||||||
|
观察小牛发言后,群里后续是否自然接住。如果经常发完没人理,就降低主动度;如果经常有人顺着聊,才允许在非强触发场景更积极一点。
|
||||||
|
|
||||||
|
- `human_solver_suppression`
|
||||||
|
如果最近几条里已经明显有群友在认真解题,小牛除非被 `@`,否则优先收着,避免像“抢答机器人”。
|
||||||
|
|
||||||
|
这两层加上后,小牛会更像一个会看场合的老成员,而不是看见关键词就扑上去。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 向量记忆设计
|
## 向量记忆设计
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class FlowManager:
|
|||||||
if since_reply <= 180 and event.get("message_after_bot"):
|
if since_reply <= 180 and event.get("message_after_bot"):
|
||||||
state.score += float(self.config.get("response_accepted_boost", 15))
|
state.score += float(self.config.get("response_accepted_boost", 15))
|
||||||
state.accepted_reply_count += 1
|
state.accepted_reply_count += 1
|
||||||
|
state.bot_reply_streak = 0
|
||||||
state.last_human_message_at = now
|
state.last_human_message_at = now
|
||||||
state.last_topic = event.get("topic") or state.last_topic
|
state.last_topic = event.get("topic") or state.last_topic
|
||||||
state.state = self._score_to_state(state.score)
|
state.state = self._score_to_state(state.score)
|
||||||
@@ -66,6 +67,12 @@ class FlowManager:
|
|||||||
|
|
||||||
def note_bot_reply(self, room_id: str) -> FlowState:
|
def note_bot_reply(self, room_id: str) -> FlowState:
|
||||||
state = self.decay(room_id)
|
state = self.decay(room_id)
|
||||||
|
now = datetime.now()
|
||||||
|
if state.last_bot_reply_at and (not state.last_human_message_at or state.last_human_message_at < state.last_bot_reply_at):
|
||||||
|
since_last_bot_reply = (now - state.last_bot_reply_at).total_seconds()
|
||||||
|
if since_last_bot_reply <= 180:
|
||||||
|
state.ignored_reply_count += 1
|
||||||
|
state.score = max(0.0, state.score - float(self.config.get("ignored_reply_penalty", 20)))
|
||||||
state.last_bot_reply_at = datetime.now()
|
state.last_bot_reply_at = datetime.now()
|
||||||
state.bot_reply_streak += 1
|
state.bot_reply_streak += 1
|
||||||
max_streak = int(self.config.get("max_bot_reply_streak", 3))
|
max_streak = int(self.config.get("max_bot_reply_streak", 3))
|
||||||
@@ -74,6 +81,20 @@ class FlowManager:
|
|||||||
state.state = self._score_to_state(state.score)
|
state.state = self._score_to_state(state.score)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
def get_acceptance_state(self, room_id: str) -> str:
|
||||||
|
state = self.get_state(room_id)
|
||||||
|
accepted = int(state.accepted_reply_count)
|
||||||
|
ignored = int(state.ignored_reply_count)
|
||||||
|
total = accepted + ignored
|
||||||
|
if total < 3:
|
||||||
|
return "neutral"
|
||||||
|
ratio = accepted / max(total, 1)
|
||||||
|
if ratio >= 0.7 and accepted >= 3:
|
||||||
|
return "warm"
|
||||||
|
if ratio <= 0.35 and ignored >= 2:
|
||||||
|
return "cold"
|
||||||
|
return "neutral"
|
||||||
|
|
||||||
def _score_to_state(self, score: float) -> str:
|
def _score_to_state(self, score: float) -> str:
|
||||||
idle_threshold = float(self.config.get("idle_threshold", 20))
|
idle_threshold = float(self.config.get("idle_threshold", 20))
|
||||||
warming_threshold = float(self.config.get("warming_threshold", 40))
|
warming_threshold = float(self.config.get("warming_threshold", 40))
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
|||||||
"timestamp": message.get("timestamp"),
|
"timestamp": message.get("timestamp"),
|
||||||
}
|
}
|
||||||
self._append_group_message(room_id, normalized_message)
|
self._append_group_message(room_id, normalized_message)
|
||||||
|
recent_messages = self.group_messages.get(room_id) or self.memory_store.get_recent_messages(room_id)
|
||||||
|
conversation_hints = self._build_conversation_hints(recent_messages, sender, content)
|
||||||
|
|
||||||
memory_hints = self.memory_store.build_memory_hints(room_id, sender)
|
memory_hints = self.memory_store.build_memory_hints(room_id, sender)
|
||||||
self._sync_member_memory(room_id, sender, sender_name, memory_hints.get("member_context", {}))
|
self._sync_member_memory(room_id, sender, sender_name, memory_hints.get("member_context", {}))
|
||||||
@@ -198,8 +200,15 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
allow_proactive = bool(self.mode_config.get("allow_proactive_reply", True))
|
allow_proactive = bool(self.mode_config.get("allow_proactive_reply", True))
|
||||||
|
acceptance_state = self.flow_manager.get_acceptance_state(room_id)
|
||||||
reply_mode = self.response_planner.choose_reply_mode(trigger.__dict__, flow_state.state)
|
reply_mode = self.response_planner.choose_reply_mode(trigger.__dict__, flow_state.state)
|
||||||
should_reply = self.response_planner.should_reply(trigger.__dict__, flow_state.state, allow_proactive)
|
should_reply = self.response_planner.should_reply(
|
||||||
|
trigger.__dict__,
|
||||||
|
flow_state.state,
|
||||||
|
allow_proactive,
|
||||||
|
acceptance_state,
|
||||||
|
conversation_hints,
|
||||||
|
)
|
||||||
if not should_reply:
|
if not should_reply:
|
||||||
self._log_event(
|
self._log_event(
|
||||||
"skip",
|
"skip",
|
||||||
@@ -209,6 +218,8 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
|||||||
trigger_type=trigger.trigger_type,
|
trigger_type=trigger.trigger_type,
|
||||||
reply_mode=reply_mode,
|
reply_mode=reply_mode,
|
||||||
flow_state=flow_state.state,
|
flow_state=flow_state.state,
|
||||||
|
acceptance_state=acceptance_state,
|
||||||
|
solver=self._yn(conversation_hints.get("has_recent_human_solver")),
|
||||||
)
|
)
|
||||||
return False, "skip"
|
return False, "skip"
|
||||||
if not self._pass_cooldown(room_id, trigger.__dict__):
|
if not self._pass_cooldown(room_id, trigger.__dict__):
|
||||||
@@ -222,7 +233,6 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
|||||||
)
|
)
|
||||||
return False, "cooldown"
|
return False, "cooldown"
|
||||||
|
|
||||||
recent_messages = self.group_messages.get(room_id) or self.memory_store.get_recent_messages(room_id)
|
|
||||||
vector_memories = []
|
vector_memories = []
|
||||||
if self.vector_memory.should_search(reply_mode, trigger.trigger_type, memory_hints.get("returning_member_state", "")):
|
if self.vector_memory.should_search(reply_mode, trigger.trigger_type, memory_hints.get("returning_member_state", "")):
|
||||||
vector_memories = self.vector_memory.search(content, room_id, sender)
|
vector_memories = self.vector_memory.search(content, room_id, sender)
|
||||||
@@ -232,6 +242,7 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
|||||||
sender=sender,
|
sender=sender,
|
||||||
group_mode=group_profile.get("mode", ""),
|
group_mode=group_profile.get("mode", ""),
|
||||||
knowledge_domain=group_profile.get("knowledge_domain", ""),
|
knowledge_domain=group_profile.get("knowledge_domain", ""),
|
||||||
|
acceptance_state=acceptance_state,
|
||||||
reply_mode=reply_mode,
|
reply_mode=reply_mode,
|
||||||
recent_message_count=len(recent_messages),
|
recent_message_count=len(recent_messages),
|
||||||
vector_hit_count=len(vector_memories),
|
vector_hit_count=len(vector_memories),
|
||||||
@@ -372,6 +383,54 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
|||||||
f"12. 回答时优先服从当前群画像里的知识域和回答风格,不要跨领域乱发挥。\n"
|
f"12. 回答时优先服从当前群画像里的知识域和回答风格,不要跨领域乱发挥。\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_conversation_hints(recent_messages: List[Dict], current_sender: str, current_content: str) -> Dict[str, Any]:
|
||||||
|
previous_messages = list(recent_messages[:-1]) if recent_messages else []
|
||||||
|
recent_window = previous_messages[-4:]
|
||||||
|
solver_count = 0
|
||||||
|
solver_senders = set()
|
||||||
|
current_tokens = AIAutoResponsePlugin._extract_overlap_tokens(current_content)
|
||||||
|
for item in recent_window:
|
||||||
|
sender = str(item.get("sender", "") or "")
|
||||||
|
if not sender or sender == current_sender:
|
||||||
|
continue
|
||||||
|
content = str(item.get("content") or item.get("message") or "").strip().lower()
|
||||||
|
if AIAutoResponsePlugin._looks_like_answer(content) and AIAutoResponsePlugin._has_topic_overlap(current_tokens, content):
|
||||||
|
solver_count += 1
|
||||||
|
solver_senders.add(sender)
|
||||||
|
return {
|
||||||
|
"has_recent_human_solver": solver_count >= 2 and len(solver_senders) >= 1,
|
||||||
|
"solver_count": solver_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _looks_like_answer(content: str) -> bool:
|
||||||
|
if not content:
|
||||||
|
return False
|
||||||
|
answer_keywords = [
|
||||||
|
"先", "然后", "重启", "配置", "日志", "接口", "看一下", "试试", "排查",
|
||||||
|
"报错", "原因", "因为", "改成", "装", "部署", "重现", "检查", "确认",
|
||||||
|
]
|
||||||
|
if len(content) >= 18:
|
||||||
|
return True
|
||||||
|
return any(keyword in content for keyword in answer_keywords)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_overlap_tokens(content: str) -> set[str]:
|
||||||
|
text = str(content or "").lower()
|
||||||
|
tokens = set(re.findall(r"[a-z0-9_\\-]{3,}", text))
|
||||||
|
for keyword in ["报错", "日志", "配置", "接口", "插件", "部署", "docker", "python", "openclaw", "机器人", "qdrant", "ollama"]:
|
||||||
|
if keyword in text:
|
||||||
|
tokens.add(keyword)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_topic_overlap(current_tokens: set[str], previous_content: str) -> bool:
|
||||||
|
if not current_tokens:
|
||||||
|
return False
|
||||||
|
previous_tokens = AIAutoResponsePlugin._extract_overlap_tokens(previous_content)
|
||||||
|
return bool(current_tokens & previous_tokens)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanitize_response(response: str) -> str:
|
def _sanitize_response(response: str) -> str:
|
||||||
if not response:
|
if not response:
|
||||||
@@ -528,12 +587,15 @@ class AIAutoResponsePlugin(MessagePluginInterface):
|
|||||||
f"reason={data.get('reason', '')} "
|
f"reason={data.get('reason', '')} "
|
||||||
f"trigger={data.get('trigger_type', 'none')} "
|
f"trigger={data.get('trigger_type', 'none')} "
|
||||||
f"mode={data.get('reply_mode', '')} "
|
f"mode={data.get('reply_mode', '')} "
|
||||||
|
f"acc={data.get('acceptance_state', '-') or '-'} "
|
||||||
|
f"solver={data.get('solver', '-') or '-'}"
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
if event == "context":
|
if event == "context":
|
||||||
return (
|
return (
|
||||||
f"[XIAONIU] CTX room={room} user={sender} "
|
f"[XIAONIU] CTX room={room} user={sender} "
|
||||||
f"mode={data.get('reply_mode', '')} "
|
f"mode={data.get('reply_mode', '')} "
|
||||||
|
f"acc={data.get('acceptance_state', '-') or '-'} "
|
||||||
f"recent={data.get('recent_message_count', 0)} "
|
f"recent={data.get('recent_message_count', 0)} "
|
||||||
f"vector={data.get('vector_hit_count', 0)}"
|
f"vector={data.get('vector_hit_count', 0)}"
|
||||||
).strip()
|
).strip()
|
||||||
|
|||||||
@@ -15,9 +15,19 @@ class ResponsePlanner:
|
|||||||
return "social_short"
|
return "social_short"
|
||||||
return "social_short" if flow_state in {"deep_engaged"} else "refuse_or_skip"
|
return "social_short" if flow_state in {"deep_engaged"} else "refuse_or_skip"
|
||||||
|
|
||||||
def should_reply(self, trigger: Dict, flow_state: str, allow_proactive: bool) -> bool:
|
def should_reply(
|
||||||
|
self,
|
||||||
|
trigger: Dict,
|
||||||
|
flow_state: str,
|
||||||
|
allow_proactive: bool,
|
||||||
|
acceptance_state: str = "neutral",
|
||||||
|
conversation_hints: Dict | None = None,
|
||||||
|
) -> bool:
|
||||||
|
conversation_hints = conversation_hints or {}
|
||||||
if trigger.get("is_at"):
|
if trigger.get("is_at"):
|
||||||
return True
|
return True
|
||||||
|
if trigger.get("is_question") and conversation_hints.get("has_recent_human_solver") and flow_state != "deep_engaged":
|
||||||
|
return False
|
||||||
if trigger.get("is_question"):
|
if trigger.get("is_question"):
|
||||||
return True
|
return True
|
||||||
if trigger.get("is_followup"):
|
if trigger.get("is_followup"):
|
||||||
@@ -28,4 +38,8 @@ class ResponsePlanner:
|
|||||||
return False
|
return False
|
||||||
if not allow_proactive:
|
if not allow_proactive:
|
||||||
return False
|
return False
|
||||||
return flow_state in {"deep_engaged"} and trigger.get("priority", 0) >= 0.65
|
if acceptance_state == "cold":
|
||||||
|
return False
|
||||||
|
if acceptance_state == "neutral":
|
||||||
|
return flow_state in {"deep_engaged"} and trigger.get("priority", 0) >= 0.8
|
||||||
|
return flow_state in {"engaged", "deep_engaged"} and trigger.get("priority", 0) >= 0.65
|
||||||
|
|||||||
Reference in New Issue
Block a user