- 修正 preview 接口路径(去掉多余的 /api 前缀) - 修正响应示例中 chunk id 格式(文件名_序号,非 UUID) - 补充 preview 响应中 status、version 字段 - 明确前端引用匹配方式为 chunk_id 查找而非数组下标 - fix_image_paths.py 改为遍历知识库子目录,避免生成空 chroma.sqlite3
197 lines
8.3 KiB
Markdown
197 lines
8.3 KiB
Markdown
## RAG 引用溯源跳转 — 接口说明与实现思路
|
||
|
||
### 一、整体流程
|
||
|
||
```
|
||
用户提问 → /rag 流式返回 → 回答文本中嵌入 [ref:chunk_id] 标记 + citations 数组
|
||
↓
|
||
前端解析标记,渲染为可点击的 [1] [2] 上标
|
||
↓
|
||
用户点击引用 → 前端调 /documents/{path}/preview 获取切片上下文
|
||
↓
|
||
前端展示:左侧原文档预览(跳转/高亮) + 右侧切片内容
|
||
```
|
||
|
||
---
|
||
|
||
### 二、/rag 返回的引用数据结构
|
||
|
||
SSE 流结束时,`finish` 事件的 `citations` 字段是一个数组,每个元素结构如下:
|
||
|
||
```jsonc
|
||
{
|
||
// ===== 通用字段(所有文档类型都有) =====
|
||
"chunk_id": "三峡公报.pdf_5", // 原始切片 ID(文件名_序号)
|
||
"chunk_index": 5, // 全局切片序号(从 0 开始递增,入库时写入)
|
||
"source": "三峡公报.pdf", // 来源文件名
|
||
"collection": "public_kb", // 所属知识库
|
||
"doc_type": "pdf", // 文档类型:pdf / word / excel / txt / other
|
||
"section": "第一章 > 1.2 概述", // 章节层级路径
|
||
"preview": "元数据中的预览文本", // 入库时生成的摘要/预览
|
||
"content": "切片正文(截断至 300 字)", // 实际切片内容的前 300 字
|
||
"chunk_type": "text", // 切片类型:text / table / image_caption
|
||
|
||
// ===== PDF 专属字段 =====
|
||
"page": 3, // 起始页码
|
||
"page_end": 4, // 结束页码(跨页时存在)
|
||
"bbox": [100, 200, 500, 600], // 边界框坐标(可用于页内精准定位)
|
||
"bbox_mode": "pixel", // 坐标模式
|
||
|
||
// ===== Word 专属字段 =====
|
||
"section_chunk_id": "sec_3_para_2" // 章节内段落序号
|
||
|
||
// ===== Excel 专属字段 =====
|
||
"page": 1 // 工作表序号
|
||
}
|
||
```
|
||
|
||
**关键字段说明:**
|
||
|
||
| 字段 | 用途 | 备注 |
|
||
|------|------|------|
|
||
| `chunk_index` | 调用 preview 接口的核心参数,精准定位切片在文档中的位置 | 从 0 开始,入库时按顺序分配 |
|
||
| `source` + `collection` | 拼接文档路径 `{collection}/{source}`,用于调 preview 接口 | 例:`public_kb/三峡公报.pdf` |
|
||
| `doc_type` | 前端据此选择预览方式(PDF 渲染 / DOCX 渲染 / 纯文本) | |
|
||
| `page` | PDF 页码跳转、Excel 工作表定位 | |
|
||
| `bbox` | PDF 页内矩形高亮(可选,精度更高) | |
|
||
| `section` | 给用户展示定位信息(如"第三章 > 3.1 市场分析") | |
|
||
| `content` | 截断的正文预览,用于 DOCX 文本匹配高亮 | 仅 300 字,完整内容需调 preview 接口 |
|
||
|
||
**回答文本中的引用标记:**
|
||
|
||
后端在 LLM 回答的段落末尾自动插入 `[ref:chunk_id]` 标记,前端需要将其解析为可点击的上标:
|
||
|
||
```
|
||
原文:根据相关研究,三峡工程年均发电量约 882 亿千瓦时。[ref:三峡公报.pdf_5]
|
||
渲染:根据相关研究,三峡工程年均发电量约 882 亿千瓦时。[5] ← 可点击上标
|
||
```
|
||
|
||
---
|
||
|
||
### 三、文档预览接口
|
||
|
||
```
|
||
GET /documents/{collection}/{source}/preview?chunk_index={N}&context={K}
|
||
```
|
||
|
||
**参数:**
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `collection` | path | 是 | 知识库名称(如 `public_kb`) |
|
||
| `source` | path | 是 | 文件名(如 `三峡公报.pdf`) |
|
||
| `chunk_index` | query | 是 | 目标切片序号(来自 citation 的 `chunk_index`) |
|
||
| `context` | query | 否 | 前后各取几个切片,默认 2 |
|
||
|
||
**响应示例:**
|
||
|
||
```jsonc
|
||
{
|
||
"success": true,
|
||
"collection": "public_kb",
|
||
"source": "三峡公报.pdf",
|
||
"total_chunks": 42, // 文档总切片数
|
||
"target_index": 5, // 请求的 chunk_index
|
||
"chunks": [
|
||
{
|
||
"id": "三峡公报.pdf_3", // 切片 ID(文件名_序号格式,序号 = chunk_index)
|
||
"document": "完整切片正文...", // 完整内容(不截断)
|
||
"metadata": {
|
||
"chunk_index": 3,
|
||
"source": "三峡公报.pdf",
|
||
"page": 2,
|
||
"section": "第一章 > 1.1 项目背景",
|
||
"doc_type": "pdf",
|
||
...
|
||
},
|
||
"status": "active", // 文档状态
|
||
"version": "v1", // 版本号
|
||
"is_target": false // 是否为目标切片
|
||
},
|
||
{
|
||
"id": "三峡公报.pdf_5",
|
||
"document": "目标切片的完整正文...",
|
||
"metadata": { "chunk_index": 5, "page": 3, ... },
|
||
"status": "active",
|
||
"version": "v1",
|
||
"is_target": true // ← 这是用户点击的那个切片
|
||
},
|
||
// ... 上下文切片
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 四、前端实现思路
|
||
|
||
#### 4.1 引用标记解析
|
||
|
||
```
|
||
1. 从 finish 事件取 answer(回答全文)和 citations(引用数组)
|
||
2. 用正则 /\[ref:([^\]]+)\]/g 从 answer 中按出现顺序提取所有 chunk_id
|
||
3. 以 chunk_id 为键从 citations 数组中查找对应的引用信息(注意:是按 chunk_id 匹配,不是按数组下标),建立 chunk_id → 显示序号 [1] [2] ... 的映射
|
||
4. 渲染回答时,将 [ref:xxx] 替换为可点击的上标元素
|
||
```
|
||
|
||
#### 4.2 点击引用后的跳转
|
||
|
||
```
|
||
用户点击引用 [N]
|
||
│
|
||
├─ 1. 从 citation 中取 source + collection,拼出文档路径
|
||
│ docPath = `${collection}/${source}`
|
||
│
|
||
├─ 2. 调用 preview 接口获取切片上下文
|
||
│ GET /documents/${docPath}/preview?chunk_index=${chunk_index}&context=3
|
||
│
|
||
├─ 3. 根据 doc_type 选择预览方式:
|
||
│ ├─ PDF → 用 page 字段跳转到对应页(可选:用 bbox 绘制高亮矩形)
|
||
│ ├─ DOCX → 用目标切片的 document 字段做文本匹配,高亮并滚动到对应段落
|
||
│ └─ TXT → 直接显示文本,搜索目标内容并滚动定位
|
||
│
|
||
└─ 4. 右侧面板展示切片上下文列表
|
||
├─ 高亮标记 is_target=true 的切片
|
||
├─ 显示每个切片的 section、page 信息
|
||
└─ 点击其他切片可切换预览位置
|
||
```
|
||
|
||
#### 4.3 各文档类型的定位策略
|
||
|
||
**PDF 文档:**
|
||
|
||
最直接的方案是利用 `page` 字段做页码跳转。如果需要更精确的页内定位,`bbox` 字段提供了文本区域的坐标 `[x1, y1, x2, y2]`,可以在 PDF 渲染层上叠加一个半透明高亮矩形。
|
||
|
||
**Word 文档:**
|
||
|
||
Word 没有页码概念,定位方式是文本匹配。用 preview 接口返回的目标切片 `document`(完整正文)在渲染后的文档中做模糊匹配,找到最相似的段落并高亮滚动。`content` 字段只有 300 字可能不够用,建议用 preview 接口的 `document` 字段。
|
||
|
||
**纯文本 / 其他:**
|
||
|
||
直接渲染文本内容,通过字符串搜索定位目标切片并滚动到视口中央。
|
||
|
||
#### 4.4 弹窗内切片切换
|
||
|
||
右侧面板展示上下文切片后,用户可以点击其他切片切换预览位置。切换时需要:
|
||
|
||
```
|
||
1. 从切片的 metadata.chunk_index 获取序号(注意:切片的 id 是 UUID,不能用来定位)
|
||
2. 更新目标标记(is_target)
|
||
3. 用切片的 document 字段更新左侧文档的高亮/定位
|
||
4. 如果切片 metadata 中有 page 字段,同步更新 PDF 页码
|
||
```
|
||
|
||
---
|
||
|
||
### 五、注意事项
|
||
|
||
1. **`chunk_index` 是核心定位字段**。它在入库时按文档切片顺序从 0 递增分配,preview 接口通过它查找目标切片。不要与数组下标混淆——后端已按 `metadata.chunk_index` 排序后查找,不依赖数组位置。
|
||
|
||
2. **`content` 字段被截断到 300 字**。这是为了控制 SSE 流的数据量。前端如果需要完整切片内容(比如 DOCX 高亮匹配),应调用 preview 接口获取 `document` 字段。
|
||
|
||
3. **`bbox` 坐标的可用性**。目前 PDF 解析器会提取文本块的 bbox 信息,但不是所有切片都有。前端使用 bbox 时应做空值判断。
|
||
|
||
4. **`collection` 字段的回退策略**。citation 中带有 `collection` 字段,但如果为空,前端可以用当前用户选中的知识库名称作为回退。
|
||
|
||
5. **LLM 可能自行生成 `[1]`、`[2]` 等引用标记**,与后端注入的 `[ref:chunk_id]` 格式不同。前端应先清理 LLM 的 `[数字]` 标记,再处理后端的 `[ref:xxx]` 标记,避免冲突。
|