From a3aa6cbcf349a628c1cb3a91304b9ceb8f3bf4ed Mon Sep 17 00:00:00 2001 From: lacerate551 <128470311+lacerate551@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:23:52 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E5=87=BA=E9=A2=98?= =?UTF-8?q?=E6=89=B9=E5=8D=B7=E7=B3=BB=E7=BB=9F=E8=AE=BE=E8=AE=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E8=A1=A5=E5=85=85=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E5=AE=9E=E6=B5=8B=E9=AA=8C=E8=AF=81=E5=A4=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generate-smart 的 requested_types 可能包含值为0的题型(AI分析结果) - actual_types 仅包含实际生成数量大于0的题型 - 文档已通过生产服务器三端点实测验证 --- docs/出题批卷系统设计.md | 1276 +++++++++++++++++++++----------------- 1 file changed, 704 insertions(+), 572 deletions(-) diff --git a/docs/出题批卷系统设计.md b/docs/出题批卷系统设计.md index 35e37c1..02a6900 100644 --- a/docs/出题批卷系统设计.md +++ b/docs/出题批卷系统设计.md @@ -1,635 +1,767 @@ # 出题批卷系统设计 -> **文档类型**: 系统设计文档 +> **文档类型**: 系统设计文档 & 后端对接手册 > **创建日期**: 2026-04-10 -> **最后更新**: 2026-06-04 +> **最后更新**: 2026-06-22 +> **版本**: 3.0 > **状态**: 已实施 --- ## 一、系统概述 -### 1.1 背景 +### 1.1 定位 -出题批卷系统是 RAG 知识库系统的扩展模块,支持: -- **按文件出题**:根据指定文档自动生成题目 -- **智能批卷**:支持选择题、填空题、简答题的自动批改 -- **溯源追踪**:每道题可追溯到来源文件和知识片段 +出题批卷系统是 RAG 知识库服务的扩展模块(`exam_pkg`),负责基于知识库文档自动生成题目和自动批阅。RAG 服务只负责出题和批题的无状态计算,题目的存储、审核、状态管理由后端服务负责。 -### 1.2 模块结构 +### 1.2 职责边界 + +| 职责 | RAG 服务 | 后端服务 | +|------|----------|----------| +| 题目生成 | 基于文档生成题目 + 溯源 | 审核入库 + 状态管理 | +| 题目批阅 | 逐题评分 + 反馈 | 汇总报告 + 成绩存储 | +| 题目存储 | 无状态,不存储 | MySQL 持久化 | + +### 1.3 模块结构 ``` -exam_pkg/ # 考试系统 -├── generator.py # 出题逻辑(按文件/按主题生成题目) -├── grader.py # 批卷逻辑(选择题/填空题/简答题批改) -├── manager.py # 试卷管理与协调逻辑 -├── api.py # Flask Blueprint (exam_bp) -└── local_db.py # 本地题库 (SQLite) +exam_pkg/ +├── generator.py # 出题引擎(v2管线:章节分组→知识点提取→精准出题→去重补题) +├── grader.py # 批阅引擎(客观题精确匹配 + 填空题模糊匹配 + 主观题LLM评分) +├── manager.py # 编排层(检索切片→调用引擎→组装返回) +├── api.py # Flask Blueprint(exam_bp, url_prefix=/exam) +└── local_db.py # 本地题库缓存(SQLite,辅助功能) ``` -**认证模块**: `auth/gateway.py` - 网关认证 +### 1.4 服务信息 + +| 项目 | 值 | +|------|-----| +| 服务地址 | `http://:5001` | +| URL前缀 | `/exam` | +| 健康检查 | `GET /exam/health` | +| 运行模式 | gunicorn + gthread (2 workers) | --- -## 二、出题系统设计 +## 二、统一响应格式 -### 2.1 按文件出题接口 +所有接口返回统一 JSON 信封: -**接口路径**:`POST /exam/generate-by-file` +### 2.1 成功响应 -**请求参数**: ```json { - "file_path": "public/产品手册.pdf", + "success": true, + "status": "success", + "status_code": 2020, + "message": "出题成功", + "data": { ... } +} +``` + +### 2.2 失败响应 + +```json +{ + "success": false, + "status": "failed", + "error_code": "MISSING_PARAMS", + "status_code": 4000, + "message": "缺少 file_path 或 collection 参数" +} +``` + +### 2.3 业务状态码 + +| 状态码 | 常量名 | 说明 | +|--------|--------|------| +| 2020 | EXAM_SUCCESS | 出题成功 | +| 2021 | GRADE_SUCCESS | 批阅完成 | +| 4000 | BAD_REQUEST | 请求参数错误 | +| 4001 | UNAUTHORIZED | 未授权 | +| 4002 | FORBIDDEN | 权限不足 | +| 5020 | EXAM_ERROR | 出题失败 | +| 5021 | GRADE_ERROR | 批阅失败 | + +--- + +## 三、API 接口 + +### 3.1 POST /exam/generate — 参数出题 + +根据指定的题型和数量,基于文档内容生成题目。 + +#### 请求体 + +```json +{ + "file_path": "public_kb/产品手册.pdf", "collection": "public_kb", - "choice_count": 5, - "blank_count": 2, - "short_answer_count": 2, - "difficulty": 3, - "choice_score": 2, - "blank_score": 3 -} -``` - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `file_path` | string | ✅ | - | 文件路径 | -| `collection` | string | ✅ | - | 向量库名称 | -| `choice_count` | int | ❌ | 3 | 选择题数量 | -| `blank_count` | int | ❌ | 2 | 填空题数量 | -| `short_answer_count` | int | ❌ | 2 | 简答题数量 | -| `difficulty` | int | ❌ | 3 | 难度等级 (1-5) | -| `choice_score` | int | ❌ | 2 | 每道选择题分值 | -| `blank_score` | int | ❌ | 3 | 每道填空题分值 | - -**返回结果**: -```json -{ - "exam_id": "uuid-xxxx-xxxx", - "source_file": { - "path": "public/产品手册.pdf", - "collection": "public_kb" + "question_types": { + "single_choice": 3, + "multiple_choice": 2, + "true_false": 2, + "fill_blank": 2, + "subjective": 1 }, - "choice_questions": [ - { - "id": "q_choice_001", - "content": "根据保密制度,公司最高机密的处理原则是什么?", - "options": ["A. 可向客户透露", "B. 严禁外传", "C. 部门内共享", "D. 仅领导知晓"], - "answer": "B", - "analysis": "根据保密制度第1条规定...", - "knowledge_points": ["保密制度", "信息安全"], - "difficulty": 2, - "score": 2, - "source_file": "public/产品手册.pdf", - "source_snippet": "该题依据的知识片段..." - } - ], - "blank_questions": [...], - "short_answer_questions": [...], - "total_count": 9, - "total_score": 22, - "generated_at": "2026-04-10T14:00:00" -} -``` - -### 2.2 试卷状态流程 - -``` -生成试卷 → draft (草稿) - ↓ -提交审核 → pending_review (待审核) - ↓ -管理员审核 → approved (通过) / rejected (驳回) - ↓ -学生答题 → 批阅 → 生成报告 -``` - -**状态说明**: -| 状态 | 说明 | 可见范围 | -|------|------|----------| -| `draft` | 草稿,刚生成尚未提交审核 | 创建者可见 | -| `pending_review` | 待审核,已提交等待管理员审核 | 管理员可见 | -| `approved` | 已通过,可用于学生答题 | 所有用户可见 | -| `rejected` | 已驳回,不可使用 | 创建者可见 | - ---- - -## 三、题目格式规范 - -### 3.1 选择题 - -```json -{ - "id": "q_choice_001", - "content": "根据保密制度,公司最高机密的处理原则是什么?", - "options": [ - "A. 可向客户透露", - "B. 严禁外传", - "C. 部门内共享", - "D. 仅领导知晓" - ], - "answer": "B", - "analysis": "根据保密制度第1条规定,公司最高机密严禁外传,仅限特定人员知晓。", - "knowledge_points": ["保密制度", "信息安全"], - "difficulty": 2, - "score": 2, - "source_file": "public/产品手册.pdf", - "source_snippet": "原文相关片段..." -} -``` - -**字段说明**: - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `id` | string | ✅ | 题目唯一标识 | -| `content` | string | ✅ | 题干内容 | -| `options` | array | ✅ | 选项列表,格式为 `["A. 选项内容", ...]` | -| `answer` | string | ✅ | 正确答案,单个字母(如 "A", "B") | -| `analysis` | string | ✅ | 答案解析 | -| `knowledge_points` | array | ❌ | 知识点标签 | -| `difficulty` | int | ❌ | 难度等级 1-5,默认 3 | -| `score` | int | ✅ | 题目分值 | -| `source_file` | string | ❌ | 来源文件路径 | -| `source_snippet` | string | ❌ | 来源文本片段 | - -### 3.2 填空题 - -```json -{ - "id": "q_blank_001", - "content": "公司财务报表应在每季度结束后______天内提交。", - "answer": "15", - "analysis": "根据财务管理制度第5条规定,季度报表需在季后15天内提交。", - "knowledge_points": ["财务管理"], "difficulty": 3, - "score": 3 -} -``` - -**字段说明**: - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `id` | string | ✅ | 题目唯一标识 | -| `content` | string | ✅ | 题干内容,空缺处用 `______` 表示 | -| `answer` | string | ✅ | 正确答案 | -| `analysis` | string | ✅ | 答案解析 | -| `knowledge_points` | array | ❌ | 知识点标签 | -| `difficulty` | int | ❌ | 难度等级 1-5 | -| `score` | int | ✅ | 题目分值 | - -### 3.3 简答题 - -```json -{ - "id": "q_short_001", - "content": "简述公司数据安全的三道防线。", - "reference_answer": { - "points": [ - {"point": "技术防线(防火墙、加密、访问控制等)", "score": 3}, - {"point": "制度防线(安全规定、审批流程、应急预案)", "score": 3}, - {"point": "人员防线(安全培训、意识教育、考核机制)", "score": 4} - ], - "total_score": 10 - }, - "analysis": "评分要点说明...", - "knowledge_points": ["数据安全"], - "difficulty": 4, - "score": 10 -} -``` - -**字段说明**: - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `id` | string | ✅ | 题目唯一标识 | -| `content` | string | ✅ | 题干内容 | -| `reference_answer` | object | ✅ | 参考答案,包含评分要点 | -| `reference_answer.points` | array | ✅ | 得分点列表 | -| `reference_answer.points[].point` | string | ✅ | 得分点描述 | -| `reference_answer.points[].score` | int | ✅ | 该得分点分值 | -| `analysis` | string | ❌ | 整体解析 | -| `knowledge_points` | array | ❌ | 知识点标签 | -| `difficulty` | int | ❌ | 难度等级 1-5 | -| `score` | int | ✅ | 题目总分值 | - ---- - -## 四、批卷系统设计 - -### 4.1 批卷输入格式 - -**接口路径**:`POST /exam/grade-from-mysql` - -**当前格式(完整字段)**: -```json -{ - "exam_id": "uuid-xxxx-xxxx", - "student_id": "STU_2023001", - "student_name": "张三", - "answers": [ - { - "question_id": "q_choice_001", - "question_type": "choice", - "question_content": "根据保密制度,公司最高机密的处理原则是什么?", - "options": ["A. 可向客户透露", "B. 严禁外传", "C. 部门内共享", "D. 仅领导知晓"], - "correct_answer": "B", - "max_score": 2, - "student_answer": "B" - }, - { - "question_id": "q_blank_001", - "question_type": "blank", - "question_content": "公司财务报表应在每季度结束后______天内提交。", - "correct_answer": "15", - "max_score": 3, - "student_answer": "10" - }, - { - "question_id": "q_short_001", - "question_type": "short_answer", - "question_content": "简述公司数据安全的三道防线。", - "correct_answer": "{\"points\":[{\"point\":\"技术防线\",\"score\":3},{\"point\":\"制度防线\",\"score\":3},{\"point\":\"人员防线\",\"score\":4}]}", - "max_score": 10, - "student_answer": "第一道是技术防护,包括防火墙和加密;第二道是制度管理;第三道是员工培训。" - } - ] -} -``` - -**优化后格式(最小字段)**: -```json -{ - "exam_id": "uuid", - "student_id": "STU_001", - "student_name": "张三", - "answers": [ - { - "question_id": "q_choice_001", - "question_type": "choice", - "student_answer": "B" - }, - { - "question_id": "q_blank_001", - "question_type": "blank", - "student_answer": "15" - }, - { - "question_id": "q_short_001", - "question_type": "short_answer", - "student_answer": "第一道是技术防护..." - } - ] -} -``` - -### 4.2 批卷输出格式 - -```json -{ - "report_id": "report-uuid-xxxx", - "exam_id": "uuid-xxxx-xxxx", - "student_id": "STU_2023001", - "student_name": "张三", - "total_score": 12, - "max_score": 15, - "score_rate": 80.0, - "graded_at": "2026-04-12T14:30:00", - "results": [ - { - "question_id": "q_choice_001", - "question_type": "choice", - "correct": true, - "score": 2, - "max_score": 2, - "student_answer": "B", - "correct_answer": "B", - "feedback": "回答正确!" - }, - { - "question_id": "q_blank_001", - "question_type": "blank", - "correct": false, - "score": 0, - "max_score": 3, - "student_answer": "10", - "correct_answer": "15", - "feedback": "正确答案是15天,请复习财务管理制度。" - }, - { - "question_id": "q_short_001", - "question_type": "short_answer", - "score": 8, - "max_score": 10, - "student_answer": "第一道是技术防护...", - "score_details": [ - {"point": "技术防线", "earned": 3, "max": 3}, - {"point": "制度防线", "earned": 2, "max": 3}, - {"point": "人员防线", "earned": 3, "max": 4} - ], - "feedback": "整体回答较好,制度防线描述不够具体。", - "highlights": ["技术防线表述准确"], - "shortcomings": ["制度防线未具体说明"], - "suggestions": ["建议补充具体的制度名称"] - } - ], - "summary": { - "strengths": ["选择题掌握较好", "简答题要点覆盖全面"], - "weaknesses": ["填空题记忆不准确"], - "recommendations": ["重点复习财务管理制度第3章"] + "exclude_stems": ["已有题目的题干1", "已有题目的题干2"], + "request_id": "uuid-optional", + "options": { + "include_explanation": true, + "max_source_chunks": 50 } } ``` -### 4.3 批改流程 +#### 请求参数说明 -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ 批量批改流程 │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. 前端传入 answers (最小字段) │ -│ └─ 只有 question_id + question_type + student_answer │ -│ │ -│ 2. 后端查询题目详情 │ -│ └─ 从数据库/缓存获取 correct_answer, max_score, content │ -│ │ -│ 3. 按题型分组 │ -│ ├─ choice 组 → 批量调用 Dify 代码执行节点 │ -│ └─ blank/short_answer 组 → 批量调用 Dify LLM 节点 │ -│ │ -│ 4. 合并结果返回 │ -│ └─ 统一格式返回所有批改结果 │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ -``` +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `file_path` | string | 是 | — | 文件路径(向量库内相对路径) | +| `collection` | string | 是 | — | 向量库名称 | +| `question_types` | object | 是 | — | 题型及数量,见下方合法值 | +| `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`。值为非负整数,总题数上限 **20**。 -## 五、数据库设计 +#### 成功响应 -### 5.1 题目表 (questions) - -```sql -CREATE TABLE questions ( - id VARCHAR(64) PRIMARY KEY, -- 题目ID(UUID) - question_type ENUM('choice', 'blank', 'short_answer') NOT NULL, - content TEXT NOT NULL, -- 题干内容 - options JSON, -- 选择题选项(JSON数组) - correct_answer TEXT NOT NULL, -- 正确答案 - analysis TEXT, -- 解析 - knowledge_points JSON, -- 知识点(JSON数组) - difficulty TINYINT DEFAULT 3, -- 难度(1-5) - score INT NOT NULL, -- 分值 - - -- 溯源字段(核心) - source_file VARCHAR(255) NOT NULL, -- 来源文件路径 - source_collection VARCHAR(64) NOT NULL, -- 来源向量库 - source_snippet TEXT, -- 来源知识片段 - source_hash VARCHAR(64), -- 文件哈希(用于检测文件变更) - - -- 审核状态 - status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending', - reviewed_by VARCHAR(64), - reviewed_at DATETIME, - - -- 元数据 - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR(64), - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - INDEX idx_source_file (source_file), - INDEX idx_source_collection (source_collection), - INDEX idx_question_type (question_type), - INDEX idx_status (status) -); -``` - -### 5.2 试卷表 (exams) - -```sql -CREATE TABLE exams ( - id VARCHAR(64) PRIMARY KEY, -- 试卷ID - name VARCHAR(255) NOT NULL, -- 试卷名称 - description TEXT, -- 描述 - total_score INT NOT NULL, -- 总分 - total_count INT NOT NULL, -- 题目总数 - duration INT DEFAULT 60, -- 考试时长(分钟) - - -- 状态 - status ENUM('draft', 'pending', 'published', 'archived') DEFAULT 'draft', - published_at DATETIME, - - -- 元数据 - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR(64), - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - INDEX idx_status (status) -); -``` - -### 5.3 试卷题目关联表 (exam_questions) - -```sql -CREATE TABLE exam_questions ( - exam_id VARCHAR(64) NOT NULL, - question_id VARCHAR(64) NOT NULL, - question_order INT NOT NULL, -- 题目顺序 - - PRIMARY KEY (exam_id, question_id), - FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE, - FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE, - - INDEX idx_exam_id (exam_id), - INDEX idx_question_id (question_id) -); -``` - -### 5.4 学生答卷表 (student_answers) - -```sql -CREATE TABLE student_answers ( - id VARCHAR(64) PRIMARY KEY, - exam_id VARCHAR(64) NOT NULL, - student_id VARCHAR(64) NOT NULL, - student_name VARCHAR(100), - - question_id VARCHAR(64) NOT NULL, - question_type ENUM('choice', 'blank', 'short_answer') NOT NULL, - student_answer TEXT NOT NULL, -- 学生答案 - - -- 批阅结果 - score INT DEFAULT 0, - max_score INT NOT NULL, - feedback TEXT, - score_details JSON, -- 评分详情(JSON) - - -- 元数据 - submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP, - graded_at DATETIME, - - FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE, - FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE, - - INDEX idx_exam_student (exam_id, student_id), - INDEX idx_student_id (student_id) -); -``` - -### 5.5 批阅报告表 (grade_reports) - -```sql -CREATE TABLE grade_reports ( - id VARCHAR(64) PRIMARY KEY, - exam_id VARCHAR(64) NOT NULL, - student_id VARCHAR(64) NOT NULL, - student_name VARCHAR(100), - - total_score INT NOT NULL, - max_score INT NOT NULL, - score_rate DECIMAL(5,2), - - -- 整卷分析(可选) - analysis JSON, -- AI生成的整卷分析 - - graded_at DATETIME DEFAULT CURRENT_TIMESTAMP, - - FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE, - - INDEX idx_exam_id (exam_id), - INDEX idx_student_id (student_id) -); -``` - ---- - -## 六、文件修改联动 - -### 6.1 触发条件 - -当文件被修改或删除时,通过 `source_file` 字段查找受影响的题目。 - -### 6.2 联动逻辑 - -```sql --- 查找受影响的题目 -SELECT id, source_file, source_hash -FROM questions -WHERE source_file = 'public/产品手册.pdf'; - --- 如果文件哈希变更,标记题目需要重新审核 -UPDATE questions -SET status = 'pending', - source_hash = 'new_hash_value' -WHERE source_file = 'public/产品手册.pdf'; -``` - -### 6.3 联动接口 - -**接口路径**:`POST /exam/check-file-changes` - -**请求参数**: -```json -{ - "file_path": "public/产品手册.pdf", - "new_hash": "新的文件哈希" -} -``` - -**返回结果**: ```json { "success": true, - "file_path": "public/产品手册.pdf", - "affected_questions": ["q_uuid_001", "q_uuid_002", ...], - "count": 15, - "recommendation": "建议重新生成该文件的题目" + "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 道"] + } } ``` ---- +**data 字段说明**: -## 七、API 接口汇总 - -### 7.1 出题接口 - -| 接口 | 方法 | 说明 | +| 字段 | 类型 | 说明 | |------|------|------| -| `/exam/generate` | POST | 按主题生成试卷 | -| `/exam/generate-by-file` | POST | 按文件生成题目 | -| `/exam/list` | GET | 获取试卷列表 | -| `/exam/` | GET | 获取试卷详情 | -| `/exam/` | PUT | 更新试卷 | -| `/exam/` | DELETE | 删除试卷 | -| `/exam//submit` | POST | 提交审核 | -| `/exam//review` | POST | 审核试卷(仅管理员) | -| `/exam/by-file` | GET | 查询文件关联的题目 | - -### 7.2 批卷接口 - -| 接口 | 方法 | 说明 | -|------|------|------| -| `/exam/grade-from-mysql` | POST | 基于传入题目批卷 | -| `/exam//grade` | POST | 批阅试卷 | -| `/exam/report/` | GET | 获取批阅报告 | -| `/exam/report/list` | GET | 批阅报告列表 | - -### 7.3 题库接口 - -| 接口 | 方法 | 说明 | -|------|------|------| -| `/exam/questions/search` | GET | 搜索题目 | - -### 7.4 联动接口 - -| 接口 | 方法 | 说明 | -|------|------|------| -| `/exam/check-file-changes` | POST | 检查文件变更影响的题目 | +| `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 智能出题 -### 8.1 错误响应格式 +AI 自动分析文档内容,决定题型和数量后生成题目。与 `/exam/generate` 的区别是**不需要传 question_types**。 + +#### 请求体 ```json { - "error": "错误类型", - "message": "详细错误信息", - "details": {} + "file_path": "public_kb/产品手册.pdf", + "collection": "public_kb", + "difficulty": 3, + "max_total": 20, + "exclude_stems": ["已有题目的题干1"], + "request_id": "uuid-optional", + "options": {} } ``` -### 8.2 常见错误码 +#### 请求参数说明 -| HTTP状态码 | 错误类型 | 说明 | -|-----------|---------|------| -| 400 | bad_request | 请求参数格式错误 | -| 401 | unauthorized | 未认证 | -| 403 | forbidden | 权限不足 | -| 404 | not_found | 资源不存在 | -| 500 | internal_error | 服务器内部错误 | +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `file_path` | string | 是 | — | 文件路径 | +| `collection` | string | 是 | — | 向量库名称 | +| `difficulty` | int | 否 | 3 | 难度等级 1-5 | +| `max_total` | int | 否 | — | AI出题总数上限(正整数)。**不传则不限制** | +| `exclude_stems` | string[] | 否 | — | 排除已有题干 | +| `request_id` | string | 否 | — | 幂等性标识 | +| `options` | object | 否 | {} | 同 /exam/generate | + +#### 成功响应 + +与 `/exam/generate` 格式相同,`data` 中额外包含 `ai_analysis` 字段: + +```json +{ + "success": true, + "status": "success", + "status_code": 2020, + "message": "AI 智能出题成功", + "data": { + "success": true, + "request_id": "uuid", + "questions": [ ... ], + "total": 15, + "requested_types": {"single_choice": 8, "true_false": 4, "fill_blank": 3}, + "actual_types": {"single_choice": 8, "true_false": 4, "fill_blank": 3}, + "source_chunks_used": 30, + "ai_analysis": { + "total_knowledge_points": 54, + "question_types": {"single_choice": 8, "true_false": 4, "fill_blank": 3}, + "suitable_types": ["single_choice", "true_false", "fill_blank"], + "reason": "文档涵盖大量概念性内容..." + } + } +} +``` + +**ai_analysis 字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `total_knowledge_points` | int | AI 提取的知识点总数 | +| `question_types` | object | AI 推荐的题型和数量 | +| `suitable_types` | string[] | 适合该文档的题型列表 | +| `reason` | string | AI 的分析说明 | + +> **注意**:`requested_types` 来自 AI 分析结果(与 `ai_analysis.question_types` 一致),可能包含值为 0 的题型(表示 AI 认为该题型不适合当前文档)。`actual_types` 仅包含实际生成数量大于 0 的题型。 --- -## 九、注意事项 +### 3.3 POST /exam/grade — 批题接口 -1. **题目ID生成**:使用UUID,确保全局唯一 -2. **文件哈希**:用于检测文件变更,建议使用MD5或SHA256 -3. **批量批卷性能**:简答题批卷耗时,建议使用异步处理或并发 -4. **错误处理**:批卷失败时返回默认结果,不影响整体流程 -5. **认证方式**:出题系统使用 JWT Bearer Token 认证 +逐题批阅并返回评分结果。支持 5 种题型混合批阅。 + +#### 请求体 + +```json +{ + "request_id": "uuid-optional", + "answers": [ + { + "question_id": "uuid-001", + "question_type": "single_choice", + "content": {"answer": "B"}, + "student_answer": "A", + "max_score": 2 + }, + { + "question_id": "uuid-002", + "question_type": "multiple_choice", + "content": {"answer": ["A", "C"]}, + "student_answer": ["A", "B"], + "max_score": 3 + }, + { + "question_id": "uuid-003", + "question_type": "true_false", + "content": {"answer": "对"}, + "student_answer": "错", + "max_score": 2 + }, + { + "question_id": "uuid-004", + "question_type": "fill_blank", + "content": {"answer": [["答案1", "同义词"], ["答案2"]]}, + "student_answer": ["学生答案1", "学生答案2"], + "max_score": 4 + }, + { + "question_id": "uuid-005", + "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 | 是 | 题型,合法值同 question_types 键名 | +| `content` | object | 是 | 题目内容(与出题接口返回的 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位小数) | --- -## 十、变更记录 +### 3.4 GET /exam/health — 健康检查 + +```json +{ + "status": "ok", + "service": "exam-api", + "version": "2.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字符..." + } + ] + } +} +``` + +### 4.1 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `question_type` | string | 题型标识 | +| `difficulty` | int | 难度 1-5 | +| `content.stem` | string | 题干文本 | +| `content.data` | object | 题型相关数据(见下方) | +| `content.answer` | any | 正确答案(类型随题型变化) | +| `content.explanation` | string | 答案解析(可选) | +| `source_trace` | object | 溯源信息 | + +### 4.2 各题型 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": "..." + } +} +``` + +**填空题 (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 按要点逐项评分。 + +### 4.3 溯源信息 (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 字符 | + +--- + +## 五、批题结果数据结构 + +`results` 数组中每道题的评分结果,结构因题型而异: + +### 5.1 客观题 (single_choice / multiple_choice / true_false) + +```json +{ + "question_id": "uuid-001", + "score": 2, + "max_score": 2, + "grading_status": "success", + "details": { + "correct": true, + "student_answer": "B", + "correct_answer": "B", + "feedback": "正确!" + } +} +``` + +### 5.2 填空题 (fill_blank) + +```json +{ + "question_id": "uuid-004", + "score": 2.0, + "max_score": 4, + "grading_status": "success", + "details": { + "blank_scores": [2.0, 0], + "total_blanks": 2, + "correct_blanks": 1 + } +} +``` + +> 填空题支持部分给分。`blank_scores` 为每空得分,支持模糊匹配(编辑距离容错 + 标点归一化)。 + +### 5.3 主观题 (subjective) + +```json +{ + "question_id": "uuid-005", + "score": 7.5, + "max_score": 10, + "grading_status": "success", + "details": { + "scoring_breakdown": [ + { + "point": "技术防线", + "weight": 0.4, + "achieved": 0.9, + "comment": "表述准确,覆盖了主要技术手段" + }, + { + "point": "制度防线", + "weight": 0.3, + "achieved": 0.5, + "comment": "提到了审批流程但缺少具体制度名称" + }, + { + "point": "人员防线", + "weight": 0.3, + "achieved": 0.8, + "comment": "安全培训和意识教育均有涉及" + } + ], + "highlights": ["技术防线表述全面"], + "shortcomings": ["制度防线描述不够具体"], + "overall_feedback": "整体回答较好,建议补充制度层面的具体规定。" + } +} +``` + +### 5.4 批阅失败(兜底) + +```json +{ + "question_id": "uuid-005", + "score": 0, + "max_score": 10, + "grading_status": "failed", + "details": { + "error": "LLM 调用超时" + } +} +``` + +> 单题批阅失败不影响其他题目,失败题目得 0 分。 + +### 5.5 grading_status 取值 + +| 值 | 说明 | +|----|------| +| `success` | 评分成功 | +| `failed` | 评分失败(LLM超时/解析异常),score 为 0 | + +--- + +## 六、出题管线内部逻辑(供参考) + +v2 出题管线分四个阶段: + +**Phase 1 — 章节分组**:检索文档切片,按章节标题聚类,构建文档结构概览。 + +**Phase 2 — 知识点提取**:逐章节调用 LLM 提取知识点列表。每次 LLM 调用间隔 1 秒防限流。 + +**Phase 3 — 精准出题**:按知识点 + 题型分配方案,逐知识点调用 LLM 生成题目。支持 429 限流自动重试(指数退避 1s→2s→4s)。 + +**Phase 4 — 后处理**: +- 四层去重:题干精确去重 → 语义相似度去重 → 知识点均衡 → 跨调用去重(exclude_stems) +- 补题机制:某知识点出题失败时自动从其他知识点补题,补题与已有题干交叉去重 + +--- + +## 七、批阅逻辑说明(供参考) + +| 题型 | 批阅方式 | 匹配策略 | +|------|----------|----------| +| single_choice | 精确匹配 | 大小写不敏感 | +| multiple_choice | 精确匹配 | 全部选对才给分 | +| true_false | 精确匹配 | 完全一致 | +| fill_blank | 模糊匹配 | 标点归一化 + 编辑距离容错(>=4字符允许<=2差异) | +| subjective | LLM 评分 | 按 scoring_points 逐项评分,支持并发批阅 | + +--- + +## 八、后端对接指南 + +### 8.1 出题 → 存储 → 批题的典型流程 + +``` +1. 后端调用 POST /exam/generate 或 /exam/generate-smart + ↓ +2. RAG 返回 questions 数组 + ↓ +3. 后端将 questions 存入 MySQL(保存完整 content 对象和 source_trace) + ↓ +4. 学生答题后,后端组装 answers 数组 + - 将存储的 content 对象直接透传到 answers[].content + - 填入 student_answer 和 max_score + ↓ +5. 后端调用 POST /exam/grade + ↓ +6. RAG 返回 results 数组,后端存储评分结果 +``` + +### 8.2 后端存储建议 + +题目表建议至少存储以下字段: + +| 字段 | 来源 | 说明 | +|------|------|------| +| `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` | 来源向量库 | + +### 8.3 调用示例 (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 智能出题(限制20题)**: +```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": 20 + }' +``` + +**批题**: +```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 + } + ] + }' +``` + +--- + +## 九、性能参考 + +| 操作 | 预估耗时 | 说明 | +|------|----------|------| +| 参数出题(5题) | 60-90s | 主要耗时在多次 LLM 调用 | +| AI 智能出题(20题) | 3-5min | 含 AI 分析 + 多次 LLM 调用 | +| 批题(10题混合) | 15-30s | 主观题 LLM 评分较慢 | +| 批题(纯客观题) | 1-3s | 无需 LLM,纯匹配 | + +> 耗时受 LLM 响应速度影响较大。使用推理模型(如 mimo-v2.5)时已自动关闭推理链以提速。 + +--- + +## 十、错误码汇总 + +| HTTP 状态码 | error_code | 场景 | +|------------|------------|------| +| 400 | MISSING_PARAMS | 缺少必填参数 | +| 400 | INVALID_PARAMS | 参数格式或值无效 | +| 500 | EXAM_ERROR | 出题过程异常 | +| 500 | GRADE_ERROR | 批阅过程异常 | + +--- + +## 十一、注意事项 + +1. **题目 ID**:出题接口不生成 ID,由后端在存储时分配 UUID。 +2. **总题数上限**:`/exam/generate` 单次请求不超过 20 题;`/exam/generate-smart` 通过 `max_total` 参数控制。 +3. **排除题干**:`exclude_stems` 最多 100 条,用于跨批次去重。后端可在多轮出题时将前批题干传入。 +4. **填空题答案**:格式为二维数组 `[["标准答案", "同义词"], ...]`,批题时支持模糊匹配。 +5. **主观题评分**:依赖 LLM,存在一定非确定性。同一答案多次评分可能有小幅波动。 +6. **同步阻塞**:所有接口为同步阻塞模式,请求直到完成后才返回。请后端设置合理的请求超时(建议 5 分钟以上)。 + +--- + +## 十二、变更记录 | 日期 | 版本 | 变更内容 | |------|------|---------| -| 2026-06-04 | 2.1 | 更新模块结构:移除已删除的 analysis.py、question_hook.py,新增 generator.py、grader.py | -| 2026-04-13 | 2.0 | 合并出题批卷功能改造计划、批卷工作流优化计划、批卷接口规范 | -| 2026-04-12 | 1.2 | 新增最小字段输入格式,优化批量批改流程 | -| 2026-04-10 | 1.0 | 初始版本:按文件出题功能设计 | +| 2026-06-22 | 3.0 | 全面重写:对接 v2 管线,更新为实际 API 格式(5种题型、统一响应、generate-smart、max_total) | +| 2026-06-04 | 2.1 | 更新模块结构 | +| 2026-04-13 | 2.0 | 合并出题批卷功能改造计划 | +| 2026-04-12 | 1.2 | 新增最小字段输入格式 | +| 2026-04-10 | 1.0 | 初始版本 |