
做了几年Java后端接手过的教育类项目不算少。去年团队接了个挺有意思的需求给在线学习平台做一个“智能学习辅导”模块项目名就叫“springboot基于机器学习的智能学习辅导系统开发”。这个模块的定位很直白——学生刷题之后系统不能只告诉他答对没有还得能分析出他哪里薄弱、下一题该刷什么、期末考试大概能考多少分最好还能自动生成一条个性化的学习路径。说白了就是把老师平时做的那套学情分析用代码跑出来。这个项目做完我的一个强烈感受是机器学习在教育系统里落地难点根本不在算法而在怎么把业务数据和算法模型对接起来让SpringBoot后端真正把算法跑成业务价值。这篇文我就围绕这个项目把从需求拆解、算法选型、特征工程到SpringBoot后端集成、部署排错的完整过程都捋一遍。里面包含大量我实际踩过的坑和验证过的做法适合准备做类似毕设或真实项目的同学参考也适合想了解机器学习如何嵌进Web系统的Java开发。1. 项目整体设计先把“智能”落在哪个功能上1.1 核心需求拆解学情画像、智能推荐、成绩预测、路径规划很多同学一听到“机器学习”就想着上深度学习、神经网络其实在教学辅导这个场景里我们真正要解决的是四个非常具体的问题。第一个是学情画像。学生刷了100道题系统要知道他对“二次函数”“三角函数”“数列”这些知识点分别掌握到什么程度。这本质上是把学生的答题行为转化成一组可量化的特征比如正确率、平均耗时、知识点覆盖率、题目难度匹配度。有了画像后面所有推荐和预测才有了数据支撑。第二个是智能题目推荐。每个学生的薄弱点不一样按统一顺序刷题就是浪费效率。系统要能根据学生的历史答题数据预测他做某道题的正确概率然后优先推那些“跳一跳够得着”的题目——太简单了没提升太难了打击信心。第三个是成绩预测。通过学生过去N次测验成绩和日常刷题数据预测下次考试可能考多少分。这个功能在教育系统里特别有说服力因为它直接触达了学生和家长最关心的东西。实际落地时用的不是多高深的算法线性回归就够了关键是特征要选对。第四个是学习路径规划。把知识图谱中的前置依赖关系比如先学“函数”再学“导数”和学生的薄弱点结合起来生成一条个性化的学习顺序。这块最考验业务理解算法只是辅助。这四个功能不是孤立的它们是递进关系画像是一切的基础推荐依赖画像预测可以反过来验证画像准不准路径规划则是画像和知识图谱的综合应用。做技术方案时如果把这一步想清楚后面开发的思路就会清晰很多。1.2 技术选型与架构为什么是SpringBoot而不是其他组合技术选型上我们几乎没有太多纠结直接定了SpringBoot 2.7.18 MyBatis-Plus MySQL 8 Redis MinIO Vue3前后端分离。理由有三点。首先是SpringBoot的生态成熟度和团队熟悉度。做这类系统团队成员最熟的技术栈就是Java SpringBoot招聘和协作成本最低。SpringBoot的自动装配机制让配置化程度很高一个内嵌Tomcat的jar包就能跑起来不像早期SSH那样要配一堆XML。开发效率在同类框架里属于第一梯队。第二是数据量级决定了技术复杂度。这个系统服务的用户是几千名在校学生题库量3万左右日均答题记录20万条。这个量级下KNN、协同过滤这类算法用Java在单机内存里就能算完根本不需要上Hadoop、Spark那套大数据体系也不需要在算法服务化和Java之间引入额外的微服务。用SpringBoot一个进程包打天下最省事也最好维护。第三是前后端分离的体验优势。Vue3 Element Plus负责交互层SpringBoot只提供RESTful API。学生端的学情雷达图、推荐列表、成绩趋势曲线用vue-echarts这类库能做出比较好看的效果。前后端分离也方便日后再做教师端、家长端API复用就行。整体架构就是标准的单体应用浏览器请求到Nginx转发给SpringBoot应用业务数据放MySQL推荐结果和热数据缓存到Redis上传的讲义PDF、题目图片统一走MinIO机器学习部分以本地算法模块的形式嵌在Service层里不单独拆服务。这套架构撑到万级用户基本没问题。2. 机器学习部分的设计与实现算法选型与特征工程2.1 算法选型KNN、协同过滤、线性回归各管一段机器学习算法有很多但落到智能辅导系统里我用的是三个非常经典的算法K近邻KNN、协同过滤Collaborative Filtering和线性回归Linear Regression。每个算法解决一类问题选型逻辑并不复杂。先说KNN。很多学过机器学习的人应该都见过那个电影分类的例子把电影按“打斗镜头数”和“接吻镜头数”两个特征画在坐标系里新电影《唐人街探案》的类别未知就计算它和已知样本的距离找最近的K个邻居投票决定它属于“动作片”还是“爱情片”。这个思路放到学生身上一模一样把一个学生当成一个样本把他的知识点掌握度向量当成特征要判断“这个学生和哪类学生最像”就计算他和所有历史学生的相似度取前K个看这些“学习伙伴”都集中在什么能力区间、在刷什么题。KNN不需要训练过程实现简单、解释性强非常适合做学情分类和相似学生查找。再说协同过滤。推荐系统里最经典的流派分基于用户的UserCF和基于物品的ItemCF两种。UserCF的思路是“和你相似的学生做对了的题你也大概率能做对”ItemCF的思路是“和你做过题目相似的题你也可能会做”。在教学场景下我建议以UserCF为主因为学生的能力水平变化会直接体现在相似学生集合的变化上推荐结果更贴近当前状态。线性回归则用来做成绩预测。输入特征是学生最近几次考试成绩、平均正确率、日均刷题量等连续变量输出是预测成绩。这个算法虽然简单但胜在可解释性强——我可以直接跟老师说系统预测这个学生下次考85分主要依据是他最近刷题正确率提升了、薄弱知识点覆盖率在下降。老师听着直观学生也服气。这三个算法的特点可以汇总成一张表算法解决的问题输入特征输出实现复杂度KNN相似学生查找、学情分类知识点掌握度向量相似学生列表/分类标签低UserCF智能题目推荐学生-题目交互矩阵TopN推荐题目列表中线性回归成绩预测、预警历史成绩学习行为指标预测分数低选算法有个原则先从最简单的算法跑通流程再考虑复杂度。很多教学场景业务数据没那么“复杂”经典算法足够用而且出了问题容易排查。深度学习那种黑盒模型暂不考虑解释性太差教育领域对解释性是有要求的。2.2 数据准备与特征工程答题记录怎么变成特征向量算法选好之后最费功夫的其实是特征工程。这也是很多把所有精力放在调算法上的同学最容易忽略的地方。机器学习界有句话叫“Garbage in garbage out”特征没做好再先进的算法也白搭。我们先梳理系统里有哪些原始数据。一张答题记录表answer_record字段包括学生ID、题目ID、对错标记、作答耗时、作答时间一张题目表question_bank字段包括知识点ID、题目难度、题型一张知识点表knowledge_point字段包括知识点名称、学科、在知识图谱中的层级。数据分析的第一步就是把这些原始数据变成算法能吃的“特征向量”。以学情画像为例我们对每个知识点计算三类指标。第一类是正确率就是该知识点下答对次数除以总作答次数取值范围0到1。第二类是平均作答耗时反映熟练程度需要先归一化处理因为不同难度的题本身耗时基准不同。第三类是刷题量占比就是该知识点的刷题量占学生总刷题量的比例防止学生主动避开薄弱点刷题。这样一来每个学生在每个知识点上就得到三个数值。如果有20个知识点特征向量就是60维。这个维度在KNN里完全可以接受。构建向量的代码如下public StudentFeature buildFeature(Long studentId) { ListKpStats statsList answerRecordMapper.selectKpStatsByStudentId(studentId); double[] feature new double[statsList.size() * 3]; int idx 0; for (KpStats stats : statsList) { feature[idx] stats.getAccuracy(); // 正确率 feature[idx] normalizeTime(stats.getAvgDuration()); // 归一化耗时 feature[idx] stats.getQuestionRatio(); // 刷题量占比 } return new StudentFeature(studentId, feature); }特征处理好之后归一化是必须做的一步。正确率本身就是0到1但作答耗时的量纲可能是几十秒如果不归一化KNN算欧氏距离时耗时维度会主导整个距离计算正确率反而没什么影响。我用的归一化方式是极差归一化把每个维度的值映射到0到1之间public double normalize(double value, double min, double max) { if (max - min 0) return 0.5; return (value - min) / (max - min); }这里有个我踩过的坑归一化的min和max必须用全量训练集的统计值而不是单条数据的当前值。如果你用当前值实时归一化那算法看到的每个学生“最慢”都是1、“最快”都是0学生之间的相对差异就完全消失了。2.3 模型训练与SpringBoot集成直接内嵌还是单独拆服务搞定了特征下一个核心问题就是模型怎么和SpringBoot项目集成在一起我试验过两种路线。第一种路线是“Python训练 Java调用”。用Python的scikit-learn训练好模型保存成pickle或pmml文件Java端加载文件做预测。这种方案的好处是能用到Python机器学习生态里丰富的算法库。但你得解决跨语言调用的问题要么用PyEcore这类库做进程级调用要么把模型文件解析成Java能读的格式。模型一多链路就长排查问题特别费劲。第二种路线是“纯Java实现算法”把KNN、UserCF、线性回归直接写成Java工具类嵌在SpringBoot的Service层里。我当时评估了一下这个系统的KNN计算就是算欧氏距离排序取TopKUserCF的核心就是构建用户-题目矩阵计算余弦相似度排序线性回归则是最小二乘法求解代码量都不大每个算法200行Java代码以内就能搞定。而且在几万用户的数据规模下计算延迟完全可控。于是果断选了这条路线。KNN的实现其实很简单核心就是距离计算加排序public ListStudentSimilarity findSimilarStudents(Long studentId, int k) { StudentFeature target featureService.getFeature(studentId); ListStudentFeature allFeatures featureService.getAllFeatures(); ListStudentSimilarity similarities new ArrayList(); for (StudentFeature feature : allFeatures) { if (feature.getStudentId().equals(studentId)) { continue; } double distance euclideanDistance(target.getVector(), feature.getVector()); similarities.add(new StudentSimilarity(feature.getStudentId(), distance)); } similarities.sort(Comparator.comparingDouble(StudentSimilarity::getDistance)); return similarities.subList(0, Math.min(k, similarities.size())); }线性回归的模型参数权重向量w和偏置b也是用Java写的普通最小二乘法求解训练完成后把参数存到Redis里下次预测直接加载参数做矩阵乘法public double predictScore(StudentBehavior behavior) { double[] weights modelCache.getWeights(); double score modelCache.getBias(); for (int i 0; i weights.length; i) { score weights[i] * behavior.getFeature(i); } return Math.min(100.0, Math.max(0.0, score)); }关于模型参数存储我想多说一句。直接用Java训练好的模型参数我建议序列化后存到Redis里key可以设计成model:linear:score:v1。这样有两个好处一是模型更新时只需要覆盖Redis里的键不用改代码重新发版二是多实例部署时所有节点读同一份参数不会出现各节点模型不一致的情况。也可以用文件方式存在服务器本地但多实例部署时文件同步麻烦Redis始终是我首选的方案。3. SpringBoot后端核心功能实现与实操细节3.1 数据库设计与ORM层分页插件和关联查询那些事数据库设计是整个系统的基础。核心表我设计成这几张student_user表存学生基本信息knowledge_point表存知识点question_bank表存题目answer_record表存学生答题记录student_profile表存学情画像的中间结果learning_path表存推荐的学习路径。其中answer_record是数据量增长最快的表建议按时间字段做分区或者至少建好联合索引(student_id, question_id, answer_time)否则推荐环节查历史记录时SQL会非常慢。ORM这层我用的是MyBatis-Plus看名字就知道它不是MyBatis的原生版而是增强版。最常用的几个能力是BaseMapper里的selectById、selectList、insert所有单表CRUD都免写XML。分页需要单独配置内置分页插件PaginationInnerInterceptor配置方法如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }加了这个拦截器分页查询只需要在Mapper接口方法里传入Page对象MyBatis-Plus会自动生成count查询和limit语句PageQuestionBank page questionBankMapper.selectPage( new Page(current, size), new LambdaQueryWrapperQuestionBank() .eq(QuestionBank::getKnowledgePointId, kpId) .eq(QuestionBank::getType, objective) );不配这个拦截器会怎样分页直接变成查全表再内存分页数据量一大就OOM别问我怎么知道的。第一次跑测试的时候5000条数据内存分页还没事跑到2万条就明显卡顿。配置插件一分钟就能解决但忘了配置排查起来新手可能折腾一晚上。3.2 学情画像与学习路径推荐把特征变成“看得懂的结论”学情画像的生成逻辑我在设计时把它分成了三个环节。第一个环节是算能力维度。把当前学科的知识点按照教学大纲分成若干个能力域例如“函数与导数”“几何与图形”“概率与统计”“代数与方程”等。把学生在这几个能力域下的正确率、耗时、刷题量聚合成三个维度上的平均指标。这个聚合逻辑用MyBatis-Plus写一个稍微复杂一点的聚合SQL就能完成SELECT kp.domain AS domain, AVG(CASE WHEN ar.is_correct 1 THEN 1.0 ELSE 0.0 END) AS accuracy, AVG(ar.duration) AS avg_duration, COUNT(*) AS total_count FROM answer_record ar JOIN question_bank qb ON ar.question_id qb.id JOIN knowledge_point kp ON qb.knowledge_point_id kp.id WHERE ar.student_id #{studentId} GROUP BY kp.domain第二个环节是算掌握度标签。根据正确率和刷题量把每个知识点或能力域映射成“已掌握”“巩固中”“薄弱”“未学习”四个标签。阈值不能拍脑袋定我们当时是用所有学生的正确率分布做了分位数划分后25%算薄弱前25%算已掌握中间是巩固中。这个做法比固定阈值更合理因为每个知识点本身的难度不同。第三个环节是生成画像报告。把特征向量、标签和最近的变化趋势汇总成JSON返回给前端渲染成雷达图、柱状图和概率曲线。前端用vue-echarts的雷达图展示六维能力时呈现出来的效果非常直观学生看到自己“函数”那块明显凹下去自然就有针对性刷题的动力了。学习路径推荐的逻辑稍微复杂一点。它需要结合知识点的前置依赖关系生成一条“该学什么、再学什么”的路径。我们的做法是先取出学生所有薄弱知识点再查知识图谱得到它们之间的依赖关系按“前置知识点必须先于后置知识点”的原则给薄弱点排序每个知识节点内部再按从易到难的顺序推荐题目。这个路径不是一次生成就完事每次画像更新后要重新计算。3.3 题目推荐与成绩预测UserCF和线性回归的工程化实现题目推荐的工程实现里最核心的是UserCF的计算流程。用户-题目交互矩阵我用一个MapLong, Set 来表示key是学生IDvalue是该学生做对过的题目ID集合。计算推荐时先捞目标学生的相似学生TopK再从这些相似学生做对过的题目里去掉目标学生自己也做对过的剩下的候选题目按“被多少个相似学生做对”排序取前N个作为推荐结果。这个逻辑看起来不复杂实际写的时候有几个性能细节要注意。第一是相似学生的K值我建议取10到20之间。取大了会把不相关的学生也拉进来推荐结果泛化得失去个性取小了推荐结果容易被个别学生带偏。第二是候选题目的过滤条件除了去掉做对的题还要去掉做过但做错的题因为做错的题已经暴露了薄弱点再推一次意义不大应该放在下一个知识点的题目里。第三是要按难度和知识点做分流不能一股脑全推同一个知识点的题要遵循“二八原则”80%的题聚焦当前薄弱知识点20%用于复习巩固之前学过的内容。成绩预测这块我用的特征向量设计成五维最近一次考试成绩、最近三次考试的平均成绩、近7天刷题正确率、近7天日均刷题量、薄弱知识点覆盖率越低越好。这套特征在实际验证中拟合效果不错预测值和真实值的平均绝对误差控制在6分以内。训练数据从student_user表、exam_score表、answer_record表聚合而来用历史两个学期的数据做训练集当前学期数据做验证集。这个预测结果我把它用在了“成绩预警”功能上。系统每周跑一次预测如果预测成绩低于设定的及格线就在家长端和教师端生成一条预警记录。但这个功能一上线就出现了争议有的家长看到预警直接焦虑了。后来我们调整了策略预警消息从“你的孩子预计不及格”改成“孩子最近在XX知识点存在明显薄弱情况建议加强练习”同样的预测数据换个表达方式接受度完全不一样。这也是我在这个项目里学到的产品化经验技术给出的结论表达方式同样重要。3.4 文件存储、全局过滤器与系统安全容易被忽视却必须处理的事智能辅导系统里有大量非结构化数据要处理。题目可以带图片讲义是PDF用户有头像。这些文件不能塞进MySQL我用的是MinIO来做对象存储。MinIO的接入成本非常低本质上就是兼容S3协议的对象存储服务Docker一条命令就能起一个实例。SpringBoot端只需要往容器里注册一个MinioClient的BeanConfiguration public class MinioConfig { Bean public MinioClient minioClient(MinioProperties properties) { return MinioClient.builder() .endpoint(properties.getEndpoint()) .credentials(properties.getAccessKey(), properties.getSecretKey()) .build(); } }文件上传接口控制在5MB以内PDF压缩后再上传MinIO的bucket按业务类型分开questions存题目图片materials存讲义avatars存头像。上传成功后返回文件路径路径存到业务表对应字段里。整个流程很常规但做了之后系统才像一个真正的生产级项目不依赖本地磁盘扩容方便文件也算有备份。系统安全这块我处理了两个问题。第一个是XSS攻击用户通过富文本编辑器提交的题解、笔记里可能藏了script标签。解决方案是加了一个全局过滤器对请求体里的文本内容做HTML转义和非法标签过滤。这里有个容易踩的坑让我印象深刻一开始我用的过滤器是拦截所有请求统一处理字符串内容结果上传PDF文件时过滤器试图把PDF当文本过滤直接把文件内容干坏了上传的文件全部打不开。原因很简单XSS过滤只应该处理文本Content-Type的请求比如application/json、text/plain、application/x-www-form-urlencoded遇到multipart/form-data或者application/pdf这类二进制类型必须直接放行。修改判断逻辑后问题解决Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { ContentCachingRequestWrapper wrapper new ContentCachingRequestWrapper((HttpServletRequest) request); String contentType wrapper.getContentType(); if (contentType ! null (contentType.contains(json) || contentType.contains(text) || contentType.contains(urlencoded))) { // 只有文本类型才做XSS过滤 chain.doFilter(new XssContentWrapper(wrapper), response); } else { chain.doFilter(wrapper, response); } }第二个是跨域问题。SpringBoot后端默认只允许同源访问Vue前端跑在8080端口后端跑在8081端口浏览器直接拒绝跨域请求。解决方式是用CorsFilter注册一个全局CORS配置。这个配置不复杂但非常容易漏没有它前端联调阶段第一个接口调不通。别问我怎么知道的队友当时对着浏览器控制台的“Access-Control-Allow-Origin”报错盯了半小时。4. 常见问题与排查技巧实录4.1 数据与模型层面的坑冷启动、数据稀疏、特征更新第一个绕不开的问题是冷启动。新注册的学生没有任何答题记录特征向量全为零KNN找相似学生时算出来的距离对所有人都一样推荐结果就变成随机了。我的解法是“冷启动阶段不走个性化推荐走热门兜底”新用户前30道题左右推送的是全站范围内正确率适中、平均耗时合理的高人气题目先积累数据再切到个性化推荐。这个策略在用户体验上几乎没有感知。第二个问题是数据稀疏。KNN和UserCF都依赖用户-物品交互矩阵而实际系统里一个学生可能只做了一两百道题题库有几万道题交互矩阵的稀疏度可能超过95%。矩阵太稀疏会导致相似度计算失真。我做了两个缓解措施一是先用ItemCF找相似题给交互记录扩充样本相当于数据增强二是画像特征里至少保留“平均正确率”“平均耗时”这种全局统计特征即使单个知识点的数据不够全局特征也能保证基础区分度。第三个问题是特征更新的时效性。推荐结果如果只依赖历史数据跟不上学生能力的变化。学生最近几天疯狂练习薄弱知识点正确率明显提升画像如果不及时更新推荐的题还是老一套。我的做法是画像计算做成异步任务学生每次答题后通过消息队列触发一次该学生的缓存清理下次请求画像时重新计算每日凌晨再用批处理任务全量重算一遍兼顾实时性和数据一致性。4.2 框架与配置层面的坑SpringBoot版本、分页插件和首次构建SpringBoot版本这个问题看起来不起眼实际能坑到不少人。我们项目组有新同事之前照着SpringBoot 3.x的教程写代码单元测试跑起来直接报错查下来发现SpringBoot 3.x强制要求JDK 17而且很多第三方库的starter还没有适配新版。生产环境是JDK 8只能用SpringBoot 2.7.x系列。如果你打算拿这个系统做毕设或练手我建议用2.7.18这个版本它在2.x里算是长期维护的稳定版本兼容性最好网上踩坑参考也最多。分页插件要特别提醒一句PaginationInnerInterceptor必须在MybatisPlusInterceptor这个主拦截器里注册顺序还不能乱。如果还有别的拦截器要一起加分页拦截器要加在链路靠后的位置否则分页SQL可能生成失败查询出来还是全量数据。首次构建SpringBoot项目时还有一个特别容易出问题的点Maven依赖下载速度。国内不配置镜像源的话拉依赖能卡到怀疑人生。IDEA新建项目时默认用的是中央仓库建议新建后在settings.xml里配置阿里云镜像maven的central切换到国内节点首建时间从十几分钟压缩到一两分钟。4.3 部署与线上排错的经验Docker部署和jar反编译实战部署我们用的Docker。SpringBoot项目打出来的jar包Dockerfile写个多阶段构建先用Maven镜像编译再把jar拷到OpenJDK镜像里跑FROM maven:3.8-jdk8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jre-slim WORKDIR /app COPY --frombuilder /app/target/study-assistant.jar app.jar EXPOSE 8080 ENV JAVA_OPTS-Xms512m -Xmx1024m -Duser.timezoneAsia/Shanghai ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]这里JAVA_OPTS的堆内存设置值得多说几句。容器默认是宿主机有多少内存就给JVM分配多少如果一台服务器上跑了MySQL、Redis、MinIO再加这个应用Java默认堆分配很有可能把整台机搞到内存不足。512M到1024M的堆在几千学生用户的量级下完全够用别一上来就给4G。线上排错有个很经典的场景生产环境某个推荐结果异常但代码包是哪个版本、功能是否完整本地和线上经常对不上。这时候就需要把线上的jar包反编译成可读的源码快速定位版本差异。工具我推荐JD-GUI把jar包拖进去class文件就变成可读的Java代码。不过JD-GUI对新特性的支持一般遇到lambda表达式反编译出来逻辑有点绕就用IDEA自带的Fernflower反编译插件两者结合基本能解决大部分排查需求。还有一个我遇到的真实问题Redis里缓存的推荐结果过期后接口偶发性地慢到两三秒。排查下来发现是缓存击穿推荐结果过期瞬间大量请求同时回源计算。解决方案是加分布式锁做缓存重建只有一个请求能去算推荐其他请求先返回上一次的缓存副本可以是略过期的数据避免全部打穿到计算层。用Redisson的RLock实现起来就是几行代码RLock lock redissonClient.getLock(recommend:lock: studentId); try { if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { ListQuestion questions recommender.recommend(studentId); redisCache.set(cacheKey, questions, 30, TimeUnit.MINUTES); return questions; } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } return redisCache.getFromOutdated(cacheKey);这类问题不复杂但要么想不起来加锁要么加了锁一跑就死锁都要在压力测试阶段提前验证。5. 最后再聊几句项目心得这个项目做下来我最深的体会是智能学习辅导系统里的“智能”靠的是清晰业务目标加合适算法加工程化落地而不是堆砌高深的模型。KNN、UserCF、线性回归这些在机器学习教科书里属于入门级的内容但把它们放到真实的SpringBoot业务系统里把数据流转、缓存策略、接口设计、异常边界都处理到位就是一个非常完整、非常有说服力的毕业设计或实战项目。如果让我给准备做类似系统的人一个建议那就是先跑通一个最小的闭环一个学生进来系统根据他的答题记录推荐下一道题推荐完再记录新数据再更新画像。这个闭环跑通之后再加成绩预测、学习路径、教师端分析都是水到渠成的事。千万别一上来就规划十个模块最后每个模块都只做了个空壳。还有人纠结要不要引入深度学习模型。以我个人的经验现阶段教学场景的数据量还撑不起深度学习而且学业建议这种场景很依赖逻辑可解释性。如果你后续确实想往更深的方向扩展可以研究一下知识追踪模型KT系列比如DKT深度知识追踪但那是“进阶玩法”先把本文这套经典算法的流程跑扎实再考虑升级思路会清晰得多。