From e40989eeab3ac1b0b49f8cbede307260bef93f6a Mon Sep 17 00:00:00 2001 From: lacerate551 <128470311+lacerate551@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:33:55 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=85=A8=E9=9D=A2=E9=87=8D=E5=86=99?= =?UTF-8?q?=E5=87=BA=E9=A2=98=E6=89=B9=E9=A2=98=E5=90=8E=E7=AB=AF=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E6=8C=87=E5=8D=97=20v2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 generate-smart 接口文档(含 ai_analysis 字段) - 判断题返回 bool(true/false)格式说明 - 填空题二维数组答案格式详细说明 - 统一响应格式、状态码、错误码 - 5种题型批阅结果真实示例 - curl 调用示例、性能参考 - 基于生产服务器实测数据 --- docs/出题批题后端对接指南.md | 1076 +++++++++++++++++++++------------- 1 file changed, 676 insertions(+), 400 deletions(-) diff --git a/docs/出题批题后端对接指南.md b/docs/出题批题后端对接指南.md index 0b672eb..4e54c06 100644 --- a/docs/出题批题后端对接指南.md +++ b/docs/出题批题后端对接指南.md @@ -1,468 +1,744 @@ -# 出题批题接口 - 后端对接指南 +# 出题批题接口 — 后端对接指南 + +> **版本**: v2.0 | **更新**: 2026-06-30 | **基于生产服务器实测** + +--- ## 一、接口概览 -| 接口 | 方法 | 功能 | 超时建议 | -|------|------|------|----------| -| `/exam/generate` | POST | 生成题目 | 120秒 | -| `/exam/grade` | POST | 批阅答案 | 60秒 | +| 接口 | 方法 | 功能 | 模式 | 超时建议 | +|------|------|------|------|----------| +| `/exam/generate` | POST | 参数出题 | 同步阻塞 | 5 分钟 | +| `/exam/generate-smart` | POST | AI 智能出题 | 同步阻塞 | 5 分钟 | +| `/exam/grade` | POST | 批阅答案 | 同步阻塞 | 2 分钟 | +| `/exam/health` | GET | 健康检查 | 即时 | 5 秒 | + +> **服务地址**: `http://:5001`,URL 前缀 `/exam` +> +> **重要**: 所有接口为同步阻塞模式,请求直到完成后才返回。后端请设置合理超时(建议 5 分钟以上)。 --- -## 二、出题接口 +## 二、统一响应格式 -### 2.1 请求 - -``` -POST /exam/generate -Content-Type: application/json -``` - -**请求体字段说明:** - -| 字段 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `file_path` | string | ✅ | 文档路径(相对于 documents 目录) | -| `collection` | string 或 string[] | ✅ | 向量库名称,支持数组(按优先级顺序检索,找到文件即停止) | -| `question_types` | object | ✅ | 题型及数量 | -| `difficulty` | int | ❌ | 难度等级 1-5,默认 3 | -| `request_id` | string | ❌ | 请求ID,相同ID返回缓存结果(幂等性) | - -**请求示例:** +所有接口返回统一 JSON 信封。 +**成功**: ```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" + "success": true, + "status": "success", + "status_code": 2020, + "message": "出题成功", + "data": { ... } } ``` -> **collection 检索逻辑说明**: -> - 出题接口采用**按优先级顺序检索**策略 -> - 先在 `dept_hr` 中查找文件,找到则停止 -> - 未找到则继续在 `public_kb` 中查找 -> - 这与 RAG 问答接口的**并行检索+融合**策略不同 -> - 原因:出题需要特定文件的完整内容,而非广泛搜索 - -### 2.2 响应 - -**成功响应:** - +**失败**: ```json { - "success": true, - "status": "success", - "status_code": 2011, - "message": "出题完成", - "data": { - "request_id": "uuid-xxx", - "total": 10, - "source_chunks_used": 25, - "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", - "chunks_count": 3 - } - } - ] - } + "success": false, + "status": "failed", + "error_code": "MISSING_PARAMS", + "status_code": 4000, + "message": "缺少 file_path 或 collection 参数" } ``` -**失败响应:** +**业务状态码**: -```json -{ - "success": false, - "status": "failed", - "error_code": "FILE_NOT_FOUND", - "message": "文件不存在" -} -``` - -### 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 | 说明 | -|--------|------|------| -| `FILE_NOT_FOUND` | 404 | 指定文件不存在 | -| `COLLECTION_NOT_FOUND` | 404 | 指定向量库不存在 | -| `NO_CONTENT` | 400 | 文件内容为空,无法出题 | -| `LLM_ERROR` | 500 | LLM 调用失败 | -| `PARSE_ERROR` | 500 | 解析失败 | +| status_code | 常量 | 说明 | +|-------------|------|------| +| 2020 | EXAM_SUCCESS | 出题成功 | +| 2021 | GRADE_SUCCESS | 批阅完成 | +| 4000 | BAD_REQUEST | 请求参数错误 | +| 5020 | EXAM_ERROR | 出题失败 | +| 5021 | GRADE_ERROR | 批阅失败 | --- -## 三、批阅接口 +## 三、出题接口 -### 3.1 请求 +### 3.1 POST /exam/generate — 参数出题 -``` -POST /exam/grade -Content-Type: application/json -``` +根据指定的题型和数量,基于文档内容生成题目。 -**请求体字段说明:** - -| 字段 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `request_id` | string | ❌ | 请求ID,用于追踪 | -| `answers` | array | ✅ | 答案列表 | - -**answers 数组中每个对象的字段:** - -| 字段 | 类型 | 必需 | 说明 | 来源 | -|------|------|------|------|------| -| `question_id` | string | ✅ | 题目ID | 后端数据库 | -| `question_type` | string | ✅ | 题型 | 后端数据库 | -| `question_content` | object | ✅ | 题目内容(含正确答案) | 后端数据库 | -| `student_answer` | any | ✅ | 学生答案 | 学生提交 | -| `max_score` | number | ✅ | 满分 | 后端数据库 | - -**请求示例:** +#### 请求体 ```json { - "request_id": "grade-uuid-xxx", - "answers": [ - { - "question_id": "q-001", - "question_type": "single_choice", - "question_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 + "file_path": "public_kb/产品手册.pdf", + "collection": "public_kb", + "question_types": { + "single_choice": 3, + "multiple_choice": 2, + "true_false": 2, + "fill_blank": 2, + "subjective": 1 }, - { - "question_id": "q-002", - "question_type": "multiple_choice", - "question_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", - "question_content": { - "stem": "公司规定员工每月绩效奖金上限为工资的20%。", - "answer": "F" - }, - "student_answer": "T", - "max_score": 2.0 - }, - { - "question_id": "q-004", - "question_type": "fill_blank", - "question_content": { - "stem": "员工薪资由___、___和___三部分组成。", - "data": {"blank_count": 3}, - "answer": [["基本工资"], ["绩效奖金", "绩效"], ["津贴补贴", "补贴"]] - }, - "student_answer": ["基本工资", "绩效奖金", "交通补贴"], - "max_score": 6.0 - }, - { - "question_id": "q-005", - "question_type": "subjective", - "question_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 + "difficulty": 3, + "exclude_stems": ["已有题目的题干1", "已有题目的题干2"], + "request_id": "uuid-optional", + "options": { + "include_explanation": true, + "max_source_chunks": 50 } - ] } ``` -### 3.2 响应 +#### 请求参数 -**成功响应:** +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `file_path` | string | 是 | — | 文件路径(向量库内相对路径) | +| `collection` | string | 是 | — | 向量库名称 | +| `question_types` | object | 是 | — | 题型及数量,键名见下方合法值,值为非负整数,总题数上限 **20** | +| `difficulty` | int | 否 | 3 | 难度等级 1-5 | +| `exclude_stems` | string[] | 否 | — | 排除已有题干(跨调用去重),最多 100 条 | +| `request_id` | string | 否 | — | 幂等性标识,原样返回 | +| `options.include_explanation` | bool | 否 | true | 是否生成答案解析 | +| `options.max_source_chunks` | int | 否 | 50 | 最大检索切片数 | + +**question_types 合法键值**: `single_choice`、`multiple_choice`、`true_false`、`fill_blank`、`subjective` + +#### 成功响应 ```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": "得分点:提到公平性概念,但未展开说明激励性和竞争力原则" - } - ] - } + "success": true, + "status": "success", + "status_code": 2020, + "message": "出题成功", + "data": { + "success": true, + "request_id": "uuid", + "questions": [ ... ], + "total": 10, + "requested_types": {"single_choice": 3, "fill_blank": 2}, + "actual_types": {"single_choice": 3, "fill_blank": 2}, + "source_chunks_used": 15, + "warnings": ["fill_blank: 请求 3 道,实际生成 2 道"] + } } ``` -### 3.3 返回字段说明 +**data 字段说明**: | 字段 | 类型 | 说明 | |------|------|------| -| `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 | 说明 | -|--------|------|------| -| `INVALID_ANSWER_FORMAT` | 400 | 答案格式不正确 | -| `GRADING_ERROR` | 500 | 批阅过程出错 | +| `success` | bool | 是否成功 | +| `request_id` | string/null | 请求标识原样返回 | +| `questions` | array | 题目列表(结构见第五章) | +| `total` | int | 实际生成题数 | +| `requested_types` | object | 请求的题型和数量 | +| `actual_types` | object | 实际生成的题型和数量 | +| `source_chunks_used` | int | 使用的文档切片数 | +| `warnings` | string[] | 警告信息(某题型实际生成少于请求时出现) | --- -## 四、完整调用流程 +### 3.2 POST /exam/generate-smart — AI 智能出题 -### 4.1 出题流程 +AI 自动分析文档内容,决定题型和数量后生成题目。与 `/exam/generate` 的区别是**不需要传 question_types**。 -``` -后端调用 /exam/generate - │ - ▼ - RAG 返回题目 - (question_type, difficulty, content, source_trace) - │ - ▼ - 后端生成 question_id - 后端设置 score (满分) - 后端设置 status - │ - ▼ - 后端存入数据库 +#### 请求体 + +```json +{ + "file_path": "public_kb/产品手册.pdf", + "collection": "public_kb", + "difficulty": 3, + "max_total": 20, + "exclude_stems": ["已有题目的题干1"], + "request_id": "uuid-optional" +} ``` -### 4.2 批阅流程 +#### 请求参数 +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `file_path` | string | 是 | — | 文件路径 | +| `collection` | string | 是 | — | 向量库名称 | +| `difficulty` | int | 否 | 3 | 难度等级 1-5 | +| `max_total` | int | 否 | — | AI 出题总数上限(正整数),不传则不限制 | +| `exclude_stems` | string[] | 否 | — | 排除已有题干 | +| `request_id` | string | 否 | — | 幂等性标识 | + +#### 成功响应 + +与 `/exam/generate` 格式相同,`data` 中额外包含 `ai_analysis` 字段: + +```json +{ + "success": true, + "status": "success", + "status_code": 2020, + "message": "AI 智能出题成功", + "data": { + "success": true, + "request_id": "uuid", + "questions": [ ... ], + "total": 5, + "requested_types": {"single_choice": 2, "true_false": 1, "fill_blank": 1, "multiple_choice": 1}, + "actual_types": {"single_choice": 2, "true_false": 1, "fill_blank": 1, "multiple_choice": 1}, + "source_chunks_used": 30, + "ai_analysis": { + "total_knowledge_points": 29, + "question_types": {"single_choice": 2, "true_false": 1, "fill_blank": 1, "multiple_choice": 1}, + "suitable_types": ["single_choice", "true_false", "fill_blank", "multiple_choice"], + "reason": "文档涵盖大量概念性内容..." + } + } +} ``` -学生提交答案 - │ - ▼ - 后端从数据库查询: - - question_id - - question_type - - question_content (含正确答案) - - score (满分) - │ - ▼ - 后端组装请求调用 /exam/grade - │ - ▼ - RAG 返回批阅结果 - (score, feedback, is_correct) - │ - ▼ - 后端存入成绩表 -``` + +> **注意**: `requested_types` 来自 AI 分析结果,可能包含值为 0 的题型(表示 AI 认为该题型不适合当前文档)。`actual_types` 仅包含实际生成数量大于 0 的题型。 + +**ai_analysis 字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total_knowledge_points` | int | AI 提取的知识点总数 | +| `question_types` | object | AI 推荐的题型和数量 | +| `suitable_types` | string[] | 适合该文档的题型列表 | +| `reason` | string | AI 的分析说明 | --- -## 五、字段来源速查表 +## 四、批阅接口 -### 5.1 出题接口 +### 4.1 POST /exam/grade — 批题 -| 字段 | 返回方 | 存储方 | 说明 | -|------|--------|--------|------| -| question_id | ❌ 不返回 | 后端生成 | UUID | -| question_type | ✅ RAG | 后端存储 | 题型 | -| difficulty | ✅ RAG | 后端存储 | 难度 1-5 | -| content | ✅ RAG | 后端存储 | 题目内容 | -| source_trace | ✅ RAG | 后端存储 | 来源追踪 | -| **score** | ❌ 不返回 | **后端设定** | 满分 | -| tags | ❌ 不返回 | 后端设定 | 标签 | -| status | ❌ 不返回 | 后端设定 | 状态 | +逐题批阅并返回评分结果。支持 5 种题型混合批阅。 -### 5.2 批阅接口 +#### 请求体 + +```json +{ + "request_id": "uuid-optional", + "answers": [ + { + "question_id": "q-001", + "question_type": "single_choice", + "content": {"answer": "B"}, + "student_answer": "B", + "max_score": 2 + }, + { + "question_id": "q-002", + "question_type": "true_false", + "content": {"answer": "对"}, + "student_answer": "对", + "max_score": 2 + }, + { + "question_id": "q-003", + "question_type": "fill_blank", + "content": {"answer": [["答案1", "同义词"], ["答案2"]]}, + "student_answer": ["学生答案1", "学生答案2"], + "max_score": 4 + }, + { + "question_id": "q-004", + "question_type": "subjective", + "content": { + "stem": "简述...", + "data": {"scoring_points": [{"point": "要点1", "weight": 0.5}, {"point": "要点2", "weight": 0.5}]}, + "answer": "参考范文..." + }, + "student_answer": "学生作答内容...", + "max_score": 10 + } + ] +} +``` + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `request_id` | string | 否 | 幂等性标识 | +| `answers` | array | 是 | 答案列表 | + +**answers 每道题的字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `question_id` | string | 是 | 题目唯一标识 | +| `question_type` | string | 是 | 题型 | +| `content` | object | 是 | 题目内容(与出题接口返回的 `questions[].content` 结构一致,后端直接透传) | +| `student_answer` | any | 是 | 学生答案(类型随题型变化,见下方表格) | +| `max_score` | number | 否 | 该题满分,默认:客观题 2 / 填空 4 / 主观 10 | + +**各题型 content 和 student_answer 格式**: + +| 题型 | content.answer | student_answer | +|------|---------------|----------------| +| single_choice | `"B"` | `"A"` | +| multiple_choice | `["A", "C"]` | `["A", "B"]` | +| true_false | `"对"` 或 `"错"` | `"对"` 或 `"错"` | +| fill_blank | `[["答案1", "同义词"], ["答案2"]]` | `["学生答案1", "学生答案2"]` | +| subjective | 参考范文字符串 | 学生作答字符串 | + +> **content 透传说明**: `content` 字段的结构与出题接口返回的 `questions[].content` 完全一致。后端在存储题目时保存整个 `content` 对象,批题时原样传入即可。 + +#### 成功响应 + +```json +{ + "success": true, + "status": "success", + "status_code": 2021, + "message": "批阅完成", + "data": { + "success": true, + "request_id": "uuid", + "results": [ ... ], + "total_score": 10.5, + "total_max_score": 21.0, + "score_rate": 50.0 + } +} +``` + +**data 顶层字段**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `success` | bool | 是否成功 | +| `request_id` | string/null | 请求标识 | +| `results` | array | 逐题评分结果(顺序与输入一致) | +| `total_score` | float | 总得分(保留 1 位小数) | +| `total_max_score` | float | 总满分 | +| `score_rate` | float | 得分率百分比(保留 1 位小数) | + +--- + +### 4.2 批阅结果 — 逐题结构 + +每道题的评分结果在 `results` 数组中,结构因题型而异。 + +#### 客观题 (single_choice / multiple_choice / true_false) + +```json +{ + "question_id": "q-001", + "score": 2, + "max_score": 2, + "grading_status": "success", + "details": { + "correct": true, + "student_answer": "B", + "correct_answer": "B", + "feedback": "正确!" + } +} +``` + +#### 判断题 — 特殊说明 + +判断题的 `student_answer` 和 `correct_answer` **统一返回 bool**(`true`/`false`),无论输入时传的是 `"对"`/`"错"` 还是 `true`/`false`: + +```json +{ + "question_id": "q-tf-001", + "score": 2, + "max_score": 2, + "grading_status": "success", + "details": { + "correct": true, + "student_answer": true, + "correct_answer": true, + "feedback": "正确!" + } +} +``` + +答错时: + +```json +{ + "question_id": "q-tf-002", + "score": 0, + "max_score": 2, + "grading_status": "success", + "details": { + "correct": false, + "student_answer": true, + "correct_answer": false, + "feedback": "正确答案: false" + } +} +``` + +> 后端对接时判断题的 `student_answer` / `correct_answer` 直接按 `boolean` 类型处理即可。 + +#### 填空题 (fill_blank) + +```json +{ + "question_id": "q-fb-001", + "score": 2.0, + "max_score": 4, + "grading_status": "success", + "details": { + "blank_scores": [2.0, 0], + "total_blanks": 2, + "correct_blanks": 1 + } +} +``` + +> 填空题支持部分给分。`blank_scores` 为每空得分,支持模糊匹配(编辑距离容错 + 标点归一化)。 + +#### 主观题 (subjective) + +```json +{ + "question_id": "q-sub-001", + "score": 7.5, + "max_score": 10, + "grading_status": "success", + "details": { + "scoring_breakdown": [ + { + "point": "技术防线", + "weight": 0.4, + "achieved": 0.9, + "comment": "表述准确,覆盖了主要技术手段" + } + ], + "highlights": ["技术防线表述全面"], + "shortcomings": ["制度防线描述不够具体"], + "overall_feedback": "整体回答较好,建议补充制度层面的具体规定。" + } +} +``` + +#### 批阅失败(兜底) + +```json +{ + "question_id": "q-005", + "score": 0, + "max_score": 10, + "grading_status": "failed", + "details": { + "error": "LLM 调用超时" + } +} +``` + +> 单题批阅失败不影响其他题目,失败题目得 0 分。 + +#### grading_status 取值 + +| 值 | 说明 | +|----|------| +| `success` | 评分成功 | +| `failed` | 评分失败(LLM 超时/解析异常),score 为 0 | + +--- + +## 五、题目数据结构 + +出题接口 `questions` 数组中每道题的完整结构: + +```json +{ + "question_type": "single_choice", + "difficulty": 3, + "content": { + "stem": "根据XX制度,以下哪项是正确的?", + "data": { + "options": [ + {"key": "A", "content": "选项A内容"}, + {"key": "B", "content": "选项B内容"}, + {"key": "C", "content": "选项C内容"}, + {"key": "D", "content": "选项D内容"} + ] + }, + "answer": "B", + "explanation": "根据XX制度第3条规定..." + }, + "source_trace": { + "document_name": "public_kb/产品手册.pdf", + "chunk_ids": ["chunk_001", "chunk_002"], + "page_numbers": [3, 5], + "sources": [ + { + "chunk_id": "chunk_001", + "page": 3, + "section": "第三章 管理制度", + "snippet": "原文相关片段前200字符..." + } + ] + } +} +``` + +### 5.1 各题型 content 差异 + +**单选题 (single_choice)**: +```json +{ + "content": { + "stem": "以下哪项是正确的?", + "data": { + "options": [ + {"key": "A", "content": "..."}, + {"key": "B", "content": "..."}, + {"key": "C", "content": "..."}, + {"key": "D", "content": "..."} + ] + }, + "answer": "B", + "explanation": "..." + } +} +``` + +**多选题 (multiple_choice)**: +```json +{ + "content": { + "stem": "以下哪些是正确的?(多选)", + "data": { + "options": [ + {"key": "A", "content": "..."}, + {"key": "B", "content": "..."}, + {"key": "C", "content": "..."}, + {"key": "D", "content": "..."} + ] + }, + "answer": ["A", "C"], + "explanation": "..." + } +} +``` + +**判断题 (true_false)**: +```json +{ + "content": { + "stem": "公司数据应定期备份(判断对错)", + "data": {}, + "answer": "对", + "explanation": "..." + } +} +``` + +> 出题时 `content.answer` 为 `"对"` 或 `"错"`(字符串)。批阅接口返回时会自动归一化为 `true`/`false`(bool)。 + +**填空题 (fill_blank)**: +```json +{ + "content": { + "stem": "公司财务报表应在每季度结束后______天内提交。", + "data": { + "blank_count": 1 + }, + "answer": [["15", "十五日"]], + "explanation": "..." + } +} +``` + +> `answer` 为二维数组:外层对应每空,内层为该空的可接受答案列表(第一个为标准答案,其余为同义词/等价答案)。 + +**主观题 (subjective)**: +```json +{ + "content": { + "stem": "简述公司数据安全的三道防线。", + "data": { + "scoring_points": [ + {"point": "技术防线(防火墙、加密、访问控制)", "weight": 0.4}, + {"point": "制度防线(安全规定、审批流程)", "weight": 0.3}, + {"point": "人员防线(安全培训、意识教育)", "weight": 0.3} + ] + }, + "answer": "公司数据安全的三道防线包括:1.技术防线...", + "explanation": "..." + } +} +``` + +> `scoring_points[].weight` 为评分要点的权重(0-1),批题时 LLM 按要点逐项评分。 + +### 5.2 溯源信息 (source_trace) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `document_name` | string | 来源文件名 | +| `chunk_ids` | string[] | 来源切片 ID 列表 | +| `page_numbers` | int[] | 来源页码(已排序去重) | +| `sources` | object[] | 详细来源信息 | +| `sources[].chunk_id` | string | 切片 ID | +| `sources[].page` | int/null | 页码 | +| `sources[].section` | string | 所属章节标题 | +| `sources[].snippet` | string | 来源切片前 200 字符 | + +--- + +## 六、后端对接指南 + +### 6.1 典型流程 + +``` +1. 后端调用 POST /exam/generate 或 /exam/generate-smart + ↓ +2. RAG 返回 questions 数组 + ↓ +3. 后端将 questions 存入 MySQL(保存完整 content 对象和 source_trace) + - 后端自行分配 question_id (UUID) + - 后端自行设定 score (满分) 和 status + ↓ +4. 学生答题后,后端组装 answers 数组 + - 将存储的 content 对象直接透传到 answers[].content + - 填入 student_answer 和 max_score + ↓ +5. 后端调用 POST /exam/grade + ↓ +6. RAG 返回 results 数组,后端存储评分结果 +``` + +### 6.2 后端存储建议 + +题目表建议至少存储以下字段: | 字段 | 来源 | 说明 | |------|------|------| -| question_id | 后端数据库 | 题目唯一标识 | -| question_type | 后端数据库 | 题型 | -| question_content | 后端数据库 | 题目内容(含正确答案) | -| student_answer | 学生提交 | 学生作答 | -| **max_score** | **后端数据库** | 满分(决定得分上限) | +| `question_id` | 后端自行分配 UUID | 出题接口不返回 ID | +| `question_type` | `questions[].question_type` | 题型标识 | +| `difficulty` | `questions[].difficulty` | 难度 | +| `content` | `questions[].content` | 完整 JSON 存储(含 stem/data/answer/explanation) | +| `source_trace` | `questions[].source_trace` | 溯源 JSON | +| `source_file` | 请求参数 `file_path` | 来源文件 | +| `collection` | 请求参数 `collection` | 来源向量库 | +| `score` | 后端自行设定 | 满分 | +| `status` | 后端自行设定 | 状态(待审核/已通过/已拒绝) | + +> **重要**: RAG 服务不返回 `question_id` 和 `score` 字段,均由后端在入库时指定。 + +### 6.3 字段来源速查 + +| 字段 | 出题接口返回 | 批阅接口需要 | 说明 | +|------|:-----------:|:-----------:|------| +| question_id | 不返回(后端生成) | 原样传回 | UUID | +| question_type | 返回 | 原样传回 | 题型 | +| difficulty | 返回 | 不需要 | 难度 | +| content | 返回 | **透传** | 完整题目内容 JSON | +| source_trace | 返回 | 不需要 | 溯源信息 | +| score/max_score | 不返回(后端设定) | 传入 | 满分 | +| student_answer | — | 传入 | 学生答案 | + +### 6.4 curl 调用示例 + +**参数出题**: +```bash +curl -X POST http://:5001/exam/generate \ + -H "Content-Type: application/json" \ + -d '{ + "file_path": "public_kb/产品手册.pdf", + "collection": "public_kb", + "question_types": {"single_choice": 3, "true_false": 2}, + "difficulty": 3 + }' +``` + +**AI 智能出题(限制 5 题)**: +```bash +curl -X POST http://:5001/exam/generate-smart \ + -H "Content-Type: application/json" \ + -d '{ + "file_path": "public_kb/产品手册.pdf", + "collection": "public_kb", + "max_total": 5 + }' +``` + +**批题**: +```bash +curl -X POST http://:5001/exam/grade \ + -H "Content-Type: application/json" \ + -d '{ + "answers": [ + { + "question_id": "q-001", + "question_type": "single_choice", + "content": {"answer": "B"}, + "student_answer": "B", + "max_score": 2 + }, + { + "question_id": "q-002", + "question_type": "true_false", + "content": {"answer": "对"}, + "student_answer": "对", + "max_score": 2 + } + ] + }' +``` --- -## 六、常见问题 +## 七、批阅规则 -### Q1: 出题接口返回的题目需要审核吗? - -建议后端设置 `status` 字段进行审核流程,审核通过后再用于组卷。 - -### Q2: 分值如何动态调整? - -后端在入库时自由设定 `score` 字段,批阅时传入 `max_score` 即可。RAG 服务不干预分值。 - -### Q3: 填空题同义词匹配是如何实现的? - -填空题答案格式为 `[["主答案", "同义词1", "同义词2"], ...]`,学生答案匹配任意一个即视为正确。 - -### Q4: 批阅接口超时怎么办? - -建议: -- 单次批阅不超过 50 道题 -- 设置 60 秒超时 -- 主观题较多时适当延长 +| 题型 | 批阅方式 | 得分规则 | +|------|----------|----------| +| single_choice | 精确匹配(大小写不敏感) | 正确得满分,错误得 0 | +| multiple_choice | 精确匹配 | 全部选对才给分,少选/多选/错选均得 0 | +| true_false | 精确匹配 | 正确得满分,错误得 0。输入支持 "对"/"错"/"true"/"false"/bool,输出统一为 bool | +| fill_blank | 模糊匹配 | 每空独立评分。支持同义词匹配 + 标点归一化 + 编辑距离容错(>=4 字符允许 <=2 差异)。每空得分 = 满分 / 总空数 | +| subjective | LLM 评分 | 按 scoring_points 逐项评分(0~1),最终得分 = Σ(weight × achieved × max_score) | --- -**文档版本**: v1.1 -**更新时间**: 2026-05-17 -**相关文档**: [后端对接规范.md](./后端对接规范.md) +## 八、注意事项 + +1. **题目 ID**: 出题接口不生成 ID,由后端在存储时分配 UUID。 +2. **分值**: RAG 服务不返回分值,满分由后端在入库时设定,批题时通过 `max_score` 传入。 +3. **总题数上限**: `/exam/generate` 单次请求不超过 20 题;`/exam/generate-smart` 通过 `max_total` 参数控制。 +4. **排除题干**: `exclude_stems` 最多 100 条,用于跨批次去重。后端可在多轮出题时将前批题干传入。 +5. **判断题返回格式**: 批阅接口返回的 `student_answer` / `correct_answer` 统一为 `true`/`false`(bool 类型),后端直接按 boolean 处理。 +6. **填空题答案格式**: 二维数组 `[["标准答案", "同义词"], ...]`,外层每空一个元素,内层为该空的可接受答案列表。批阅时支持模糊匹配。 +7. **主观题评分**: 依赖 LLM,存在一定非确定性。同一答案多次评分可能有小幅波动。 +8. **同步阻塞**: 所有接口为同步阻塞模式,请求直到完成后才返回。请后端设置合理的请求超时(建议 5 分钟以上)。 +9. **单题失败不影响整体**: 批阅时单题失败(如 LLM 超时),该题得 0 分,其余题目正常评分。`grading_status` 字段标识每题的批阅状态。 + +--- + +## 九、错误码汇总 + +| HTTP | error_code | 场景 | +|------|------------|------| +| 400 | MISSING_PARAMS | 缺少必填参数 | +| 400 | INVALID_PARAMS | 参数格式或值无效 | +| 500 | EXAM_ERROR | 出题过程异常 | +| 500 | GRADE_ERROR | 批阅过程异常 | + +--- + +## 十、性能参考 + +| 操作 | 预估耗时 | 说明 | +|------|----------|------| +| 参数出题(5 题) | 60-90s | 主要耗时在多次 LLM 调用 | +| AI 智能出题(20 题) | 3-5min | 含 AI 分析 + 多次 LLM 调用 | +| 批题(10 题混合) | 15-30s | 主观题 LLM 评分较慢 | +| 批题(纯客观题) | 1-3s | 无需 LLM,纯匹配 | + +> 耗时受 LLM 响应速度影响较大。 + +--- + +## 变更记录 + +| 日期 | 版本 | 变更内容 | +|------|------|---------| +| 2026-06-30 | 2.0 | 全面重写:统一响应格式、新增 generate-smart、判断题返回 bool、填空题 2D 格式说明、生产实测数据 | +| 2026-05-17 | 1.1 | 初版 |