fix: 下载意图检查提前 + 语义去重防双回复
问题1: 核心词覆盖检查在下载意图检查之前执行
- 查询'那个登录插件是在哪个地方下载?'含3个核心词{登录,插件,下载}
- sdf1_login.md的'## 下载地址'只含'下载'(1/3),不满足阈值>=2
- 直接return False,下载意图检查从未执行
- 修复:将下载意图检查移到核心词检查之前,提前放行
问题2: 相同意图不同表达导致双回复
- hash('{platform}:{msg}:{answer[:80]}')要求完全一致
- OCR噪声导致同义重复(如'在哪下载'vs'下载地址')
- 修复:添加Jaccard相似度(CJK bigram)语义去重
- TTL=30秒,阈值>=0.3视为重复,缓存上限50条
commit 8d7c9e2 (2026-08-16)
This commit is contained in:
+52
-20
@@ -3,6 +3,7 @@
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import jieba
|
||||
|
||||
logger = logging.getLogger("msgbot.autoreply")
|
||||
@@ -270,6 +271,24 @@ class AutoReply:
|
||||
doc_text = heading_clean + " " + title.lower() + " " + body_clean[:800]
|
||||
core_hits = sum(1 for kw in core_kw if kw in doc_text)
|
||||
|
||||
# ⭐ 下载意图检查提前到核心词检查之前
|
||||
# 原因:当查询含"下载"且heading也含"下载"时,即使其他核心词未命中,
|
||||
# 也应放行(如"哪个地方下载登录插件"→ "## 下载地址"应匹配)
|
||||
# 原逻辑在核心词检查后执行,核心词覆盖不足时直接return False,
|
||||
# 导致下载意图检查永远无法触发
|
||||
_download_match = False
|
||||
if any(w in query for w in ('下载', '下到', '哪下', '在哪下', '怎么下')) and '下载' in heading_clean:
|
||||
if '<br>' not in heading and '- [' not in heading and '|-' not in heading:
|
||||
_download_match = True
|
||||
else:
|
||||
# 复合标题:检查标题主体部分(<br>或列表之前)是否含"下载"
|
||||
_title_part = re.split(r'<br>|- |\|-', heading_clean)[0]
|
||||
if '下载' in _title_part:
|
||||
_download_match = True
|
||||
if _download_match:
|
||||
logger.debug(f"下载意图放行: query={query[:30]!r} heading={heading[:30]!r}")
|
||||
return True
|
||||
|
||||
# ⭐ 当heading含核心词时(has_ht=True),也需检查覆盖比例
|
||||
# 防止"草原"等通用词单一命中导致误选sdf1.md等不相关文档
|
||||
# 规则:
|
||||
@@ -305,25 +324,6 @@ class AutoReply:
|
||||
return False
|
||||
return True
|
||||
|
||||
# ⭐ 下载意图放行:查询含"下载/下到/哪下"等口语,且section heading含"下载"
|
||||
# 修复"在哪儿能下到那登录插件啊?"——原文无"下载"二字(口语"下到"),
|
||||
# q_mean 不含"下载","## 4. 下载地址"heading 虽有"下载"仍被相关性拒绝,
|
||||
# 导致误回"插件设置"(body 恰含"登录超时秒数")
|
||||
# ⚠ 只检查真正的标题行:如果heading含<br>或列表标记,说明是复合标题,
|
||||
# 需检查"下载"是否出现在标题主体部分(<br>之前),而非列表项内容
|
||||
_download_match = False
|
||||
if any(w in query for w in ('下载', '下到', '哪下', '在哪下', '怎么下')) and '下载' in heading_clean:
|
||||
if '<br>' not in heading and '- [' not in heading and '|-' not in heading:
|
||||
_download_match = True
|
||||
else:
|
||||
# 复合标题:检查标题主体部分(<br>或列表之前)是否含"下载"
|
||||
_title_part = re.split(r'<br>|- |\|-', heading_clean)[0]
|
||||
if '下载' in _title_part:
|
||||
_download_match = True
|
||||
if _download_match:
|
||||
logger.debug(f"下载意图放行: query={query[:30]!r} heading={heading[:30]!r}")
|
||||
return True
|
||||
|
||||
# 子串回退:jieba 可能产生碎片 token,导致交集为空但文档确实含查询短语
|
||||
full = heading_clean + " " + title.lower() + " " + body_clean[:800]
|
||||
stripped = re.sub(r'[??!!。,,\s]', '', query).lower()
|
||||
@@ -390,16 +390,48 @@ class AutoReply:
|
||||
logger.debug(f"[{platform}] 答案为免责声明/通用说明,跳过: {answer[:60]!r}")
|
||||
return None
|
||||
|
||||
# 防重复指纹
|
||||
# 防重复指纹(精确匹配)
|
||||
fp = hash(f"{platform}:{msg}:{answer[:80]}")
|
||||
replied_set = self._replied.setdefault(platform, set())
|
||||
if fp in replied_set:
|
||||
logger.debug(f"[{platform}] 重复命中,跳过: {msg[:40]}")
|
||||
return None
|
||||
replied_set.add(fp)
|
||||
|
||||
# ⭐ 语义去重:相同意图的不同表达(如"在哪下载登录插件"vs"登录插件下载地址")
|
||||
# 使用Jaccard相似度(CJK bigram)检查,阈值>=0.3视为重复
|
||||
# 防止OCR碎片导致的同义重复回复
|
||||
_now = time.time()
|
||||
_semantic_cache = getattr(self, '_semantic_reply_cache', [])
|
||||
# 清理过期条目(TTL=30秒)
|
||||
self._semantic_reply_cache = [(t, ts) for t, ts in _semantic_cache if _now - ts <= 30.0]
|
||||
_SEMANTIC_SIM_THRESHOLD = 0.3
|
||||
_duplicated = False
|
||||
for semantic_text, _ts in self._semantic_reply_cache:
|
||||
# 提取纯中文并计算Jaccard相似度
|
||||
def _cjk_bigrams(text):
|
||||
cjk = ''.join(c for c in text if '\u4e00' <= c <= '\u9fff')
|
||||
return set(cjk[i:i+2] for i in range(len(cjk) - 1))
|
||||
sa, sb = _cjk_bigrams(msg), _cjk_bigrams(semantic_text)
|
||||
if sa and sb:
|
||||
intersection = len(sa & sb)
|
||||
union = len(sa | sb)
|
||||
sim = intersection / union if union > 0 else 0.0
|
||||
if sim >= _SEMANTIC_SIM_THRESHOLD:
|
||||
logger.debug(
|
||||
f"[{platform}] 语义重复,跳过: {msg[:30]!r} vs {semantic_text[:30]!r} (sim={sim:.2f})"
|
||||
)
|
||||
_duplicated = True
|
||||
break
|
||||
if _duplicated:
|
||||
return None
|
||||
self._semantic_reply_cache.append((msg, _now))
|
||||
|
||||
# 控制缓存上限
|
||||
if len(replied_set) > 200:
|
||||
self._replied[platform] = set(list(replied_set)[-100:])
|
||||
if len(self._semantic_reply_cache) > 50:
|
||||
self._semantic_reply_cache = self._semantic_reply_cache[-30:]
|
||||
|
||||
logger.info(
|
||||
f"[{platform}] 🎯 自动回复触发 | 匹配度 {score:.2f} | "
|
||||
|
||||
Reference in New Issue
Block a user