# 出题批阅接口变更说明(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` | 可选 | 提示评分可信度 |