## RAG 引用溯源跳转 — 优化计划 本文档基于 dev-ui 前端(Vue3 + NaiveUI)和 chat_routes.py 后端的现有实现,分析引用跳转功能的优化方向。供讨论后实施。 --- ### 一、现有实现现状 #### 1.1 数据流 ``` SSE finish 事件 → citations[] 数组(_build_citation 构建) ↓ Chat.vue extractCitations() → 解析 [ref:chunk_id] → 渲染为 [N] ↓ 用户点击引用项 → 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 段落匹配,`` 高亮 + 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:`` **风险点**: - 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 [{{ c.displayIndex }}] 表格 {{ c.sub_type || '图片' }} 第{{ c.page }}页 {{ c.section }} ``` **效果**:用户可以一眼看出引用的是文本、表格还是图表,点击前有预期。 --- #### 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 表格 {{ chunk.metadata.sub_type }} {{ chunk.document || chunk.content }} ``` **注意**:preview API 返回的 chunk metadata 中是否包含 `table_html`、`image_path` 等字段,需确认后端 `/documents//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 编辑器集成,超出引用溯源范畴。 - **引用覆盖率统计**:回答中每个段落的引用覆盖情况可视化。需要前端渲染层深度改造。 - **跨文档对比视图**:多个引用来自不同文档时,左右分屏对比。当前单文档预览已满足需求。