修复知识库搜索:表格内容不再被切分,表格section优先返回

- knowledge_base.py: _split_body_by_list_items检测表格区域,不切分表格行
- knowledge_base.py: 添加独立表格section,确保表格内容可被搜索匹配
- knowledge_base.py: 表格兜底策略:最佳文档含操作/支持表格时强制加入并置顶
- knowledge_base.py: reranking后无条件优先返回含操作/支持的表格section
- .gitignore: 添加msg_history.db和诊断脚本到忽略列表
This commit is contained in:
youpaishidifu
2026-08-05 13:22:27 +08:00
parent 271ea21489
commit 4d7017be6a
5 changed files with 162 additions and 14 deletions
+8
View File
@@ -51,3 +51,11 @@ worker_stdout.txt
# OS
Thumbs.db
.DS_Store
# 用户消息数据库(含聊天记录,禁止入库)
msg_history.db
# 诊断脚本/报告(临时调试文件)
_diag_*.py
_radical_*.txt
unicode_radicals.md.bak
+22 -7
View File
@@ -104,10 +104,25 @@ try:
import psutil
_mem = psutil.virtual_memory()
_avail_phys_gb = _mem.available / (1024**3)
if _avail_phys_gb < 3.0:
_used_phys_gb = _mem.used / (1024**3)
_total_phys_gb = _mem.total / (1024**3)
# ⭐ 页面文件百分比 + 总提交量检查:防页面文件94%+时仍尝试CUDA导致冻结
_pagefile_pct = _vm.get("commit_pct", 0) if _vm else 0
_pagefile_used_gb = _total_phys_gb + (_vm.get("total_pagefile_gb", 0) - _vm.get("avail_pagefile_gb", 0)) - _avail_phys_gb if _vm else 0
_commit_gb = _used_phys_gb + (_vm.get("total_pagefile_gb", 0) - _vm.get("avail_pagefile_gb", 0)) if _vm else _used_phys_gb
if _pagefile_pct > 85.0 or _commit_gb > _total_phys_gb * 1.5:
_msg = (
f"系统内存压力过高: 物理剩余{_avail_phys_gb:.1f}GB({_mem.percent}%),"
f"页面文件已用{_pagefile_pct}%,总提交{_commit_gb:.1f}GB>{_total_phys_gb*1.5:.1f}GB阈值"
)
_write_crash_log(f"{datetime.now()} [easyocr_worker] 系统内存压力预检失败: {_msg}")
os.environ["EASYOCR_FORCE_CPU"] = "1"
sys.stderr.write(f"[easyocr_worker] ⚠️ {_msg},降级CPU模式\n")
sys.stderr.flush()
elif _avail_phys_gb < 3.0:
_msg = (
f"物理内存不足: 剩余{_avail_phys_gb:.1f}GB"
f"(总计{_mem.total / (1024**3):.1f}GB,使用{_mem.percent}%)"
f"(总计{_total_phys_gb:.1f}GB,使用{_mem.percent}%)"
f",CUDA加载需要≥3GB物理内存"
)
_write_crash_log(f"{datetime.now()} [easyocr_worker] 物理内存预检失败: {_msg}")
@@ -117,12 +132,12 @@ try:
sys.stderr.flush()
except Exception:
pass # psutil不可用时不阻断
# 检查页面文件:虚拟内存不足时直接退出(无法运行)
if _vm and _vm.get("avail_pagefile_gb", 99) < 2.0:
# ⭐ 检查页面文件:百分比>85% 或 剩余<2GB 时退出(防冻结)
if _vm and (_vm.get("commit_pct", 0) > 85.0 or _vm.get("avail_pagefile_gb", 99) < 2.0):
_msg = (
f"页面文件不足: 剩余{_vm.get('avail_pagefile_gb', '?')}GB"
f"(提交{_vm.get('commit_pct', '?')}%)"
f",CUDA库需要≥2GB连续虚拟地址空间"
f"页面文件不足: 已用{_vm.get('commit_pct', '?')}%"
f"(剩余{_vm.get('avail_pagefile_gb', '?')}GB/{_vm.get('total_pagefile_gb', '?')}GB)"
f",CUDA库需要充足的虚拟地址空间"
)
_write_crash_log(f"{datetime.now()} [easyocr_worker] 页面文件预检失败: {_msg}")
_flush_exit(f"页面文件不足: {_msg}", code=2)
+1
View File
@@ -7,6 +7,7 @@ import logging
import queue
import threading
import os
import time
import customtkinter as ctk
+101
View File
@@ -165,14 +165,54 @@ class KnowledgeBase:
列表项以 "- " 开头,每个列表项及其后续内容作为独立子section。
返回子section列表,如果列表项少于2个则返回空列表(不切分)。
⭐ 修复:Markdown表格(以 | 开头的行)不应被切分到列表项中,
应作为独立section保留,确保表格内容可被搜索匹配。
"""
lines = body.split('\n')
# ⭐ 检测表格区域:找到所有表格行的范围
table_ranges = [] # [(start, end), ...]
table_start = -1
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith('|') and '|' in stripped[1:]:
if table_start == -1:
table_start = i
else:
if table_start != -1:
table_ranges.append((table_start, i - 1))
table_start = -1
if table_start != -1:
table_ranges.append((table_start, len(lines) - 1))
def is_in_table(line_idx):
"""检查行是否在表格区域内"""
for t_start, t_end in table_ranges:
if t_start <= line_idx <= t_end:
return True
return False
list_items = []
current_item = []
current_item_start = -1
for i, line in enumerate(lines):
stripped = line.strip()
# ⭐ 跳过表格行,不作为列表项处理
if is_in_table(i):
if current_item:
# 表格开始,结束当前列表项
list_items.append({
'start': current_item_start,
'end': i - 1,
'text': '\n'.join(current_item).strip(),
'title_line': current_item[0][2:].strip()
})
current_item = []
current_item_start = -1
continue
if stripped.startswith('- ') and len(stripped) > 2:
# 新的列表项开始
if current_item:
@@ -223,6 +263,23 @@ class KnowledgeBase:
"body": sub_content,
})
# ⭐ 添加独立的表格section(表格不在任何列表项中)
for t_start, t_end in table_ranges:
table_text = '\n'.join(lines[t_start:t_end + 1]).strip()
if table_text:
# 检查这个表格是否已经在某个列表项中
in_list_item = False
for item in list_items:
if t_start >= item['start'] and t_end <= item['end']:
in_list_item = True
break
if not in_list_item:
sections.append({
"heading": f"{parent_heading} - 表格",
"content": table_text,
"body": table_text,
})
return sections
def set_path(self, path: str):
@@ -436,6 +493,15 @@ class KnowledgeBase:
else:
combined_scores[i] *= 0.3 # 不包含核心名词 → 大幅惩罚
# ⭐ 表格内容奖励:当查询关于"支持/操作/方法/方式"时,包含表格的 section 巨幅加分
# 防止"领地保护支持哪些操作"匹配到不含表格的"我的领地" section
table_query_tokens = {'支持', '操作', '方法', '方式', '功能', '可以', '能够'}
if table_query_tokens & query_tokens_set:
# 检查 body 是否是 Markdown 表格(以 | 开头)
body_stripped = body.strip()
if body_stripped.startswith('|') and '|' in body_stripped[1:]:
combined_scores[i] *= 3.0 # 表格内容 → 巨幅奖励
# 短查询惩罚:分词数 ≤2 的短查询(如图标残片/短词)得分大幅降权
# 防止 OCR 碎片中的高 IDF 词(如"下载")获得超过真实用户消息的分数
n_tokens = len(tokens)
@@ -569,6 +635,29 @@ class KnowledgeBase:
doc_sections.append((i, doc, combined_scores[i]))
doc_sections.sort(key=lambda x: x[2], reverse=True)
# ⭐ 表格兜底策略:当最佳文档包含操作/支持相关表格时,强制加入并给超高分
# 防止"领地保护"因表格TF-IDF=0而丢失正确答案(表格是答案但不含查询词)
# 关键:给足够高的基础分,即使reranking bonus=0也能保持第一
if len(doc_sections) > 0:
# 检查文档是否已有包含"操作"或"支持"的表格section
has_action_table = False
for idx, _, _ in doc_sections:
body = self.docs[idx].get("body", "").strip()
if body.startswith('|') and ('操作' in body or '支持' in body):
has_action_table = True
break
if not has_action_table:
# 强制加入操作/支持相关表格section
for i, doc in enumerate(self.docs):
if doc["title"] == doc_title:
body = doc.get("body", "").strip()
if body.startswith('|') and '|' in body[1:] and ('操作' in body or '支持' in body):
# 给表格section超高分(最高分×2),确保reranking后仍排第一
max_score = max(s for _, _, s in doc_sections) if doc_sections else min_score
doc_sections.append((i, doc, max_score * 2))
break
doc_sections.sort(key=lambda x: x[2], reverse=True)
if not doc_sections:
continue
@@ -617,6 +706,18 @@ class KnowledgeBase:
reranked.sort(key=lambda x: x[2], reverse=True)
doc_sections = reranked
# ⭐ 表格优先策略(reranking后):无条件优先返回包含"操作/支持"的表格section
# 防止reranking后表格被其他section超越(表格是答案但不含查询词)
if len(doc_sections) > 1:
# 找到包含表格的section(body以|开头且含"操作"或"支持")
for i in range(len(doc_sections)):
idx = doc_sections[i][0]
body = self.docs[idx].get("body", "").strip()
if body.startswith('|') and ('操作' in body or '支持' in body):
# 表格section,移到第一位
doc_sections.insert(0, doc_sections.pop(i))
break
if has_action and len(doc_sections) > 1:
# 动作查询:提取动词+对象,精准匹配 section
# 如"怎么打印流水" → verb="打印", obj="流水",优先选 heading 同时含两者的 section
+28 -5
View File
@@ -195,6 +195,7 @@ class MessageHistory:
def __init__(self, db_path: str = "msg_history.db"):
self.db_path = db_path
self._last_bot_reply_content = {} # platform → content
self._init_db()
def _init_db(self):
@@ -1474,6 +1475,9 @@ class MessageMonitor:
初始化时间不计入请求超时。子进程崩溃/超时 → 杀进程 + 设置冷却,
冷却期后自动重试(不再永久 _easyocr_broken)。
"""
# ⭐ 检查监控是否仍在运行:stop()/急停后 warmup 线程不应重启子进程
if not self._running:
return False
if self._easyocr_broken:
return False
if self._easyocr_proc is not None:
@@ -1521,10 +1525,22 @@ class MessageMonitor:
f"页面文件 {_page_used_gb:.1f}/{_page_total_gb:.1f}GB({_page_used_pct}%),"
f"虚拟地址空间 {_virt_avail_gb:.1f}GB可用")
# 页面文件不足时跳过(避免反复崩溃循环)
if _page_avail_gb < 2.0:
logger.error(f"[ocr] ⚠️ 页面文件不足: 剩余{_page_avail_gb:.1f}GB({_page_used_pct}%),"
f"EasyOCR 冷却1小时。建议:增大 Windows 页面文件")
# ⭐ 页面文件百分比阈值:>85% 已用即视为不足(防页面文件大户系统冻结)
# 旧阈值只看绝对值<2GB,35GB总页面文件剩1.9GB看似"有空间"实则94.7%已满
if _page_used_pct > 85.0:
logger.error(f"[ocr] ⚠️ 页面文件不足: 已用{_page_used_pct}%({_page_used_gb:.1f}/{_page_total_gb:.1f}GB),"
f"剩余{_page_avail_gb:.1f}GB。EasyOCR 冷却1小时。建议:增大 Windows 页面文件")
self._easyocr_retry_until = time.time() + 3600
return False
# ⭐ 系统总提交量检查:提交量 > 1.5× 物理RAM 时冻结风险极高
# torch/CUDA 加载需额外 ~6GB,提交量已超标时再加载必然触发换页风暴
_commit_gb = _phys_used_gb + _page_used_gb
_phys_total_gb_check = _phys_total_gb
if _commit_gb > _phys_total_gb_check * 1.5:
logger.error(f"[ocr] ⚠️ 系统提交量过高: {_commit_gb:.1f}GB > 1.5×物理RAM({_phys_total_gb_check:.1f}GB),"
f"EasyOCR 冷却1小时。当前物理{_phys_used_pct}%+页面{_page_used_pct}%。"
f"建议:关闭占用内存的程序或增大物理内存")
self._easyocr_retry_until = time.time() + 3600
return False
@@ -1575,7 +1591,14 @@ class MessageMonitor:
logger.debug(f"[ocr] 内存详情:物理 {used_gb:.1f}/{total_gb:.1f}GB({mem.percent}%),"
f"页面文件 {swap_used_gb:.1f}/{swap_total_gb:.1f}GB({swap_pct}%)")
if available_gb < 3.0:
# ⭐ 页面文件百分比 + 总提交量双重检查:超阈值直接强制CPU(防冻结)
_commit_check_gb = used_gb + swap_used_gb
if swap_pct > 85.0 or _commit_check_gb > total_gb * 1.5:
_worker_env["EASYOCR_FORCE_CPU"] = "1"
logger.warning(f"[ocr] 内存压力高(物理{mem.percent}%+页面{swap_pct}%,"
f"总提交{_commit_check_gb:.1f}GB>{total_gb*1.5:.1f}GB阈值),"
f"EasyOCR 子进程强制CPU模式(避免CUDA加载冻结)")
elif available_gb < 3.0:
_worker_env["EASYOCR_FORCE_CPU"] = "1"
logger.warning(f"[ocr] 可用内存不足({available_gb:.1f}GB/总{total_gb:.1f}GB,"
f"使用{mem.percent}%),页面文件{swap_avail_gb:.1f}GB可用({swap_pct}%),"