fix(parser): PDF/DOCX 切片层级结构修复与 MinerU V2 解析增强
核心修复: - _post_process_chunks: 用 _buffer_has_body 标志替代 buffer.text_level=0, 标题+正文合并后保留 text_level 不置零,修复层级信息丢失 - heading_rules numeric_level2: 正则从 ^\d+\.\d+[\.、\s] 改为 ^\d+\.\d+(?!\.\d),修复无空格标题如"2.1运行调度"无法匹配 - V2 title handler: heading_rules 优先(模式匹配可靠),VLM 仅兜底 (VLM 常给所有标题 level=1) MinerU V2 解析增强: - 两轮 TOC 过滤:多行目录块检测 + 孤立标题/单字符残留清理 - 封面 logo 过滤、重复标题去重 - chart VLM 描述和 Markdown 数据表提取 - ChromaDB metadata 新增 text_level/bbox/table_type/sub_type 字段 配置调整: - CLUSTER_SECTION_PREFIX_LEVELS 1→2(两级 section 聚类更精确) - MINERU_LOCAL_BACKEND 默认改为 pipeline 文档: - RAG检索流程逻辑.md 更新 MinerUChunk 字段、ChromaDB metadata、 MinerU 解析策略等章节 - 新增 RAG引用跳转-优化计划.md、云端MinerU输出分析与优化方案.md
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
10. [返回格式与溯源信息](#十返回格式与溯源信息)
|
||||
11. [配置项完整列表](#十一配置项完整列表)
|
||||
12. [单/多知识库路径说明](#十二单多知识库路径说明)
|
||||
13. [切片策略与检索策略兼容性分析](#十三切片策略与检索策略兼容性分析)
|
||||
14. [MinerU 解析策略](#十四mineru-解析策略)
|
||||
|
||||
---
|
||||
|
||||
@@ -121,6 +123,13 @@ class MinerUChunk:
|
||||
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 生成逻辑
|
||||
@@ -210,6 +219,11 @@ ChromaDB 使用 `collection.add(ids, documents, metadatas, embeddings)` 批量
|
||||
| `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 查找。两者值相同但用途不同。
|
||||
|
||||
@@ -730,7 +744,7 @@ LLM 生成回答后,用回答内容反向过滤图片:
|
||||
| `CLUSTER_MAX_BOOST_PER_SECTION` | `8` | 引擎层每 section 最大提升数 | ✅ 活跃 |
|
||||
| `CLUSTER_MAX_SECTIONS` | `3` | 全局最大提升 section 数 | ✅ 活跃 |
|
||||
| `CLUSTER_MAX_RESCUE_PER_SECTION` | `6` | 路由层每 section 最大救援数 | ✅ 活跃 |
|
||||
| `CLUSTER_SECTION_PREFIX_LEVELS` | `1` | section 归一化层级 | ✅ 活跃 |
|
||||
| `CLUSTER_SECTION_PREFIX_LEVELS` | `2` | section 归一化层级(按章节前两级分组) | ✅ 活跃 |
|
||||
|
||||
### LLM 预算配置
|
||||
|
||||
@@ -772,3 +786,287 @@ LLM 生成回答后,用回答内容反向过滤图片:
|
||||
- 被 `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`。
|
||||
|
||||
Reference in New Issue
Block a user