From 6f863ddfd7c687db05187a24759f26bcfb5d6c58 Mon Sep 17 00:00:00 2001 From: lacerate551 <128470311+lacerate551@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:06:17 +0800 Subject: [PATCH] =?UTF-8?q?docs(exam):=20=E5=87=BA=E9=A2=98=E6=89=B9?= =?UTF-8?q?=E5=8D=B7=E6=8E=A5=E5=8F=A3=E5=8F=98=E6=9B=B4=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E5=8F=8A=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增《出题批阅接口变更说明(2026-06-05)》面向后端的迁移指南 - 同步更新 curl测试手册 和 后端对接规范 中的出题批卷章节 --- docs/curl测试手册.md | 44 ++- docs/出题批阅接口变更说明(2026-06-05).md | 403 +++++++++++++++++++++ docs/后端对接规范.md | 101 ++++-- 3 files changed, 510 insertions(+), 38 deletions(-) create mode 100644 docs/出题批阅接口变更说明(2026-06-05).md diff --git a/docs/curl测试手册.md b/docs/curl测试手册.md index acddc73..a74aeab 100644 --- a/docs/curl测试手册.md +++ b/docs/curl测试手册.md @@ -1415,6 +1415,13 @@ curl -s http://localhost:5001/exam/health - 需要手动指定 `question_types`(每种题型的数量) - 适合有明确需求的场景(如"出 5 道单选题") +**入参校验**: +- `question_types` 中每个题型必须属于: `single_choice`, `multiple_choice`, `true_false`, `fill_blank`, `subjective` +- 每种题型数量必须为非负整数 +- `difficulty` 必须为 1-5 的整数 +- **总题数上限 20 道**(所有题型数量之和不能超过 20) +- 校验失败返回 HTTP 400,error_code 为 `INVALID_PARAMS` + ```bash curl -s -X POST http://localhost:5001/exam/generate \ -H "Content-Type: application/json" \ @@ -1430,6 +1437,7 @@ curl -s -X POST http://localhost:5001/exam/generate \ | `difficulty` | int | ❌ | 难度等级(1-5),默认 3 | | `options` | object | ❌ | 附加选项(如 `max_source_chunks`) | | `request_id` | string | ❌ | 请求 ID(幂等性) | +| `exclude_stems` | string[] | ❌ | 已有题目题干列表(跨调用去重,最多 100 条) | > **注意**:生产模式(`DEV_MODE=false`)下无需传 `Authorization` header,直接放行。开发模式下可传 `Authorization: Bearer mock-token-admin` 模拟管理员身份。 @@ -1474,7 +1482,10 @@ curl -s -X POST http://localhost:5001/exam/generate \ "request_id": null, "source_chunks_used": 15, "success": true, - "total": 10 + "total": 10, + "requested_types": {"single_choice": 5, "true_false": 3, "fill_blank": 2}, + "actual_types": {"single_choice": 5, "true_false": 3, "fill_blank": 2}, + "warnings": [] }, "message": "出题成功", "status": "success", @@ -1483,7 +1494,9 @@ curl -s -X POST http://localhost:5001/exam/generate \ } ``` -**验证结果**:✅ 通过 +> **说明**:`warnings` 字段在某题型实际生成数量少于请求数量时返回提示信息。 + +**验证结果**:✅ 通过(2026-06-05) --- @@ -1562,7 +1575,7 @@ curl -s -X POST http://localhost:5001/exam/generate-smart \ 批阅答案。 ```bash -echo '{"answers":[{"question_id":"q1","question_type":"single_choice","question_content":{"answer":"B"},"student_answer":"B","max_score":2}]}' > /tmp/grade.json +echo '{"answers":[{"question_id":"q1","question_type":"single_choice","content":{"answer":"B"},"student_answer":"B","max_score":2}]}' > /tmp/grade.json curl -s -X POST http://localhost:5001/exam/grade \ -H "Content-Type: application/json" \ -d @/tmp/grade.json @@ -1578,10 +1591,12 @@ curl -s -X POST http://localhost:5001/exam/grade \ |------|------|------|------| | `question_id` | string | ✅ | 题目 ID | | `question_type` | string | ✅ | 题型 | -| `question_content` | object | ✅ | 题目内容(含标准答案) | +| `content` | object | ✅ | 题目内容(与出题接口返回的 `content` 字段结构一致,后端可直接透传) | | `student_answer` | string/array | ✅ | 学生答案 | | `max_score` | int | ✅ | 满分 | +> **注意**:`question_type` 必须为以下值之一:`single_choice`, `multiple_choice`, `true_false`, `fill_blank`, `subjective`。传入无效类型返回 HTTP 400。 + **响应示例**: ```json { @@ -1589,11 +1604,16 @@ curl -s -X POST http://localhost:5001/exam/grade \ "request_id": null, "results": [ { - "correct": true, - "feedback": "正确!", - "max_score": 2, "question_id": "q1", - "score": 2 + "score": 2, + "max_score": 2, + "grading_status": "success", + "details": { + "correct": true, + "student_answer": "B", + "correct_answer": "B", + "feedback": "正确!" + } } ], "score_rate": 100.0, @@ -1608,7 +1628,13 @@ curl -s -X POST http://localhost:5001/exam/grade \ } ``` -**验证结果**:✅ 通过 +**`grading_status` 取值说明**: +- `success`:评分成功 +- `failed`:评分失败(主观题 LLM 解析失败或超时),此时 `details` 包含 `error` 字段 + +> **注意**:当主观题缺少 `scoring_points` 评分标准时,`details.warnings` 会包含警告信息,评分结果仅供参考。 + +**验证结果**:✅ 通过(2026-06-05) --- diff --git a/docs/出题批阅接口变更说明(2026-06-05).md b/docs/出题批阅接口变更说明(2026-06-05).md new file mode 100644 index 0000000..e39a396 --- /dev/null +++ b/docs/出题批阅接口变更说明(2026-06-05).md @@ -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` | 可选 | 提示评分可信度 | diff --git a/docs/后端对接规范.md b/docs/后端对接规范.md index fa73807..b95dfb2 100644 --- a/docs/后端对接规范.md +++ b/docs/后端对接规范.md @@ -2,9 +2,11 @@ --- -## 📋 变更记录(2026-05-28 更新) +## 📋 变更记录(2026-06-05 更新) > **本次更新内容**:新增 AI 智能出题端口、更新生产环境测试结果 +> +> **2026-06-05 更新**:出题批卷接口格式优化与输入校验增强 ### 新增端口 @@ -55,6 +57,17 @@ | `/images/list` | 200 | ✅ 正常 | | `/documents/sync` | 200 | ✅ 正常(已修复) | +### 出题批卷接口变更(2026-06-05) + +| 变更项 | 旧值 | 新值 | 影响 | +|--------|------|------|------| +| 批卷请求字段名 | `question_content` | `content` | ⚠️ **破坏性变更**,后端需同步修改 | +| 出题总题数上限 | 无限制 | **20 道** | 超过返回 400 | +| 批卷 question_type | 无校验 | 必须属于 5 种合法题型 | 无效值返回 400 | +| 批卷结果格式 | 各题型格式不统一 | 统一 `grading_status` + `details` | 后端需适配新格式 | +| **跨调用去重** | 无 | **`exclude_stems` 参数** | 追加出题时传入已有题干避免重复 | +| 出题结果 | 无短缺提示 | 新增 `warnings` 字段 | 可选消费 | + --- ## 一、服务概述 @@ -1387,6 +1400,7 @@ POST /exam/generate | `question_types` | object | ✅ | 题型及数量,键为题型名,值为数量 | | `difficulty` | int | ❌ | 难度等级 1-5,默认 3 | | `request_id` | string | ❌ | 请求 ID,相同 ID 返回缓存结果(幂等性) | +| `exclude_stems` | string[] | ❌ | 已有题目题干列表,用于跨调用去重(最多 100 条,传空或不传则不去重) | **collection 参数说明:** @@ -1394,6 +1408,15 @@ POST /exam/generate - 单个向量库:`"dept_a_kb"` - 多个向量库:`["dept_a_kb", "public_kb"]`(按优先级排序,优先在第一个库检索) +#### 入参校验规则(2026-06-05 新增) + +| 校验项 | 规则 | 失败返回 | +|--------|------|----------| +| question_types 题型 | 必须属于: single_choice, multiple_choice, true_false, fill_blank, subjective | HTTP 400 INVALID_PARAMS | +| question_types 数量 | 每种题型数量必须为非负整数 | HTTP 400 INVALID_PARAMS | +| difficulty | 必须为 1-5 的整数 | HTTP 400 INVALID_PARAMS | +| **总题数上限** | **所有题型数量之和不能超过 20** | HTTP 400 INVALID_PARAMS | + **响应:** **完整响应格式**(包含外层包装): @@ -1420,6 +1443,9 @@ POST /exam/generate "request_id": "xxx", "total": 10, "source_chunks_used": 15, + "requested_types": {"single_choice": 3, "true_false": 2, "fill_blank": 2}, + "actual_types": {"single_choice": 3, "true_false": 2, "fill_blank": 2}, + "warnings": [], "questions": [ { "question_type": "single_choice", @@ -1455,6 +1481,8 @@ POST /exam/generate } ``` +> **`warnings` 字段说明**:当某题型实际生成数量少于请求数量时,`warnings` 数组会返回短缺提示(如 `["single_choice: 请求 5 道,实际生成 3 道"]`)。后端可据此判断是否需要重新出题。正常情况该数组为空。 + **题型与 answer 格式对照:** | 题型 | question_type | answer 格式 | data 字段 | @@ -1492,6 +1520,7 @@ POST /exam/generate | `FILE_NOT_FOUND` | 404 | 指定文件不存在 | | `COLLECTION_NOT_FOUND` | 404 | 指定向量库不存在 | | `NO_CONTENT` | 400 | 文件内容为空,无法出题 | +| `INVALID_PARAMS` | 400 | 入参校验失败(题型不合法、数量超限、difficulty 范围错误等) | | `LLM_ERROR` | 500 | LLM 调用失败 | | `PARSE_ERROR` | 500 | 解析 LLM 响应失败 | @@ -1503,6 +1532,8 @@ POST /exam/generate ### 7.2 批改答案 +> **⚠️ 重要变更(2026-06-05)**:批卷请求中的 `question_content` 字段已更名为 `content`,与出题接口返回的题目 `content` 字段保持一致。后端可直接将出题结果的 `content` 透传到批卷接口,无需额外转换。 + ``` POST /exam/grade ``` @@ -1516,7 +1547,7 @@ POST /exam/grade { "question_id": "uuid-001", "question_type": "single_choice", - "question_content": { + "content": { "stem": "题干内容", "data": {"options": [{"key": "A", "content": "选项A"}, {"key": "B", "content": "选项B"}]}, "answer": "B" @@ -1527,7 +1558,7 @@ POST /exam/grade { "question_id": "uuid-002", "question_type": "multiple_choice", - "question_content": { + "content": { "stem": "多选题题干", "data": {"options": [...]}, "answer": ["A", "C"] @@ -1538,7 +1569,7 @@ POST /exam/grade { "question_id": "uuid-003", "question_type": "true_false", - "question_content": { + "content": { "stem": "判断题题干", "answer": "T" }, @@ -1548,7 +1579,7 @@ POST /exam/grade { "question_id": "uuid-004", "question_type": "fill_blank", - "question_content": { + "content": { "stem": "填空题有___个空", "answer": [["答案1", "同义词1"], ["答案2"]] }, @@ -1558,7 +1589,7 @@ POST /exam/grade { "question_id": "uuid-005", "question_type": "subjective", - "question_content": { + "content": { "stem": "简答题题干", "data": { "scoring_points": [ @@ -1587,8 +1618,8 @@ POST /exam/grade | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `question_id` | string | **是** | 题目 ID(后端生成,用于匹配结果) | -| `question_type` | string | 是 | 题型:single_choice/multiple_choice/true_false/fill_blank/subjective | -| `question_content` | object | 是 | 题目内容(从出题结果中获取) | +| `question_type` | string | 是 | 题型,**必须属于**:single_choice/multiple_choice/true_false/fill_blank/subjective,无效值返回 HTTP 400 | +| `content` | object | 是 | 题目内容(从出题结果中获取) | | `student_answer` | any | 是 | 学生答案(格式见下表) | | `max_score` | number | 是 | 该题满分 | @@ -1636,36 +1667,31 @@ POST /exam/grade "question_id": "uuid-001", "score": 0, "max_score": 2.0, - "correct": false, - "feedback": "正确答案: B" - }, - { - "question_id": "uuid-002", - "score": 0, - "max_score": 4.0, - "correct": false, - "feedback": "正确答案: ['A', 'C']" - }, - { - "question_id": "uuid-003", - "score": 0, - "max_score": 2.0, - "correct": false, - "feedback": "正确答案: T" + "grading_status": "success", + "details": { + "correct": false, + "student_answer": "A", + "correct_answer": "B", + "feedback": "正确答案: B" + } }, { "question_id": "uuid-004", "score": 2.0, "max_score": 4.0, + "grading_status": "success", "details": { + "total_blanks": 2, + "correct_blanks": 1, "blank_scores": [2.0, 0], - "correct_answers": [["答案1", "同义词1"], ["答案2"]] + "feedback": "2 个空中答对 1 个" } }, { "question_id": "uuid-005", "score": 7.5, "max_score": 10.0, + "grading_status": "success", "details": { "scoring_breakdown": [ {"point": "要点1", "weight": 0.4, "achieved": 0.35, "comment": "部分掌握"}, @@ -1673,13 +1699,30 @@ POST /exam/grade ], "highlights": ["思路清晰"], "shortcomings": ["细节不够完整"], - "overall_feedback": "整体回答良好,建议补充细节。" + "overall_feedback": "整体回答良好", + "warnings": ["缺少评分标准(scoring_points),评分结果仅供参考"] + } + }, + { + "question_id": "uuid-006", + "score": 0, + "max_score": 10.0, + "grading_status": "failed", + "details": { + "error": "评分结果解析失败" } } ] } ``` +#### grading_status 状态说明(2026-06-05 新增) + +| 状态 | 说明 | +|------|------| +| `success` | 评分成功,score 和 details 有效 | +| `failed` | 评分失败(LLM 解析失败或超时),score 为 0,details 包含 error 描述 | + #### 批卷逻辑说明 | 题型 | 批卷方式 | 说明 | @@ -1766,7 +1809,7 @@ def build_grade_request(answer_list, questions_map): grade_answers.append({ "question_id": qid, "question_type": question.question_type, - "question_content": question.content, # 含正确答案 + "content": question.content, # 含正确答案,与出题接口 content 结构一致 "student_answer": ans['answer'], "max_score": question.score }) @@ -1854,8 +1897,8 @@ def grade_student_exam(student_answers): |------|------|------| | `question_id` | 后端数据库 | 用于匹配返回结果,更新成绩 | | `question_type` | 后端数据库 | 题型,决定批卷方式 | -| `question_content.answer` | 后端数据库 | 正确答案(客观题直接比对,主观题作为参考) | -| `question_content.data` | 后端数据库 | 题目附加数据(选项、评分标准等) | +| `content.answer` | 后端数据库 | 正确答案(客观题直接比对,主观题作为参考) | +| `content.data` | 后端数据库 | 题目附加数据(选项、评分标准等) | | `student_answer` | 学生提交 | 学生作答内容 | | `max_score` | 后端数据库 | 该题满分 |