教育AI应用开发:个性化学习系统的Java实现
教育AI应用开发:个性化学习系统的Java实现
开篇故事:课程完成率从23%到61%
李明在一家在线教育平台做Java后端,2023年公司核心数据让CEO很头疼:付费用户的课程完成率只有23%。
100个人付了钱,最后只有23个人学完。这意味着:77%的用户在中途放弃了,既没有获得知识,公司也没有得到复购和口碑。
CEO给了李明3个月时间,预算100万,要求把这个数字提升到40%。
李明做了一件事:引入AI个性化学习系统。
3个月后,完成率达到了61%。老板以为他说错了,让他重复了两遍。
李明做了什么?简单说就是三件事:
- 不让用户按固定顺序学:AI根据用户的基础和学习速度,动态调整学习路径
- 卡关了立刻帮助:用户在某道题做错超过2次,AI立刻推送相关解析视频
- 给每个人写个性化报告:每周一封"你上周学了什么、哪里还有薄弱点"的邮件
这三件事翻译成技术,就是:自适应题目推荐 + 智能答疑系统 + 学情报告生成。
这篇文章把这三件事的Java实现全部拆开给你看。
一、教育AI的核心场景架构
二、学习者画像:基于行为数据构建能力模型
2.1 数据库设计
-- 知识点定义
CREATE TABLE knowledge_point (
kp_id VARCHAR(36) PRIMARY KEY,
subject VARCHAR(50) NOT NULL COMMENT '科目',
kp_name VARCHAR(100) NOT NULL COMMENT '知识点名称',
difficulty TINYINT NOT NULL COMMENT '难度 1-5',
parent_kp_id VARCHAR(36) COMMENT '父知识点',
prerequisite_ids JSON COMMENT '前置知识点ID列表',
INDEX idx_subject (subject)
) COMMENT='知识点体系';
-- 学生知识点掌握度
CREATE TABLE student_knowledge_mastery (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
student_id VARCHAR(36) NOT NULL,
kp_id VARCHAR(36) NOT NULL,
mastery_score DECIMAL(5,4) NOT NULL DEFAULT 0.0 COMMENT '掌握度 0-1',
practice_count INT NOT NULL DEFAULT 0 COMMENT '练习次数',
correct_count INT NOT NULL DEFAULT 0 COMMENT '答对次数',
last_practiced DATETIME,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_student_kp (student_id, kp_id),
INDEX idx_student (student_id),
INDEX idx_mastery (mastery_score)
) COMMENT='学生知识点掌握度';
-- 学习行为日志
CREATE TABLE learning_behavior_log (
log_id BIGINT AUTO_INCREMENT PRIMARY KEY,
student_id VARCHAR(36) NOT NULL,
behavior_type VARCHAR(30) NOT NULL COMMENT 'ANSWER/VIDEO/SEEK_HELP',
content_id VARCHAR(36) NOT NULL COMMENT '题目ID/视频ID',
kp_ids JSON COMMENT '涉及的知识点ID列表',
result VARCHAR(20) COMMENT 'CORRECT/WRONG/PARTIAL',
time_spent_sec INT COMMENT '花费时间(秒)',
created_at DATETIME NOT NULL,
INDEX idx_student_time (student_id, created_at)
) COMMENT='学习行为日志';2.2 掌握度更新算法(艾宾浩斯遗忘曲线)
package com.edu.ai.mastery;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.Duration;
@Slf4j
@Service
@RequiredArgsConstructor
public class MasteryUpdateService {
private final StudentKnowledgeMasteryRepository masteryRepository;
/**
* 更新知识点掌握度
* 基于间隔重复算法(类SM-2)
*
* @param studentId 学生ID
* @param kpId 知识点ID
* @param isCorrect 是否答对
* @param timeSpentSec 答题用时(秒)
*/
public void updateMastery(
String studentId,
String kpId,
boolean isCorrect,
int timeSpentSec,
int questionDifficulty) {
StudentKnowledgeMastery mastery = masteryRepository
.findByStudentIdAndKpId(studentId, kpId)
.orElse(StudentKnowledgeMastery.newMastery(studentId, kpId));
double oldScore = mastery.getMasteryScore();
double newScore;
if (isCorrect) {
// 答对:掌握度提升
// 考虑时间因素:答得快说明掌握得好
double speedBonus = calculateSpeedBonus(timeSpentSec, questionDifficulty);
double increment = 0.1 * (1 + speedBonus);
newScore = Math.min(oldScore + increment, 1.0);
} else {
// 答错:掌握度下降(但不会降到0以下)
// 下降幅度考虑当前掌握度:掌握度越高说明这次失误越意外,下降越多
double decrement = 0.05 + oldScore * 0.1;
newScore = Math.max(oldScore - decrement, 0.05);
}
// 遗忘衰减:如果长时间没练习,掌握度自然下降
if (mastery.getLastPracticed() != null) {
long daysSince = Duration.between(
mastery.getLastPracticed(), LocalDateTime.now()).toDays();
double forgetDecay = calculateForgetDecay(daysSince, oldScore);
newScore = Math.max(newScore - forgetDecay, 0.05);
}
mastery.setMasteryScore(newScore);
mastery.setPracticeCount(mastery.getPracticeCount() + 1);
if (isCorrect) mastery.setCorrectCount(mastery.getCorrectCount() + 1);
mastery.setLastPracticed(LocalDateTime.now());
mastery.setUpdatedAt(LocalDateTime.now());
masteryRepository.save(mastery);
log.debug("掌握度更新 学生:{} 知识点:{} {} → {}", studentId, kpId,
String.format("%.2f", oldScore), String.format("%.2f", newScore));
}
/**
* 速度奖励:参考题目难度计算答题时间是否快于平均水平
*/
private double calculateSpeedBonus(int timeSpentSec, int difficulty) {
int expectedSeconds = difficulty * 60; // 每难度等级预期60秒
if (timeSpentSec <= 0) return 0;
double ratio = (double) expectedSeconds / timeSpentSec;
// 速度是平均值的2倍→奖励0.5,1倍→奖励0,0.5倍→扣分
return Math.max(-0.5, Math.min(0.5, (ratio - 1) * 0.5));
}
/**
* 遗忘衰减:艾宾浩斯遗忘曲线的简化实现
*/
private double calculateForgetDecay(long daysSince, double currentMastery) {
if (daysSince <= 1) return 0;
// 遗忘速度:掌握度低的知识点遗忘更快
double retentionRate = Math.exp(-0.05 * daysSince * (1.0 - currentMastery * 0.5));
return currentMastery * (1 - retentionRate);
}
/**
* 获取学生的全科目薄弱点分析
*/
public List<WeakPoint> analyzeWeakPoints(String studentId, String subject) {
return masteryRepository.findByStudentIdAndSubject(studentId, subject)
.stream()
.filter(m -> m.getMasteryScore() < 0.6 && m.getPracticeCount() >= 3)
.sorted(Comparator.comparingDouble(StudentKnowledgeMastery::getMasteryScore))
.limit(10)
.map(m -> WeakPoint.builder()
.kpId(m.getKpId())
.masteryScore(m.getMasteryScore())
.practiceCount(m.getPracticeCount())
.correctRate((double) m.getCorrectCount() / m.getPracticeCount())
.build())
.collect(java.util.stream.Collectors.toList());
}
}三、自适应题目推荐:根据掌握程度推荐适合难度
3.1 推荐策略
自适应推荐的核心逻辑:
掌握度 < 0.3 → 推送难度1-2的基础题(打基础)
掌握度 0.3-0.6 → 推送难度2-3的练习题(巩固)
掌握度 0.6-0.8 → 推送难度3-4的进阶题(提高)
掌握度 > 0.8 → 推送难度4-5的挑战题 或 推送相关进阶知识点3.2 推荐引擎
package com.edu.ai.recommendation;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class AdaptiveQuestionRecommender {
private final MasteryUpdateService masteryService;
private final QuestionRepository questionRepository;
private final StudentAnswerHistoryRepository answerHistoryRepo;
/**
* 获取下一题推荐
*
* @param studentId 学生ID
* @param subject 科目
* @param targetKpId 目标知识点(为空则自动选择)
*/
public QuestionRecommendation getNextQuestion(
String studentId,
String subject,
String targetKpId) {
// 1. 确定本次练习目标知识点
String kpId = targetKpId != null ? targetKpId
: selectTargetKnowledgePoint(studentId, subject);
// 2. 获取该知识点的掌握度
double mastery = masteryService.getMasteryScore(studentId, kpId);
// 3. 根据掌握度确定题目难度范围
int[] difficultyRange = getDifficultyRange(mastery);
// 4. 排除已做过的题目
Set<String> answeredIds = answerHistoryRepo
.findAnsweredQuestionIds(studentId, kpId);
// 5. 查找合适的题目
List<Question> candidates = questionRepository
.findByKpIdAndDifficultyBetween(
kpId,
difficultyRange[0],
difficultyRange[1]
)
.stream()
.filter(q -> !answeredIds.contains(q.getId()))
.collect(Collectors.toList());
// 6. 如果所有题目都做过了,随机选一道
if (candidates.isEmpty()) {
candidates = questionRepository
.findByKpIdAndDifficultyBetween(kpId, difficultyRange[0], difficultyRange[1]);
}
if (candidates.isEmpty()) {
return QuestionRecommendation.noQuestion(kpId);
}
// 7. 从候选中随机选一道(避免总是推同一题)
Question selected = candidates.get(new Random().nextInt(
Math.min(candidates.size(), 5) // 从top5中随机选
));
return QuestionRecommendation.builder()
.question(selected)
.kpId(kpId)
.currentMastery(mastery)
.reasonText(buildReasonText(mastery, selected.getDifficulty()))
.encouragement(buildEncouragement(mastery))
.build();
}
/**
* 智能选择目标知识点
* 优先选择:已开始但未掌握(0.3-0.6)的知识点
* 其次:完全未学的前置知识点
*/
private String selectTargetKnowledgePoint(String studentId, String subject) {
List<WeakPoint> weakPoints = masteryService.analyzeWeakPoints(studentId, subject);
if (!weakPoints.isEmpty()) {
// 优先补薄弱点
return weakPoints.get(0).getKpId();
}
// 推进到新知识点
return getNextUnlearnedKnowledgePoint(studentId, subject);
}
private String getNextUnlearnedKnowledgePoint(String studentId, String subject) {
// 获取已掌握的知识点(>0.6)
Set<String> masteredKps = masteryService
.getMasteredKnowledgePoints(studentId, subject, 0.6);
// 找到前置知识点已掌握的、尚未学习的知识点
return questionRepository.findNextKnowledgePoint(subject, masteredKps)
.map(KnowledgePoint::getId)
.orElse(null);
}
private int[] getDifficultyRange(double mastery) {
if (mastery < 0.3) return new int[]{1, 2};
if (mastery < 0.6) return new int[]{2, 3};
if (mastery < 0.8) return new int[]{3, 4};
return new int[]{4, 5};
}
private String buildReasonText(double mastery, int difficulty) {
if (mastery < 0.3) return "先从基础题练起,打牢地基";
if (mastery < 0.6) return "你对这个知识点有一定了解,继续巩固";
if (mastery < 0.8) return "基础不错!来挑战进阶题";
return "接近精通!来看看最难的题";
}
private String buildEncouragement(double mastery) {
if (mastery < 0.3) return "加油!每道题都是进步";
if (mastery < 0.6) return "已经掌握了基础,继续保持!";
if (mastery < 0.8) return "进步明显,快要精通了!";
return "接近满分掌握,来冲最后一关!";
}
}四、智能答疑系统:结合知识库的AI辅导
4.1 答疑架构
4.2 个性化答疑服务
package com.edu.ai.tutoring;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.document.Document;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class IntelligentTutoringService {
private final ChatClient chatClient;
private final VectorStore courseKnowledgeBase;
private final MasteryUpdateService masteryService;
private final AdaptiveQuestionRecommender questionRecommender;
/**
* 智能答疑:根据学生水平个性化解答
*/
public TutoringResponse answerQuestion(
String studentId,
String question,
String subject,
String relatedKpId) {
log.info("答疑请求,学生: {}, 问题: {}", studentId, question);
// 1. 获取学生水平(影响解释深度)
double masteryLevel = masteryService.getOverallMastery(studentId, subject);
String studentLevel = assessLevel(masteryLevel);
// 2. RAG检索相关课程内容
List<Document> relatedContent = courseKnowledgeBase.similaritySearch(
SearchRequest.query(question)
.withTopK(3)
.withSimilarityThreshold(0.65)
);
String context = relatedContent.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
// 3. 构建个性化提示词
String systemPrompt = buildTutorPrompt(studentLevel, subject);
String userMessage = String.format("""
学生问题:%s
相关课程内容:
%s
请根据该学生的水平(%s)给出合适深度的解答。
""", question, context, studentLevel);
// 4. AI解答
String answer = chatClient.prompt()
.system(systemPrompt)
.user(userMessage)
.call()
.content();
// 5. 推荐相关练习题
QuestionRecommendation nextQuestion = null;
if (relatedKpId != null) {
nextQuestion = questionRecommender.getNextQuestion(
studentId, subject, relatedKpId);
}
return TutoringResponse.builder()
.answer(answer)
.relatedSources(extractSources(relatedContent))
.recommendedQuestion(nextQuestion)
.build();
}
private String buildTutorPrompt(String studentLevel, String subject) {
String levelGuidance = switch (studentLevel) {
case "beginner" -> """
该学生是初学者,请:
1. 使用简单直白的语言,避免专业术语
2. 多用生活中的类比帮助理解
3. 一步一步分解,不要跳过细节
4. 结尾鼓励学生,增加信心
""";
case "intermediate" -> """
该学生有一定基础,请:
1. 可以使用标准专业术语
2. 解释原理而不只是步骤
3. 可以提及相关概念的联系
4. 适当提出一个思考问题
""";
default -> """
该学生基础较好,请:
1. 深入原理和底层实现
2. 讨论边界情况和优化
3. 可以介绍行业实践和进阶资料
4. 引导学生自己总结规律
""";
};
return String.format("""
你是一名耐心的%s辅导老师。
%s
回答规范:
- 核心解答部分不超过300字
- 可以用代码示例(如果适合)
- 最后可以推荐1个相关知识点让学生进一步探索
""", subject, levelGuidance);
}
private String assessLevel(double mastery) {
if (mastery < 0.35) return "beginner";
if (mastery < 0.65) return "intermediate";
return "advanced";
}
private List<String> extractSources(List<Document> docs) {
return docs.stream()
.map(d -> (String) d.getMetadata().getOrDefault("source_title", "课程资料"))
.distinct()
.collect(Collectors.toList());
}
}五、学习路径规划:个性化课程推荐
5.1 路径规划算法
package com.edu.ai.pathway;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class LearningPathwayService {
private final ChatClient chatClient;
private final MasteryUpdateService masteryService;
private final KnowledgePointRepository kpRepository;
private final CourseRepository courseRepository;
/**
* 生成个性化学习路径
*
* @param studentId 学生ID
* @param subject 科目
* @param targetGoal 学习目标(如"通过Java初级认证")
* @param availableWeeks 可用学习周数
*/
public LearningPathway generatePathway(
String studentId,
String subject,
String targetGoal,
int availableWeeks) {
// 1. 评估当前水平
Map<String, Double> currentMastery = masteryService
.getAllMasteryScores(studentId, subject);
// 2. 找出目标的所需知识点
List<KnowledgePoint> requiredKps = kpRepository
.findRequiredForGoal(subject, targetGoal);
// 3. 找出差距
List<LearningGap> gaps = requiredKps.stream()
.map(kp -> {
double current = currentMastery.getOrDefault(kp.getId(), 0.0);
double target = 0.75; // 目标掌握度
return LearningGap.of(kp, current, target);
})
.filter(gap -> gap.getGapSize() > 0.1) // 差距超过10%才需要学
.sorted(Comparator.comparingInt(g -> g.getKnowledgePoint().getPrerequisiteCount()))
.collect(Collectors.toList());
// 4. 估算学习时间
int totalHoursNeeded = estimateTotalHours(gaps);
int hoursAvailable = availableWeeks * 5; // 每周5小时
// 5. AI生成学习计划
String aiPlan = generateAIPlan(studentId, gaps, targetGoal, availableWeeks, hoursAvailable);
// 6. 构建结构化学习路径
List<WeeklyPlan> weeklyPlans = buildWeeklyPlans(gaps, availableWeeks);
return LearningPathway.builder()
.studentId(studentId)
.targetGoal(targetGoal)
.totalGaps(gaps.size())
.estimatedHours(totalHoursNeeded)
.availableHours(hoursAvailable)
.feasible(totalHoursNeeded <= hoursAvailable * 1.2)
.weeklyPlans(weeklyPlans)
.aiNarrative(aiPlan)
.build();
}
private String generateAIPlan(
String studentId,
List<LearningGap> gaps,
String targetGoal,
int weeks,
int hoursAvailable) {
String gapSummary = gaps.stream()
.limit(10)
.map(g -> String.format("- %s(当前掌握度:%.0f%%,需提升至75%%)",
g.getKnowledgePoint().getName(),
g.getCurrentMastery() * 100))
.collect(Collectors.joining("\n"));
String prompt = String.format("""
一位学生需要在%d周内达成目标:%s
目前的薄弱点:
%s
每周可学习时间:约%d小时
请用鼓励的语气,给出一个实际可行的学习建议:
1. 第一步从哪里开始(最重要)
2. 如何利用碎片时间
3. 遇到瓶颈时怎么做
4. 如何检验自己是否达标
控制在200字以内,语气像学长/学姐,不要像教科书。
""",
weeks, targetGoal, gapSummary, hoursAvailable / weeks
);
return chatClient.prompt().user(prompt).call().content();
}
private List<WeeklyPlan> buildWeeklyPlans(List<LearningGap> gaps, int weeks) {
List<WeeklyPlan> plans = new ArrayList<>();
// 按前置关系拓扑排序知识点
List<LearningGap> sorted = topologicalSort(gaps);
int kpsPerWeek = Math.max(1, sorted.size() / weeks);
for (int week = 0; week < weeks && !sorted.isEmpty(); week++) {
List<LearningGap> weekGaps = sorted.stream()
.skip((long) week * kpsPerWeek)
.limit(kpsPerWeek)
.collect(Collectors.toList());
if (!weekGaps.isEmpty()) {
plans.add(WeeklyPlan.builder()
.weekNumber(week + 1)
.knowledgePoints(weekGaps.stream()
.map(g -> g.getKnowledgePoint().getName())
.collect(Collectors.toList()))
.estimatedHours(weekGaps.stream()
.mapToInt(g -> estimateHours(g))
.sum())
.build());
}
}
return plans;
}
private List<LearningGap> topologicalSort(List<LearningGap> gaps) {
// 拓扑排序:确保先学前置知识点
// 简化实现:按prerequisiteCount升序
return gaps.stream()
.sorted(Comparator.comparingInt(g ->
g.getKnowledgePoint().getPrerequisiteCount()))
.collect(Collectors.toList());
}
private int estimateHours(LearningGap gap) {
double gapSize = gap.getGapSize();
int difficulty = gap.getKnowledgePoint().getDifficulty();
return (int) Math.ceil(gapSize * difficulty * 2);
}
private int estimateTotalHours(List<LearningGap> gaps) {
return gaps.stream().mapToInt(this::estimateHours).sum();
}
}六、作业批改:主观题的AI评分
6.1 评分维度设计
package com.edu.ai.grading;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class SubjectiveGradingService {
private final ChatClient chatClient;
/**
* 主观题AI评分
*
* @param question 题目
* @param referenceAnswer 参考答案
* @param studentAnswer 学生答案
* @param rubric 评分标准
*/
public GradingResult gradeSubjectiveAnswer(
String question,
String referenceAnswer,
String studentAnswer,
GradingRubric rubric) {
String prompt = String.format("""
请对以下主观题答案进行评分:
题目:%s
参考答案:%s
评分标准:
%s
学生答案:%s
请按照以下格式输出(严格JSON格式):
{
"totalScore": <实际得分,整数>,
"maxScore": %d,
"dimensionScores": {
"accuracy": <知识准确性得分>,
"completeness": <要点完整性得分>,
"expression": <表达清晰度得分>
},
"strengths": ["优点1", "优点2"],
"improvements": ["改进建议1", "改进建议2"],
"detailedFeedback": "详细反馈(100字以内)",
"missedKeyPoints": ["遗漏的要点1", "遗漏的要点2"]
}
""",
question,
referenceAnswer,
formatRubric(rubric),
studentAnswer,
rubric.getTotalPoints()
);
String response = chatClient.prompt()
.system("你是一名专业、公正的教育评分助手。评分要客观,反馈要建设性。")
.user(prompt)
.call()
.content();
GradingResult result = GradingResult.fromJson(response);
result.setQuestion(question);
result.setStudentAnswer(studentAnswer);
log.info("主观题评分完成:得分 {}/{}", result.getTotalScore(), rubric.getTotalPoints());
return result;
}
/**
* 批量作业批改
*/
public List<GradingResult> batchGrade(
String question,
String referenceAnswer,
List<StudentSubmission> submissions,
GradingRubric rubric) {
return submissions.parallelStream()
.map(submission -> {
GradingResult result = gradeSubjectiveAnswer(
question, referenceAnswer, submission.getAnswer(), rubric);
result.setStudentId(submission.getStudentId());
return result;
})
.collect(java.util.stream.Collectors.toList());
}
/**
* 作文评分(特殊处理)
*/
public EssayGradingResult gradeEssay(
String essayPrompt,
String studentEssay,
EssayGradingCriteria criteria) {
String prompt = String.format("""
请对以下作文进行评分:
作文题目:%s
学生作文:
%s
评分标准(满分100分):
- 主题(25分):是否紧扣题目
- 结构(20分):段落清晰,逻辑连贯
- 语言(25分):表达准确,用词恰当
- 内容(20分):观点深度,论据充分
- 字数(10分):要求%d字,实际字数及扣分
请输出JSON格式评分结果,包含各维度得分和具体改进建议。
""",
essayPrompt,
studentEssay,
criteria.getRequiredWordCount()
);
String response = chatClient.prompt()
.system("你是一名语文老师,评分公正,善于给出建设性反馈。")
.user(prompt)
.call()
.content();
return EssayGradingResult.fromJson(response);
}
private String formatRubric(GradingRubric rubric) {
StringBuilder sb = new StringBuilder();
rubric.getDimensions().forEach((dim, points) ->
sb.append(String.format("- %s:%d分\n", dim, points))
);
return sb.toString();
}
}七、学习报告生成:个性化学情分析
7.1 周报生成服务
package com.edu.ai.report;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class LearningReportService {
private final ChatClient chatClient;
private final MasteryUpdateService masteryService;
private final LearningBehaviorRepository behaviorRepo;
private final ReportEmailService emailService;
/**
* 每周一早8点,生成上周学情报告
*/
@Scheduled(cron = "0 0 8 * * MON")
public void generateWeeklyReports() {
log.info("开始生成每周学情报告...");
LocalDateTime weekStart = LocalDateTime.now().minusWeeks(1);
LocalDateTime weekEnd = LocalDateTime.now();
List<String> activeStudents = behaviorRepo
.findActiveStudentIds(weekStart, weekEnd);
log.info("本周活跃学生数: {}", activeStudents.size());
activeStudents.forEach(studentId -> {
try {
WeeklyReport report = generateStudentWeeklyReport(
studentId, weekStart, weekEnd);
emailService.sendWeeklyReport(studentId, report);
} catch (Exception e) {
log.warn("学生 {} 周报生成失败", studentId, e);
}
});
}
/**
* 生成单个学生的周报
*/
public WeeklyReport generateStudentWeeklyReport(
String studentId,
LocalDateTime start,
LocalDateTime end) {
// 1. 收集本周行为数据
WeeklyStats stats = collectWeeklyStats(studentId, start, end);
// 2. 掌握度变化
MasteryChange masteryChange = masteryService.getMasteryChange(studentId, start);
// 3. AI生成个性化叙述
String narrative = generateNarrative(studentId, stats, masteryChange);
// 4. 下周学习建议
String nextWeekPlan = generateNextWeekPlan(studentId, masteryChange);
return WeeklyReport.builder()
.studentId(studentId)
.weekStart(start.toLocalDate())
.weekEnd(end.toLocalDate())
.stats(stats)
.masteryChange(masteryChange)
.narrative(narrative)
.nextWeekPlan(nextWeekPlan)
.build();
}
private String generateNarrative(
String studentId,
WeeklyStats stats,
MasteryChange change) {
String prompt = String.format("""
为以下学习数据生成一段温暖、鼓励的周报叙述:
本周学习数据:
- 学习时长:%d分钟
- 完成题目:%d道
- 正确率:%.0f%%
- 新学知识点:%d个
- 掌握度提升最大的知识点:%s(+%.0f%%)
- 仍需加强的知识点:%s
要求:
1. 语气像一个了解学生的辅导老师,不像机器人
2. 先肯定本周的努力,再指出可以改进的地方
3. 结尾给出一个具体可行的下周小目标
4. 100-150字,不要用"首先""其次""总之"等模板词
""",
stats.getTotalMinutes(),
stats.getTotalQuestions(),
stats.getAccuracyRate() * 100,
stats.getNewKnowledgePoints(),
change.getTopImprovedKp(),
change.getTopImprovedDelta() * 100,
change.getWeakestKp()
);
return chatClient.prompt().user(prompt).call().content();
}
private String generateNextWeekPlan(String studentId, MasteryChange change) {
List<WeakPoint> weakPoints = masteryService
.analyzeWeakPoints(studentId, "all");
if (weakPoints.isEmpty()) {
return "本周表现优秀!建议挑战更难的题目,或探索新的知识领域。";
}
String prompt = String.format("""
学生下周需要重点攻克的薄弱点:
%s
请给出3个具体的下周学习建议:
1. 每条建议都是可执行的(不要说"多练习"这类空话)
2. 包含时间估计(如"每天10分钟")
3. 语言简洁,类似于好朋友的建议
""",
weakPoints.stream()
.limit(3)
.map(w -> String.format("- %s(掌握度%.0f%%)", w.getKpName(), w.getMasteryScore() * 100))
.collect(java.util.stream.Collectors.joining("\n"))
);
return chatClient.prompt().user(prompt).call().content();
}
private WeeklyStats collectWeeklyStats(
String studentId,
LocalDateTime start,
LocalDateTime end) {
List<LearningBehaviorLog> logs = behaviorRepo
.findByStudentIdAndCreatedAtBetween(studentId, start, end);
int totalMinutes = logs.stream()
.mapToInt(l -> l.getTimeSpentSec() / 60)
.sum();
long correctCount = logs.stream()
.filter(l -> "CORRECT".equals(l.getResult()))
.count();
long totalAnswers = logs.stream()
.filter(l -> "ANSWER".equals(l.getBehaviorType()))
.count();
return WeeklyStats.builder()
.totalMinutes(totalMinutes)
.totalQuestions((int) totalAnswers)
.accuracyRate(totalAnswers > 0 ? (double) correctCount / totalAnswers : 0)
.newKnowledgePoints(countNewKnowledgePoints(studentId, logs))
.build();
}
private int countNewKnowledgePoints(String studentId, List<LearningBehaviorLog> logs) {
return (int) logs.stream()
.flatMap(l -> l.getKpIds().stream())
.distinct()
.count();
}
}八、隐私保护:学生数据的安全处理
8.1 未成年人数据特殊保护
package com.edu.ai.privacy;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
/**
* 学生数据隐私保护服务
* 特别针对未成年人数据的额外保护
* 依据:《个人信息保护法》第28条(敏感个人信息)
*/
@Service
@RequiredArgsConstructor
public class StudentPrivacyService {
private final StudentRepository studentRepository;
private final ConsentRepository consentRepository;
private final DataRetentionService retentionService;
/**
* 验证是否有合法授权处理学生数据
*/
public boolean hasValidConsent(String studentId, DataProcessingPurpose purpose) {
Student student = studentRepository.findById(studentId).orElse(null);
if (student == null) return false;
// 未成年人需要监护人同意
if (student.isMinor()) {
return consentRepository
.hasGuardianConsent(studentId, purpose.name());
}
return consentRepository.hasStudentConsent(studentId, purpose.name());
}
/**
* 数据导出:学生有权获取自己的数据
* 依据《个人信息保护法》第45条
*/
public StudentDataExport exportStudentData(String studentId, String requestorId) {
// 验证请求人是学生本人或监护人
validateExportAuthority(studentId, requestorId);
return StudentDataExport.builder()
.basicInfo(getAnonymizedBasicInfo(studentId))
.learningHistory(getLearningHistory(studentId))
.masteryScores(getMasteryScores(studentId))
.aiInteractionLogs(getAiInteractionLogs(studentId))
.exportedAt(java.time.LocalDateTime.now())
.build();
}
/**
* 数据删除:学生/监护人有权申请删除
*/
public void deleteStudentData(String studentId, String requestorId) {
validateDeleteAuthority(studentId, requestorId);
// 删除顺序:先删AI相关数据,再删业务数据
retentionService.deleteAiVectorData(studentId);
retentionService.deleteAiInteractionLogs(studentId);
retentionService.deleteLearningBehaviorLogs(studentId);
retentionService.deleteMasteryRecords(studentId);
// 基础账号数据可能需要保留(合同/财务要求)
// 实际保留时长按法律要求和合同约定
}
/**
* AI模型训练数据匿名化
* 使用学习数据改进模型时,必须完全匿名化
*/
public AnonymizedLearningData anonymizeForModelTraining(String studentId) {
// k-匿名化:确保每条数据无法唯一对应某个学生
// 删除所有直接标识符,泛化准标识符
return AnonymizedLearningData.builder()
.anonymousId(generateAnonymousId(studentId)) // 不可逆映射
.ageGroup(getAgeGroup(studentId))
.learningPattern(extractLearningPattern(studentId))
// 注意:绝不包含姓名、账号、学校等标识信息
.build();
}
private String generateAnonymousId(String studentId) {
// 使用单向散列函数,无法还原原始ID
return org.springframework.util.DigestUtils.md5DigestAsHex(
(studentId + System.getenv("ANONYMIZATION_SALT")).getBytes()
);
}
private void validateExportAuthority(String studentId, String requestorId) {
if (!studentId.equals(requestorId) &&
!isGuardian(studentId, requestorId)) {
throw new UnauthorizedDataAccessException(
"无权限访问该学生数据:" + studentId);
}
}
private void validateDeleteAuthority(String studentId, String requestorId) {
validateExportAuthority(studentId, requestorId);
}
private boolean isGuardian(String studentId, String requestorId) {
return consentRepository.isGuardianOf(requestorId, studentId);
}
private String getAgeGroup(String studentId) {
Student s = studentRepository.findById(studentId).orElseThrow();
int age = s.getAge();
if (age < 12) return "小学";
if (age < 15) return "初中";
if (age < 18) return "高中";
return "大学及以上";
}
private Object extractLearningPattern(String studentId) {
// 仅提取学习行为模式特征,不含任何个人标识
return null; // 实际实现
}
public enum DataProcessingPurpose {
PERSONALIZED_RECOMMENDATION,
PROGRESS_TRACKING,
MODEL_TRAINING,
REPORT_GENERATION
}
}九、效果数据
| 指标 | 改造前 | 改造后3个月 | 变化 |
|---|---|---|---|
| 课程完成率 | 23% | 61% | +165% |
| 日均学习时长 | 18分钟 | 34分钟 | +89% |
| 题目正确率(第一次做) | 52% | 68% | +31% |
| 答疑人工处理占比 | 100% | 23% | -77% |
| 学生NPS(净推荐值) | 31 | 58 | +87% |
| 运营人员处理效率 | 基准 | 基准×4 | +300% |
FAQ
Q:自适应推荐效果不好,用户还是觉得题目太难/太简单怎么办?
给用户手动调整难度的入口。AI的掌握度模型是概率估计,用户的真实感知更准确。"觉得太难"→下调一个难度级别;"太简单了"→提前进入挑战题。用户的反馈操作本身也是模型训练数据。
Q:作文批改AI会有偏差吗?如何保证公平?
两个措施:(1) 建立人工抽检机制,每天随机抽取5%的AI评分人工复核;(2) 设置分数分布监控,如果AI给出的分数分布异常(比如过于集中在某个分段),触发人工审查。另外,AI评分仅供参考,正式考试不依赖AI。
Q:学生数据存储在云上,合规有问题吗?
K12教育的学生数据属于敏感个人信息,需要单独告知和授权。使用国内云服务商(如阿里云、腾讯云)的教育版本,他们通常已完成教育行业的数据合规认证。关键是:不要把学生数据传到海外服务器。
Q:Spring AI的流式输出怎么用到答疑场景?
答疑场景非常适合流式输出,让学生感受到"AI在思考"。用chatClient.prompt().stream()替代.call(),通过SSE或WebSocket推送到前端。注意:流式输出时要在服务端做内容安全过滤(不能等全部生成完再过滤)。
