# RAG 检索流程逻辑 本文档描述 RAG 知识库服务从文档解析入库到 LLM 回答的完整数据流,供开发排查和系统优化参考。 --- ## 目录 1. [整体架构](#一整体架构) 2. [文档解析与入库流程](#二文档解析与入库流程) 3. [ChromaDB 存储字段详解](#三chromadb-存储字段详解) 4. [BM25 索引两种实现](#四bm25-索引两种实现) 5. [检索管线完整数据流](#五检索管线完整数据流) 6. [distances / scores 语义变化](#六distances--scores-语义变化) 7. [路由层处理流程](#七路由层处理流程) 8. [三重救援机制详解](#八三重救援机制详解) 9. [图片召回与选择](#九图片召回与选择) 10. [返回格式与溯源信息](#十返回格式与溯源信息) 11. [配置项完整列表](#十一配置项完整列表) 12. [单/多知识库路径说明](#十二单多知识库路径说明) 13. [切片策略与检索策略兼容性分析](#十三切片策略与检索策略兼容性分析) 14. [MinerU 解析策略](#十四mineru-解析策略) --- ## 一、整体架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 文档入库流程 │ │ │ │ 文件上传 → MinerU 解析 → MinerUChunk → 语义增强 → ChromaDB 存储 │ │ ↓ │ │ BM25 索引构建 │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ 检索问答流程 │ │ │ │ 用户提问 │ │ ↓ │ │ 意图分析(改写/子查询/意图分类) │ │ ↓ │ │ 语义缓存检查 ──命中──→ 直接返回缓存答案 │ │ ↓ 未命中 │ │ 混合检索(向量 + BM25 + 图片 + FAQ) │ │ ↓ │ │ RRF 融合 → 过滤链 → MMR → Rerank → 后处理 → 扩展 → 自适应 TopK │ │ ↓ │ │ search_hybrid(distances → scores 转换) │ │ ↓ │ │ 路由层:contexts 构建 → 三重救援 → 排序 → 预算截断 │ │ ↓ │ │ Prompt 构建 → LLM 流式生成 → 后处理 → 返回 │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 二、文档解析与入库流程 ### 2.1 入口方法 ```python # knowledge/manager.py def add_file_to_kb(self, kb_name, filepath, embedding_model=None, extra_metadata=None, enable_table_summary=True, enable_image_description=False, file_content=None) -> int ``` **完整流程**: ``` 1. 调用 parsers.parse_document(filepath) 进行 MinerU 解析 2. 合并跨页表格(_merge_cross_page_tables) 3. 清理同名旧切片(查询 source == filename 的旧切片并删除) 4. 逐切片处理: a. 获取 chunk_type、page、section_path b. 构建语义增强内容(semantic_content) c. 构建 metadata 字典 d. 生成 embedding 向量 e. 可选:LLM 生成表格摘要 / VLM 生成图片描述 5. 批量写入 ChromaDB(collection.add) 6. 更新 BM25 索引(bm25.add_documents + save) ``` ### 2.2 MinerU 解析流程 **三个解析入口**: | 函数 | 用途 | 调用方式 | |------|------|---------| | `parse_with_mineru()` | 本地 GPU 解析 | 由 `parse_with_mineru_persistent` 在在线失败时调用 | | `parse_with_mineru_online()` | 在线 API 解析 | 由 `parse_with_mineru_persistent` 优先调用 | | `parse_with_mineru_persistent()` | 持久化解析(主入口) | 由 `parsers.parse_document()` 调用 | **实际主入口是 `parse_with_mineru_persistent()`**,内部根据 `config.MINERU_PREFER_ONLINE` 自动选择在线或本地,在线失败自动回退本地。 **解析核心流程**(以在线为例): 1. 文件校验(存在性、大小 ≤ 100MB、格式校验) 2. 计算文件 MD5 hash(前12位),用于隔离输出目录 3. 申请上传链接 → PUT 上传文件 → 轮询查询结果(5秒间隔)→ 下载 zip 包 4. 解析 content_list(优先 v2 格式 `_content_list_v2.json`) 5. 逐项解析 content_list 构建 `MinerUChunk` **`MinerUChunk` 数据结构**: ```python @dataclass class MinerUChunk: content: str # 文本内容 chunk_type: str # text / heading / table / image / chart / equation page_start: int = 1 # 起始页码(1-based) page_end: int = 1 # 结束页码 text_level: int = 0 # 标题级别 (0=正文, 1=h1, 2=h2, 3=h3) title: str = "" # 标题文本 section_path: str = "" # 章节路径," > " 连接 bbox: Optional[List[float]] = None # 边界框 [x0, y0, x1, y1] source_file: str = "" # 源文件名 table_html: Optional[str] = None # 表格 HTML(仅 table) image_path: Optional[str] = None # 图片路径(独立图片) images: Optional[List[Dict]] = None # 关联图片: [{"id":"abc.jpg","order":1}] context_before: str = "" # 图片前文本上下文 context_after: str = "" # 图片后文本上下文 # VLM 增强信息(云端 MinerU V4 VLM 后端提供) vlm_description: str = "" # VLM 视觉描述(图片/图表) chart_markdown: str = "" # VLM 提取的图表数据表(Markdown 格式) # MinerU 结构化元数据 table_type: str = "" # 表格类型(cflow/table/text,来自 V2 _v2_table_type) table_nest_level: str = "" # 表格嵌套层级(来自 V2 _v2_table_nest_level) sub_type: str = "" # 图片/图表子类型(natural_image/table_image/bar/line 等) ``` ### 2.3 section_path 生成逻辑 维护 `section_stack: [(level, title), ...]`,遇到新标题时: - 弹出栈中 `level >= 当前 level` 的项 - 压入 `(level, title)` - `section_path = " > ".join([栈中所有项的 title])` ### 2.4 后处理三阶段 1. **过滤空切片**:移除 content 为空的 chunk 2. **合并碎片**:标题+正文合并、短文本合并(阈值 `MIN_CHUNK_SIZE//2`),表格/图片/公式不参与 3. **拆分超长**:超过 `MAX_CHUNK_SIZE` 的文本用 `split_text_with_limit` 拆分 ### 2.5 图片处理 **图片来源三类**: 1. 独立图片/图表(content_list 中 type=image/chart)→ `chunk.image_path` 2. 表格的图片形式(type=table 有 img_path)→ `chunk.image_path` 3. 嵌入表格 HTML 的图片(``)→ `chunk.images` 列表 **图片路径重映射**(`parse_with_mineru_persistent` 中): 1. 从 MinerU 输出找到图片源文件 2. 按文件内容 MD5 重命名,移动到 `.data/images/` 目录 3. 更新 chunk.image_path 和 chunk.images 中的路径 ### 2.6 语义内容构建 **文本类型** `_build_semantic_content_for_text()`: ``` {标题} 主题:{章节路径(截断到3级)} {正文内容} ``` **表格类型** `_build_semantic_content_for_table()`: ``` 主题:{章节路径} 表格:{标题}(非"表格"时) 字段:{表头列表} 描述:该表包含N行数据,记录各字段信息 示例:字段1=值1, 字段2=值2 表格内容: | ... | ... | ``` **图片类型** `generate_lightweight_image_description()`: ``` 图片:图2.1,系统架构图,位于「第一章 > 1.2 概述」,第5页 前文:系统由三个模块组成... 后文:如图所示,各模块之间... ``` > **设计意图**:语义增强内容存入 ChromaDB 的 `documents` 字段,用于向量检索。增强后的内容比原始文本包含更多上下文信息,提升检索召回率。 --- ## 三、ChromaDB 存储字段详解 ### 3.1 存储结构 ChromaDB 使用 `collection.add(ids, documents, metadatas, embeddings)` 批量写入。 | 字段 | 类型 | 说明 | |------|------|------| | `ids` | `List[str]` | 切片唯一标识,格式 `{filename}_{index}`,如 `"1.docx_14"` | | `documents` | `List[str]` | 语义增强后的内容(非原始 content) | | `metadatas` | `List[dict]` | 切片元数据(见下表) | | `embeddings` | `List[List[float]]` | 768维向量,由 `embedding_model.encode(semantic_content)` 生成 | ### 3.2 metadatas 字段详解 | 字段 | 类型 | 来源 | 作用 | 示例 | |------|------|------|------|------| | `chunk_id` | str | `{filename}_{index}` | 切片唯一标识(与 ids 相同) | `"1.docx_14"` | | `chunk_index` | int | 入库时赋值 `i` | 切片在文件中的序号,用于邻居扩展和救援 | `14` | | `chunk_type` | str | MinerU 解析 | 切片类型,驱动检索和展示逻辑 | `"text"` | | `source` | str | 文件名 | 源文件名,用于黑名单过滤和来源展示 | `"1.docx"` | | `collection` | str | 入库参数 | 所属向量库名,确保跨库可追溯 | `"public_kb"` | | `doc_type` | str | 文件扩展名映射 | 文档类型,驱动前端差异化溯源展示 | `"word"` | | `page` | int | MinerU 解析 | 起始页码(仅 PDF 可靠) | `5` | | `page_end` | int | MinerU 解析 | 结束页码 | `5` | | `section` | str | MinerU `section_path` | 章节路径,用于章节过滤、聚类救援、prompt 标题注入 | `"第一章 > 1.1 概述"` | | `status` | str | 默认 `"active"` | 切片状态,`deprecated` 时被引擎过滤 | `"active"` | | `version` | str | 默认 `"v1"` | 版本号,用于缓存失效 | `"v1"` | | `images_json` | str | `json.dumps(chunk.images)` | 关联图片列表的 JSON 序列化,图片选择时反序列化 | `'[{"id":"abc.jpg","order":1}]'` | | `image_path` | str | MinerU 解析 | 图片文件名(不含目录),用于图片 URL 生成 | `"0569dd285537.jpg"` | | `text_level` | int | MinerU 解析 | 标题级别(0=正文, 1=H1, 2=H2, 3=H3),用于层次感知检索 | `2` | | `bbox` | str | MinerU 解析(PDF) | 边界框 JSON 序列化 `[x0,y0,x1,y1]`,用于前端页内精准定位。仅 PDF 有值,DOCX 为 None 不存储 | `"[100,200,500,600]"` | | `table_type` | str | MinerU V2 解析 | 表格类型(cflow/table/text),用于区分表格渲染方式 | `"table"` | | `table_nest_level` | str | MinerU V2 解析 | 表格嵌套层级,标识嵌套表格的深度 | `"2"` | | `sub_type` | str | MinerU V2 解析 | 图片/图表子类型(natural_image/table_image/bar/line/bar_line 等),用于前端差异化展示 | `"bar"` | > **注意**:`chunk_id` 与 `ids` 重复存储。`ids` 是 ChromaDB 的主键,`chunk_id` 存在 metadata 中便于路由层按 metadata 查找。两者值相同但用途不同。 > **命名不一致**:MinerU 中字段名 `section_path`,入库时映射为 metadata 的 `section`。路由层代码中两种名称混用(`meta.get('section')` 和 `meta.get('section_path')`),通常用 `or` 兼容:`meta.get('section', '') or meta.get('section_path', '')`。 ### 3.3 chunk_type 取值 | chunk_type | 说明 | 检索行为 | 展示行为 | |-----------|------|---------|---------| | `text` | 普通文本 | 正常参与向量检索 | 正常展示 | | `heading` | 标题 | 正常参与向量检索 | 正常展示 | | `table` | 表格 | 走图片独立检索通道;有表格保护 | 受 `max_chunks` 豁免;可被表格救援找回 | | `image` | 图片 | 走图片独立检索通道 | 用 `full_description` 替换 doc | | `chart` | 图表 | 走图片独立检索通道 | 同 image | | `equation` | 公式 | 正常参与向量检索 | 正常展示 | | `faq` | FAQ | FAQ 独立检索通道 | 享受分数加权 | --- ## 四、BM25 索引两种实现 项目中存在两个 `BM25Index` 类,返回格式有重要差异。 ### 4.1 对比表 | 维度 | `core/bm25_index.py` | `knowledge/base.py` | |------|---------------------|---------------------| | **使用场景** | 单知识库路径(当前不执行) | 多知识库路径(当前活跃) | | **实例管理** | 全局单例 `RAGEngine.bm25_index` | 每个向量库独立实例 | | **持久化** | 单一 `bm25_index.pkl` | `{kb_name}.pkl` 每库独立 | | **search() 返回** | `Dict`(ChromaDB 兼容格式) | `Tuple[List, List, List, List]` | | **返回值解构** | `result['ids'][0]` | `ids, docs, metas, scores = bm25.search()` | | **列表嵌套** | `[['id1', 'id2']]`(嵌套列表) | `['id1', 'id2']`(扁平列表) | | **分数字段名** | `distances` | 第4个返回值(匿名) | | **add_documents 模式** | 替换(覆盖) | 追加 + 去重 | ### 4.2 兼容处理 多知识库路径中,`_search_multi_kb` 方法对 `knowledge/base.py` 的 tuple 返回值做了兼容转换: ```python # core/engine.py _search_multi_kb 中 if isinstance(bm25_res, tuple): _ids, _docs, _metas, _dists = bm25_res bm25_res = { 'ids': [_ids], 'documents': [_docs], 'metadatas': [_metas], 'distances': [_dists] } ``` > **设计建议**:两个 BM25Index 实现有重叠功能,未来可考虑统一为 dict 返回格式,消除兼容转换代码。 --- ## 五、检索管线完整数据流 ### 5.1 入口方法 ```python # api/chat_routes.py search_result = search_hybrid(retrieval_query, top_k, candidates, allowed_collections, sub_queries) # search_hybrid 内部调用 result = engine.search_knowledge(query, top_k, allowed_levels, collections, sub_queries) # 然后添加 scores 字段 ``` ### 5.2 search_knowledge 完整流程 ``` query (str) │ ├─ [1] 缓存检查 ──命中──→ 直接返回 │ ├─ [2] 外部子查询(IntentAnalyzer 生成的 sub_queries) │ → _search_with_sub_queries() → 合并 → 返回 │ ├─ [3] 查询拆分(QueryDecomposer 判断) │ → _search_with_decomposition() → 合并 → 返回 │ ├─ [4] 查询扩展(当前仅构建 expanded_queries,未用于多查询检索) │ ├─ [5] 分支:USE_MULTI_KB=True → _search_multi_kb() → 返回 │ USE_MULTI_KB=False → 单知识库路径(当前不执行) │ └─ 单知识库路径(以下步骤在 _search_multi_kb 中对称存在): │ ├─ [6] where 过滤构建(security_level / source) │ ├─ [7] 向量编码 → query_vector (768维) │ ├─ [8] 向量检索 → vector_results │ {ids:[[]], documents:[[]], metadatas:[[]], distances:[[]]} │ ├─ [9] 图片独立检索 → image_results → 合并 │ ├─ [10] FAQ 独立检索 → faq_results → 合并 │ ├─ [11] BM25 检索 → bm25_results │ + 捕获 BM25 原始 top-3 → _bm25_raw_top3 │ ├─ [12] 动态 RRF 权重 → (vector_w, bm25_w) │ ├─ [13] RRF 融合 → fused_results │ {distances:[[rrf_score]], '_score_source':'rrf'} │ ids 带 collection 前缀:"public_kb/filename_3" │ ├─ [14] 废止过滤 → 移除 status != "active" 的切片 │ ├─ [15] 枚举标记 → fused_results['_enum_query'] = bool │ ├─ [16] BM25 top3 传递 → fused_results['_bm25_top3'] │ ├─ [17] 章节过滤 → 按查询中的章节关键词过滤 │ ├─ [18] 上下文扩展 1(MMR前,min_score=0)→ 补齐邻居 │ ├─ [19] MMR 去重 → 缩减到 MMR_TOP_K=30 │ ├─ [20] Rerank 重排 → distances 变为 rerank 分数 [0,1] │ '_reranked'=True, '_score_source' 仍为 'rrf' │ ├─ [21] FAQ 加权 → FAQ 切片 distances -= 0.1 │ ├─ [22] 黑名单过滤 → 移除黑名单 source │ ├─ [23] 时间衰减 → 老 FAQ distances += decay │ ├─ [24] 章节聚类提升 → 低分但聚类的切片 distances = 0.65 │ ├─ [25] 上下文扩展 2(Rerank后,min_score=0.3)→ 高分种子邻居 │ ├─ [26] 自适应 TopK → 可能截断结果 │ ⚠️ 当前 bug: _score_source='rrf' 导致此步被跳过 │ └─ [27] 缓存写入 + 返回 ``` ### 5.3 特殊键在管线中的传递 以下键在过滤/截断方法中需要手动复制(否则会丢失): ```python for key in ('_debug', '_score_source', '_enum_query', '_expanded_context', '_bm25_top3'): if key in results: filtered[key] = results[key] ``` | 键 | 写入位置 | 读取位置 | 作用 | |----|---------|---------|------| | `_debug` | 每步追加 step | 返回给前端(dev 模式) | 检索调试信息 | | `_score_source` | RRF 融合写入 `'rrf'` | 自适应 TopK 判断 | 区分 distances 语义 | | `_enum_query` | 枚举检测后写入 | MMR lambda 选择、自适应 TopK | 枚举查询保留更多上下文 | | `_expanded_context` | 扩展时写入 | 返回给前端 | 扩展统计 | | `_bm25_top3` | BM25 检索后写入 | 路由层分歧检测救援 | BM25 原始 top-3 切片信息 | | `_reranked` | Rerank 后写入 | `search_hybrid` 中判断 | 区分 distances 是距离还是分数 | | `_cluster_boosted` | 聚类提升写入 meta | 上下文扩展时作为种子资格 | 标记被引擎层提升的切片 | ### 5.4 多知识库并发检索 `_search_multi_kb` 使用 `ThreadPoolExecutor` 并行查询各向量库: ```python with ThreadPoolExecutor(max_workers=len(target_collections)) as executor: futures = {executor.submit(_query_single_collection, name): name for name in target_collections} for future in as_completed(futures): coll_results, bm25_raw_items = future.result() all_results.extend(coll_results) _bm25_raw_top3.extend(bm25_raw_items) ``` 每个向量库独立查询:向量检索 + BM25 检索,结果标记 `_collection = coll_name`。 ### 5.5 子查询路径的 BM25 top3 传递 `_search_with_sub_queries` 和 `_search_with_decomposition` 合并结果时,会收集各子查询的 `_bm25_top3`,按 `bm25_score` 降序取全局 top-3,确保路由层分歧检测在子查询路径下也能工作。 --- ## 六、distances / scores 语义变化 `distances` 字段在管线各阶段含义不同,是理解检索逻辑的关键。 ### 6.1 语义变化表 | 阶段 | distances 含义 | 方向 | 范围 | 标记 | |------|---------------|------|------|------| | ChromaDB 向量检索 | cosine distance | 越小越好 | [0, 2] | - | | BM25 检索 | BM25 原始分数 | 越大越好 | [0, ∞) | - | | RRF 融合后 | RRF 分数 | 越大越好 | [0, ~0.05] | `_score_source='rrf'` | | Rerank 后 | CrossEncoder 相关性分数 | 越大越好 | [0, 1] | `_reranked=True` | | FAQ 加权后 | rerank 分数 - 0.1 | 越大越好 | [-0.1, 1] | - | | 时间衰减后 | 距离 + decay | 越大越差 | - | - | | 聚类提升后 | 被设为 `1.0 - CLUSTER_SEED_FLOOR` = 0.65 | 越大越好 | - | `_cluster_boosted=True` | ### 6.2 search_hybrid 中的 distances → scores 转换 ```python # api/chat_routes.py search_hybrid() if result.get('_reranked'): # Rerank 后:distances 就是相关性分数,直接使用 scores = [float(d) for d in distances] else: # 未 Rerank:将向量距离转为相似度分数 scores = [1.0 - d if d <= 1.0 else 1.0 / (1.0 + d) for d in distances] result['scores'] = [scores] ``` ### 6.3 自适应 TopK 中的距离→相似度转换 ```python # engine.py 第 790 行 top_score = 1.0 - fused_results['distances'][0][0] # 距离转相似度 ``` > **⚠️ 已知问题**:RRF+Rerank 后,`_score_source` 仍为 `'rrf'`,导致自适应 TopK 被跳过(第 788 行条件 `fused_results.get('_score_source') != 'rrf'` 不满足)。但此时 distances 已是有效的 rerank 分数,自适应 TopK 本应可以应用。此 bug 导致高置信度查询无法收缩结果,低置信度查询无法扩展。 --- ## 七、路由层处理流程 ### 7.1 generate_stream 完整流程 ``` 用户提问 (message) │ ├─ [0] 意图分析 → IntentAnalysis(rewritten_query, need_retrieval, use_context, sub_queries, intent) │ need_retrieval=False + use_context=True → 直接用历史上下文回答 → return │ ├─ [0.5] retrieval_query = intent.rewritten_query │ ├─ [1] 语义缓存检查 ──命中──→ 流式返回缓存答案 → return │ ├─ [2] 混合检索 → search_hybrid() → search_result │ ├─ [3] 构建 contexts 列表 │ 从 search_result 的 documents[0] / metadatas[0] / scores[0] 三数组构建 │ contexts = [{'doc': display_doc, 'meta': meta, 'score': score}, ...] │ 图片/图表切片:doc 替换为 meta.full_description(如果存在) │ ├─ [3.5] 补充检索:从文本切片提取图号/表号引用,补充检索缺失图片 │ ├─ [4] 懒加载增强(当前禁用) │ ├─ [5] 图片选择 → select_images() │ ├─ [6] 三重救援 │ ├─ _rescue_bm25_divergence(BM25 分歧检测救援) │ ├─ _rescue_lexical_match(词法匹配救援) │ └─ _rescue_section_cluster(章节聚类救援) │ ├─ [7] 排序 + 预算截断 │ _order_text_contexts_for_prompt() → min_score 过滤 → max_chunks 截断 │ ├─ [8] 表格救援 → _rescue_table_chunks() │ ├─ [9] Prompt 构建 │ ├─ 置信度分数(top-3 平均 rerank 分数) │ ├─ 图片描述注入 │ ├─ 意图驱动指令注入(对比/推理/操作/枚举) │ └─ 置信度指令注入 │ ├─ [10] LLM 流式生成 → engine.generate_answer_stream() │ └─ [11] 后处理 ├─ 答案对齐过滤(提取图号/表号,反向筛选图片) ├─ 去引用标记(移除 [1][2]) ├─ 附加引用标注([ref:chunk_id]) ├─ 敏感信息过滤 ├─ 保存会话 └─ 写入语义缓存 ``` ### 7.2 contexts 构建关键转换 ```python # search_result 格式(来自 search_hybrid) search_result = { 'documents': [[doc1, doc2, ...]], 'metadatas': [[meta1, meta2, ...]], 'scores': [[score1, score2, ...]], # search_hybrid 添加 'ids': [['public_kb/filename_3', ...]], # 多知识库模式带前缀 'distances': [[rerank_score, ...]], # 原始 distances 仍保留 '_bm25_top3': [{id, doc, meta, bm25_score, rank}, ...], '_debug': {...}, } # contexts 列表格式(路由层使用) contexts = [ {'doc': display_doc, 'meta': meta, 'score': score}, ... ] ``` > **注意**:contexts 中**没有 `id` 字段**。切片 ID 只能通过 `meta.chunk_id` 获取(格式如 `1.docx_14`),而 engine 的 `ids` 可能带 collection 前缀(如 `public_kb/1.docx_14`)。 ### 7.3 min_score 过滤 `RERANK_CONTEXT_MIN_SCORE = 0.05` 在 `_order_text_contexts_for_prompt` 中: - `score >= min_score` 的切片直接通过 - **表格保护**:同 section 有切片通过阈值时,同 section 的 table 切片保留下限为 `min_score * 0.3` = 0.015 --- ## 八、三重救援机制详解 三重救援是双层保护架构:**引擎层**在 rerank 后提升低分但可信的切片(分数较高),**路由层**在 min_score 过滤前做最终安全网(分数较低)。 ### 8.1 BM25 分歧检测救援 `_rescue_bm25_divergence` **触发条件**: - `BM25_DIVERGENCE_RESCUE_ENABLED = True` - `search_result._bm25_top3` 非空 - BM25 项的 `rank <= BM25_DIVERGENCE_MAX_RANK`(= 3) **保底分数**:`CLUSTER_RESCUE_FLOOR = 0.06` **两种情况**: | 情况 | 条件 | 处理 | 示例 | |------|------|------|------| | **A — 分数压制** | 切片在 contexts 中但 `score < min_score` | 提升 score 至 `CLUSTER_RESCUE_FLOOR` | q003: 0.0034 → 0.06 | | **B — 截断丢失** | 切片不在 contexts 中(被 rerank top_k 截断) | 从 `_bm25_top3` 备份注入新 context | q014: 不在 → 注入 score=0.06 | **保护机制**: - 仅救援 BM25 rank ≤ 3 的切片(top-3 是精确关键词匹配的强信号) - 救援分数固定为 `CLUSTER_RESCUE_FLOOR`(0.06),不会高于 rerank 正常通过的切片 - 匹配使用 `meta.chunk_id`(不带 collection 前缀),与 BM25 top3 的 `id` 字段匹配 **数据来源**:`_bm25_top3` 由 `engine.search_knowledge` 在 BM25 搜索后保存,格式为: ```python [{'id': '1.docx_14', 'doc': '...', 'meta': {...}, 'bm25_score': 12.4, 'rank': 1}, ...] ``` ### 8.2 词法匹配救援 `_rescue_lexical_match` **触发条件**: - contexts 非空且 retrieval_query 非空 - 清理后查询长度 ≥ 2 - 能提取 bigram **保底分数**:`CLUSTER_RESCUE_FLOOR = 0.06` **Phase 1 — 词法匹配救援**: 1. 清理查询(去除 markdown 格式和标点) 2. 提取查询 bigram 集合(连续两字组) 3. 对每个 `score < min_score` 的切片,计算 bigram 命中率 4. 命中率 > 0.35 → 提升 score 至 `max(原score, CLUSTER_RESCUE_FLOOR)` 5. 记录被救援的 `(source, chunk_index)` 作为种子 **Phase 2 — 邻居救援**: - 对每个词法匹配救援的种子,同时救援同 source 下 `chunk_index` 后续 8 个相邻切片 - 目的:枚举类问题的 header 切片被救援后,其后续子条目也应被保留 ### 8.3 章节聚类救援 `_rescue_section_cluster` **触发条件**: - `SECTION_CLUSTER_RESCUE_ENABLED = True` - 存在"全灭 section":某 section 的成员数 ≥ `CLUSTER_MIN_MEMBERS`(= 3),类型多样性 ≥ `CLUSTER_MIN_TYPES`(= 2),且所有成员 score < min_score **保底分数**:`CLUSTER_RESCUE_FLOOR = 0.06` **处理逻辑**: 1. 按 `(source, normalized_section)` 分组 2. 检测"全灭 section":所有成员 score < min_score 3. 计算聚类强度 = 成员数 × 类型多样性 × (1 + 查询匹配度) 4. 按强度降序,救援 top `CLUSTER_MAX_SECTIONS`(= 3)个 section 5. 每个 section 最多救援 `CLUSTER_MAX_RESCUE_PER_SECTION`(= 6)个切片 ### 8.4 引擎层 vs 路由层的双层保护 | 维度 | 引擎层 `_section_cluster_boost` | 路由层 `_rescue_section_cluster` | |------|-------------------------------|-------------------------------| | 执行时机 | Rerank 后、扩展前 | min_score 过滤前 | | 提升方式 | 修改 distances | 修改 contexts score | | 保底分数 | `CLUSTER_SEED_FLOOR = 0.35`(dist=0.65) | `CLUSTER_RESCUE_FLOOR = 0.06` | | 覆盖范围 | 聚类切片在 rerank 之前就能被后续步骤看到 | 最终安全网,确保不遗漏 | > **设计意图**:引擎层提升分数较高(0.35 对应 dist=0.65),使得这些切片能在后续的扩展步骤中被当作种子。路由层是最终安全网,分数较低(0.06)仅确保通过 min_score 过滤。 --- ## 九、图片召回与选择 ### 9.1 独立图片检索(P0 通道) 图片/图表切片走独立检索通道,不被文本切片挤占名额: ```python image_recall_k = max(5, top_k // 2) # 图片独立召回数量 image_results = _search_image_chunks(query_vector, image_recall_k, where_filter) ``` - 仅检索 `chunk_type` 为 `image`、`chart`、`table` 的切片 - 多知识库模式下对每个 collection 分别调用 - 图片结果与文本结果通过 `_merge_results()` 合并 ### 9.2 图片相关性提升(Boost) 在路由层对图片/图表切片做相关性评估: - **编号匹配**:查询提到"图2.1"且图片 caption 匹配 → boost_factor = 2.0 - **语义重叠**:caption 与查询有足够字符重叠 → boost_factor = 1.5 - Boost 以 `_image_boost` 标记记录在 metadata 中,不改变排序 ### 9.3 图片选择(`select_images`) 从召回结果中筛选最终展示给 LLM 的图片: 1. 动态预算:精确查图 2 张,有图片数据 5 张,有引用 3 张,默认 2 张 2. 对有 `image_path` 的切片用 `score_image_relevance` 打分 3. VLM 相关性筛选 + 章节关联检测 + 图号/表号匹配加分 4. 按分数降序取 top `MAX_IMAGES` ### 9.4 图片后置过滤 LLM 生成回答后,用回答内容反向过滤图片: - 提取回答关键词 - 检查每张图片描述与回答关键词的重叠度 - 超过阈值的保留;兜底:若全部被过滤则保留最高分 1 张 --- ## 十、返回格式与溯源信息 ### 10.1 SSE 事件序列 | 事件类型 | 说明 | |---------|------| | `intent_result` | 意图分析结果(仅 dev) | | `retrieval_debug` | 检索管线调试信息(仅 dev) | | `start` | 开始生成 | | `sources` | 检索来源列表 | | `chunks_retrieved` | 召回切片详情(仅 dev) | | `section_cluster_rescue` | 章节聚类救援(仅 dev) | | `chunk` | 每个 token(流式) | | `finish` | 完成事件(含完整回答和元数据) | ### 10.2 来源信息(sources) ```json { "source": "文件名.docx", "page": 12, "page_end": 14, "page_range": "12-14", "section": "第三章 > 第二节 > 小节名", "chunk_type": "text", "doc_type": "word", "section_chunk_id": 5, "score": 0.892 } ``` ### 10.3 引用格式(citations) `_attach_citations()` 自动插入 `[ref:chunk_id]` 标记: ``` 根据相关规定,安全检查应包括以下几个方面[ref:3.docx_154]... ``` --- ## 十一、配置项完整列表 ### 检索管线配置 | 配置项 | 默认值 | 作用 | 状态 | |-------|--------|------|------| | `USE_MULTI_KB` | `True` | 多向量库模式 | ✅ 活跃 | | `USE_HYBRID_SEARCH` | `True` | 向量+BM25混合检索 | ✅ 活跃 | | `USE_RERANK` | `True` | Rerank 重排 | ✅ 活跃 | | `RERANK_CANDIDATES` | `20` | Rerank 候选数 | ✅ 活跃 | | `RERANK_CONTEXT_MIN_SCORE` | `0.05` | 路由层最低分数阈值 | ✅ 活跃 | | `RERANK_BACKEND` | `"local"` | local/cloud/fallback | ✅ 活跃 | | `RERANK_USE_ONNX` | env("true") | ONNX 加速 | ✅ 活跃 | | `DYNAMIC_RRF_ENABLED` | `True` | 动态 RRF 权重 | ✅ 活跃 | | `RRF_K` | `60` | RRF 常数 | ✅ 活跃 | | `MMR_ENABLED` | `True` | MMR 去重 | ✅ 活跃 | | `MMR_USE_EMBEDDING` | env("false") | 高精度/轻量版 | ✅ 活跃 | | `MMR_TOP_K` | `30` | MMR 保留数量 | ✅ 活跃 | | `MMR_LAMBDA` | `0.5` | 相关性vs多样性 | ✅ 活跃 | | `ENUM_QUERY_MMR_LAMBDA` | `0.85` | 枚举查询 lambda | ✅ 活跃 | | `QUERY_EXPANSION_ENABLED` | `True` | 查询扩展 | ⚠️ 构建但未用于多查询 | | `SECTION_FILTER_ENABLED` | `True` | 章节过滤 | ✅ 活跃 | | `ADAPTIVE_TOPK_ENABLED` | `True` | 自适应 TopK | ⚠️ RRF+Rerank 后被跳过(bug) | | `CONTEXT_EXPANSION_ENABLED` | `True` | 上下文扩展 | ✅ 活跃 | | `CONTEXT_EXPANSION_BEFORE` | `1` | 向前扩展数 | ✅ 活跃 | | `CONTEXT_EXPANSION_AFTER` | `8` | 向后扩展数 | ✅ 活跃 | | `CONTEXT_EXPANSION_MAX_CHUNKS` | `50` | 最大扩展总数 | ✅ 活跃 | | `SECTION_CLUSTER_BOOST_ENABLED` | `True` | 引擎层聚类提升 | ✅ 活跃 | | `SECTION_CLUSTER_RESCUE_ENABLED` | `True` | 路由层聚类救援 | ✅ 活跃 | | `BM25_DIVERGENCE_RESCUE_ENABLED` | `True` | BM25 分歧救援 | ✅ 活跃 | | `BM25_DIVERGENCE_MAX_RANK` | `3` | 救援 BM25 排名阈值 | ✅ 活跃 | | `RAG_SEARCH_TOP_K` | `30` | 传给 search_hybrid 的 top_k | ✅ 活跃 | | `RAG_SEARCH_CANDIDATES` | `100` | 传给 search_hybrid 的 candidates | ❌ 不传递给 engine | | `ENABLE_WEB_SEARCH` | `False` | 网络搜索 | ❌ 预留接口,未实现 | | `VECTOR_WEIGHT` / `BM25_WEIGHT` | `0.5` | 静态 RRF 权重 | ⚠️ 动态 RRF 启用时被覆盖 | ### 缓存配置 | 配置项 | 默认值 | 作用 | 状态 | |-------|--------|------|------| | `QUERY_CACHE_ENABLED` | `True` | 查询缓存 | ✅ 活跃 | | `EMBEDDING_CACHE_ENABLED` | `True` | Embedding 缓存 | ✅ 活跃 | | `RERANK_CACHE_ENABLED` | `True` | Rerank 分数缓存 | ✅ 活跃 | | `SEMANTIC_CACHE_ENABLED` | `True` | 语义缓存 | ✅ 活跃 | ### 聚类/救援配置 | 配置项 | 默认值 | 作用 | 状态 | |-------|--------|------|------| | `CLUSTER_MIN_MEMBERS` | `3` | 触发聚类最小成员数 | ✅ 活跃 | | `CLUSTER_MIN_TYPES` | `2` | 触发聚类最小类型数 | ✅ 活跃 | | `CLUSTER_SEED_FLOOR` | `0.35` | 引擎层聚类提升阈值 | ✅ 活跃 | | `CLUSTER_RESCUE_FLOOR` | `0.06` | 路由层救援保底分数 | ✅ 活跃 | | `CLUSTER_MAX_BOOST_PER_SECTION` | `8` | 引擎层每 section 最大提升数 | ✅ 活跃 | | `CLUSTER_MAX_SECTIONS` | `3` | 全局最大提升 section 数 | ✅ 活跃 | | `CLUSTER_MAX_RESCUE_PER_SECTION` | `6` | 路由层每 section 最大救援数 | ✅ 活跃 | | `CLUSTER_SECTION_PREFIX_LEVELS` | `2` | section 归一化层级(按章节前两级分组) | ✅ 活跃 | ### LLM 预算配置 | 配置项 | 默认值 | 作用 | 状态 | |-------|--------|------|------| | `MAX_LLM_CALLS_PER_QUERY` | `2` | 每查询 LLM 调用上限 | ⚠️ 仅 llm_budget.py 使用(模块未集成到主流程) | | `MAX_QUERY_REWRITES` | `1` | 查询改写上限 | ⚠️ 同上 | --- ## 十二、单/多知识库路径说明 ### 12.1 当前配置 `USE_MULTI_KB = True`(硬编码在 `config.py`) **效果**:`search_knowledge` 在第 612 行直接分支到 `_search_multi_kb()`,跳过第 628-811 行的单知识库路径。 ### 12.2 两条路径对比 | 维度 | 单知识库路径 | 多知识库路径 | |------|------------|------------| | 向量库 | `self.collection` | `self.kb_manager.get_collection(coll_name)` | | BM25 | `self.bm25_index`(core/bm25_index.py) | `kb_manager.get_bm25_index(coll_name)`(knowledge/base.py) | | 并发 | 无 | `ThreadPoolExecutor` 并行查各库 | | RRF 权重 | `[VECTOR_WEIGHT, BM25_WEIGHT]` | `[vector_w, bm25_w, ...]` 交替 | | ID 前缀 | 无 | 带 collection 前缀 | | 安全过滤 | ChromaDB where 过滤 | source 过滤 | ### 12.3 单知识库路径保留原因 - 逻辑与多知识库路径对称,作为回退方案 - 小规模部署可能不需要多知识库 - `self.bm25_index`(core/bm25_index.py)仅在此路径使用 ### 12.4 独立检索路径 `knowledge/search.py` 的 `SearchMixin` 和 `SearchResult` 是独立于 engine 的检索路径: - 被 `knowledge/router.py` 使用(知识库路由推荐功能) - **不走 engine 主流程** - 返回 `SearchResult` dataclass(扁平列表格式),与 engine 的 ChromaDB 格式不兼容 --- ## 十三、切片策略与检索策略兼容性分析 本节基于 `public_kb` 三份文档(1.docx / 2.docx / 3.docx)的实际切片数据,分析切片策略与检索管线各环节的搭配情况,识别已确认的失配点并提出优化方向。 ### 13.1 public_kb 切片统计 | 指标 | 1.docx | 2.docx | 3.docx | |------|--------|--------|--------| | 总切片数 | 65 | 193 | 411 | | 文本切片 | 60 | 144 | 385 | | 表格切片 | 3 | 49 | 21 | | 图片切片 | 2 | 0 | 5 | | 纯标题切片(<20字) | 16 (27%) | 61 (42%) | 97 (25%) | | 正文碎片(<100字) | 2 | 4 | 5 | | 文本中位长度 | 122字 | 24字 | 66字 | | 文本最大长度 | 725字 | 798字 | 757字 | | 连续标题链(≥2个) | 3条 | 22条 | 34条 | **关键发现**:2.docx 的纯标题切片占比高达 42%,3.docx 有 34 条连续标题链(其中一条包含 21 个连续标题——附件列表)。这些标题切片不包含实质内容,但在向量库中占据存储空间,并参与检索管线的所有阶段。 ### 13.2 切片策略概述 当前切片策略以**标题层级**为核心拆分依据: ``` MinerU 解析 → content_list → 逐项构建 MinerUChunk → heading_rules 引擎识别标题级别(text_level) → 每个标题开始新的 section_path → _post_process_chunks 三阶段后处理: Phase 1: 过滤空切片 Phase 2: 合并碎片(标题+正文、短文本 < min_merge_size=100) Phase 3: 拆分超长(> max_chunk_size=1000) ``` **合并规则**: - 标题 chunk(`text_level > 0`)刷新缓冲并开始新合并组 - 正文 chunk 若 `< min_merge_size`(100字)则并入缓冲 - 正文 chunk 若 `≥ min_merge_size` 且缓冲已满则直接输出 - 合并上限 `max_merged_size = 800` 字 **后果**:每个标题级别(H1/H2/H3)的段落都会产生独立的 chunk。当标题下方没有正文(或正文在子标题下)时,产生"纯标题切片"。 ### 13.3 已确认的失配点 #### 失配 1:纯标题切片的语义内容冗余(严重) **问题**:纯标题切片存入 ChromaDB 的 `documents` 字段是同一文本的三次重复: ``` 三、做好货源投放前的基础工作 ← title(text_level > 0 时追加) 主题:三、做好货源投放前的基础工作 ← section_path(最多3级) 三、做好货源投放前的基础工作 ← content(原始文本) ``` 生成代码见 `knowledge/base.py:231-254`(`_build_semantic_content_for_text`)。 **影响**: - Embedding 向量几乎无区分度——所有标题的向量高度相似 - 向量检索几乎不会命中这些切片(内容查询与重复标题的相似度极低) - 浪费向量库存储空间(2.docx 有 61 个此类切片) #### 失配 2:上下文扩展的 section 精确匹配(严重) **问题**:`_expand_contiguous_chunks`(`engine.py:1334`)在 section 过滤时使用**精确匹配**: ```python where_filter = {"$and": [{"source": source}, {"section": section}]} ``` 当种子是父级标题(如 `section = "三、做好货源投放前的基础工作"`)时: - 子标题下的正文切片 `section = "三 > (一)货源投放要求 > ..."` **不匹配** - 回退到 source-only 模式,从整个文档中拉取相邻 `chunk_index` 的切片 - 对大文档,回退模式可能拉取到不相关的切片 **影响**:父级标题被检索到时,无法通过 section 过滤拉取其子节内容,降低了上下文完整性。 #### 失配 3:~~`text_level` 未传入检索管线~~ ✅ 已修复 **原问题**:`text_level`(标题级别 0/1/2/3)在切片阶段被精确计算,但未存入 ChromaDB metadata,检索管线无法感知标题层级。 **修复**:`knowledge/manager.py` 的 metadata 构建处已新增 `text_level` 字段: ```python metadata = { ... 'text_level': getattr(chunk, 'text_level', 0), # 已新增 } ``` **当前状态**:`text_level` 已存入 ChromaDB metadata,为后续层次感知检索提供了数据基础。引擎层和路由层尚未利用此字段做特殊处理(如标题切片降权、导航锚点扩展),但数据层已就绪。 #### 失配 4:~~章节聚类归一化粒度过粗~~ ✅ 已调整 **原问题**:`CLUSTER_SECTION_PREFIX_LEVELS = 1` 将 section_path 归一化到第一级,不同二级章节的切片被归入同一聚类组,可能触发误提升。 **修复**:`config.py` 中已将 `CLUSTER_SECTION_PREFIX_LEVELS` 从 `1` 调整为 `2`: ```python CLUSTER_SECTION_PREFIX_LEVELS = 2 # section_path 归一化保留的层级数(按章节前两级分组,提升聚类精确度) ``` **当前状态**:归一化到前两级(如 `(二)货源投放要求 > 7.关于主导品规投放`),聚类信号精确度提升。对深层嵌套文档(3 级及以上 section)的影响已通过 `CLUSTER_MAX_SECTIONS=3` 和 `CLUSTER_MIN_TYPES=2` 约束。 #### 失配 5:预算构建器中标题切片的开销(轻微) **问题**:`_build_context_with_budget`(`chat_routes.py:866`)按 `(source, section)` 分组后,每个组添加 `━ {section} ━` 分隔行。纯标题切片形成单例组: ``` ━ 三、做好货源投放前的基础工作 ━ ← ~25字符开销 三、做好货源投放前的基础工作 ← ~14字符内容(信息量≈0) ``` **影响**:约 40 字符的预算被浪费在无信息量的组上。在 `CONTEXT_MAX_CHARS = 8000` 的预算下,少量标题切片影响可控,但 2.docx 的 61 个标题切片如果被拉入就会累积显著开销。 #### 失配 6:MMR 去重对标题切片的过度消除(模式相关) **问题**:Embedding-based MMR(`MMR_USE_EMBEDDING=True`)模式下,标题切片因 Embedding 高度相似而相互惩罚。Jaccard 模式(`MMR_USE_EMBEDDING=False`,当前设置)因词级分词有一定区分度,问题较轻。 **影响**:当标题切片恰好是某子章节的唯一入口时,被 MMR 消除后该子章节在检索结果中完全丢失。当前使用 Jaccard 模式,此问题暂未触发。 #### 失配 7:~~3.docx 的 section_path 污染~~ ✅ 已修复 **原问题**:3.docx 中存在一个完整的合同条款段落被误识别为 H1 标题,导致 section_path 极长且无语义意义,该 section 下 59 个切片分组失真。 **修复**:`parsers/heading_rules.py` 中新增了两层防护: 1. **`_validate_level` 超长文本降级**:H1 > 40字、H2 > 60字、H3 > 50字时自动降为正文(level=0)。覆盖所有返回路径(v1 常规匹配、v2 style 匹配、bold_short_text 兜底)。 2. **规则级 `max_length` + `exclude_pattern`**:部分 H1 规则(如 `numeric_level1`)设置 `max_length=50` 和排除句末标点(`;;。,、::`)的 `exclude_pattern`,在匹配阶段即过滤段落文本。 **当前状态**:长段落不再被误判为标题,section_path 污染问题已消除。 ### 13.4 切片-检索兼容性总结 | 失配点 | 严重度 | 影响范围 | 现状 | |--------|--------|---------|------| | 标题语义内容冗余 | 🔴 严重 | 全部文档 | 670 个向量库切片中约 174 个是纯标题 | | section 精确匹配 | 🔴 严重 | 父级标题检索 | 回退到 source-only,可能拉取不相关内容 | | ~~text_level 未传入~~ | ~~🟠 显著~~ | ~~全局~~ | ✅ 已修复:text_level 已存入 ChromaDB metadata | | ~~聚类归一化过粗~~ | ~~🟡 中等~~ | ~~聚类提升/救援~~ | ✅ 已修复:CLUSTER_SECTION_PREFIX_LEVELS 调整为 2 | | 预算构建开销 | 🟢 轻微 | 上下文构建 | 标题单例组浪费字符预算 | | MMR 过度消除 | 🟢 模式相关 | MMR 去重 | 当前 Jaccard 模式暂未触发 | | ~~section_path 污染~~ | ~~🟡 中等~~ | ~~3.docx~~ | ✅ 已修复:heading_rules 超长文本降级防护 | ### 13.5 优化建议 #### 短期(改动小,收益明确) **1. ~~后处理合并连续标题链~~ ✅ 已实施** 已在 `_post_process_chunks` Phase 2 中实现连续标题链合并: - **H1 级标题是章节边界**,强制断开,不参与链合并 - **非 H1 连续标题**(H2/H3)合并为单个 chunk,合并后取更高层级(数值更小),保留第一个标题的 section_path - 合并上限仍为 `max_merged_size = 800` 字 **2. ~~存储 `text_level` 到 ChromaDB metadata~~ ✅ 已实施** `knowledge/manager.py` 中已新增 `text_level` 字段存储,为后续层次感知检索提供数据基础。 **3. 后处理合并标题与首个子节正文** 当一个标题切片的下一个切片是子标题(更高 `text_level` 数值)下的正文时,将标题文本前置到子节切片中: ``` 当前:#7 [H2] "四、相关的指标与分类"(10字) → #8 [H2] "(一)货源属性分类\n1.紧俏品规..."(349字) 优化:#7 [H2] "四、相关的指标与分类\n(一)货源属性分类\n1.紧俏品规..."(359字) ``` 预期收益:消除父级标题的孤立切片,同时为子节切片提供上层上下文。 #### 中期(需要评测验证) **4. 上下文扩展支持 section 前缀匹配** 将 `_expand_contiguous_chunks` 的 section 过滤从精确匹配改为前缀匹配: ```python # 当前:精确匹配 {"section": section} # 优化:前缀匹配(拉取所有子节切片) {"section": {"$regex": f"^{re.escape(section)}"}} ``` 需评估:前缀匹配可能拉取过多切片,需要配合 `CONTEXT_EXPANSION_MAX_CHUNKS` 上限控制。 **5. ~~调整 `CLUSTER_SECTION_PREFIX_LEVELS`~~ ✅ 已实施** 已从 `1` 调整为 `2`,聚类信号精确度提升。 #### 长期(架构级改进) **6. 层次感知检索** 利用已存入 metadata 的 `text_level` 实现结构感知的检索策略: - 标题切片作为"导航锚点",被检索到时自动拉取其 section_path 前缀下的所有子切片 - MMR 去重时对标题切片设置保护阈值,确保每个主要章节至少保留一个入口 - 预算构建器对标题单例组做特殊处理(合并到子节组或跳过) **7. ~~修复 section_path 污染~~ ✅ 已实施** `parsers/heading_rules.py` 中 `_validate_level` 方法已实现超长文本降级防护(H1>40字、H2>60字、H3>50字降为正文)。 **8. bbox 空间感知检索**(新增) 利用已存入 metadata 的 `bbox` 坐标实现空间感知的检索策略: - 同一页面相邻区域的切片在检索时可做空间聚类 - 图片/图表切片的 bbox 可用于判断其在文档中的物理位置关系 - 仅 PDF 切片有 bbox,DOCX 切片无此信息 **9. VLM 描述增强图片检索**(新增) 利用已存入 MinerUChunk 的 `vlm_description` 和 `chart_markdown` 字段: - 将 VLM 视觉描述注入图片切片的 semantic_content,提升向量检索的语义匹配度 - chart_markdown(图表数据表)可增强图表切片的 BM25 关键词匹配 - 仅云端 MinerU V4 VLM 后端提供,本地 pipeline 后端无此信息 --- ## 十四、MinerU 解析策略 ### 14.1 云端优先 + 本地备选 当前解析策略为**云端 MinerU V4 API 优先,本地 MinerU 备选**: | 维度 | 云端 MinerU V4 | 本地 MinerU | |------|---------------|------------| | **入口** | `parse_with_mineru_online()` | `parse_with_mineru()` | | **后端** | VLM(视觉语言模型) | pipeline(传统 OCR) | | **解析速度** | ~5 秒(含网络) | 取决于 GPU/CPU | | **V2 数据丰富度** | 丰富:bbox、title_content、VLM 描述、list_items、sub_type | 基础:无 bbox、无 title type、无 VLM 描述 | | **配置项** | `MINERU_PREFER_ONLINE=True` | `MINERU_LOCAL_BACKEND="pipeline"` | | **回退逻辑** | 失败自动回退本地 | 最终回退方案 | **调度流程**: ``` parse_with_mineru_persistent() → config.MINERU_PREFER_ONLINE=True → parse_with_mineru_online() ← 优先 → 成功 → 返回 → 失败 → parse_with_mineru() ← 本地回退(pipeline 后端) ``` ### 14.2 V2 content_list 解析 `_parse_v2_content_list()` 负责将 MinerU V2 格式的 content_list 转换为 MinerUChunk 列表。V2 是 MinerU 的结构化输出格式,比 V1 包含更丰富的元数据。 **V2 与 V1 的关键差异**: | 维度 | V2 格式 | V1 格式 | |------|---------|---------| | 标题文本 | `title_content` 字段 | `text` 字段 | | 图片路径 | `image_source.path` | `img_path` | | VLM 描述 | `content.content` | 无 | | 列表 | `list_items` 数组 | 无(直接文本) | | 表格标题 | `table_caption`(列表格式) | `table_caption`(字符串) | | bbox | 所有元素都有 | 仅部分元素 | **V2 解析已处理的类型**: - `title`:读取 `title_content`(PDF),回退 `paragraph_content`(DOCX) - `paragraph`:读取 `paragraph_content` - `table`:提取 `table_caption`(列表格式解析)、`table_footnote`、`image_source.path`、`table_nest_level` - `image` / `chart`:提取 `image_source.path`(回退 `img_path`)、VLM 描述、chart_markdown、`sub_type`、caption(列表格式) - `equation`:读取 `text` - `list`:拼接 `list_items` 为段落文本 ### 14.3 DOCX vs PDF 后端差异 云端 MinerU 对 DOCX 和 PDF 使用不同的解析后端,导致 V2 输出有显著差异: | 特征 | DOCX(office 后端) | PDF(hybrid/VLM 后端) | |------|---------------------|----------------------| | **bbox** | ❌ 无 | ✅ 所有元素都有 | | **title type/level** | ❌ 无(仅 bold style) | ✅ 有(H1/H2/H3) | | **VLM 描述** | ❌ 无 | ✅ 图片/图表有视觉描述 | | **chart_markdown** | ❌ 无 | ✅ 图表有数据表 Markdown | | **list_items** | ❌ 无 | ✅ 结构化列表 | | **sub_type** | ❌ 无 | ✅ natural_image/bar/line 等 | | **title 字段名** | `paragraph_content` | `title_content` | | **图片路径** | `image_source.path` | `image_source.path` | **策略**:DOCX 的标题层级由本地 `heading_rules.py` 引擎基于文本模式补充推断;PDF 的标题层级直接使用 MinerU 提供的 `text_level`。