## 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 /api/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": "uuid-xxx",
      "document": "完整切片正文...",   // 完整内容（不截断）
      "metadata": {
        "chunk_index": 3,
        "source": "三峡公报.pdf",
        "page": 2,
        "section": "第一章 > 1.1 项目背景",
        "doc_type": "pdf",
        ...
      },
      "is_target": false        // 是否为目标切片
    },
    {
      "id": "uuid-yyy",
      "document": "目标切片的完整正文...",
      "metadata": { "chunk_index": 5, "page": 3, ... },
      "is_target": true         // ← 这是用户点击的那个切片
    },
    // ... 上下文切片
  ]
}
```

---

### 四、前端实现思路

#### 4.1 引用标记解析

```
1. 从 finish 事件取 answer（回答全文）和 citations（引用数组）
2. 用正则 /\[ref:([^\]]+)\]/g 从 answer 中按出现顺序提取所有 chunk_id
3. 与 citations 数组匹配，建立 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]` 标记，避免冲突。
