1163 lines
34 KiB
Markdown
1163 lines
34 KiB
Markdown
# 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`)同步
|