# AI 流式问答与出题接口 - 前端使用指南

> **适用范围**：知识库问答、会话管理、题目生成、答案批改  
> **Base URL**：`http://localhost:8080/api`  
> **认证方式**：`Authorization: Bearer <JWT Token>`  
> **接口版本**：v2.17.0  
> **更新时间**：2026-06-05

---

## 目录

1. [快速开始](#一快速开始)
2. [AI 流式问答接口](#二ai-流式问答接口)
3. [AI 会话管理接口](#三ai-会话管理接口)
4. [题目生成接口](#四题目生成接口)
5. [题目批改接口](#五题目批改接口)
6. [答题记录接口](#六答题记录接口)
7. [前后端字段对照表](#七前后端字段对照表)
8. [常见问题 FAQ](#八常见问题-faq)
9. [附录：cURL 完整示例](#九附录curl-完整示例)

---

## 一、快速开始

### 1.1 认证流程

所有接口（登录接口除外）均需携带 JWT Token：

```http
POST /api/auth/login
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}
```

**响应**：

```json
{
  "code": 200,
  "data": {
    "token": "eyJhbGciOiJIUzUxMiJ9...",
    "user": {
      "id": 1,
      "username": "admin",
      "role": 1
    }
  }
}
```

后续所有接口请求头需添加：

```
Authorization: Bearer <token>
```

### 1.2 接口总览

| 模块 | 接口 | 方法 | 功能 |
|------|------|------|------|
| **流式问答** | `/ai/chat/stream` | `POST` | 发起 AI 流式问答 |
| **中断问答** | `/ai/chat/stop?sessionId=xxx` | `POST` | 中断流式回答 |
| **会话列表** | `/ai/sessions?page=1&pageSize=10` | `GET` | 获取当前用户的会话列表 |
| **会话详情** | `/ai/session/{sessionId}` | `GET` | 获取指定会话的消息记录 |
| **消息引用** | `/ai/message/{messageId}/references` | `GET` | 获取某条消息的引用列表 |
| **消息文件来源** | `/ai/message/{messageId}/sources` | `GET` | 获取某条消息引用的文件来源（去重） |
| **会话引用** | `/ai/session/{sessionId}/references` | `GET` | 获取会话的全部引用 |
| **删除会话** | `/ai/session/{sessionId}` | `DELETE` | 删除单个会话及相关数据 |
| **批量删除会话** | `/ai/sessions` | `DELETE` | 批量删除多个会话 |
| **生成题目** | `/exam/generate` | `POST` | 根据文档生成题目（异步） |
| **生成试卷** | `/exam/paper/generate` | `POST` | 动态生成试卷 |
| **开始考试** | `/exam/paper/{paperId}/start` | `POST` | 获取试卷题目 |
| **考试结果** | `/exam/record/{recordId}/result` | `GET` | 获取批阅结果 |
| **正式考试批改** | `/exam/record/{recordId}/grade` | `POST` | 正式考试提交并批改 |
| **批改答案** | `/exam/grade` | `POST` | 批改学生答案（通用） |
| **查询答题记录** | `/exam/answers/query` | `POST` | 查询用户答题记录 |
| **考试记录列表** | `/exam/records?type=practice` | `GET` | 获取考试/练习记录列表 |

---

## 二、AI 流式问答接口

### 2.1 发起流式问答

**接口**：`POST /api/ai/chat/stream`  
**Content-Type**：`application/json`  
**返回类型**：`text/event-stream` (Server-Sent Events)

#### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `message` | string | ✅ | 用户提问内容 |
| `session_id` | string | ❌ | 会话ID，不传则自动生成；传空字符串 `""` 也会自动生成 |
| `id` | string | ❌ | 自定义请求ID，用于追踪 |

**请求示例**：

```json
{
  "message": "请介绍一下知识库管理系统",
  "session_id": "sess_1780410472658_9qg9a9hhp"
}
```

#### 内部逻辑说明

- 后端自动获取用户可访问的知识库（`collections`），无需前端传递
- 后端自动从数据库加载该会话的历史消息作为上下文
- 流式返回过程中，finish 事件到达时会自动保存：
  - 用户提问 → `context_message`（role=user）
  - AI 回答 → `context_message`（role=assistant，含 answer、images 等）
  - 引用切片 → `context_reference`（自动从 RAG 获取 preview 字段）
  - 图片 → `context_image`（保留 score 字段）

#### 流式事件类型

| 事件 type | 说明 | 是否入库 |
|-----------|------|----------|
| `start` | 对话开始事件 | ❌ |
| `connected` | RAG 服务连接成功 | ❌ |
| `thinking` | AI 思考中 | ❌ |
| `searching` | 知识库检索中 | ❌ |
| `sources` | 检索到的候选来源信息 | ❌ 仅前端展示，**不写入数据库** |
| `chunk` | 流式内容片段 | ❌ 前端拼接显示 |
| `result` / `finish` | 最终完整结果（最重要） | ✅ 存入数据库 |
| `error` | 错误信息 | ❌ |

#### finish 事件核心字段

```json
{
  "type": "finish",
  "answer": "根据【参考资料】...完整的AI回答内容，其中包含 [ref:文档名_86] 这样的引用标记。",
  "duration_ms": 75512,
  "citations": [
    {
      "chunk_id": "文档名_86",
      "chunk_index": 86,
      "source": "文档名.docx",
      "collection": "dept_1_kb",
      "doc_type": "other",
      "section": "3.1 权重计算公式",
      "chunk_type": "text",
      "page": 1,
      "score": 0.838,
      "preview": "【系统自动从 RAG 接口获取的目标切片完整正文】",
      "content": {
        "stem": "切片摘要内容",
        "data": {}
      }
    }
  ],
  "images": [
    {
      "image_id": "49d2910b.jpg",
      "path": "/api/image/49d2910b.jpg/data",
      "source": "来源文档.docx",
      "page": 1,
      "chunk_type": "image",
      "description": "图片描述",
      "score": 0.85,
      "collection": "dept_1_kb"
    }
  ]
}
```

> **重要说明**：
> - `answer` 中的 `[ref:chunk_id]` 标记：前端需解析为可点击链接，点击后跳转/展示对应引用信息
> - `preview` 字段：后端写入 `context_reference` 表时，会自动调用 RAG 预览接口 `GET /documents/{collection}/{source}/preview?chunk_index=N&context=0` 填充该字段；若接口报错，则填入错误信息
> - `context_message.answer` 字段保留完整的 `[ref:chunk_id]` 标记文本

#### answer 字段引用标记解析

前端需解析 `[ref:chunk_id]` 为可点击元素：

```text
"市场状态 = Σ（二级指标均值 × 权重）[ref:文档名_86]。
此外，一级指标合计权重不低于70%[ref:文档名_93]。"
```

#### 前端 JavaScript 调用示例（fetch + ReadableStream）

```javascript
// 发起流式问答
async function chatStream(message, sessionId, token) {
  const response = await fetch('/api/ai/chat/stream', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token
    },
    body: JSON.stringify({ message, session_id: sessionId })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let fullAnswer = '';

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop() || '';  // 保留未完成的行

    for (const line of lines) {
      if (!line.startsWith('data:')) continue;
      try {
        const data = JSON.parse(line.slice(5).trim());
        handleEvent(data);  // 分发到事件处理器
      } catch (e) {
        console.warn('解析失败:', line);
      }
    }
  }
}

// 事件处理器（前端自行实现 UI 更新）
function handleEvent(data) {
  switch (data.type) {
    case 'chunk':
      // 实时拼接显示到对话框
      fullAnswer += data.content;
      // renderAnswer(fullAnswer);
      break;
    case 'finish':
      // 对话完成，拿到完整 answer、citations、images
      console.log('完整回答:', data.answer);
      console.log('引用列表:', data.citations);
      console.log('图片列表:', data.images);
      break;
    case 'error':
      console.error('AI 错误:', data.message);
      break;
  }
}
```

> **为什么不用 EventSource**：EventSource 只能 GET 请求，且无法自定义请求头（无法传 Token）。推荐使用 `fetch + ReadableStream` 方式。

### 2.2 中断流式问答

**接口**：`POST /api/ai/chat/stop?sessionId=sess_xxx`

**响应**：

```json
{
  "success": true,
  "message": "已中断AI回答"
}
```

---

## 三、AI 会话管理接口

> 统一响应结构（`Map`）：
> ```json
> {
>   "success": true,
>   "data": [...],
>   "total": 10,
>   "page": 1,
>   "pageSize": 10,
>   "pages": 1
> }
> ```

### 3.1 获取用户会话列表

**接口**：`GET /api/ai/sessions?page=1&pageSize=10`

**查询参数**：

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `page` | int | ❌ | 1 | 页码 |
| `pageSize` | int | ❌ | 10 | 每页数量 |

**响应 data 元素字段**（ChatSession）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | long | 数据库主键 |
| `sessionId` | string | 会话唯一ID |
| `userId` | long | 用户ID |
| `createTime` | datetime | 创建时间 |

### 3.2 获取会话详情（消息记录）

**接口**：`GET /api/ai/session/{sessionId}?page=1&pageSize=20`

**权限检查**：后端自动校验该会话是否属于当前用户

**响应 data 元素字段**（ChatMessage）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | long | 消息ID |
| `conversationId` | long | 会话ID（数据库主键） |
| `role` | string | `user` / `assistant` |
| `content` | string | 用户提问原文（role=user）或 AI 回答内容（role=assistant 时通常与 answer 一致） |
| `messageType` | string | `rag` 等 |
| `isFinished` | int | 1=完成 |
| `answer` | string | AI 完整回答（含 `[ref:chunk_id]` 标记），仅 role=assistant 时有值 |
| `images` | string(JSON数组) | 图片列表（JSON 字符串，需前端解析） |
| `durationMs` | long | 耗时（毫秒） |
| `createTime` | datetime | 创建时间 |

> **注意**：`ChatMessage` 实体中已**移除 `sources` 字段**。引用信息需通过 `/ai/message/{messageId}/references` 接口单独获取。

### 3.3 获取消息引用列表

**接口**：`GET /api/ai/message/{messageId}/references?docName=xxx`

**查询参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `docName` | string | ❌ | 按文档名称过滤引用 |

**响应 data 元素字段**（ChatReference）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | long | 引用记录ID |
| `messageId` | long | 关联的消息ID |
| `chunkId` | string | 切片唯一标识 `{docName}_{chunkIndex}` |
| `chunkIndex` | int | 切片序号 |
| `collection` | string | 向量库名称 |
| `docName` | string | 文档名称 |
| `docType` | string | 文档类型 |
| `page` | int | 页码 |
| `section` | string | 章节/段落位置 |
| `chunkType` | string | `text` / `image` / `table` |
| `excerpt` | string | 切片摘要内容 |
| **`preview`** | string | **目标切片完整正文（系统自动从 RAG 获取，失败则填错误信息）** |
| `score` | decimal | 相关性评分 0-1 |
| `createTime` | datetime | 创建时间 |

### 3.4 获取消息文件来源列表（去重）

**接口**：`GET /api/ai/message/{messageId}/sources`

**用途**：对同一条消息的引用按文档名做去重汇总，用于展示"参考文档列表"侧栏

**响应 data 元素字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `docName` | string | 文档名称（去重后的唯一标识） |
| `docType` | string | 文档类型 |
| `referenceCount` | int | 该文档被引用的次数 |
| `pages` | string | 页码范围，如 `"5"` 或 `"1-10"` |

**响应示例**：

```json
{
  "success": true,
  "data": [
    {
      "docName": "薪酬制度.docx",
      "docType": "other",
      "referenceCount": 5,
      "pages": "1-8"
    },
    {
      "docName": "考核办法.docx",
      "docType": "other",
      "referenceCount": 3,
      "pages": "3"
    }
  ],
  "total": 2
}
```

### 3.5 获取会话全部引用

**接口**：`GET /api/ai/session/{sessionId}/references`

**用途**：返回整个会话中所有 AI 回答的引用汇总，字段与单条消息引用接口一致

### 3.6 删除单个会话

**接口**：`DELETE /api/ai/session/{sessionId}`

删除指定会话及其所有消息和引用数据。后端自动校验会话所属权。

### 3.7 批量删除会话

**接口**：`DELETE /api/ai/sessions`

**请求体**：

```json
["sess_xxx1", "sess_xxx2", "sess_xxx3"]
```

**响应**：

```json
{
  "success": true,
  "message": "会话批量删除成功",
  "deletedCount": 3
}
```

---

## 四、题目生成接口

> 本模块接口统一响应结构（`Result<T>`）：
> ```json
> {
>   "code": 200,
>   "message": "操作成功",
>   "data": { ... }
> }
> ```

### 4.1 生成题目（异步）

**接口**：`POST /api/exam/generate`  
**Content-Type**：`application/json`

#### 请求体字段

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `file_id` | Long | ⭐二选一 | 文件ID，系统自动从 `knowledge_file` 表查询向量库信息（**推荐**） |
| `file_path` | string | ⭐二选一 | 文件路径（如 `dept_1_kb/薪酬制度.docx`） |
| `collection` | string | file_path 必填 | 向量库名称（使用 file_path 时必须提供） |
| `question_types` | object | ❌ | 指定题型及数量；不传则使用 AI 智能生成模式 |
| `question_scores` | object | ❌ | 自定义题型分数配置 |
| `difficulty` | int | ❌ | 难度等级 1-5，默认 3 |
| `request_id` | string | ❌ | 请求ID，用于幂等性 |

#### question_types 可选值

| 题型 | key | 说明 |
|------|-----|------|
| 单选题 | `single_choice` | 四个选项单选 |
| 多选题 | `multiple_choice` | 四个选项多选 |
| 判断题 | `true_false` | 对/错两选 |
| 填空题 | `fill_blank` | 按空格填写答案，支持同义词 |
| 简答题 | `subjective` | 开放回答，由 LLM 评分 |

#### 请求示例：指定题型模式

```json
{
  "file_id": 1,
  "question_types": {
    "single_choice": 3,
    "multiple_choice": 2,
    "true_false": 2,
    "fill_blank": 2,
    "subjective": 1
  },
  "difficulty": 3,
  "request_id": "gen_req_001"
}
```

#### 请求示例：智能生成模式（推荐）

不提供 `question_types` 参数时，系统调用 AI 智能出题接口，自动根据文档内容判断题型和数量：

```json
{
  "file_id": 1,
  "difficulty": 3
}
```

#### 响应示例（成功）

```json
{
  "code": 200,
  "message": "收到请求",
  "data": {
    "requestId": "gen_req_001",
    "fileId": 1,
    "fileMatchStatus": "success",
    "matchedCollection": "dept_1_kb",
    "documentName": "薪酬制度.docx",
    "collectionFilled": true,
    "asyncTaskStarted": true,
    "nextStep": "请通过文件状态接口查询生成进度",
    "status": "success"
  }
}
```

#### 响应示例（文件验证失败）

```json
{
  "code": 400,
  "message": "文件不存在，请确认 file_id 是否正确",
  "data": {
    "status": "error",
    "errorType": "FILE_VALIDATION_ERROR",
    "errorDetail": "文件不存在",
    "suggestion": "请检查 file_id 是否正确、文件是否已删除、或文件是否已完成向量化"
  }
}
```

**fileMatchStatus 可能值**：

| 值 | 说明 | HTTP 状态码 |
|----|------|------------|
| `success` | 文件验证通过，启动异步任务 | 200 |
| `FILE_NOT_FOUND` | 文件不存在 | 400 |
| `FILE_DELETED` | 文件已被删除 | 400 |
| `NOT_VECTORIZED` | 文件尚未完成向量化 | 400 |
| `PARAM_MISSING` | 参数不完整 | 400 |
| `INTERNAL_ERROR` | 服务器内部错误 | 500 |

### 4.2 轮询题目生成进度

**接口**：`GET /api/file/{fileId}`

**关键字段**：

| 字段 | 说明 |
|------|------|
| `processStatus` | 文件处理状态（如 `INDEXED`） |
| `examStatus` | **题目生成状态**：`UNGENERATED` / `GENERATING` / `GENERATED` / `FAILED` |
| `processMessage` | 生成详情，包含题目数量和 AI 分析结果 |

**examStatus 流转**：

```
UNGENERATED ──► GENERATING ──► GENERATED
                       │
                       └──────► FAILED
```

**轮询策略建议**：初始间隔 2-3 秒；超时 5-10 分钟；examStatus 达到终态则停止。

### 4.3 生成试卷

**接口**：`POST /api/exam/paper/generate`

**请求体**：

```json
{
  "single_choice_count": 5,
  "multiple_choice_count": 3,
  "true_false_count": 2,
  "fill_blank_count": 2,
  "subjective_count": 1,
  "difficulty": 3,
  "include_personal": false,
  "file_ids": [1, 2, 3]
}
```

**响应 data 字段**（`ExamPaperGenerateResponse`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `paperId` | string | 试卷ID |
| `paperTitle` | string | 试卷标题 |
| `totalScore` | decimal | 总分 |
| `questionCount` | int | 题目总数 |
| `generatedAt` | datetime | 生成时间 |
| `questions` | array | 题目列表 |

---

## 五、题目批改接口

### 5.1 批改答案（日常练习 / 模拟考 / 正式考试通用）

**接口**：`POST /api/exam/grade`

**请求体字段**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `request_id` | string | ❌ | 请求ID |
| `type` / `answerType` | string | ❌ | `practice` / `mock_exam` / `formal_exam` |
| `record_id` | string | ❌ | 正式考试记录ID；提供时走正式考试提交/批改分支 |
| `paper_id` | string | ❌ | 试卷ID |
| `session_id` | string | ❌ | 会话ID |
| `answers` | array | ✅ | 答案列表 |

**answers 数组元素字段**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `question_id` | string | ✅ | 题目ID |
| `question_type` | string | ✅ | `single_choice` / `multiple_choice` / `true_false` / `fill_blank` / `subjective` |
| `question_content` | object | ✅ | 题目内容（含 stem / options / answer 等） |
| `student_answer` | any | ✅ | 学生提交的答案 |
| `max_score` | number | ✅ | 满分 |

**请求示例**：

```json
{
  "request_id": "grade_req_001",
  "answer_type": "practice",
  "answers": [
    {
      "question_id": "q_001",
      "question_type": "single_choice",
      "question_content": {
        "stem": "根据公司规定，员工薪资由哪几部分组成？",
        "data": {
          "options": [
            { "key": "A", "content": "基本工资+奖金" },
            { "key": "B", "content": "基本工资+绩效奖金+津贴补贴" }
          ]
        },
        "answer": "B"
      },
      "student_answer": "B",
      "max_score": 5.0
    },
    {
      "question_id": "q_002",
      "question_type": "multiple_choice",
      "question_content": {
        "stem": "以下哪些属于绩效奖金的评定因素？",
        "data": { "options": [...] },
        "answer": ["A", "B", "D"]
      },
      "student_answer": ["A", "B"],
      "max_score": 4.0
    },
    {
      "question_id": "q_003",
      "question_type": "true_false",
      "question_content": {
        "stem": "公司规定员工每月绩效奖金上限为工资的20%。",
        "answer": "F"
      },
      "student_answer": "T",
      "max_score": 2.0
    },
    {
      "question_id": "q_004",
      "question_type": "fill_blank",
      "question_content": {
        "stem": "员工薪资由___、___和___三部分组成。",
        "data": { "blank_count": 3 },
        "answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]]
      },
      "student_answer": ["基本工资", "绩效奖金", "交通补贴"],
      "max_score": 6.0
    },
    {
      "question_id": "q_005",
      "question_type": "subjective",
      "question_content": {
        "stem": "请简述公司薪酬制度的核心原则。",
        "data": {
          "scoring_points": [
            { "point": "公平性", "weight": 0.3 },
            { "point": "激励性", "weight": 0.3 },
            { "point": "竞争力", "weight": 0.2 },
            { "point": "合法性", "weight": 0.2 }
          ]
        },
        "answer": "公司薪酬制度遵循公平、激励、竞争、合法四大原则..."
      },
      "student_answer": "我认为公司的薪酬制度应该公平公正...",
      "max_score": 10.0
    }
  ]
}
```

**响应 data 字段**（`ExamGradeResponse`）：

| 字段 | 类型 | 说明 |
|------|------|------|
| `success` | boolean | 是否成功 |
| `totalScore` | decimal | 得分 |
| `totalMaxScore` | decimal | 满分 |
| `scoreRate` | decimal | 得分率（百分比） |
| `results` | array | 每题批改结果 |

**results 元素字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `questionId` | string | 题目ID |
| `questionType` | string | 题型 |
| `score` | decimal | 本题得分 |
| `maxScore` | decimal | 本题满分 |
| `correct` / `isCorrect` | boolean | 是否答对 |
| `studentAnswer` | any | 学生答案 |
| `correctAnswer` | any | 正确答案 |
| `feedback` | string | 批改反馈 |

**批阅规则**：

| 题型 | 批阅方式 | 得分规则 |
|------|----------|----------|
| 单选题 | 精确匹配 | 正确得满分，错误得 0 |
| 多选题 | 集合比对 | 全对满分，少选一半，错选得 0 |
| 判断题 | 精确匹配 | 正确得满分，错误得 0 |
| 填空题 | 多答案匹配 | 每空独立评分，支持同义词匹配 |
| 简答题 | LLM 评分 | 根据得分点评分，不超过 max_score |

### 5.2 正式考试提交并批改

**接口**：`POST /api/exam/record/{recordId}/grade`

**路径参数**：`recordId`（正式考试记录ID）

**请求体**（`ExamSaveRequest`）：

```json
{
  "answers": [
    {
      "question_id": "q_001",
      "question_type": "single_choice",
      "answer": "B"
    }
  ]
}
```

---

## 六、答题记录接口

### 6.1 开始考试（获取题目）

**接口**：`POST /api/exam/paper/{paperId}/start`

系统自动查找或创建考试记录，返回试卷题目内容。

### 6.2 获取考试结果

**接口**：`GET /api/exam/record/{recordId}/result`

**权限检查**：后端校验该记录是否属于当前用户

**响应 data 字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `recordId` | string | 记录ID |
| `paperId` | string | 试卷ID |
| `paperTitle` | string | 试卷标题 |
| `status` | string | `submitted` 等 |
| `totalScore` | decimal | 得分 |
| `totalMaxScore` | decimal | 满分 |
| `scoreRate` | decimal | 得分率 |
| `submitTime` | datetime | 提交时间 |
| `answers` | array | 每题结果 |

### 6.3 查询用户答题记录

**接口**：`POST /api/exam/answers/query`

**请求体**（`UserAnswerQueryRequest`）：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `paper_id` | string | ⭐二选一 | 按试卷ID查询 |
| `session_id` | string | ⭐二选一 | 按会话ID查询 |
| `answer_type` | string | ❌ | 过滤类型：practice / mock_exam / formal_exam |
| `page` | int | ❌ | 页码，默认 1 |
| `page_size` | int | ❌ | 每页数量，默认 20 |

**请求示例**：

```json
{
  "paper_id": "paper_abc123",
  "answer_type": "practice",
  "page": 1,
  "page_size": 20
}
```

### 6.4 获取考试记录列表

**接口**：`GET /api/exam/records?type=practice`

**查询参数**：

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `type` | string | ❌ | `practice` | `practice` / `mock_exam` / `formal_exam` |

**分支逻辑**：

- `type=practice` → 返回日常练习答题记录（`UserAnswerQueryResponse`）
- `type=mock_exam` 或 `formal_exam` → 返回试卷考试记录列表（`List<ExamRecordResponse>`）

---

## 七、前后端字段对照表

### 7.1 ChatReference 引用字段映射

| 后端字段 | 前端字段（AI返回） | 类型 | 说明 |
|----------|-------------------|------|------|
| `chunkId` | `chunk_id` | string | 切片唯一标识 `{docName}_{chunkIndex}` |
| `chunkIndex` | `chunk_index` | int | 切片序号 |
| `collection` | `collection` | string | 向量库名称 |
| `docName` | `source` / `doc_name` | string | 文档名称 |
| `docType` | `doc_type` | string | 文档类型 |
| `page` | `page` | int | 页码 |
| `section` | `section` | string | 章节/段落位置 |
| `chunkType` | `chunk_type` | string | `text` / `image` / `table` |
| `excerpt` | `content.stem` | string | 切片摘要 |
| **`preview`** | —（系统自动填充） | string | **目标切片完整正文**，由后端从 RAG 预览接口获取，失败则填错误信息 |
| `score` | `score` | decimal | 相关性评分 0-1 |
| `createTime` | — | datetime | 创建时间 |

### 7.2 ChatMessage 消息字段映射

| 后端字段 | 前端字段 | 类型 | 说明 |
|----------|----------|------|------|
| `id` | `message_id` | long | 消息唯一ID |
| `conversationId` | `conversation_id` | long | 会话（数据库主键）ID |
| `role` | `role` | string | `user` / `assistant` |
| `content` | `content` | string | 用户提问原文 / AI 回答 |
| `answer` | `answer` | string | AI 完整回答（含 `[ref:chunk_id]` 标记），role=assistant 有效 |
| `messageType` | `message_type` | string | `rag` 等 |
| `isFinished` | `is_finished` | int | 1=完成 |
| `images` | `images` | string(JSON数组) | 图片列表（JSON 字符串，前端需 JSON.parse） |
| `durationMs` | `duration_ms` | long | 耗时（毫秒） |
| `createTime` | `create_time` | datetime | 创建时间 |

> **已移除字段**：`sources`。引用信息需通过 `/ai/message/{messageId}/references` 接口获取。

### 7.3 Image 图片字段映射（context_image 表）

| 后端字段 | 前端字段（AI返回） | 类型 | 说明 |
|----------|-------------------|------|------|
| `imageId` | `image_id` | string | 图片标识 |
| `path` | `path` / `url` | string | 图片访问路径，如 `/api/image/xxx/data` |
| `source` | `source` | string | 来源文档 |
| `page` | `page` | int | 页码 |
| `section` | `section` | string | 章节位置 |
| `chunkType` | `chunk_type` | string | 切片类型（image） |
| `description` | `description` | string | 图片描述 |
| `score` | `score` | decimal | 相关性评分（**保留**） |
| `collection` | `collection` | string | 向量库名称 |

> **已移除字段**：`filename`、`size_bytes`、`format`、`page_end`、`type`、`full_description`

---

## 八、常见问题 FAQ

### Q1: answer 中的 `[ref:chunk_id]` 标记如何解析？

**答**：前端需要在渲染 `answer` 时，使用正则匹配 `\[ref:([^\]]+)\]`，将其替换为可点击的引用链接：

```javascript
const renderAnswer = (answer) => {
  const regex = /\[ref:([^\]]+)\]/g;
  return answer.replace(regex, (match, chunkId) => {
    return `<a class="ref-link" data-chunk-id="${chunkId}">[引用]</a>`;
  });
};
```

### Q2: 流式问答如何处理？为什么不用 EventSource？

**答**：由于 EventSource 只能 GET 请求，且无法自定义请求头（无法传 Token），推荐使用 `fetch + ReadableStream` 方式：

```javascript
const response = await fetch('/api/ai/chat/stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token
  },
  body: JSON.stringify({ message, session_id: sessionId })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullAnswer = '';

while (true) {
  const { value, done } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop() || '';

  for (const line of lines) {
    if (!line.startsWith('data:')) continue;
    const dataStr = line.slice(5).trim();
    if (!dataStr) continue;
    try {
      const data = JSON.parse(dataStr);
      if (data.type === 'chunk') {
        fullAnswer += data.content;
        // renderAnswer(fullAnswer);  // 更新 UI
      } else if (data.type === 'finish') {
        console.log('引用:', data.citations);
        console.log('图片:', data.images);
      }
    } catch (e) {
      console.warn('解析失败:', dataStr);
    }
  }
}
```

### Q3: 题目生成接口为什么是异步的？如何获取生成结果？

**答**：题目生成需要调用 AI LLM 进行内容理解和题目构造，耗时较长（通常 10-60 秒）。使用异步方式可避免前端超时等待。

获取结果方式：
1. 调用 `POST /api/exam/generate` 提交任务，得到 `fileId`
2. 轮询 `GET /api/file/{fileId}` 检查 `examStatus` 字段
3. 当 `examStatus` 变为 `GENERATED` 时，通过题目列表接口查看题目

### Q4: citations 和 sources 有什么区别？

**答**：
- **citations**（finish 事件中）：AI 回答中真正引用到的切片。会写入 `context_reference` 表，可通过引用列表接口查询
- **sources**（流式事件中）：检索阶段命中的候选来源文档。**仅用于前端展示**，**不写入数据库**，`context_message` 表也没有 `sources` 字段

### Q5: context_reference.preview 字段是怎么来的？

**答**：当 citations 写入 `context_reference` 表时，后端会对每条引用自动调用 RAG 预览接口：

```
GET /documents/{collection}/{source}/preview?chunk_index=N&context=0
```

获取该切片的完整正文写入 `preview` 字段。若接口调用失败，`preview` 填入错误信息（如"预览获取失败: ..."）。前端可直接展示 `preview` 内容，无需再调用 RAG 接口。

### Q6: 同一会话多次提问，如何区分不同消息的引用？

**答**：`context_reference` 表通过 `messageId` 字段关联到具体消息。每个消息的引用列表独立。可通过：
- `/ai/message/{messageId}/references` — 查询某条消息的引用
- `/ai/session/{sessionId}/references` — 查询整个会话的所有引用

### Q7: context_image 表中的 score 字段有什么用？

**答**：保留 score 字段用于记录 AI 返回的图片相关性评分（0-1）。前端可根据 score 排序展示图片，或过滤低分图片。

---

## 九、附录：cURL 完整示例

> 以下示例均需替换 `<token>` 为实际登录获取的 JWT Token

### 9.1 登录获取 Token

```bash
curl -X POST "http://localhost:8080/api/auth/login" \
     -H "Content-Type: application/json" \
     -d '{"username":"admin","password":"123456"}'
```

### 9.2 发起流式问答

```bash
curl -X POST "http://localhost:8080/api/ai/chat/stream" \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{"message":"市场状态评价指标体系中一级指标有哪些","session_id":""}' \
     --no-buffer
```

### 9.3 中断流式问答

```bash
curl -X POST "http://localhost:8080/api/ai/chat/stop?sessionId=sess_xxx" \
     -H "Authorization: Bearer <token>"
```

### 9.4 获取会话列表

```bash
curl -X GET "http://localhost:8080/api/ai/sessions?page=1&pageSize=10" \
     -H "Authorization: Bearer <token>"
```

### 9.5 获取会话详情

```bash
curl -X GET "http://localhost:8080/api/ai/session/sess_1780410472658_9qg9a9hhp" \
     -H "Authorization: Bearer <token>"
```

### 9.6 获取消息引用列表

```bash
curl -X GET "http://localhost:8080/api/ai/message/178/references" \
     -H "Authorization: Bearer <token>"
```

### 9.7 获取消息文件来源列表（去重）

```bash
curl -X GET "http://localhost:8080/api/ai/message/178/sources" \
     -H "Authorization: Bearer <token>"
```

### 9.8 删除单个会话

```bash
curl -X DELETE "http://localhost:8080/api/ai/session/sess_1780410472658_9qg9a9hhp" \
     -H "Authorization: Bearer <token>"
```

### 9.9 批量删除会话

```bash
curl -X DELETE "http://localhost:8080/api/ai/sessions" \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '["sess_xxx1","sess_xxx2"]'
```

### 9.10 生成题目（指定题型）

```bash
curl -X POST "http://localhost:8080/api/exam/generate" \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
       "file_id": 1,
       "question_types": {
         "single_choice": 3,
         "multiple_choice": 2,
         "true_false": 2,
         "fill_blank": 2,
         "subjective": 1
       },
       "difficulty": 3,
       "request_id": "gen_req_001"
     }'
```

### 9.11 生成题目（智能模式）

```bash
curl -X POST "http://localhost:8080/api/exam/generate" \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{"file_id":1,"difficulty":3}'
```

### 9.12 轮询题目生成状态

```bash
curl -X GET "http://localhost:8080/api/file/1" \
     -H "Authorization: Bearer <token>"
```

### 9.13 生成试卷

```bash
curl -X POST "http://localhost:8080/api/exam/paper/generate" \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
       "single_choice_count": 5,
       "multiple_choice_count": 3,
       "true_false_count": 2,
       "fill_blank_count": 2,
       "subjective_count": 1,
       "difficulty": 3,
       "include_personal": false
     }'
```

### 9.14 批改答案（日常练习）

```bash
curl -X POST "http://localhost:8080/api/exam/grade" \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
       "request_id": "grade_req_001",
       "answer_type": "practice",
       "answers": [
         {
           "question_id": "q_001",
           "question_type": "single_choice",
           "question_content": {
             "stem": "题目内容...",
             "data": { "options": [...] },
             "answer": "B"
           },
           "student_answer": "B",
           "max_score": 5.0
         }
       ]
     }'
```

### 9.15 开始考试（获取题目）

```bash
curl -X POST "http://localhost:8080/api/exam/paper/paper_xxx/start" \
     -H "Authorization: Bearer <token>"
```

### 9.16 获取考试结果

```bash
curl -X GET "http://localhost:8080/api/exam/record/record_xxx/result" \
     -H "Authorization: Bearer <token>"
```

### 9.17 查询答题记录

```bash
curl -X POST "http://localhost:8080/api/exam/answers/query" \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{"paper_id":"paper_xxx"}'
```

### 9.18 获取考试记录列表

```bash
# 日常练习
curl -X GET "http://localhost:8080/api/exam/records?type=practice" \
     -H "Authorization: Bearer <token>"

# 正式考试
curl -X GET "http://localhost:8080/api/exam/records?type=formal_exam" \
     -H "Authorization: Bearer <token>"
```

---

**文档版本**：v2.17.0  
**最后更新**：2026-06-05  
**适用代码版本**：与当前 Spring Boot 后端（`AIChatController` / `ExamController`）同步
