Files
rag/docs/出题批阅接口变更说明(2026-06-05).md
lacerate551 6f863ddfd7 docs(exam): 出题批卷接口变更说明及文档同步
- 新增《出题批阅接口变更说明(2026-06-05)》面向后端的迁移指南
- 同步更新 curl测试手册 和 后端对接规范 中的出题批卷章节
2026-06-05 14:06:17 +08:00

404 lines
14 KiB
Markdown
Raw Permalink 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.
# 出题批阅接口变更说明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` | 可选 | 提示评分可信度 |