1.修复创建多个向量库

This commit is contained in:
2026-06-09 13:34:21 +08:00
parent 6cdb38df68
commit f3543f4188
74 changed files with 19636 additions and 3514 deletions

View File

@@ -0,0 +1,192 @@
## 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]` 标记,避免冲突。

View File

@@ -85,7 +85,7 @@ curl -s http://localhost:5001/health
知识库问答SSE 流式返回)。
```bash
echo '{"message":"三峡工程","chat_history":[]}' > /tmp/rag.json
echo '{"message":"市场权重计算公式","chat_history":[]}' > /tmp/rag.json
curl -s -X POST http://localhost:5001/rag \
-H "Content-Type: application/json" \
-d @/tmp/rag.json
@@ -121,7 +121,7 @@ data: {"type": "finish", "answer": "完整回答", "sources": [...]}
**带 collections 参数**
```bash
echo '{"message":"三峡工程","chat_history":[],"collections":["public_kb"]}' > /tmp/rag.json
echo '{"message":"市场权重计算公式","chat_history":[],"collections":["dept_1_kb"]}' > /tmp/rag.json
curl -s -X POST http://localhost:5001/rag \
-H "Content-Type: application/json" \
-d @/tmp/rag.json
@@ -1375,7 +1375,7 @@ curl -s http://localhost:5001/exam/health
curl -s -X POST http://localhost:5001/exam/generate \
-H "Authorization: Bearer mock-token-admin" \
-H "Content-Type: application/json" \
-d '{"file_path":"public_kb/1.docx","collection":"public_kb","question_types":{"single_choice":5,"true_false":3,"fill_blank":2}}'
-d '{"file_path":"dept_1_kb/1.docx","collection":"dept_1_kb","question_types":{"single_choice":5,"true_false":3,"fill_blank":2}}'
```
**请求体**
@@ -1458,7 +1458,7 @@ AI 智能出题 - 自动分析文件并决定题型和数量。
```bash
curl -s -X POST http://localhost:5001/exam/generate-smart \
-H "Content-Type: application/json" \
-d '{"file_path":"dept_1_kb/test.docx","collection":"dept_1_kb"}'
-d '{"file_path":"dept_1_kb/需求分析.docx","collection":"dept_1_kb"}'
```
**请求体**

View File

@@ -0,0 +1,403 @@
# 出题批阅接口变更说明2026-06-05
> 本文档面向后端开发人员,汇总出题/批卷接口本次升级的所有变更点,以及后端需要适配的代码修改。
---
## 一、变更总览
| 变更项 | 变更类型 | 影响范围 | 后端是否必须改 |
|--------|----------|----------|---------------|
| 批卷请求字段 `question_content``content` | **破坏性变更** | `/exam/grade` 请求体 | **是** |
| 批卷结果格式统一为 `grading_status` + `details` | **格式变更** | `/exam/grade` 响应体 | **是** |
| 出题总题数上限 20 道 | 新增校验 | `/exam/generate` | 需注意 |
| 批卷 `question_type` 枚举校验 | 新增校验 | `/exam/grade` | 需注意 |
| **`exclude_stems` 跨调用去重** | **新增参数** | `/exam/generate``/exam/generate-smart` | **建议使用** |
| 出题结果新增 `requested_types``actual_types``warnings` | 新增字段 | `/exam/generate` 响应体 | 可选消费 |
| 主观题缺少评分标准时返回 `warnings` | 新增字段 | `/exam/grade` 响应体 | 可选消费 |
---
## 二、破坏性变更(必须修改)
### 2.1 批卷请求字段重命名:`question_content` → `content`
**变更前**
```json
{
"question_id": "q-001",
"question_type": "single_choice",
"question_content": {
"stem": "题干",
"answer": "B",
"data": {"options": [...]}
},
"student_answer": "B",
"max_score": 2
}
```
**变更后**
```json
{
"question_id": "q-001",
"question_type": "single_choice",
"content": {
"stem": "题干",
"answer": "B",
"data": {"options": [...]}
},
"student_answer": "B",
"max_score": 2
}
```
**为什么要改**:出题接口 `/exam/generate` 返回的每道题里,题目内容字段叫 `content`。批卷接口改用同名 `content` 后,后端可以直接把出题结果的 `content` 透传到批卷接口,不需要做任何字段映射。
**后端代码修改示例**
```python
# 修改前
grade_answers.append({
"question_id": qid,
"question_type": question.question_type,
"question_content": question.content, # ← 旧字段名
"student_answer": ans['answer'],
"max_score": question.score
})
# 修改后
grade_answers.append({
"question_id": qid,
"question_type": question.question_type,
"content": question.content, # ← 新字段名,与出题接口一致
"student_answer": ans['answer'],
"max_score": question.score
})
```
### 2.2 批卷结果格式统一
**变更前**:各题型返回格式不统一,客观题直接返回 `correct` + `feedback`,填空题返回 `details.blank_scores`,主观题返回 `details.scoring_breakdown`,没有统一的结构标识。
```json
// 旧 - 客观题
{"question_id": "q1", "score": 2, "max_score": 2, "correct": true, "feedback": "正确!"}
// 旧 - 填空题
{"question_id": "q4", "score": 2, "max_score": 4, "details": {"blank_scores": [2, 0]}}
// 旧 - 主观题
{"question_id": "q5", "score": 7, "max_score": 10, "details": {"scoring_breakdown": [...]}}
```
**变更后**:所有题型统一使用 `grading_status` + `details` 结构。
```json
// 新 - 客观题
{
"question_id": "q1",
"score": 2,
"max_score": 2,
"grading_status": "success",
"details": {
"correct": true,
"student_answer": "B",
"correct_answer": "B",
"feedback": "正确!"
}
}
// 新 - 填空题
{
"question_id": "q4",
"score": 2.0,
"max_score": 4.0,
"grading_status": "success",
"details": {
"total_blanks": 2,
"correct_blanks": 1,
"blank_scores": [2.0, 0],
"feedback": "2 个空中答对 1 个"
}
}
// 新 - 主观题
{
"question_id": "q5",
"score": 7.5,
"max_score": 10.0,
"grading_status": "success",
"details": {
"scoring_breakdown": [
{"point": "核心概念", "weight": 0.5, "achieved": 0.8, "comment": "概念描述准确"}
],
"highlights": ["条理清晰"],
"shortcomings": ["缺少应用场景"],
"overall_feedback": "整体回答较好,建议补充实际应用场景。"
}
}
// 新 - 评分失败LLM 异常或超时)
{
"question_id": "q6",
"score": 0,
"max_score": 10.0,
"grading_status": "failed",
"details": {
"error": "评分结果解析失败"
}
}
```
**后端需要关注的点**
1. **判断评分是否成功**:改用 `grading_status` 字段判断,不再依赖 `correct``score > 0`
- `"success"` → 正常评分,`score``details` 有效
- `"failed"` → 评分失败,`score` 为 0`details.error` 包含失败原因
2. **获取反馈信息**:原来客观题的 `correct`/`feedback` 现在统一在 `details`
3. **成绩更新逻辑**`grading_status: "failed"` 的题目,后端可选择标记为"待重批"而非直接记 0 分
**后端代码修改示例**
```python
# 修改前
for result in grade_result['results']:
is_correct = result.get('correct', False)
feedback = result.get('feedback', '')
# 更新成绩...
# 修改后
for result in grade_result['results']:
status = result.get('grading_status', 'success')
details = result.get('details', {})
if status == 'failed':
# 评分失败,标记待重批
mark_for_regrade(result['question_id'], details.get('error', ''))
continue
is_correct = details.get('correct', None) # 仅客观题有此字段
feedback = details.get('feedback', '')
# 更新成绩...
```
---
## 三、新增校验规则(需注意)
### 3.1 出题接口入参校验
`POST /exam/generate` 新增以下校验,不满足时返回 **HTTP 400**`error_code``INVALID_PARAMS`
| 校验项 | 规则 | 示例(会返回 400 |
|--------|------|-------------------|
| 题型合法性 | `question_types` 的 key 必须属于 5 种题型 | `{"essay": 2}` → 不支持 |
| 题型数量 | 每种题型数量必须为非负整数 | `{"single_choice": -1}` → 不合法 |
| 难度范围 | `difficulty` 必须为 1-5 的整数 | `"difficulty": 10` → 超范围 |
| **总题数上限** | **所有题型数量之和不能超过 20** | `{"single_choice": 15, "true_false": 10}` → 超限 |
合法的 5 种题型 key`single_choice``multiple_choice``true_false``fill_blank``subjective`
**后端建议**:在前端提交出题请求时做前置校验,避免不必要的网络请求。
### 3.2 批卷接口 question_type 校验
`POST /exam/grade``answers` 数组中,每道题的 `question_type` 现在也会校验。无效值返回 **HTTP 400**
```json
// 返回 400
{"answers": [{"question_type": "essay", ...}]}
// 错误信息
{"error_code": "INVALID_PARAMS", "message": "第 1 题的 question_type 无效: essay合法值: fill_blank, multiple_choice, single_choice, subjective, true_false"}
```
### 3.3 错误响应格式
校验失败统一返回:
```json
{
"success": false,
"error_code": "INVALID_PARAMS",
"message": "具体错误描述",
"status": "failed",
"status_code": 4000
}
```
---
## 三点五、跨调用去重:`exclude_stems`(强烈建议使用)
### 背景问题
同一份文档多次调用出题接口时,由于知识点来源相同,新生成的题目很可能与已有题目高度重复。单次调用上限 20 道,当需要更多题目时,这个问题尤为突出。
### 解决方式
两个出题接口(`/exam/generate``/exam/generate-smart`)新增可选参数 `exclude_stems`后端把该文件已入库的题目题干传过来RAG 在生成后自动排除匹配的题目。
**请求示例**
```json
POST /exam/generate
{
"file_path": "public_kb/产品手册.pdf",
"collection": "public_kb",
"question_types": {"single_choice": 10},
"exclude_stems": [
"重购率的定义是下列哪一项?",
"以下哪项属于现代终端分类?",
"客户满意度的计算公式为___"
]
}
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `exclude_stems` | string[] | 否 | 已有题目的题干列表,最多 100 条 |
### 去重策略
- 基于题干前 80 个字符进行匹配
- 新生成的题目如果与 `exclude_stems` 中任意一条匹配,会被自动过滤
- 不传或传空数组时行为不变(向后兼容)
### 后端使用流程
```
第一次出题:
POST /exam/generate { question_types: {...} }
→ 返回 20 道新题 → 审核后入库
第二次出题(想多出 10 道选择题):
1. 查询数据库,获取该文件已有的所有题目题干
2. POST /exam/generate {
question_types: {"single_choice": 10},
exclude_stems: ["已有题干1", "已有题干2", ...]
}
→ 返回 10 道不与已有题目重复的新题
```
**后端代码示例**
```python
def generate_more_questions(file_path, collection, question_types):
# 1. 查询该文件已有的题目题干
existing_stems = db.query("""
SELECT JSON_EXTRACT(content, '$.stem')
FROM questions
WHERE source_file = ?
""", [file_path])
# 2. 调用出题接口,传入已有题干
response = requests.post('http://rag-service:5001/exam/generate', json={
'file_path': file_path,
'collection': collection,
'question_types': question_types,
'exclude_stems': [row[0] for row in existing_stems]
})
return response.json()
```
---
## 四、新增字段(可选消费)
### 4.1 出题结果新增字段
`POST /exam/generate``POST /exam/generate-smart``data` 对象新增三个字段:
```json
{
"data": {
"questions": [...],
"total": 8,
"requested_types": {"single_choice": 5, "true_false": 3, "fill_blank": 2},
"actual_types": {"single_choice": 5, "true_false": 3, "fill_blank": 0},
"warnings": ["fill_blank: 请求 2 道,实际生成 0 道"],
...
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `requested_types` | object | 后端请求的题型和数量(原样回传) |
| `actual_types` | object | 实际生成的题型和数量 |
| `warnings` | string[] | 短缺提示数组,全部生成成功时为空数组 |
**后端建议**:检查 `warnings` 是否为空,不为空时可提示用户"部分题型生成不足",或自动补发请求。
### 4.2 主观题评分警告
当主观题的 `content` 中缺少 `data.scoring_points` 评分标准时,批卷结果会在 `details.warnings` 中给出提示:
```json
{
"question_id": "q5",
"score": 7.0,
"max_score": 10.0,
"grading_status": "success",
"details": {
"scoring_breakdown": [...],
"overall_feedback": "...",
"warnings": ["缺少评分标准(scoring_points),评分结果仅供参考"]
}
}
```
此时 `grading_status` 仍为 `"success"`,但评分准确度可能受影响。后端可在前端展示时附上"评分仅供参考"的提示。
---
## 五、完整的出题→入库→批卷数据流(更新后)
```
┌──────────────────────────────────────────────────────────────────────────┐
│ 后端完整流程 │
│ │
│ 1. 调用出题接口 │
│ POST /exam/generate │
│ → 首次出题:不传 exclude_stems │
│ → 追加出题:查询已有题干,传入 exclude_stems 避免重复 │
│ → 返回 questions[],每题包含 question_type, content, source_trace │
│ → 检查 warnings 是否有短缺提示 │
│ │
│ 2. 审核入库(后端自行决定) │
│ → 可删除/修改不满意的题目 │
│ → 存入数据库时保留 content 字段原样 │
│ → 生成 question_id (UUID),设置 max_score │
│ │
│ 3. 学生作答 │
│ → 收集 student_answer │
│ │
│ 4. 调用批卷接口 │
│ POST /exam/grade │
│ → content 直接从数据库取出透传(与出题接口返回的结构一致) │
│ → 检查 grading_status 判断每题是否评分成功 │
│ → grading_status=failed 的题目可标记待重批 │
│ │
│ 5. 更新成绩 │
│ → 根据 question_id 匹配结果,写入学生成绩表 │
└──────────────────────────────────────────────────────────────────────────┘
```
**关键要点**:出题返回的 `content` 和批卷接收的 `content` 结构完全一致,后端只需原样存取即可,无需任何字段转换。
---
## 六、后端修改清单
| 序号 | 修改项 | 紧急程度 | 说明 |
|------|--------|----------|------|
| 1 | 批卷请求 `question_content` 改为 `content` | **必须** | 否则批卷接口无法读取题目内容,所有评分失败 |
| 2 | 批卷结果解析改用 `grading_status` + `details` | **必须** | 否则无法正确获取评分详情 |
| 3 | 处理 `grading_status: "failed"` 情况 | **建议** | 避免将 LLM 异常导致的 0 分直接记入成绩 |
| 4 | **追加出题时传入 `exclude_stems`** | **强烈建议** | 避免同一文件多次出题产生重复题目 |
| 5 | 出题请求总题数控制在 20 以内 | **建议** | 否则返回 400 错误 |
| 6 | 前端校验 question_types 和 difficulty | **建议** | 减少无效请求 |
| 7 | 消费 `warnings` 字段做短缺提示 | 可选 | 提升用户体验 |
| 8 | 消费主观题 `details.warnings` | 可选 | 提示评分可信度 |

View File

@@ -1,468 +0,0 @@
# 出题批题接口 - 后端对接指南
## 一、接口概览
| 接口 | 方法 | 功能 | 超时建议 |
|------|------|------|----------|
| `/exam/generate` | POST | 生成题目 | 120秒 |
| `/exam/grade` | POST | 批阅答案 | 60秒 |
---
## 二、出题接口
### 2.1 请求
```
POST /exam/generate
Content-Type: application/json
```
**请求体字段说明:**
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `file_path` | string | ✅ | 文档路径(相对于 documents 目录) |
| `collection` | string 或 string[] | ✅ | 向量库名称,支持数组(按优先级顺序检索,找到文件即停止) |
| `question_types` | object | ✅ | 题型及数量 |
| `difficulty` | int | ❌ | 难度等级 1-5默认 3 |
| `request_id` | string | ❌ | 请求ID相同ID返回缓存结果幂等性 |
**请求示例:**
```json
{
"file_path": "薪酬制度.docx",
"collection": ["dept_hr", "public_kb"],
"question_types": {
"single_choice": 3,
"multiple_choice": 2,
"true_false": 2,
"fill_blank": 2,
"subjective": 1
},
"difficulty": 3,
"request_id": "uuid-for-idempotency"
}
```
> **collection 检索逻辑说明**
> - 出题接口采用**按优先级顺序检索**策略
> - 先在 `dept_hr` 中查找文件,找到则停止
> - 未找到则继续在 `public_kb` 中查找
> - 这与 RAG 问答接口的**并行检索+融合**策略不同
> - 原因:出题需要特定文件的完整内容,而非广泛搜索
### 2.2 响应
**成功响应:**
```json
{
"success": true,
"status": "success",
"status_code": 2011,
"message": "出题完成",
"data": {
"request_id": "uuid-xxx",
"total": 10,
"source_chunks_used": 25,
"questions": [
{
"question_type": "single_choice",
"difficulty": 3,
"content": {
"stem": "题干内容",
"data": {
"options": [
{"key": "A", "content": "选项A"},
{"key": "B", "content": "选项B"},
{"key": "C", "content": "选项C"},
{"key": "D", "content": "选项D"}
]
},
"answer": "B",
"explanation": "答案解析"
},
"source_trace": {
"document_name": "薪酬制度.docx",
"chunks_count": 3
}
}
]
}
}
```
**失败响应:**
```json
{
"success": false,
"status": "failed",
"error_code": "FILE_NOT_FOUND",
"message": "文件不存在"
}
```
### 2.3 返回字段说明
**RAG 服务返回的字段(后端需存储):**
| 字段 | 类型 | 说明 | 后端是否需要存储 |
|------|------|------|------------------|
| `question_type` | string | 题型 | ✅ 存储 |
| `difficulty` | int | 难度 1-5 | ✅ 存储 |
| `content` | object | 题目内容 | ✅ 存储 |
| `content.stem` | string | 题干 | ✅ |
| `content.data` | object | 附加数据(选项、评分标准等) | ✅ |
| `content.answer` | any | 正确答案 | ✅ 批阅时需要 |
| `content.explanation` | string | 答案解析 | ✅ |
| `source_trace` | object | 来源追踪 | ✅ 存储 |
**后端需要自己生成的字段:**
| 字段 | 说明 |
|------|------|
| `question_id` | UUID唯一标识 |
| `score` | 满分,根据业务需求设定 |
| `tags` | 标签 |
| `status` | 状态(待审核/已通过/已拒绝) |
> **重要**RAG 服务不返回 `score` 字段,分值由后端在入库时指定。
### 2.4 题型格式对照表
| 题型 | question_type | content.answer 格式 | content.data |
|------|---------------|---------------------|--------------|
| 单选题 | `single_choice` | `"B"` | `{options: [{key, content}]}` |
| 多选题 | `multiple_choice` | `["A", "C"]` | `{options: [{key, content}]}` |
| 判断题 | `true_false` | `"T"``"F"` | 无 |
| 填空题 | `fill_blank` | `[["答案1"], ["答案2", "同义词"]]` | `{blank_count: 2}` |
| 简答题 | `subjective` | `"参考范文..."` | `{scoring_points: [...]}` |
### 2.5 错误码
| 错误码 | HTTP | 说明 |
|--------|------|------|
| `FILE_NOT_FOUND` | 404 | 指定文件不存在 |
| `COLLECTION_NOT_FOUND` | 404 | 指定向量库不存在 |
| `NO_CONTENT` | 400 | 文件内容为空,无法出题 |
| `LLM_ERROR` | 500 | LLM 调用失败 |
| `PARSE_ERROR` | 500 | 解析失败 |
---
## 三、批阅接口
### 3.1 请求
```
POST /exam/grade
Content-Type: application/json
```
**请求体字段说明:**
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `request_id` | string | ❌ | 请求ID用于追踪 |
| `answers` | array | ✅ | 答案列表 |
**answers 数组中每个对象的字段:**
| 字段 | 类型 | 必需 | 说明 | 来源 |
|------|------|------|------|------|
| `question_id` | string | ✅ | 题目ID | 后端数据库 |
| `question_type` | string | ✅ | 题型 | 后端数据库 |
| `question_content` | object | ✅ | 题目内容(含正确答案) | 后端数据库 |
| `student_answer` | any | ✅ | 学生答案 | 学生提交 |
| `max_score` | number | ✅ | 满分 | 后端数据库 |
**请求示例:**
```json
{
"request_id": "grade-uuid-xxx",
"answers": [
{
"question_id": "q-001",
"question_type": "single_choice",
"question_content": {
"stem": "根据公司规定,员工薪资由哪几部分组成?",
"data": {
"options": [
{"key": "A", "content": "基本工资+奖金"},
{"key": "B", "content": "基本工资+绩效奖金+津贴补贴"},
{"key": "C", "content": "基本工资+加班费"},
{"key": "D", "content": "基本工资"}
]
},
"answer": "B"
},
"student_answer": "B",
"max_score": 5.0
},
{
"question_id": "q-002",
"question_type": "multiple_choice",
"question_content": {
"stem": "以下哪些属于绩效奖金的评定因素?",
"data": {
"options": [
{"key": "A", "content": "工作质量"},
{"key": "B", "content": "工作态度"},
{"key": "C", "content": "考勤情况"},
{"key": "D", "content": "团队协作"}
]
},
"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
}
]
}
```
### 3.2 响应
**成功响应:**
```json
{
"success": true,
"status": "success",
"status_code": 2012,
"message": "批阅完成",
"data": {
"request_id": "grade-uuid-xxx",
"total_score": 14.0,
"total_max_score": 27.0,
"score_rate": 51.85,
"results": [
{
"question_id": "q-001",
"question_type": "single_choice",
"score": 5.0,
"max_score": 5.0,
"is_correct": true,
"student_answer": "B",
"correct_answer": "B",
"feedback": "回答正确"
},
{
"question_id": "q-002",
"question_type": "multiple_choice",
"score": 2.0,
"max_score": 4.0,
"is_correct": false,
"student_answer": ["A", "B"],
"correct_answer": ["A", "B", "D"],
"feedback": "少选了 D 选项,正确答案: A, B, D"
},
{
"question_id": "q-003",
"question_type": "true_false",
"score": 0.0,
"max_score": 2.0,
"is_correct": false,
"student_answer": "T",
"correct_answer": "F",
"feedback": "正确答案: F"
},
{
"question_id": "q-004",
"question_type": "fill_blank",
"score": 4.0,
"max_score": 6.0,
"is_correct": false,
"student_answer": ["基本工资", "绩效奖金", "交通补贴"],
"correct_answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]],
"feedback": "第1、2空正确第3空答案应为津贴补贴或补贴"
},
{
"question_id": "q-005",
"question_type": "subjective",
"score": 3.0,
"max_score": 10.0,
"is_correct": false,
"student_answer": "我认为公司的薪酬制度应该公平公正...",
"correct_answer": "公司薪酬制度遵循公平、激励、竞争、合法四大原则...",
"feedback": "得分点:提到公平性概念,但未展开说明激励性和竞争力原则"
}
]
}
}
```
### 3.3 返回字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `total_score` | number | 总得分 |
| `total_max_score` | number | 总满分 |
| `score_rate` | number | 得分率(百分比) |
| `results` | array | 每道题的批阅结果 |
| `results[].question_id` | string | 题目ID |
| `results[].score` | number | 该题得分 |
| `results[].max_score` | number | 该题满分 |
| `results[].is_correct` | boolean | 是否完全正确 |
| `results[].feedback` | string | 批阅反馈 |
### 3.4 批阅规则
| 题型 | 批阅方式 | 得分规则 |
|------|----------|----------|
| 单选题 | 精确匹配 | 正确得满分,错误得 0 |
| 多选题 | 集合比对 | 全对满分,少选得一半,错选得 0 |
| 判断题 | 精确匹配 | 正确得满分,错误得 0 |
| 填空题 | 多答案匹配 | 每空独立评分,支持同义词匹配 |
| 简答题 | LLM 评分 | 根据得分点评分,不超过 `max_score` |
### 3.5 错误码
| 错误码 | HTTP | 说明 |
|--------|------|------|
| `INVALID_ANSWER_FORMAT` | 400 | 答案格式不正确 |
| `GRADING_ERROR` | 500 | 批阅过程出错 |
---
## 四、完整调用流程
### 4.1 出题流程
```
后端调用 /exam/generate
RAG 返回题目
(question_type, difficulty, content, source_trace)
后端生成 question_id
后端设置 score (满分)
后端设置 status
后端存入数据库
```
### 4.2 批阅流程
```
学生提交答案
后端从数据库查询:
- question_id
- question_type
- question_content (含正确答案)
- score (满分)
后端组装请求调用 /exam/grade
RAG 返回批阅结果
(score, feedback, is_correct)
后端存入成绩表
```
---
## 五、字段来源速查表
### 5.1 出题接口
| 字段 | 返回方 | 存储方 | 说明 |
|------|--------|--------|------|
| question_id | ❌ 不返回 | 后端生成 | UUID |
| question_type | ✅ RAG | 后端存储 | 题型 |
| difficulty | ✅ RAG | 后端存储 | 难度 1-5 |
| content | ✅ RAG | 后端存储 | 题目内容 |
| source_trace | ✅ RAG | 后端存储 | 来源追踪 |
| **score** | ❌ 不返回 | **后端设定** | 满分 |
| tags | ❌ 不返回 | 后端设定 | 标签 |
| status | ❌ 不返回 | 后端设定 | 状态 |
### 5.2 批阅接口
| 字段 | 来源 | 说明 |
|------|------|------|
| question_id | 后端数据库 | 题目唯一标识 |
| question_type | 后端数据库 | 题型 |
| question_content | 后端数据库 | 题目内容(含正确答案) |
| student_answer | 学生提交 | 学生作答 |
| **max_score** | **后端数据库** | 满分(决定得分上限) |
---
## 六、常见问题
### Q1: 出题接口返回的题目需要审核吗?
建议后端设置 `status` 字段进行审核流程,审核通过后再用于组卷。
### Q2: 分值如何动态调整?
后端在入库时自由设定 `score` 字段,批阅时传入 `max_score` 即可。RAG 服务不干预分值。
### Q3: 填空题同义词匹配是如何实现的?
填空题答案格式为 `[["主答案", "同义词1", "同义词2"], ...]`,学生答案匹配任意一个即视为正确。
### Q4: 批阅接口超时怎么办?
建议:
- 单次批阅不超过 50 道题
- 设置 60 秒超时
- 主观题较多时适当延长
---
**文档版本**: v1.1
**更新时间**: 2026-05-17
**相关文档**: [后端对接规范.md](./后端对接规范.md)