Files
rag/docs/出题批卷系统设计.md
lacerate551 a3aa6cbcf3 docs: 完善出题批卷系统设计文档,补充服务器实测验证备注
- generate-smart 的 requested_types 可能包含值为0的题型(AI分析结果)
- actual_types 仅包含实际生成数量大于0的题型
- 文档已通过生产服务器三端点实测验证
2026-06-22 19:23:52 +08:00

22 KiB
Raw Permalink Blame History

出题批卷系统设计

文档类型: 系统设计文档 & 后端对接手册 创建日期: 2026-04-10 最后更新: 2026-06-22 版本: 3.0 状态: 已实施


一、系统概述

1.1 定位

出题批卷系统是 RAG 知识库服务的扩展模块(exam_pkg负责基于知识库文档自动生成题目和自动批阅。RAG 服务只负责出题和批题的无状态计算,题目的存储、审核、状态管理由后端服务负责。

1.2 职责边界

职责 RAG 服务 后端服务
题目生成 基于文档生成题目 + 溯源 审核入库 + 状态管理
题目批阅 逐题评分 + 反馈 汇总报告 + 成绩存储
题目存储 无状态,不存储 MySQL 持久化

1.3 模块结构

exam_pkg/
├── generator.py      # 出题引擎v2管线章节分组→知识点提取→精准出题→去重补题
├── grader.py         # 批阅引擎(客观题精确匹配 + 填空题模糊匹配 + 主观题LLM评分
├── manager.py        # 编排层(检索切片→调用引擎→组装返回)
├── api.py            # Flask Blueprintexam_bp, url_prefix=/exam
└── local_db.py       # 本地题库缓存SQLite辅助功能

1.4 服务信息

项目
服务地址 http://<host>:5001
URL前缀 /exam
健康检查 GET /exam/health
运行模式 gunicorn + gthread (2 workers)

二、统一响应格式

所有接口返回统一 JSON 信封:

2.1 成功响应

{
    "success": true,
    "status": "success",
    "status_code": 2020,
    "message": "出题成功",
    "data": { ... }
}

2.2 失败响应

{
    "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 — 参数出题

根据指定的题型和数量,基于文档内容生成题目。

请求体

{
    "file_path": "public_kb/产品手册.pdf",
    "collection": "public_kb",
    "question_types": {
        "single_choice": 3,
        "multiple_choice": 2,
        "true_false": 2,
        "fill_blank": 2,
        "subjective": 1
    },
    "difficulty": 3,
    "exclude_stems": ["已有题目的题干1", "已有题目的题干2"],
    "request_id": "uuid-optional",
    "options": {
        "include_explanation": true,
        "max_source_chunks": 50
    }
}

请求参数说明

参数 类型 必填 默认值 说明
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_choicemultiple_choicetrue_falsefill_blanksubjective。值为非负整数,总题数上限 20

成功响应

{
    "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 道"]
    }
}

data 字段说明

字段 类型 说明
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 智能出题

AI 自动分析文档内容,决定题型和数量后生成题目。与 /exam/generate 的区别是不需要传 question_types

请求体

{
    "file_path": "public_kb/产品手册.pdf",
    "collection": "public_kb",
    "difficulty": 3,
    "max_total": 20,
    "exclude_stems": ["已有题目的题干1"],
    "request_id": "uuid-optional",
    "options": {}
}

请求参数说明

参数 类型 必填 默认值 说明
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 字段:

{
    "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 — 批题接口

逐题批阅并返回评分结果。支持 5 种题型混合批阅。

请求体

{
    "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 对象,批题时原样传入即可。

成功响应

{
    "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 — 健康检查

{
    "status": "ok",
    "service": "exam-api",
    "version": "2.0"
}

四、题目数据结构

每道题(questions 数组中的元素)的完整结构:

{
    "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)

{
    "content": {
        "stem": "以下哪项是正确的?",
        "data": {
            "options": [
                {"key": "A", "content": "..."},
                {"key": "B", "content": "..."},
                {"key": "C", "content": "..."},
                {"key": "D", "content": "..."}
            ]
        },
        "answer": "B",
        "explanation": "..."
    }
}

多选题 (multiple_choice)

{
    "content": {
        "stem": "以下哪些是正确的?(多选)",
        "data": {
            "options": [
                {"key": "A", "content": "..."},
                {"key": "B", "content": "..."},
                {"key": "C", "content": "..."},
                {"key": "D", "content": "..."}
            ]
        },
        "answer": ["A", "C"],
        "explanation": "..."
    }
}

判断题 (true_false)

{
    "content": {
        "stem": "公司数据应定期备份(判断对错)",
        "data": {},
        "answer": "对",
        "explanation": "..."
    }
}

填空题 (fill_blank)

{
    "content": {
        "stem": "公司财务报表应在每季度结束后______天内提交。",
        "data": {
            "blank_count": 1
        },
        "answer": [["15", "十五日"]],
        "explanation": "..."
    }
}

answer 格式为二维数组:外层对应每空,内层为该空的可接受答案列表(第一个为标准答案,其余为同义词/等价答案)。

主观题 (subjective)

{
    "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)

{
    "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)

{
    "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)

{
    "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 批阅失败(兜底)

{
    "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)

参数出题

curl -X POST http://<host>: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题

curl -X POST http://<host>:5001/exam/generate-smart \
  -H "Content-Type: application/json" \
  -d '{
    "file_path": "public_kb/产品手册.pdf",
    "collection": "public_kb",
    "max_total": 20
  }'

批题

curl -X POST http://<host>: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-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 初始版本