简介:面向计算机专业毕业设计与人工智能教育应用开发者的智能刷题平台完整资源包,以基于大语言模型的人工智能题目生成、智能批阅、在线练习和一键组卷为核心,解决传统刷题平台智能化不足、手动组卷耗时等痛点,同时内置自定义角色与层次化权限管理,支持按年级、学科、题型、难度多维度出题。技术实现上,前端采用React与Tailwind CSS,后端基于FastAPI,配合RabbitMQ、MySQL及Agent服务,并通过Docker Compose完成容器化部署,整体工程结构清晰,便于本地运行与二次开发。资源共147个文件,涵盖前后端源代码、Docker配置、论文、开题报告、演示视频等主要类型,压缩包约19.58MB。目前已有99人学习/下载,适合需要快速搭建完整毕设项目或撰写系统设计与论文文档的读者,既可直接对照源码理解业务逻辑,也可复用其部署方案、文档框架和演示素材,显著降低从零开发与撰写成本;其中的论文与开题报告还可为系统设计、数据库建模和测试章节提供现成参考。
1. 智能刷题平台的核心不是题库,而是模型替你完成出题、判分、组卷
刷题平台加上大语言模型以后,系统的重心从“题库管理”变成了“内容生成”。题目不再靠人工录入,而是模型根据知识点和难度实时生成;主观题答案不再做字符串比对,而是按得分要点自动判分;试卷按总分、难度和知识点约束一键拼装。这套系统输出的就是标题里那四样:源代码、论文、开题报告和演示视频。
适合的人有两类:毕业设计要交完整交付物的本科生,以及想把LLM生成能力落到业务流程里的教育类开发者。前者关心模块怎么拆、数据怎么备、创新点写哪里;后者关心生成质量、批阅稳定性和组卷约束。把提示词工程、向量检索和序列生成串在同一条业务链路上,代码和论文就有了统一主线。
2. LLM接入与RAG数据准备:智能刷题平台的架构底座
2.1 业务层、检索层、推理层:为什么LLM平台要多出一层
普通刷题平台通常是两层结构:前端负责答题交互,后端负责题库增删改查。引入LLM之后必须再多分出检索和推理两层,不然生成逻辑会跟业务逻辑纠缠在一起,改一次提示词就要重新部署整个后端服务。分层之后各层职责明确,两个人可以并行开发:一个人写业务接口,另一个人整理知识库,互不阻塞。
| 层级 | 承担内容 | 典型组件 |
|---|---|---|
| 业务层 | 用户、练习记录、试卷管理、错题本 | FastAPI / Spring Boot |
| 检索层 | 知识库分块、向量化、相似度检索 | Qdrant、Embedding API |
| 推理层 | 题目生成、批阅、结构化输出 | LLM API / 本地推理端 |
检索层的存在是为了解决“模型不知道你的知识点范围”的问题。LLM在训练阶段见过大量通用内容,但不知道你这门课的教材表述、教学大纲里的知识点划分和考试的难度分布。做法是把教材、课件和考纲做成分块并向量化,写入向量数据库。推理层则接收业务层请求,把检索到的知识片段、用户输入和输出格式要求组合成提示词,最终返回结构化数据。三个层级全部打通之后,才能继续往下谈AI题目生成和智能批阅。
2.2 模型接入的两种方式:API调用与本地部署怎么选
在检索“LLM怎么搭建”相关资料时,最常见的困惑是到底要部署什么模型。这个问题的答案在毕设语境里往往不是“从头预训练”,而是选一个可用的模型服务。学校提供模型网关就直接调用网关;没有网关就用主流厂商的API,开发速度快,提示词调整的反馈循环短。LangChain这类LLM框架虽然能把链式调用抽象得很好看,但封装层越厚越难定位问题,毕设阶段直接使用官方SDK更可控。
下面这段代码把模型调用统一封装成一个类,后续生成题目和批阅都走同一个入口。
from openai import OpenAI class LLMClient: def __init__(self, api_key: str, base_url: str, model: str): self.client = OpenAI(api_key=api_key, base_url=base_url) self.model = model def complete( self, prompt: str, system_prompt: str = "", temperature: float = 0.7, max_tokens: int = 1024, ) -> str: messages = [] if system_prompt: messages.append({"role": "system", "content": system_prompt}) messages.append({"role": "user", "content": prompt}) resp = self.client.chat.completions.create( model=self.model, messages=messages, temperature=temperature, max_tokens=max_tokens, ) return resp.choices[0].message.content代码里的temperature参数需要根据场景分别设置。生成题目时可以调到0.85左右,让题目表述更多样;智能批阅建议固定到0.1以下,保证评分稳定可复现。max_tokens也要按题型区分:单选题300到500足够,涉及解析和主观题批阅时需要给到1500以上。base_url字段用于切换不同的模型网关,切换模型时只改这一个配置即可。
2.3 垂域数据准备:课程资料如何变成LLM的知识上下文
模型接入只是第一步,真正决定出题质量的是知识库。常见做法是把这门课的教学大纲、教材章节和历年真题收集起来,去除页眉页脚和无关水印,按章节或知识点切分文本,再调用embedding接口把每个分块转成向量写入向量库。这一套流程就是论文里“基于RAG增强LLM”表述的落地依据,没有这一步,AI题目生成只能依赖模型自身记忆,很容易偏离课程范围。
分块大小直接决定检索效果。我一般控制在300到500字:太短会把一个知识点拦腰切断,太长则把多个知识点混在同一个文本块里,检索结果噪音大。切分时优先按段落边界,段落过长再按句号补切。每个分块都要带上知识点编码,这样组卷阶段可以按编码直接过滤题目,不需要每次都用余弦相似度做分类。
import os from openai import OpenAI from qdrant_client import QdrantClient def build_knowledge_base(docs, collection_name="course_knowledge"): embed_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) vector_db = QdrantClient(url="http://localhost:6333") points = [] for idx, doc in enumerate(docs): vec = embed_client.embeddings.create( model="text-embedding-3-small", input=doc["content"], ).data[0].embedding points.append({ "id": idx, "vector": vec, "payload": {"content": doc["content"], "point_code": doc["point_code"]}, }) vector_db.upsert(collection_name=collection_name, points=points)这里的docs来自前置清洗环节,每个元素至少包含content和point_code两个字段。content是分块后的文本内容,point_code对应知识点编码。向量数据库选型方面,Qdrant和Milvus都能在普通机器上跑;如果想把部署复杂度降到最低,用scikit-learn对全量分块做TF-IDF然后在内存里算余弦相似度,也能支撑几千道题的检索。数据量不需要追求大,几百个高质量分块配合好的提示词,效果远好过堆几万条未整理的文本。
提示:向量库如果因为环境问题装不上,退路是把embedding结果存成文件并在启动时加载到内存,不要让部署问题卡住整个项目进度。
3. AI题目生成与智能批阅:两条最常翻车的LLM链路
3.1 题目生成的第一步:用提示词约束模型输出JSON
AI题目生成看起来是生成式任务,工程上最难的却是“让模型按统一格式返回结果”。后端拿到结果后要入库,前端要渲染题干和选项,模型每次返回不同结构,整个下游代码都会变得脆弱。所以提示词里必须强制约定输出结构,并且给一个示例对象。output format只在提示词里写“请返回JSON”还不够,要给出完整的示例,模型才会稳定跟随。
下面的generate_questions函数接收知识点信息和题目数量,返回一个题目列表。知识点描述已经在上游从知识库对应分块拼接好了,模型拿到的是真实课程资料,而不是自己记忆里的通用知识。
import json def generate_questions(client: LLMClient, point_info: dict, count: int = 5): prompt = f""" 请围绕知识点「{point_info['name']}」生成{count}道{point_info['question_type']}, 难度要求:{point_info['difficulty']}。 参考资料: {point_info['reference']} 输出要求: 1. 只返回一个JSON数组; 2. 每个元素包含question、options、answer、analysis、points、difficulty字段; 3. difficulty只能取easy/medium/hard。 示例: [{{"question":"...","options":{{"A":"...","B":"...","C":"...","D":"..."}}, "answer":"A","analysis":"...","points":["知识点1"],"difficulty":"medium"}}] """ content = client.complete(prompt, temperature=0.85, max_tokens=2000) return json.loads(clean_json_content(content))clean_json_content是一个容易被忽略的工具函数。模型偶尔会在JSON前后加上“好的,这是生成的结果”这类解释文字,直接调用json.loads会抛异常。最稳妥的处理方式是提取第一个“[”到最后一个“]”之间的内容再解析。题目入库前还要做字段完整性校验,缺少options或analysis的记录下来并重新生成,不要在业务代码里尝试修补模型输出的脏数据。
3.2 生成质量兜底:规则校验与向量去重
生成内容的随机性决定了系统必须有一层规则校验。常规校验项可以做成一张配置表,每项规则对应一种处理策略:
| 校验项 | 规则 | 处理方式 |
|---|---|---|
| 字段完整性 | 必填字段不为空 | 丢弃并重新生成 |
| 选项数量 | 选择题必须有4个选项 | 丢弃并重新生成 |
| 答案合法性 | 答案必须存在于选项中 | 丢弃并重新生成 |
| 难度一致性 | 模型标注难度与题干文本匹配 | 重新生成 |
| 内容重复 | 与历史题目文本相似度低于0.85 | 丢弃并重新生成 |
向量去重这一步容易被偷懒成字符串匹配,但字符串匹配对模型生成的内容几乎无效,因为同一个知识点换一种措辞,字符层面完全看不出相似。正确做法是把新生成题目的题干也做一次embedding,和同一知识点下的历史题目算余弦相似度,超过0.85就丢弃。这套去重逻辑在后面的组卷阶段还会复用,提前实现能省掉不少重复工作。
3.3 智能批阅:按得分要点给分而不是凭感觉打分
客观题批阅没有太多空间,直接比对选项即可。难点全在主观题:如果只让模型看一遍参考答案和学生答案就打分,结果会很不稳定,同一个学生答案在不同请求里可能得到不同分数。直接原因就是没有给出可执行的评分依据。合理的做法是先让模型列出学生的答案命中了哪些得分点,再给出各部分分值。
批阅服务同样需要独立接口和结构化输出。实现时让模型按答案要点、过程完整度、结论正确性三个维度分别给分,再合成总分,最后输出一条简短评语。
def grade_subjective(client: LLMClient, question: dict, student_answer: str): prompt = f""" 题目:{question['question']} 参考答案:{question['answer']} 得分要点:{question['scoring_points']} 本题满分:{question['full_score']} 学生答案:{student_answer} 请先从答案要点、过程完整度、结论正确性三个维度分别给分, 再输出总分,最后给出80字以内的改进建议。 """ content = client.complete(prompt, temperature=0.1, max_tokens=800) return parse_grade_result(content)这段提示词里的scoring_points字段是容易被忽略但价值很高的设计。生成题目时让出题链路顺带产出这个字段,批阅阶段就能拿到结构化的评分依据。parse_grade_result负责把模型返回的JSON解析成评分结果,同样要处理模型在多输出解释文字的情况。批阅时有一个常见坑:模型给出的总分偶尔会超过full_score,解析函数里要做一个归一化,按满分缩放或直接截断,避免前端展示出超过满分的分数。
这一块的素材足够撑起论文里的实验章节:评分一致性评测、得分点抽取、结构化输出的稳定性,随便一个点都能展开写。4. 在线练习与一键组卷:把LLM能力串成完整闭环
4.1 在线练习闭环与练习记录表设计
在线练习不只是把题目渲染到页面上,真正的价值在于形成闭环。用户提交一道题,客观题直接返回对错和解析,主观题进入批阅服务,评分结果写入练习记录表。当同一个知识点下的错误率达到阈值,错题本自动积累并把这个知识点标记进用户的薄弱列表,下一次练习优先从薄弱知识点出题。
练习记录表的设计要考虑两个使用场景:判分后写入,以及按用户聚合错误率。题目表里答案和解析可以直接存成JSON字段,读取时整体返回,不需要二次关联,刷题场景读多写少,这样的冗余设计能省掉一次join。建表语句如下。
CREATE TABLE practice_records ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, knowledge_point VARCHAR(64), user_answer TEXT, score DECIMAL(5,2), full_score DECIMAL(5,2), is_correct TINYINT DEFAULT 0, wrong_reason VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_kp (user_id, knowledge_point) );wrong_reason字段只保存错误类型编码,比如“知识点错误”“计算错误”“审题错误”。不要往里写长文本原因,编码占空间小,也方便后续做统计分析。用户薄弱知识点可以做一个定时聚合任务,从这张表按知识点统计错误率。在线练习的最小闭环由此闭合:出题、答题、判分、记录、再出题。
4.2 一键组卷的贪心实现:总分、难度、知识点三重约束
一键组卷的本质是带约束的选题问题。常规约束有四个:总分固定、各题型数量固定、难度比例分布、知识点覆盖。很多团队第一版用纯随机抽样,跑完才发现难度分布完全不可控,再换成贪心选题。贪心算法不是全局最优,但对毕设场景足够实用,速度快、实现简单、选出的卷子质量可以接受。
贪心的核心思路是逐条满足约束。先把题库按知识点和难度分组,在每个知识点内遍历候选题目,只有同时满足剩余总分和难度配额才选入。所有已选题目记录到集合里,避免同一道题在卷内重复出现。
def build_paper(pool, config): selected = [] selected_ids = set() used_score = {d: 0 for d in config["difficulty_quota"]} remaining_score = config["total_score"] for point in config["points"]: candidates = [ q for q in pool if q["point_code"] == point and q["qid"] not in selected_ids and q["score"] <= remaining_score ] candidates.sort(key=lambda q: (q["difficulty_order"], -q["score"])) for q in candidates: quota = config["difficulty_quota"][q["difficulty"]] if used_score[q["difficulty"]] + q["score"] > quota: continue selected.append(q) selected_ids.add(q["qid"]) remaining_score -= q["score"] used_score[q["difficulty"]] += q["score"] if remaining_score <= 0: break if remaining_score <= 0: break if len(selected) < config["min_question_count"]: raise PaperBuildError("题目数量不足,请补充题库") return selecteddifficulty_order是难度排序权重,把easy、medium、hard映射成1、2、3,排序时能让同知识点内优先选难度匹配的题目。used_score按难度档记录当前已经选入的分数总和,quota则是该难度档的分数上限,例如easy最多30分。remaining_score判断剩余总分。min_question_count是兜底条件,如果某门课题库只有两三百道题,组卷大概率选不够,这个异常要返回“题库题量不足”的提示给前端,而不是输出半张卷子。
4.3 组卷后的二次校验:平衡性和重复度检查
贪心选出来的卷子可能在某一个知识点上集中了过多分值,必须做二次校验。平衡性检查统计各知识点的分值占比,如果某个知识点超过60%,就标记告警。重复度检查则计算卷内题目两两之间的向量相似度,防止模型生成的同义题互相撞车,这一步复用前面题目生成时的向量去重服务即可,不需要新增组件。
平衡性检查不通过时,可以打回重排,也可以只做标记提醒出卷人。实操建议选择后者:全自动完美组卷在毕设时间范围内很难做好,把校验结果透明地展示给用户,比偷偷替换题目更可靠,也更能体现系统的工程完整度。做到这一步,写题、判题、练习、组卷四条链路已经闭环,接下来要解决的是怎么让评委看到这些工作。
5. 论文与演示视频的素材组织:让项目成果能被看见
5.1 开题报告里最值得写的技术路线
开题报告不需要写编码细节,但要写清楚三件事:用什么模型、系统分几层、数据从哪里来。技术路线可以用一条流水线描述,从知识库构建、题目生成服务、批阅服务到组卷引擎。最好在这个阶段就做一个最小实验,比如让模型生成50道题再人工评估与课程大纲的契合度,把评估结果写进开题报告,后续论文的对比实验就有了基线。
5.2 论文创新点的三种写法:生成稳定性优先于功能数量
创新点不建议写“实现了在线刷题功能”,那是需求描述。三个方向可以参考。第一个是提示词工程化,把对抗生成幻觉的结构化输出和规则校验写成设计章节。第二个是RAG数据准备对生成效果的影响,对比有知识库和无知识库时题目的偏离程度,用表格展示差异。第三个是批阅一致性方案,写清楚temperature降低和评分要点前置对分数稳定性的提升。三个方向选一个深入展开,比罗列功能更能打动评审。
5.3 演示视频的十分钟脚本:把四条链路讲成连续故事
演示视频一般控制在十分钟,脚本按时间轴切分会更清楚。开场两分钟讲架构图和数据流向,中间六分钟按“AI题目生成到智能批阅、在线练习到一键组卷”的顺序操作,最后两分钟完整走一遍“用户刷题到错题自动进入下一轮练习”的体验。每个功能演示前先说一句“我现在要做什么”,再操作,哪怕评委没看懂代码,也能从操作逻辑里感受到系统的完成度。
| 时间段 | 演示内容 | 要展示的关键点 |
|---|---|---|
| 0-2分钟 | 系统架构与数据流 | 分层设计、RAG数据准备 |
| 2-4分钟 | AI题目生成 | 输入知识点,返回结构化题目 |
| 4-6分钟 | 智能批阅 | 得分要点明细和评语 |
| 6-8分钟 | 在线练习与错题闭环 | 错误知识点被记录并再次出题 |
| 8-10分钟 | 一键组卷 | 总分、难度、知识点约束展示 |
录制演示视频前,先把常见批阅结果和整卷缓存到本地,避免现场等待模型输出时出现网络超时。这是最容易被忽视的细节,却是答辩现场最重要的保底动作。
本文还有配套的精品资源,点击获取