fix(kb): 多关键词全覆盖防御 + 语义意图验证增强

1. auto_reply.py 多关键词覆盖防御:
   - 区分核心词与非核心词(排除疑问词、OCR碎片等)
   - 要求文档覆盖≥50%核心词(最低2个),防止单一关键词误选
   - 例:查询含核心词={'草原','近期','公告'},sdf1.md只含'草原'(1/3)→拒绝

2. auto_reply.py 语义意图验证增强:
   - php查询:heading需含'php'或'管理'或'后端'(放宽规则支持'php用户管理')
   - php密码查询:heading必须含'管理员'或'后端'(区分玩家端vs管理端)
   - 新增php后端查询规则:query含'后端'但heading不含时拒绝

3. gui.py 搜索范围扩展:
   - top_k从3增至5,确保正确结果不被截断

修复问题:
- '服务器近期的一些公告有哪些' → 返回index.md的'近期公告'
- 'PHP默认的那管理后台管理密码是多少' → 返回phpadmin.md的'ypshidifu2026'
- '那pHP后端的那管理员默认密码是多少' → 返回phpadmin.md(拒绝beibao.md)
This commit is contained in:
youpaishidifu
2026-08-13 21:24:32 +08:00
parent 2a5f16ae1f
commit a0ace5ec13
2 changed files with 70 additions and 31 deletions
+69 -30
View File
@@ -161,51 +161,90 @@ class AutoReply:
# ⭐ 后台/前台关键词区分:查询含"后台"但文档不含"后台"→ 不相关
full = heading_clean + " " + title.lower() + " " + body_clean[:800]
# 注意:"管理后台"是整体概念,拆分后"后台"可能不在heading中,但"管理员"+"后端"已表达管理意图
# 所以当heading含"管理员"或"后端"时,不应因缺"后台"而拒绝
if "后台" in query and "后台" not in full:
logger.debug(f"后台关键词缺失: query={query[:40]!r} doc={doc.get('heading', '')[:30]!r}")
return False
# 检查是否已通过其他语义规则确认是管理端查询
is_admin_intent = ('管理员' in query) or ('管理' in query and '密码' in query)
if not is_admin_intent and ('管理员' not in heading_clean and '后端' not in heading_clean):
logger.debug(f"后台关键词缺失: query={query[:40]!r} doc={doc.get('heading', '')[:30]!r}")
return False
if "前台" in query and "前台" not in full:
logger.debug(f"前台关键词缺失: query={query[:40]!r} doc={doc.get('heading', '')[:30]!r}")
return False
# ⭐ 语义意图验证:OCR查询可能含错字(如"录"→"登"、"php"→"pHp"),
# 需要结合heading确认查询意图,防止"云背包管理员密码"压过"php管理员后端"
# 查询含"php"时,heading必须含"php"(而非仅"管理",因为beibao也有管理员)
# 查询含"php"时,heading必须含"php"或满足以下例外规则:
if 'php' in query.lower():
if 'php' not in heading_clean:
logger.debug(f"php查询意图不匹配: query={query[:40]!r} heading={heading[:30]!r}")
return False
# 查询含"管理员密码"且含"php"时,heading必须含"管理员"(防止php.md玩家端被误选,应返回phpadmin.md)
# 例:"PHP默认的那管理后台管理密码是多少" → 应返回phpadmin.md(heading含"管理员"),而非php.md(玩家端)
if '管理员' in query and '密码' in query and 'php' in query.lower():
if '管理员' not in heading_clean:
logger.debug(f"管理员密码查询意图不匹配(玩家端): query={query[:40]!r} heading={heading[:30]!r}")
return False
# 查询含"管理员密码"且不含"php"时,heading必须含"密码"或"php"
# (防止beibao的"管理员密码qweasd"被误选,应返回phpadmin的"ypshidifu2026")
if '管理员' in query and '密码' in query and 'php' not in query.lower():
if '密码' not in heading_clean and 'php' not in heading_clean:
logger.debug(f"管理员密码查询意图不匹配: query={query[:40]!r} heading={heading[:30]!r}")
# 例外1:查询含"管理"+"密码"类词,heading含"管理"即可(指向phpadmin的管理功能)
# 例:"php用户管理可以看到哪些信息" → 允许返回"### 用户管理"
has_management_intent = ('管理' in query and '管理' in heading_clean)
# 例外2:查询含"后端"概念,heading含"后端"即可
# 例:"那pHP后端的那管理员默认密码是多少" → 允许返回"## php管理员后端"
has_backend_intent = ('后端' in query and '后端' in heading_clean)
if not has_management_intent and not has_backend_intent:
logger.debug(f"php查询意图不匹配: query={query[:40]!r} heading={heading[:30]!r}")
return False
# ⭐ 增强:区分php.md(玩家端)和phpadmin.md(管理端)
# 当查询含"密码"且含"php"时,heading必须含"管理员"或"后端"(防止php.md玩家端被误选)
if '密码' in query and 'php' in query.lower():
if '管理员' not in heading_clean and '后端' not in heading_clean:
logger.debug(
f"php密码查询意图不匹配(疑似玩家端): query={query[:40]!r} "
f"heading={heading[:30]!r}"
)
return False
# ⭐ 新增:当query含"后端"但heading不含"后端"也不含"php"时,可能是云背包等无关文档
# 例:"那pHP后端的那管理员默认密码是多少" vs beibao.md的"## 使用方法-管理员"
# beibao不含"php"也不含"后端"→ 拒绝
if '后端' in query and '后端' not in heading_clean and 'php' not in heading_clean:
logger.debug(
f"php后端查询意图不匹配: query={query[:40]!r} "
f"heading={heading[:30]!r} (heading不含'后端'或'php')"
)
return False
if has_ht or has_body:
# ⭐ 正文覆盖不足防御(牛头不对马嘴修复):
# 当 heading 无命中但有 body 命中时,检查 body 覆盖了多少查询词。
# 例:"领地系统有哪些权限级别?" vs sdf1_login 的 feature list
# | 领地系统 | 保护自己领地不受损失 | — body 只命中2/5个词(领地,系统)→ 误选
# 策略:
# - has_ht=True 时 heading 已提供语义锚定,只需任意 body_hit 即可(正确结果往往 score 低但方向对)
# - has_ht=False 且 has_body=True 时,要求 body 覆盖足够比例,防止单点散命中通过
# min_body_hits = min(ceil(q_count*40%), 4) 且至少为 2
# 5词查询需≥2,6词需≥3,7词需≥3,8词需≥4
# ⭐ 多关键词覆盖防御(2026-08-13):
# 区分核心词与非核心词,防止通用短语因单一关键词误选不相关文档。
# 核心词:实词(名词/动词/形容词),排除疑问词、量词、方位词等
# 规则:
# 1. 如果heading含核心词命中(has_ht=True)→ 放宽检查,不强制覆盖全部
# 2. 如果heading无命中(has_ht=False)且正文有多关键词 → 要求覆盖≥50%
# 例:"草原深险服务器备用号服务器近期的一些公告有哪些"
# 核心词={'草原','近期','公告'}(排除'哪些','备用','深险'等OCR碎片)
# sdf1.md"注意事项"只含"草原"(1/3)→ 若has_ht=False则拒绝
# index.md"近期公告"含"近期"+"公告"(2/3)→ 通过
non_core_kw = {'多少', '哪些', '哪个', '什么', '怎么', '如何', '吗', '呢', '吧',
'备用', '深险', '端的', '那', '这', '些', '个', '种', '类'}
core_kw = q_mean - non_core_kw
doc_text = heading_clean + " " + title.lower() + " " + body_clean[:800]
core_hits = sum(1 for kw in core_kw if kw in doc_text)
# ⭐ 当heading含核心词时(has_ht=True),也需检查覆盖比例
# 防止"草原"等通用词单一命中导致误选sdf1.md等不相关文档
# 规则:
# - has_ht=True: 要求覆盖≥50%核心词(最低2个)
# - has_ht=False: 要求覆盖≥50%核心词(最低2个)
# - 例:查询含核心词={'草原','近期','公告'}
# sdf1.md只含"草原"(1/3)→ 拒绝
# index.md含"近期"+"公告"(2/3)→ 通过
if len(core_kw) >= 2:
min_core_hits = max(2, (len(core_kw) + 1) // 2)
if core_hits < min_core_hits:
logger.debug(
f"多关键词覆盖不足跳过(防牛头不对马嘴): query={query[:40]!r} "
f"doc_heading={heading[:30]!r} core_hits={core_hits}/{len(core_kw)} "
f"(阈值≥{min_core_hits}), missing={ [kw for kw in core_kw if kw not in doc_text]!r}, has_ht={has_ht}"
)
return False
# 原有正文覆盖不足防御(保留作为兜底)
if not has_ht and has_body:
q_count = len(q_mean)
body_hits = len(q_mean & body_tokens)
# 无 heading 命中的查询需更多 body 覆盖,防散点命中漏过:
# min_body_hits = min(q_count, ceil(q_count*60%)),最低 2
# 例:2token→ceil(1.2)=2, min(2,2)=2(全中);3token→ceil(1.8)=2,min(3,2)=2(缺1词过)
# 4token→ceil(2.4)=3,min(4,3)=3(缺1词不过);5token→ceil(3)=3
min_body_hits = min(q_count, (q_count * 3 + 4) // 5) # ceil(q_count*0.6)
min_body_hits = min(q_count, (q_count * 3 + 4) // 5)
min_body_hits = max(2, min_body_hits)
if body_hits < min_body_hits:
logger.debug(