Files
rag/docs/出题批题后端对接指南.md

576 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 出题批题接口 - 后端对接指南
## 一、接口概览
| 接口 | 方法 | 功能 | 超时建议 |
|------|------|------|----------|
| `/exam/generate` | POST | 生成题目(手动指定题型数量) | 读取超时至少 650 秒 |
| `/exam/generate-smart` | POST | 生成题目AI 自动分析文档结构出题) | 读取超时至少 650 秒 |
| `/exam/grade` | POST | 批阅答案 | 60秒 |
> **2026-07-19 变更**
> 1. 文件未完成向量化时不再返回“成功但 0 道题”,统一返回 HTTP 409、`FILE_NOT_INDEXED`、业务状态码 `4016`
> 2. `/exam/generate-smart` 会在 AI 分析前检查文件状态,未索引时不会调用模型
> 3. 正常成功响应保持不变Gunicorn 出题超时调整为 600 秒
>
> **2026-07-03 变更**
> 1. 移除出题总题数 20 道上限,超过 50 道时返回警告(不影响出题)
> 2. 填空题批阅增加 `student_answer` 格式校验(必须为字符串列表)
> 3. 认证错误状态码修正(仅 `DEV_MODE=true` 时生效,见 [认证说明](#认证模式说明)
>
> **2026-06-05 变更**`/exam/grade` 请求字段 `question_content` 已重命名为 `content`(破坏性变更)。详见 [出题批阅接口变更说明2026-06-05](出题批阅接口变更说明2026-06-05.md)。
---
## 二、出题接口
### 2.1 请求
```
POST /exam/generate
Content-Type: application/json
```
**请求体字段说明:**
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `file_path` | string | ✅ | 文档路径(相对于 documents 目录) |
| `collection` | string 或 string[] | ✅ | 向量库名称,支持数组(按优先级顺序检索,找到文件即停止) |
| `question_types` | object | ✅ | 题型及数量,**无上限**(超过 50 道时服务端返回警告但仍正常出题) |
| `difficulty` | int | ❌ | 难度等级 1-5默认 3 |
| `request_id` | string | ❌ | 请求追踪 ID服务端原样返回当前不提供幂等缓存 |
| `exclude_stems` | string[] | ❌ | 排除已有题目的题干列表,避免重复出题(最多 100 条) |
**请求示例:**
```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-tracing"
}
```
> **collection 检索逻辑说明**
> - 出题接口采用**按优先级顺序检索**策略
> - 先在 `dept_hr` 中查找文件,找到则停止
> - 未找到则继续在 `public_kb` 中查找
> - 这与 RAG 问答接口的**并行检索+融合**策略不同
> - 原因:出题需要特定文件的完整内容,而非广泛搜索
### 2.2 响应
**成功响应:**
```json
{
"success": true,
"status": "success",
"status_code": 2020,
"message": "出题成功",
"data": {
"request_id": "uuid-xxx",
"total": 10,
"source_chunks_used": 25,
"requested_types": {"single_choice": 3, "fill_blank": 2},
"actual_types": {"single_choice": 3, "fill_blank": 2},
"warnings": [],
"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",
"page_numbers": [1],
"sources": [{"chunk_id": "...", "page": 1, "section": "...", "snippet": "..."}]
}
}
]
}
}
```
**⚠️ 文件未完成向量化时的响应:**
`file_path` 在指定 `collection` 中找不到时,两个出题接口都返回 HTTP 409
```json
{
"success": false,
"status": "failed",
"error_code": "FILE_NOT_INDEXED",
"status_code": 4016,
"message": "文件未向量化: 文件 1.docx 未在向量库中找到,可能正在向量化或未上传",
"data": {
"request_id": "uuid-for-tracing",
"file_status": "not_found",
"chunk_count": 0
}
}
```
> **后端处理**:收到 HTTP 409 且 `error_code=FILE_NOT_INDEXED` 时,提示用户文件尚未完成向量化;不要把该响应当作成功题目入库,也不要立即高频重试。
>
> 当前版本要求后端传入有效的 `collection`。不存在的向量库暂时也可能表现为 `FILE_NOT_INDEXED`。
### 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 | 说明 |
|--------|------|------|
| `MISSING_PARAMS` | 400 | 缺少必填参数file_path / collection / question_types |
| `INVALID_PARAMS` | 400 | 参数校验失败题型无效、难度越界、总题数≤0 等) |
| `FILE_NOT_INDEXED` | 409 | 文件未完成向量化,业务状态码为 `4016` |
| `EXAM_ERROR` | 500 | 出题过程异常LLM 调用失败、解析错误等) |
> **注意**:即使文件已索引,模型也可能少生成题目。成功响应仍应检查 `data.total` 和 `data.warnings`;部分生成属于成功响应,不应与 `FILE_NOT_INDEXED` 混淆。
### 2.6 智能出题接口(/exam/generate-smart
`/exam/generate` 的区别:不需要传 `question_types`AI 自动分析文档后决定题型和数量。
```
POST /exam/generate-smart
Content-Type: application/json
```
**请求体字段说明:**
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `file_path` | string | ✅ | 文档路径 |
| `collection` | string 或 string[] | ✅ | 向量库名称 |
| `difficulty` | int | ❌ | 难度等级 1-5默认 3 |
| `max_total` | int | ❌ | AI 出题总数上限,不传则不限制 |
| `exclude_stems` | string[] | ❌ | 排除已有题目的题干列表 |
| `request_id` | string | ❌ | 请求ID |
**请求示例:**
```json
{
"file_path": "111/卷烟货源组织管理办法.docx",
"collection": "111",
"difficulty": 3,
"max_total": 10,
"request_id": "smart-uuid-xxx"
}
```
**响应:** 成功时与 `/exam/generate` 格式相同,并额外包含 `ai_analysis`;文件未索引时同样返回 HTTP 409且不会执行 AI 分析。
---
### 认证模式说明
RAG 服务支持两种认证模式,由环境变量 `DEV_MODE` 控制:
| 模式 | DEV_MODE | 认证行为 |
|------|----------|----------|
| **生产模式** | `false` | 认证网关**直接放行**,不校验 Header。权限由后端服务完全控制 |
| **开发模式** | `true` | 支持 `Authorization: Bearer mock-token-admin` 模拟登录,无 Header 时使用默认测试用户 |
**当前服务器为生产模式**`DEV_MODE=false`),因此:
- `Authorization` Header 会被忽略
- 不会返回 `401 UNAUTHORIZED``403 FORBIDDEN`
- 后端服务应自行完成用户认证和权限校验,再调用 RAG 出题/批阅接口
> 开发模式下,认证错误会返回正确的业务状态码(`UNAUTHORIZED=4001`、`FORBIDDEN=4002`HTTP 状态码分别为 401/403。
---
## 三、批阅接口
### 3.1 请求
```
POST /exam/grade
Content-Type: application/json
```
**请求体字段说明:**
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `request_id` | string | ❌ | 请求ID用于追踪 |
| `answers` | array | ✅ | 答案列表 |
**answers 数组中每个对象的字段:**
| 字段 | 类型 | 必需 | 说明 | 来源 |
|------|------|------|------|------|
| `question_id` | string | ✅ | 题目ID | 后端数据库 |
| `question_type` | string | ✅ | 题型 | 后端数据库 |
| `content` | object | ✅ | 题目内容(含正确答案) | 后端数据库 |
| `student_answer` | any | ✅ | 学生答案 | 学生提交 |
| `max_score` | number | ✅ | 满分 | 后端数据库 |
> **填空题 `student_answer` 格式要求**2026-07-03 新增校验):
> - 必须为 **字符串数组**,如 `["答案1", "答案2"]`
> - 不是列表(如传了字符串 `"答案1"`)→ 该题直接得 0 分,`grading_status` 为 `"failed"`
> - 列表中某项不是字符串(如传了数字 `123`)→ 该题直接得 0 分
> - 校验失败不影响其他题目的正常批阅
**请求示例:**
```json
{
"request_id": "grade-uuid-xxx",
"answers": [
{
"question_id": "q-001",
"question_type": "single_choice",
"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",
"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",
"content": {
"stem": "公司规定员工每月绩效奖金上限为工资的20%。",
"answer": "F"
},
"student_answer": "T",
"max_score": 2.0
},
{
"question_id": "q-004",
"question_type": "fill_blank",
"content": {
"stem": "员工薪资由___、___和___三部分组成。",
"data": {"blank_count": 3},
"answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]]
},
"student_answer": ["基本工资", "绩效奖金", "交通补贴"],
"max_score": 6.0
},
{
"question_id": "q-005",
"question_type": "subjective",
"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 | 说明 |
|--------|------|------|
| `MISSING_PARAMS` | 400 | 缺少 answers 字段 |
| `INVALID_PARAMS` | 400 | answers 非数组、question_type 无效等 |
| `GRADE_ERROR` | 500 | 批阅过程出错 |
> **填空题格式错误不返回错误码**:当 `student_answer` 格式不合法时,该题返回 `grading_status: "failed"` + `score: 0`,但接口整体仍返回 `success: true`。后端应检查每道题的 `grading_status` 字段。
---
## 四、完整调用流程
### 4.1 出题流程
```
后端调用 /exam/generate
RAG 返回题目
(question_type, difficulty, content, source_trace)
后端生成 question_id
后端设置 score (满分)
后端设置 status
后端存入数据库
```
### 4.2 批阅流程
```
学生提交答案
后端从数据库查询:
- question_id
- question_type
- content (含正确答案,原 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 | 后端数据库 | 题型 |
| content | 后端数据库 | 题目内容(含正确答案,原 question_content 已弃用) |
| student_answer | 学生提交 | 学生作答 |
| **max_score** | **后端数据库** | 满分(决定得分上限) |
---
## 六、常见问题
### Q1: 出题接口返回的题目需要审核吗?
建议后端设置 `status` 字段进行审核流程,审核通过后再用于组卷。
### Q2: 分值如何动态调整?
后端在入库时自由设定 `score` 字段,批阅时传入 `max_score` 即可。RAG 服务不干预分值。
### Q3: 填空题同义词匹配是如何实现的?
填空题答案格式为 `[["主答案", "同义词1", "同义词2"], ...]`,学生答案匹配任意一个即视为正确。
### Q4: 批阅接口超时怎么办?
建议:
- 单次批阅不超过 50 道题
- 设置 60 秒超时
- 主观题较多时适当延长
### Q5: 出题数量有上限吗?
2026-07-03 起,出题接口**不再限制总题数**。服务端会在请求数超过 50 道时记录警告日志,但不影响出题。建议:
- 单次出题不超过 30 道LLM 生成耗时与题数成正比30 道约需 5-8 分钟)
- 超过 30 道建议分批调用
- 后端 HTTP 客户端读取超时设置为至少 650 秒
### Q6: 填空题学生答案传错了会怎样?
2026-07-03 起,填空题增加了格式校验:
- `student_answer` 不是列表 → 该题得 0 分,`grading_status = "failed"`
- 列表中某项不是字符串 → 该题得 0 分
- 校验失败不影响同一请求中其他题目的批阅
- 接口整体仍返回 `success: true`,需检查每道题的 `grading_status`
---
**文档版本**: v1.3
**更新时间**: 2026-07-19
**相关文档**: [后端对接规范.md](./后端对接规范.md)