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

@@ -0,0 +1,303 @@
## RAG 引用溯源跳转 — 优化计划
本文档基于 dev-ui 前端Vue3 + NaiveUI和 chat_routes.py 后端的现有实现,分析引用跳转功能的优化方向。供讨论后实施。
---
### 一、现有实现现状
#### 1.1 数据流
```
SSE finish 事件 → citations[] 数组_build_citation 构建)
Chat.vue extractCitations() → 解析 [ref:chunk_id] → 渲染为 <sup>[N]</sup>
用户点击引用项 → jumpToChunk(citation)
打开 1200px 分屏弹窗:
├─ 左侧PdfViewer / DocxViewer / TextViewer按 doc_type 选择)
└─ 右侧:切片上下文列表(/documents/preview API 返回)
弹窗内切换切片 → navigateToChunk(chunk) → 更新 viewer 响应式 prop
```
#### 1.2 各组件当前能力
| 组件 | 文件 | 当前能力 | 局限 |
|------|------|---------|------|
| **PdfViewer** | `viewers/PdfViewer.vue` | pdfjs-dist 渲染,`initialPage` prop 页码跳转watch 响应外部页码变化 | 仅页级跳转,无页内 bbox 高亮;单页渲染(非连续滚动) |
| **DocxViewer** | `viewers/DocxViewer.vue` | mammoth 转 HTML2-gram Jaccard 段落匹配,`<mark>` 高亮 + scrollIntoView | 无法精确定位到文本范围(仅段落级);大文档性能未测试 |
| **Chat.vue** | `views/Chat.vue` | jumpToChunk 调用 preview API 获取完整 chunk text 替代 300 字截断 | preview API 失败时回退到截断文本,匹配精度下降 |
| **_build_citation** | `api/chat_routes.py:1147` | 按 doc_type 构建定位信息PDF 含 bbox/pageWord 含 section_chunk_id | bbox 已返回但前端未使用content 截断 300 字 |
#### 1.3 后端已提供但前端未消费的字段
| 字段 | 后端来源 | 当前状态 |
|------|---------|---------|
| `bbox` | ChromaDB metadata → `_build_citation` 解析 JSON → 返回数组 | PdfViewer 未接收,未渲染高亮 |
| `sub_type` | ChromaDB metadataMinerU V2 解析) | `_build_citation` 未返回此字段 |
| `table_type` | ChromaDB metadataMinerU V2 解析) | `_build_citation` 未返回此字段 |
| `vlm_description` | MinerUChunkVLM 视觉描述) | 未进入 citation 数据 |
| `chart_markdown` | MinerUChunk图表数据表 | 未进入 citation 数据 |
---
### 二、优化项清单
#### Phase 1PDF 页内 bbox 高亮(核心优化)
**目标**:在 PDF 页码跳转的基础上,叠加半透明矩形标注引用区域。
**涉及文件**
- `dev-ui/src/components/viewers/PdfViewer.vue`(主要改动)
- `dev-ui/src/views/Chat.vue`(传递 bbox prop
- `api/chat_routes.py`无需改动bbox 已返回)
**PdfViewer.vue 改动方案**
1. 新增 `highlightBbox` prop
```js
const props = defineProps({
url: { type: String, required: true },
initialPage: { type: Number, default: 1 },
highlightBbox: { type: Array, default: null } // [x0, y0, x1, y1] 或 null
})
```
2.`renderPage()``page.render()` 完成后叠加绘制高亮矩形:
```js
async function renderPage() {
// ... 现有的 canvas 渲染逻辑 ...
await page.render({ canvasContext: ctx, viewport }).promise
// bbox 高亮叠加
if (props.highlightBbox && currentPage.value === props.initialPage) {
drawBboxHighlight(ctx, viewport, props.highlightBbox)
}
}
```
3. 新增 `drawBboxHighlight()` 函数:
```js
function drawBboxHighlight(ctx, viewport, bbox) {
// MinerU bbox 坐标系PDF 原始坐标,原点左下角
// PDF.js viewport原点左上角已通过 getViewport 转换
const [x0, y0, x1, y1] = bbox
// 方式1直接传 viewport 转换(如果 bbox 是 PDF 用户空间坐标)
const topLeft = viewport.convertToViewportPoint(x0, y1) // 注意 y 翻转
const bottomRight = viewport.convertToViewportPoint(x1, y0)
const w = bottomRight[0] - topLeft[0]
const h = bottomRight[1] - topLeft[1]
// 半透明填充
ctx.fillStyle = 'rgba(99, 102, 241, 0.2)'
ctx.fillRect(topLeft[0], topLeft[1], w, h)
// 边框
ctx.strokeStyle = 'rgba(99, 102, 241, 0.6)'
ctx.lineWidth = 2
ctx.strokeRect(topLeft[0], topLeft[1], w, h)
}
```
4. watch `highlightBbox` 变化时重新渲染(切换切片时):
```js
watch(() => props.highlightBbox, () => {
if (pdfDoc) renderPage()
})
```
**Chat.vue 改动方案**
1. 新增响应式变量 `citationBbox`
2. `jumpToChunk()` 中从 citation 取 bbox`citationBbox.value = citation.bbox || null`
3. `navigateToChunk()` 中从 chunk metadata 取 bbox`citationBbox.value = chunk.metadata?.bbox ? JSON.parse(chunk.metadata.bbox) : null`
4. 模板中传递给 PdfViewer`<PdfViewer :highlight-bbox="citationBbox" ...>`
**风险点**
- MinerU 的 bbox 坐标系需确认。PDF 标准坐标系原点左下角、y 向上PDF.js viewport 默认原点左上角、y 向下。`viewport.convertToViewportPoint` 应能处理转换,但需实际测试坐标是否正确。
- 部分切片可能没有 bboxDOCX 来源、或 MinerU 解析遗漏),需做空值保护。
- bbox 可能是页面坐标系0-1 归一化)或绝对像素坐标,需确认 MinerU 输出格式。
---
#### Phase 2后端 citation 数据增强
**目标**:让前端获得更多结构化信息,支持差异化渲染和更精准的定位。
**涉及文件**
- `api/chat_routes.py``_build_citation()` 函数
**改动方案**
`_build_citation()` 的通用字段部分新增:
```python
citation = {
# ... 现有字段 ...
# 新增MinerU 结构化元数据
"sub_type": meta.get('sub_type', ''), # 图片/图表子类型
"table_type": meta.get('table_type', ''), # 表格类型
"chunk_type": meta.get('chunk_type', 'text'), # 已有,但确保传递
}
```
在 PDF 分支中新增 bbox 的 page 关联信息(当前已有 bbox 和 page无需额外改动
在 Word 分支中新增更长的 content 截断:
```python
elif doc_type == 'word':
citation.update({
"section_chunk_id": meta.get('section_chunk_id'),
"content": (full_content or meta.get('preview', ''))[:500], # 300→500提升 DOCX 匹配精度
})
```
**收益**
- `sub_type` 可让前端在引用列表中区分图表类型bar/line/natural_image
- `table_type` 可让前端判断是否用表格样式展示
- Word content 截断从 300→500 字,减少 preview API 依赖,提升 fallback 场景下的匹配精度
---
#### Phase 3引用列表差异化展示
**目标**:在消息底部的引用列表中,根据 chunk_type 和 sub_type 显示类型图标/标签,提升可读性。
**涉及文件**
- `dev-ui/src/views/Chat.vue`(模板和 CSS
**改动方案**
在引用列表项(`.citation-item`)中增加类型标签:
```html
<div v-for="c in group.items" :key="c.displayIndex" class="citation-item clickable" @click="jumpToChunk(c)">
<span class="citation-num">[{{ c.displayIndex }}]</span>
<!-- 新增:类型标签 -->
<n-tag v-if="c.chunk_type === 'table'" size="tiny" :bordered="false" type="warning">表格</n-tag>
<n-tag v-else-if="c.chunk_type === 'image' || c.chunk_type === 'chart'" size="tiny" :bordered="false" type="info">
{{ c.sub_type || '图片' }}
</n-tag>
<span v-if="c.page" class="citation-location">第{{ c.page }}页</span>
<span v-if="c.section" class="citation-section">{{ c.section }}</span>
</div>
```
**效果**:用户可以一眼看出引用的是文本、表格还是图表,点击前有预期。
---
#### Phase 4弹窗内图表/表格的增强展示
**目标**:在引用预览弹窗的右侧切片面板中,对表格和图片/图表类型的切片提供差异化展示。
**涉及文件**
- `dev-ui/src/views/Chat.vue`(切片面板模板)
- 可选:`dev-ui/src/components/viewers/` 下新增或复用 viewer
**改动方案**
1. 表格切片(`chunk_type === 'table'`):如果切片 metadata 中有 `table_html`,渲染为 HTML 表格而非纯文本
2. 图片/图表切片(`chunk_type === 'image' || 'chart'`):显示 `image_path` 对应的图片缩略图
3. 图表数据预览:如果 `chart_markdown` 可用,在图表切片下方展示 Markdown 数据表
```html
<div class="citation-chunk" :class="{ target: chunk.is_target }">
<div class="chunk-header">
<!-- 现有 header 内容 -->
<n-tag v-if="chunk.metadata?.chunk_type === 'table'" size="tiny" type="warning">表格</n-tag>
<n-tag v-if="chunk.metadata?.sub_type" size="tiny" type="info">{{ chunk.metadata.sub_type }}</n-tag>
</div>
<!-- 表格:尝试渲染 HTML -->
<div v-if="chunk.metadata?.chunk_type === 'table' && chunk.metadata?.table_html"
class="chunk-table" v-html="chunk.metadata.table_html" />
<!-- 图片/图表:显示缩略图 -->
<div v-else-if="['image','chart'].includes(chunk.metadata?.chunk_type) && chunk.metadata?.image_path"
class="chunk-image">
<img :src="`/api/documents/${citationPreviewSource}/image/${chunk.metadata.image_path}`" />
</div>
<!-- 默认:纯文本 -->
<div v-else class="chunk-text">{{ chunk.document || chunk.content }}</div>
</div>
```
**注意**preview API 返回的 chunk metadata 中是否包含 `table_html``image_path` 等字段,需确认后端 `/documents/<path>/preview` 接口是否透传了这些字段。如果未透传,需要后端配合补充。
---
#### Phase 5DOCX 多段落高亮(可选)
**目标**:当一个切片内容跨多个段落时,高亮所有匹配段落而非仅最佳匹配的一个。
**涉及文件**
- `dev-ui/src/components/viewers/DocxViewer.vue`
**改动方案**
修改 `highlightAndScroll()` 中的选择逻辑,从"选最佳单个"改为"选所有超过阈值的"
```js
// 当前:只保留 bestEl
if (score > bestScore) { bestScore = score; bestEl = block }
// 优化:收集所有超过阈值的匹配
const SCORE_THRESHOLD = 30
const matches = []
// ... 遍历后 ...
for (const block of blocks) {
// ... 计算 score ...
if (score >= SCORE_THRESHOLD) {
matches.push({ el: block, score })
}
}
// 按 score 降序,高亮所有匹配
matches.sort((a, b) => b.score - a.score)
matches.forEach((m, i) => {
m.el.classList.add('citation-highlight-block')
if (i === 0) {
setTimeout(() => m.el.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100)
}
})
```
**风险**:可能高亮过多不相关段落(特别是短文本切片匹配到多处相似内容时)。需配合阈值调优。
---
### 三、实施优先级建议
| 优先级 | 优化项 | 改动范围 | 预估工作量 | 收益 |
|--------|--------|---------|-----------|------|
| **P0** | Phase 1PDF bbox 高亮 | PdfViewer.vue + Chat.vue | 中2-3小时 | 核心功能补齐PDF 引用精确定位 |
| **P1** | Phase 2后端 citation 增强 | chat_routes.py | 小30分钟 | 为后续前端优化提供数据基础 |
| **P2** | Phase 3引用列表差异化展示 | Chat.vue 模板 | 小30分钟 | 提升可读性,改动低风险 |
| **P3** | Phase 4弹窗内图表/表格增强 | Chat.vue 模板 + 后端 preview | 中1-2小时 | 丰富引用预览信息 |
| **P4** | Phase 5DOCX 多段落高亮 | DocxViewer.vue | 小1小时 | 可选优化,当前单段落高亮已够用 |
---
### 四、需确认的技术细节
1. **MinerU bbox 坐标系**:需确认是 PDF 用户空间坐标72 DPI原点左下角还是归一化坐标0-1还是像素坐标。这决定了 `viewport.convertToViewportPoint` 的调用方式。建议用一个已知 PDF 的 bbox 值做实验。
2. **preview API 的 metadata 透传范围**`document_routes.py` 的 preview 接口返回 chunk metadata 时,是否包含 `table_html``image_path``bbox` 等字段?如果 ChromaDB metadata 中已存储,理论上会自动包含,但需验证。
3. **PdfViewer 连续滚动 vs 单页模式**:当前是单页渲染(一次只显示一页),切换页码时重新渲染。如果要做跨页 bbox 高亮(切片跨两页),需要支持连续滚动模式或双页渲染。当前单页模式已能覆盖大多数场景。
4. **mammoth 对复杂排版的还原度**DOCX 转 HTML 时mammoth 对表格嵌套、图片嵌入、特殊字体的还原度有限。如果文档包含复杂排版,高亮匹配可能偏离实际位置。这是 mammoth 库的固有限制。
5. **SSE citations 数据量控制**:当前 content 截断 300 字。如果增加到 500 字Phase 2每次 RAG 回答的 SSE 数据量会增加。按平均 5-8 个引用计算,增量约 1-2KB可接受。
---
### 五、不在本次范围
以下为更远期优化方向,不纳入本轮实施:
- **全文档连续滚动预览**PdfViewer 改为渲染所有页面(类似浏览器内置 PDF 查看器),支持滚动到任意位置。需要 pdfjs-dist 的多页渲染支持,性能开销大。
- **DOCX 原文编辑**:在预览弹窗中支持对 DOCX 内容的标注/批注。需要 DOCX 编辑器集成,超出引用溯源范畴。
- **引用覆盖率统计**:回答中每个段落的引用覆盖情况可视化。需要前端渲染层深度改造。
- **跨文档对比视图**:多个引用来自不同文档时,左右分屏对比。当前单文档预览已满足需求。