AI 智能生成试卷根据知识库内容或用户自由描述自动生成包含单选、多选、文本题的试卷并自动分配分值。AI 批量阅卷用户提交答案后AI 一次性批改所有题目返回包含详细评分、正确答案和评语的完整成绩单。本文将提供可直接运行的代码、数据库脚本以及提示词模板并对关键逻辑添加详细注释帮助开发者快速落地自己的 AI 应用。完整代码使用CRUD生成即可一、技术栈与架构说明技术组件版本 / 说明基础框架RuoYi-Vue-Plus 5.xSpring Boot 3.5 JDK 17 Sa-TokenORMMyBatis-PlusJSON 处理Fastjson2HTTP 客户端Hutool HttpUtilAI 模型DeepSeek兼容 OpenAI API 格式数据库MySQL 8.0支持 JSON 字段整体流程┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 前端 Vue3 │ ───→ │ Controller 层 │ ───→ │ AI 生成/阅卷服务 │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ ↓ ↓ ┌─────────────────┐ ┌─────────────────┐ │ MyBatis-Plus │ │ DeepSeek API │ └─────────────────┘ └─────────────────┘二、数据库设计含完整建表脚本以下为实际项目使用的表结构包含 RuoYi 框架的标准字段租户、部门、逻辑删除等。JSON 字段用于存储灵活的配置和答案快照。CREATE TABLE a_exam_paper ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(100) NOT NULL COMMENT 试卷名称, gen_type char(1) NOT NULL COMMENT 生成方式1知识库 2自由描述, gen_config varchar(500) DEFAULT NULL COMMENT 生成配置快照如目录ID、描述原文、模型参数, total_score int NOT NULL DEFAULT 0 COMMENT 试卷总分, question_count int NOT NULL DEFAULT 0 COMMENT 题目总数, status char(1) NOT NULL DEFAULT 0 COMMENT 状态0生成中 1已生成 2已作废, create_by varchar(64) DEFAULT NULL COMMENT 创建人, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_by varchar(64) DEFAULT NULL COMMENT 更新人, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, tenant_id varchar(20) DEFAULT 000000 COMMENT 租户编号, create_dept bigint DEFAULT NULL COMMENT 创建部门, del_flag int DEFAULT 0 COMMENT 删除标志, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT试卷主表; CREATE TABLE a_exam_question ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, paper_id bigint NOT NULL COMMENT 所属试卷ID, type varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 题型1单选 2多选 3文本, content text NOT NULL COMMENT 题干, options text COMMENT 选项列表仅单选/多选有值, answer text NOT NULL COMMENT 标准答案单选存A多生存JSON数组文本存要点, analysis text COMMENT 答案解析, score int NOT NULL DEFAULT 0 COMMENT 分值, sort int NOT NULL DEFAULT 0 COMMENT 排序, tenant_id varchar(20) DEFAULT 000000 COMMENT 租户编号, create_dept bigint DEFAULT NULL COMMENT 创建部门, del_flag int DEFAULT 0 COMMENT 删除标志, create_by varchar(64) DEFAULT NULL COMMENT 创建人, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_by varchar(64) DEFAULT NULL COMMENT 更新人, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_paper_id (paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT题目明细表; CREATE TABLE a_exam_user_record ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, paper_id bigint NOT NULL COMMENT 试卷ID, user_id bigint NOT NULL COMMENT 用户ID关联sys_user, start_time datetime DEFAULT NULL COMMENT 开始考试时间, end_time datetime DEFAULT NULL COMMENT 交卷时间, total_score int NOT NULL DEFAULT 0 COMMENT 试卷总分冗余便于统计, user_score int NOT NULL DEFAULT 0 COMMENT 用户实际得分, status char(1) NOT NULL DEFAULT 0 COMMENT 考试状态0进行中 1已完成 2已阅卷, answers_snapshot json DEFAULT NULL COMMENT 用户全部答案快照JSON格式便于回溯, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, tenant_id varchar(20) DEFAULT 000000 COMMENT 租户编号, create_dept bigint DEFAULT NULL COMMENT 创建部门, del_flag int DEFAULT 0 COMMENT 删除标志, create_by varchar(64) DEFAULT NULL COMMENT 创建人, update_by varchar(64) DEFAULT NULL COMMENT 更新人, PRIMARY KEY (id), UNIQUE KEY uk_paper_user (paper_id,user_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT用户考试记录表含总分与答案快照; DROP TABLE IF EXISTS a_kb_category; CREATE TABLE a_kb_category ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, parent_id bigint NOT NULL DEFAULT 0 COMMENT 父级ID0为根目录, name varchar(100) NOT NULL COMMENT 目录名称, sort int NOT NULL DEFAULT 0 COMMENT 排序, create_by bigint DEFAULT NULL COMMENT 创建人, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_by bigint DEFAULT NULL COMMENT 更新人, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, tenant_id varchar(20) DEFAULT 000000 COMMENT 租户编号, create_dept bigint DEFAULT NULL COMMENT 创建部门, del_flag int DEFAULT 0 COMMENT 删除标志, PRIMARY KEY (id), KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT知识库目录表; CREATE TABLE a_kb_document ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, category_id bigint NOT NULL COMMENT 所属目录ID, title varchar(200) NOT NULL COMMENT 文档标题, content longtext NOT NULL COMMENT 文档正文支持Markdown, keywords varchar(500) DEFAULT NULL COMMENT 关键词多个用逗号分隔, status char(1) NOT NULL DEFAULT 0 COMMENT 状态0正常 1停用, create_by bigint DEFAULT NULL COMMENT 创建人, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_by bigint DEFAULT NULL COMMENT 更新人, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, tenant_id varchar(20) DEFAULT 000000 COMMENT 租户编号, create_dept bigint DEFAULT NULL COMMENT 创建部门, del_flag int DEFAULT 0 COMMENT 删除标志, PRIMARY KEY (id), KEY idx_category_id (category_id), FULLTEXT KEY ft_content (title,content,keywords) /*!50100 WITH PARSER ngram */ ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT知识库文档表;三、AI 服务封装AIService我们使用 Hutool 的HttpUtil直接调用 DeepSeek API因其完全兼容 OpenAI 接口规范无需额外 SDK。package org.dromara.exam.utils; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; /** * AI 服务类封装 DeepSeek API 调用 * 使用 HTTP 直连方式兼容 OpenAI 格式 */ Slf4j Service public class AIService { Value(${deepseek.api-key}) private String apiKey; Value(${deepseek.model:deepseek-chat}) private String model; Value(${deepseek.temperature:0.7}) private Double temperature; Value(${deepseek.max-tokens:4096}) private Integer maxTokens; private static final String DEEPSEEK_URL https://api.deepseek.com/v1/chat/completions; PostConstruct public void init() { if (StrUtil.isBlank(apiKey)) { log.error(❌ DeepSeek API Key 未配置AI 服务将不可用); } else { log.info(✅ AI 服务初始化完成模型{}, model); } } /** * 发送聊天请求 * param prompt 完整的提示词内容 * return AI 生成的文本响应通常为 JSON 字符串 */ public String chat(String prompt) { if (StrUtil.isBlank(apiKey)) { throw new RuntimeException(AI 服务未就绪请检查 DeepSeek 配置); } // 构建 OpenAI 兼容格式的请求体 MapString, Object requestBody new HashMap(); requestBody.put(model, model); requestBody.put(messages, List.of(Map.of(role, user, content, prompt))); requestBody.put(temperature, temperature); requestBody.put(max_tokens, maxTokens); requestBody.put(response_format, Map.of(type, json_object)); // 强制返回 JSON String jsonBody JSON.toJSONString(requestBody); try (HttpResponse response HttpRequest.post(DEEPSEEK_URL) .header(Authorization, Bearer apiKey) .header(Content-Type, application/json) .body(jsonBody) .execute()) { if (!response.isOk()) { log.error(DeepSeek API 调用失败状态码{}响应{}, response.getStatus(), response.body()); throw new RuntimeException(AI 服务调用失败状态码 response.getStatus()); } JSONObject respJson JSON.parseObject(response.body()); String content respJson.getJSONArray(choices) .getJSONObject(0) .getJSONObject(message) .getString(content); log.debug(AI 响应{}, content); return content; } catch (Exception e) { log.error(AI 调用异常, e); throw new RuntimeException(AI 服务异常 e.getMessage(), e); } } }配置示例application.ymldeepseek: api-key: sk-your-api-key-here model: deepseek-chat temperature: 0.7 max-tokens: 4096四、提示词模板管理将提示词独立为文件便于维护和调优。使用PromptTemplateLoader从 classpath 加载。4.1 提示词加载器package org.dromara.exam.utils; import cn.hutool.core.io.resource.ResourceUtil; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; Component public class PromptTemplateLoader { public String loadTemplate(String fileName) { try { return ResourceUtil.readStr(prompts/ fileName, StandardCharsets.UTF_8); } catch (Exception e) { throw new RuntimeException(提示词模板加载失败: fileName, e); } } }4.2 试卷生成提示词generate_paper_prompt.txt你是一名资深教育专家和出题老师。请严格依据以下【参考资料】生成 {question_count} 道题目,满分 {total_score} 分题型分布建议单选 60%、多选 20%、文本 20%。 【参考资料】 {reference_content} 【输出要求】 1. 输出必须是一个合法的 JSON 对象不包含任何额外文字或注释。 2. JSON 结构必须严格遵循以下示例。 示例 JSON { questions: [ { type: single, content: Java中下列哪个关键字用于创建线程, options: [A. extends, B. implements, C. new, D. synchronized], answer: B, analysis: 实现Runnable接口使用implements关键字。, score: 1, sort: 1 }, { type: multiple, content: 以下哪些属于HTTP请求方法, options: [A. GET, B. POST, C. SEND, D. PUT, E. DELETE], answer: [A,B,D,E], analysis: HTTP常见方法有GET、POST、PUT、DELETE等。, score: 1, sort: 2 }, { type: text, content: 请简述面向对象编程的三大特性。, options: null, answer: 封装、继承、多态。, analysis: 封装隐藏内部实现继承扩展父类功能多态同一接口不同实现。, score: 1, sort: 3 } ] }4.3 批量阅卷提示词grading_batch_prompt.txt该模板要求 AI 一次性返回包含题目完整信息及评分结果的 JSON 数组实现自包含的成绩单。你是一名严格的阅卷老师。请根据以下【题目列表】一次性批改所有题目并按顺序返回每一道题的评分结果。 【题目列表】共 {question_count} 道题 {batch_questions_json} 【评分规则】 1. 单选题学生答案与标准答案完全一致忽略大小写得满分否则0分。 2. 多选题完全正确得满分部分正确得一半分全错或漏选不得分。 3. 文本题根据语义相似度和关键要点覆盖度判断满分按比例折算例如总分5分语义匹配度80%则得4分。 【输出要求】 必须返回一个严格的 JSON 数组数组长度必须与题目列表数量一致顺序一一对应。每个元素必须包含以下字段 - question_id题目ID与输入一致 - type题型与输入一致取值为 single / multiple / text - content题干与输入一致 - options选项与输入一致 - standard_answer标准答案与输入一致 - user_answer学生答案与输入一致 - score题目分值与输入一致 - is_correct1正确或 0错误 - score_earned实际得分整数 - feedback简短评语若错误需给出正确答案提示 【输出示例】 [ { question_id: 1, type: single, content: Java支持哪种继承方式, options: [A. 单继承, B. 多继承, C. 接口多继承, D. 以上都是], standard_answer: A, user_answer: A, score: 5, is_correct: 1, score_earned: 5, feedback: 回答正确 }, { question_id: 2, type: multiple, content: 以下哪些属于HTTP请求方法, options: [A. GET, B. POST, C. SEND, D. PUT, E. DELETE], standard_answer: [A,B,D,E], user_answer: [A,C], score: 5, is_correct: 0, score_earned: 2, feedback: 漏选D、E多选C正确答案为A、B、D、E }, { question_id: 3, type: text, content: 请简述面向对象编程的三大特性。, options: null, standard_answer: 封装、继承、多态, user_answer: 封装、继承、多态, score: 10, is_correct: 1, score_earned: 10, feedback: 要点齐全回答正确 } ]五、核心业务实现IAIGenerateService该类集成了试卷生成和批量阅卷两大核心功能可以使用Async实现异步处理避免阻塞前端本文章为了演示未使用。5.1 AI 生成试卷流程根据生成方式获取参考资料 → 构建提示词 → 调用 AI → 解析 JSON 并批量插入题目。Transactional(rollbackFor Exception.class) public void generatePaper(PaperBo bo ) { try{ String reference ; // 生成方式1知识库 2自由描述 if (1.equals(bo.getGenType())) { // 知识库生成genConfig 为逗号分隔的文档ID字符串 String[] ids bo.getGenConfig().split(,); //通过文档ID获取文档内容 ListDocument documents documentMapper.selectByIds(List.of(ids)); if (documents ! null !documents.isEmpty()) { // 拼接文档标题和内容 reference documents.stream() .map(doc - 【 doc.getTitle() 】\n doc.getContent()) .reduce((a, b) - a \n\n---\n\n b) .orElse(); } else { reference 暂无相关参考资料。; } } else { // 自由描述生成genConfig 为用户输入的描述 reference bo.getGenConfig(); } // 2. 构建提示词 String template promptUtil.loadTemplate(generate_paper_prompt.txt); // 替换模板中的占位符 共3个{question_count}、{reference_content}、{total_score}分别对应试卷参数中的题目数量、参考内容、总分数 String prompt template .replace({question_count}, String.valueOf(bo.getQuestionCount())) .replace({reference_content}, reference) .replace({total_score}, String.valueOf(bo.getTotalScore())) .toString(); // 3. 调用 AI 服务 String aiResponse aiService.chat(prompt); log.info(AI 响应{}, aiResponse); // 4. 解析题目并入库 parseAndSaveQuestions(aiResponse, bo.getId()); }catch (Exception e){ log.error(生成试卷失败, e); throw new RuntimeException(生成试卷失败, e); } } /** * 解析题目并入库 * param aiResponse AI 响应的 JSON 字符串 * param paperId 试卷ID */ private void parseAndSaveQuestions(String aiResponse, Long paperId) { // 解析题目并入库的逻辑 String json aiResponse.replaceAll(json, ).replaceAll(, ).trim(); JSONObject root JSON.parseObject(json); // 从 JSON 中提取 questions 数组 JSONArray questions root.getJSONArray(questions); //TODO 解析题目并入库的逻辑 //从 questions 数组中提取每个对象 ListQuestion questionsList new ArrayList(); for (Object question : questions) { Question newQuestion new Question(); newQuestion.setPaperId(paperId); JSONObject q JSON.parseObject(question.toString()); newQuestion.setType(q.getString(type)); newQuestion.setContent(q.getString(content)); newQuestion.setOptions(q.getString(options)); newQuestion.setAnswer(q.getString(answer)); newQuestion.setAnalysis(q.getString(analysis)); newQuestion.setScore(q.getLong(score)); newQuestion.setSort(q.getLong(sort)); questionsList.add(newQuestion); } boolean flag questionMapper.insertBatch(questionsList); if (flag) { LambdaUpdateWrapperPaper luw Wrappers.lambdaUpdate(); luw.eq(Paper::getId, paperId); luw.set(Paper::getStatus, 1); paperMapper.update(luw); } log.info(解析到 {} 道题目, questions.size()); }5.2 AI 批量阅卷流程解析用户提交的极简答案 → 构建包含题目完整信息的 JSON 数组 → 调用 AI 批量评分 → 将 AI 返回的完整成绩单直接覆盖answers_snapshot/** * 阅卷批量模式 */ Transactional(rollbackFor Exception.class) public void gradePaper(UserRecordBo bo) { try { //用户全部答案JSON 格式 [ // { questionId: 1, answer: B }, // { questionId: 2, answer: [\A\,\C\] } //] String answersSnapshot bo.getAnswersSnapshot(); if (StrUtil.isBlank(answersSnapshot)) { throw new IllegalArgumentException(答案快照不能为空); } //获取试卷 Paper paper paperMapper.selectById(bo.getPaperId()); if (paper null) { throw new IllegalArgumentException(试卷不存在); } // 1. 获取题目列表 LambdaQueryWrapperQuestion qw Wrappers.lambdaQuery(); qw.eq(Question::getPaperId, paper.getId()); qw.orderByAsc(Question::getSort); ListQuestion questions questionMapper.selectList(qw); if (CollUtil.isEmpty(questions)) { throw new IllegalArgumentException(试卷中没有题目); } // 2. 解析用户提交的原始答案极简格式 ListAnswerItem answerItems JSON.parseArray(answersSnapshot, AnswerItem.class); // 从用户提交的答案原始格式中提取题目ID和答案 MapLong, String answerMap answerItems.stream() .collect(Collectors.toMap(AnswerItem::getQuestionId, AnswerItem::getAnswer)); // 3. 构建批量阅卷的输入 JSON 数组 JSONArray batchInput new JSONArray(); for (Question q : questions) { JSONObject item new JSONObject(); item.put(question_id, q.getId()); item.put(type, q.getType()); item.put(content, q.getContent()); item.put(options, q.getOptions() null ? null : JSON.parseArray(q.getOptions())); item.put(standard_answer, q.getAnswer()); item.put(score, q.getScore()); // 从用户提交的答案中获取该题的答案 item.put(user_answer, answerMap.getOrDefault(q.getId(), )); batchInput.add(item); } // 4. 加载提示词模板并构建 Prompt String template promptUtil.loadTemplate(grading_batch_prompt.txt); // 替换模板中的占位符 共2个{question_count}、{batch_questions_json}分别对应试卷参数中的题目数量、批量阅卷的输入 JSON 数组 String prompt template .replace({question_count}, String.valueOf(questions.size())) .replace({batch_questions_json}, batchInput.toJSONString()); // 5. 调用 AI 批量评分 String aiResponse aiService.chat(prompt); log.info(AI 响应{}, aiResponse); // 从 JSON 中提取 results 数组 JSONArray resultArray JSON.parseArray(aiResponse); //TODO 解析 AI 返回的 JSON 数组然后入库到数据库 //6. 计算总分并校验结果长度 int totalScore 0; if (resultArray.size() ! questions.size()) { log.warn(AI 返回结果数量 {} 与题目数量 {} 不一致将按实际匹配计分, resultArray.size(), questions.size()); } for (int i 0; i resultArray.size(); i) { JSONObject gradeObj resultArray.getJSONObject(i); totalScore gradeObj.getIntValue(score_earned); } // 7. 更新考试记录总分、状态、完整成绩单快照 LambdaUpdateWrapperUserRecord luw Wrappers.lambdaUpdate(); luw.eq(UserRecord::getId, bo.getId()); luw.set(UserRecord::getUserScore, totalScore); luw.set(UserRecord::getStatus, 2); // 覆盖为完整成绩单 luw.set(UserRecord::getAnswersSnapshot, resultArray.toJSONString()); userRecordMapper.update(null, luw); } catch (Exception e) { log.error(阅卷失败, recordId: {}, bo.getId(), e); throw new RuntimeException(阅卷失败: e.getMessage(), e); } }阅卷前[ { questionId: 1, answer: B }, { questionId: 2, answer: [\A\,\C\] } ]阅卷后[ { questionId: 1, content: Java中哪个关键字用于实现封装, userAnswer: B, standardAnswer: B, scoreEarned: 5, aiFeedback: 回答正确 ... } ]六、运行效果展示6.1 生成试卷示例基于知识库目录1,2,3是自定义到数据库的IDJava 技术栈生成 5 道题、总分 100 分的试卷AI 返回并入库的题目如下截取自数据库{ questions: [ { type: single, content: Java中下列哪个关键字用于创建线程, options: [A. extends, B. implements, C. new, D. synchronized], answer: B, analysis: 实现Runnable接口使用implements关键字。, score: 1, sort: 1 }, { type: multiple, content: 以下哪些属于HTTP请求方法, options: [A. GET, B. POST, C. SEND, D. PUT, E. DELETE], answer: [A,B,D,E], analysis: HTTP常见方法有GET、POST、PUT、DELETE等。, score: 1, sort: 2 }, { type: text, content: 请简述面向对象编程的三大特性。, options: null, answer: 封装、继承、多态。, analysis: 封装隐藏内部实现继承扩展父类功能多态同一接口不同实现。, score: 1, sort: 3 } ] }6.2 阅卷结果示例用户作答后AI 批改并返回的answers_snapshot内容已格式化为可读 JSON[ { question_id: 2047120715656048642, type: single, content: Java中下列哪个特性通过访问修饰符控制对数据和方法的访问权限, options: [A. 继承, B. 封装, C. 多态, D. 抽象], standard_answer: B, user_answer: A, score: 12, is_correct: 0, score_earned: 0, feedback: 错误。访问修饰符用于实现封装正确答案是B。 } // ... 其他题目评分详情 ]七、总结与扩展建议本文详细介绍了如何在 RuoYi-Vue-Plus 框架中集成 DeepSeek 大模型实现 AI 智能生成试卷与批量阅卷功能。核心要点回顾数据库设计使用 JSON 字段存储灵活配置answers_snapshot在阅卷前后切换格式实现性能与可读性的平衡。AI 调用采用 HTTP 直连方式兼容 OpenAI 规范代码简洁且易于替换其他模型。提示词工程将提示词外置为文件通过占位符动态替换方便持续优化。批量处理阅卷采用一次请求处理全部题目大幅降低延迟与成本。希望本文能为您的 AI 应用开发提供清晰、可落地的参考。完整代码与数据库脚本已随文提供欢迎实践与交流