feat: 从 main 分支迁移解析器与检索优化(不影响 API 接口)
- 新增 heading_rules.py 标题规则引擎,替换硬编码正则链 - 重写 mineru_parser.py:v2 格式兼容、表单自动检测 - manager.py: 跨页表格合并规则收紧(防误合并)+ LLM token 上限提升 - engine.py: embedding 缓存 + 同 section 表格邻居扩展 + prompt 改进 - router.py: 路由分类 token 上限提升至 512
This commit is contained in:
122
core/engine.py
122
core/engine.py
@@ -625,7 +625,7 @@ class RAGEngine:
|
||||
elif len(conditions) > 1:
|
||||
where_filter = {"$and": conditions}
|
||||
|
||||
query_vector = self.embedding_model.encode(query).tolist()
|
||||
query_vector = self._encode_cached(query).tolist()
|
||||
recall_k = RERANK_CANDIDATES if (USE_RERANK or USE_HYBRID_SEARCH) else top_k
|
||||
recall_k = max(recall_k, top_k * RECALL_MULTIPLIER)
|
||||
|
||||
@@ -811,6 +811,76 @@ class RAGEngine:
|
||||
logger.warning(f"FAQ 集合查询失败: {e}")
|
||||
return get_empty_result()
|
||||
|
||||
def _encode_cached(self, text):
|
||||
"""
|
||||
带缓存的 embedding 编码
|
||||
|
||||
优先从 Embedding Cache(LRU)读取,未命中再调用模型编码并写入缓存。
|
||||
支持单文本和批量文本输入。
|
||||
|
||||
Args:
|
||||
text: 单个文本字符串 或 文本列表
|
||||
|
||||
Returns:
|
||||
numpy 数组(单文本为一维,批量为二维)
|
||||
"""
|
||||
import numpy as _np
|
||||
|
||||
# 检查 embedding 缓存是否启用(缓存配置查询结果,避免每次重复导入)
|
||||
if not hasattr(self, '_emb_cache_enabled'):
|
||||
self._emb_cache_enabled = True # 默认启用
|
||||
if CACHE_AVAILABLE:
|
||||
try:
|
||||
from config import EMBEDDING_CACHE_ENABLED
|
||||
self._emb_cache_enabled = EMBEDDING_CACHE_ENABLED
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not self._emb_cache_enabled:
|
||||
return self.embedding_model.encode(text)
|
||||
|
||||
try:
|
||||
_cache = get_cache_manager()
|
||||
except Exception:
|
||||
return self.embedding_model.encode(text)
|
||||
|
||||
# 批量输入
|
||||
if isinstance(text, list):
|
||||
try:
|
||||
cached_embs, missed_indices = _cache.get_embeddings_batch(text)
|
||||
if missed_indices:
|
||||
missed_texts = [text[i] for i in missed_indices]
|
||||
# encode(list) 始终返回 2D ndarray,直接按行索引即可
|
||||
new_embs = self.embedding_model.encode(missed_texts)
|
||||
if len(missed_indices) == 1:
|
||||
# 单条时 encode 可能返回 1D,需统一处理
|
||||
if new_embs.ndim == 1:
|
||||
new_embs = new_embs.reshape(1, -1)
|
||||
for idx, mi in enumerate(missed_indices):
|
||||
emb_list = new_embs[idx].tolist()
|
||||
cached_embs[mi] = emb_list
|
||||
try:
|
||||
_cache.set_embedding(text[mi], emb_list)
|
||||
except Exception:
|
||||
pass
|
||||
return _np.array(cached_embs)
|
||||
except Exception:
|
||||
# 缓存故障时优雅降级为直接编码
|
||||
return self.embedding_model.encode(text)
|
||||
|
||||
# 单文本输入
|
||||
cached = _cache.get_embedding(text)
|
||||
if cached is not None:
|
||||
return _np.array(cached)
|
||||
|
||||
embedding = self.embedding_model.encode(text)
|
||||
try:
|
||||
emb_list = embedding.tolist() if hasattr(embedding, 'tolist') else list(embedding)
|
||||
_cache.set_embedding(text, emb_list)
|
||||
except Exception:
|
||||
pass
|
||||
return embedding
|
||||
|
||||
def _search_image_chunks(self, query_vector: list, top_k: int = 5, where_filter: dict = None) -> dict:
|
||||
"""
|
||||
独立检索图片切片(P0:图片独立召回通道)
|
||||
@@ -1171,6 +1241,20 @@ class RAGEngine:
|
||||
if not neighbors.get('ids') or len(neighbors.get('ids', [])) <= 1:
|
||||
neighbors = _get_neighbors({"$and": [{"source": source}, {"chunk_type": "text"}]})
|
||||
|
||||
# 同时扩展同 section 的 table 邻居(table 切片的 rerank 分数往往偏低,
|
||||
# 但与同 section 的 text 切片属于同一语义单元,不应割裂)
|
||||
table_where = {"$and": [{"source": source}, {"chunk_type": "table"}]}
|
||||
if section:
|
||||
table_where["$and"].append({"section": section})
|
||||
table_neighbors = _get_neighbors(table_where)
|
||||
|
||||
# 当 section 为空时,table 查询只有 source 条件,可能拉入大量无关表格,
|
||||
# 缩小 chunk_index 窗口至 ±1 以降低噪音;有 section 时使用正常窗口
|
||||
if section:
|
||||
_t_before, _t_after = CONTEXT_EXPANSION_BEFORE, CONTEXT_EXPANSION_AFTER
|
||||
else:
|
||||
_t_before, _t_after = 1, 1
|
||||
|
||||
neighbor_rows = []
|
||||
for n_id, n_doc, n_meta in zip(
|
||||
neighbors.get('ids', []),
|
||||
@@ -1183,6 +1267,18 @@ class RAGEngine:
|
||||
if seed_index - CONTEXT_EXPANSION_BEFORE <= n_index <= seed_index + CONTEXT_EXPANSION_AFTER:
|
||||
neighbor_rows.append((n_index, n_id, n_doc, n_meta))
|
||||
|
||||
# 同 section 的 table 邻居也加入扩展范围
|
||||
for n_id, n_doc, n_meta in zip(
|
||||
table_neighbors.get('ids', []),
|
||||
table_neighbors.get('documents', []),
|
||||
table_neighbors.get('metadatas', [])
|
||||
):
|
||||
n_index = self._to_int(n_meta.get('chunk_index'))
|
||||
if n_index is None:
|
||||
continue
|
||||
if seed_index - _t_before <= n_index <= seed_index + _t_after:
|
||||
neighbor_rows.append((n_index, n_id, n_doc, n_meta))
|
||||
|
||||
seed_neighbors_added = 0
|
||||
for n_index, n_id, n_doc, n_meta in sorted(neighbor_rows, key=lambda row: row[0]):
|
||||
if len(items) >= max_chunks:
|
||||
@@ -1387,7 +1483,7 @@ class RAGEngine:
|
||||
if not target_collections:
|
||||
return get_empty_result()
|
||||
|
||||
query_vector = self.embedding_model.encode(query).tolist()
|
||||
query_vector = self._encode_cached(query).tolist()
|
||||
# 扩大召回数量,以便过滤废止切片后仍有足够结果
|
||||
recall_k = RERANK_CANDIDATES if (USE_RERANK or USE_HYBRID_SEARCH) else top_k
|
||||
recall_k = max(recall_k, top_k * RECALL_MULTIPLIER)
|
||||
@@ -1739,12 +1835,12 @@ class RAGEngine:
|
||||
# === 高精度版:基于语义向量 ===
|
||||
from core.mmr import mmr_rerank
|
||||
|
||||
# 获取查询向量
|
||||
query_emb = np.array(self.embedding_model.encode(query))
|
||||
# 获取查询向量(使用 embedding 缓存)
|
||||
query_emb = np.array(self._encode_cached(query))
|
||||
|
||||
# 批量编码所有文档
|
||||
# 批量编码所有文档(使用 embedding 缓存)
|
||||
docs_list = results['documents'][0]
|
||||
all_embeddings = self.embedding_model.encode(docs_list)
|
||||
all_embeddings = self._encode_cached(docs_list)
|
||||
|
||||
# 构建候选列表
|
||||
candidates = []
|
||||
@@ -2068,21 +2164,33 @@ class RAGEngine:
|
||||
"content": (
|
||||
"你是一个严谨的知识库问答助手。"
|
||||
"你必须且只能根据用户提供的【参考资料】回答问题。"
|
||||
"参考资料中每段内容前标有章节路径(━格式),请注意区分不同章节的内容,"
|
||||
"特别当不同章节标题相似或包含相同关键词时,务必根据章节路径准确定位,不要混淆。"
|
||||
"如果参考资料中有答案,必须引用对应内容回答,并在回答末尾标注引用编号(如[1]、[2])。"
|
||||
"如果参考资料中确实没有相关信息,简短说明即可,不要编造或补充资料外的内容。"
|
||||
"禁止使用参考资料以外的知识进行补充或推测。"
|
||||
"【重要-表格处理规则】当用户询问表格、要求展示表格内容时,你必须将参考资料中的 Markdown 表格原样输出(保留 | 分隔符和表格结构),"
|
||||
"不要仅用文字描述表格存在或仅列出章节名称。如果参考资料中多个章节都有表格,"
|
||||
"优先展示与用户问题最相关的表格完整内容。"
|
||||
)
|
||||
})
|
||||
|
||||
# 添加当前问题(带上下文)- 强化指令
|
||||
if context:
|
||||
# 检测用户问题是否涉及表格,加入针对性指令
|
||||
_table_hint = ""
|
||||
# 检测上下文中是否包含 Markdown 表格(数据驱动,无需硬编码关键词)
|
||||
_has_table_in_context = bool(re.search(r'\|.+\|', context)) if context else False
|
||||
if _has_table_in_context:
|
||||
_table_hint = "\n注意:参考资料中包含 Markdown 格式的表格数据,请务必将相关表格以原始 Markdown 表格格式完整展示在回答中,不要仅用文字描述。"
|
||||
|
||||
user_message = f"""【参考资料】
|
||||
{context}
|
||||
|
||||
【用户问题】
|
||||
{query}
|
||||
|
||||
请仔细阅读以上全部参考资料后回答。如果参考资料中包含相关内容,必须引用回答并标注编号。如果资料中没有相关信息,请明确说明。"""
|
||||
请仔细阅读以上全部参考资料后回答。注意参考资料中标有章节路径,请根据章节路径准确定位相关内容。如果参考资料中包含相关内容,必须引用回答并标注编号。如果资料中没有相关信息,请明确说明。{_table_hint}"""
|
||||
else:
|
||||
user_message = query
|
||||
|
||||
|
||||
Reference in New Issue
Block a user