Files
rag/docs/RAG引用跳转-优化计划.md
lacerate551 ee5295cc97 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
2026-06-19 23:56:13 +08:00

304 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 编辑器集成,超出引用溯源范畴。
- **引用覆盖率统计**:回答中每个段落的引用覆盖情况可视化。需要前端渲染层深度改造。
- **跨文档对比视图**:多个引用来自不同文档时,左右分屏对比。当前单文档预览已满足需求。