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:
lacerate551
2026-06-19 23:56:13 +08:00
parent e205eda6a8
commit ee5295cc97
7 changed files with 1193 additions and 33 deletions

View File

@@ -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` 字段是同一文本的三次重复:
```
三、做好货源投放前的基础工作 ← titletext_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 个标题切片如果被拉入就会累积显著开销。
#### 失配 6MMR 去重对标题切片的过度消除(模式相关)
**问题**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 切片有 bboxDOCX 切片无此信息
**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 输出有显著差异:
| 特征 | DOCXoffice 后端) | PDFhybrid/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`