From 5455d3560b796db1f3dde8f4d8ed597079064c8c Mon Sep 17 00:00:00 2001 From: youpaishidifu Date: Mon, 17 Aug 2026 14:16:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20OCR=E5=8F=A3=E8=AF=AD=E8=AF=8D/=E7=A2=8E?= =?UTF-8?q?=E7=89=87=E8=AF=8D=E6=B1=A1=E6=9F=93=E6=A0=B8=E5=BF=83=E8=AF=8D?= =?UTF-8?q?+=E4=B8=8B=E8=BD=BD=E5=85=A8=E6=96=87=E5=9B=9E=E9=80=80+?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E8=A1=A8=E6=A0=BC=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. non_core_kw新增: 有人(@OCR误读)/一嘴/那家/那价 2. core_kw过滤纯ASCII<=2碎片词(ij等OCR噪声) 3. 下载意图新增全文回退: section无'登录'但整文档是登录插件(全文17次)则放行 4. 动作查询表格兜底: 查询'操作模式'vs表'操作方式'词级匹配失败时, 表格+action词命中放行 --- auto_reply.py | 72 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/auto_reply.py b/auto_reply.py index ce56e00..969adf0 100644 --- a/auto_reply.py +++ b/auto_reply.py @@ -4,6 +4,8 @@ import logging import re import threading import time +import os +import functools import jieba logger = logging.getLogger("msgbot.autoreply") @@ -110,6 +112,16 @@ class AutoReply: toks = set(t.lower() for t in jieba.cut(text) if len(t) >= 2) return toks - AutoReply._STOPWORDS + @staticmethod + @functools.lru_cache(maxsize=512) + def _full_doc_text(path: str) -> str: + """读取知识库文档全文(缓存)。下载意图回退检查用——section粒度丢失跨节主题词。""" + try: + with open(path, encoding="utf-8") as f: + return f.read() + except Exception: + return "" + @staticmethod def _has_content_relevance(query: str, doc: dict, score: float = 0.0) -> bool: """查询词与匹配文档必须共享至少一个有区分度的核心词,防偶然/牛头不对马嘴匹配。 @@ -178,12 +190,25 @@ class AutoReply: non_action_body = body_tokens - action_kw has_body_nonaction = bool(non_action_q & non_action_body) if not has_body_nonaction: - # 正文(不含动作词)完全没有共同词 → 直接拒绝,无论 heading 是否命中 - logger.debug( - f"动作查询正文无共同词拒绝: query={query[:40]!r} " - f"doc_heading={heading[:30]!r} body_hits=0 ht_hit={has_ht}" - ) - return False + # ⭐ 表格兜底(2026-08-17):词级匹配可能因近义词/OCR差异失败 + # 例:查询"操作模式" vs 表格"操作方式"——body含'操作'但已被action_kw剔除, + # non_action交集为空 → 误杀 protect.md 的操作方式表格 + # 规则:heading主体命中(has_ht) + body是表格(|分隔) + 查询动作词在表格中出现 → 放行 + _is_table_body = ('|' in (body or '')) or ('-|-' in (body or '')) + # 注意:不能用 set(body_clean)——字符集不包含多字符词,须用子串判断 + _action_hits_body = any(kw in body_clean[:800] for kw in (q_mean & action_kw)) + if has_ht and _is_table_body and _action_hits_body: + logger.debug( + f"动作查询表格兜底放行: query={query[:40]!r} " + f"doc_heading={heading[:30]!r}" + ) + else: + # 正文(不含动作词)完全没有共同词 → 直接拒绝,无论 heading 是否命中 + logger.debug( + f"动作查询正文无共同词拒绝: query={query[:40]!r} " + f"doc_heading={heading[:30]!r} body_hits=0 ht_hit={has_ht}" + ) + return False if not has_ht: # 正文有非动作词命中但 heading 无命中 → 检查 body 覆盖量(见下方正文覆盖不足防御) pass @@ -280,8 +305,18 @@ class AutoReply: # 2026-08-17 新增:口语填充词"一句"不应作为核心词 # 用户常说"我再问一句","一句"是填充词,不影响查询意图 '一句', '问一句', '再问', + # 2026-08-17 新增:OCR将@提及读成"有人"("有人@拜"实为@某人) + # "有人"是口语占位/OCR噪声,无查询意图,类似"大家" + '有人', + # 2026-08-17 新增:"再问一嘴"的口语填充("问一嘴"已有,补"一嘴") + '一嘴', + # 2026-08-17 新增:口语"咱那家"的"那家"(OCR常写成"那价") + '那家', '那价', } core_kw = q_mean - non_core_kw + # ⭐ OCR碎片过滤:纯ASCII且≤2字符的词(如"丿IJ"切成'ij')是OCR噪声 + # 不参与核心词覆盖判定——否则垃圾词抬高分母,真核心词反而达不到阈值 + core_kw = {kw for kw in core_kw if not (kw.isascii() and len(kw) <= 2)} doc_text = heading_clean + " " + title.lower() + " " + body_clean[:800] core_hits = sum(1 for kw in core_kw if kw in doc_text) @@ -308,10 +343,21 @@ class AutoReply: if other_hits >= 1: _download_match = True else: - logger.debug( - f"下载意图拒绝(核心词未命中): query={query[:40]!r} " - f"doc_title={title!r} other_kw={other_core_kw!r} hits={other_hits}" + # ⭐ 2026-08-17 全文回退:section 粒度丢失跨节主题词 + # sdf1_login.md "### 2.1 下载" 正文只有仓库链接(GitHub/gitee...)不含"登录", + # 但整篇文档主题就是登录插件(全文"登录"×17) + # beibao.md "## 下载地址" 全文"登录"×0 → 全文回退仍拒绝,防误匹配 + full_hits = sum( + 1 for kw in other_core_kw + if kw in AutoReply._full_doc_text(doc.get('path', '')) ) + if full_hits >= 1: + _download_match = True + else: + logger.debug( + f"下载意图拒绝(核心词未命中): query={query[:40]!r} " + f"doc_title={title!r} other_kw={other_core_kw!r} hits={other_hits} full_hits={full_hits}" + ) else: # 复合标题:检查标题主体部分(
或列表之前)是否含"下载" _title_part = re.split(r'
|- |\|-', heading_clean)[0] @@ -323,6 +369,14 @@ class AutoReply: other_hits = sum(1 for kw in other_core_kw if kw in doc_text) if other_hits >= 1: _download_match = True + else: + # 复合标题分支同样支持全文回退(见上方注释) + full_hits = sum( + 1 for kw in other_core_kw + if kw in AutoReply._full_doc_text(doc.get('path', '')) + ) + if full_hits >= 1: + _download_match = True if _download_match: logger.debug(f"下载意图放行: query={query[:30]!r} heading={heading[:30]!r}") return True