# 出题批题接口 - 后端对接指南 ## 一、接口概览 | 接口 | 方法 | 功能 | 超时建议 | |------|------|------|----------| | `/exam/generate` | POST | 生成题目(手动指定题型数量) | 120秒 | | `/exam/generate-smart` | POST | 生成题目(AI 自动分析文档结构出题) | 120秒 | | `/exam/grade` | POST | 批阅答案 | 60秒 | > **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,相同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-idempotency" } ``` > **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` 中找不到,或 `collection` 不存在时,接口**不会返回错误**,而是返回空结果: ```json { "success": true, "status": "success", "status_code": 2020, "message": "出题成功", "data": { "request_id": null, "total": 0, "questions": [], "requested_types": {"single_choice": 1}, "actual_types": {}, "warnings": ["single_choice: 请求 1 道,实际生成 0 道"] } } ``` > **后端注意**:必须检查 `data.total` 是否 > 0 来判断出题是否成功,不能仅依赖 `success` 字段。 ``` ### 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 等) | | `EXAM_ERROR` | 500 | 出题过程异常(LLM 调用失败、解析错误等) | > **注意**:文件不存在或向量库不存在时,不会返回错误码,而是返回 `success=true` + `total=0` + `warnings`(见上方说明)。后端应检查 `total > 0`。 ### 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` 字段(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 道建议分批调用 - 设置 120 秒以上超时 ### Q6: 填空题学生答案传错了会怎样? 2026-07-03 起,填空题增加了格式校验: - `student_answer` 不是列表 → 该题得 0 分,`grading_status = "failed"` - 列表中某项不是字符串 → 该题得 0 分 - 校验失败不影响同一请求中其他题目的批阅 - 接口整体仍返回 `success: true`,需检查每道题的 `grading_status` --- **文档版本**: v1.2 **更新时间**: 2026-07-03 **相关文档**: [后端对接规范.md](./后端对接规范.md)