核心修复: - _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
304 lines
13 KiB
Markdown
304 lines
13 KiB
Markdown
## 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 转 HTML,2-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/page,Word 含 section_chunk_id | bbox 已返回但前端未使用;content 截断 300 字 |
|
||
|
||
#### 1.3 后端已提供但前端未消费的字段
|
||
|
||
| 字段 | 后端来源 | 当前状态 |
|
||
|------|---------|---------|
|
||
| `bbox` | ChromaDB metadata → `_build_citation` 解析 JSON → 返回数组 | PdfViewer 未接收,未渲染高亮 |
|
||
| `sub_type` | ChromaDB metadata(MinerU V2 解析) | `_build_citation` 未返回此字段 |
|
||
| `table_type` | ChromaDB metadata(MinerU V2 解析) | `_build_citation` 未返回此字段 |
|
||
| `vlm_description` | MinerUChunk(VLM 视觉描述) | 未进入 citation 数据 |
|
||
| `chart_markdown` | MinerUChunk(图表数据表) | 未进入 citation 数据 |
|
||
|
||
---
|
||
|
||
### 二、优化项清单
|
||
|
||
#### Phase 1:PDF 页内 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` 应能处理转换,但需实际测试坐标是否正确。
|
||
- 部分切片可能没有 bbox(DOCX 来源、或 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 5:DOCX 多段落高亮(可选)
|
||
|
||
**目标**:当一个切片内容跨多个段落时,高亮所有匹配段落而非仅最佳匹配的一个。
|
||
|
||
**涉及文件**:
|
||
- `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 1:PDF 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 5:DOCX 多段落高亮 | 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 编辑器集成,超出引用溯源范畴。
|
||
- **引用覆盖率统计**:回答中每个段落的引用覆盖情况可视化。需要前端渲染层深度改造。
|
||
- **跨文档对比视图**:多个引用来自不同文档时,左右分屏对比。当前单文档预览已满足需求。
|