教育行业 AI 落地——个性化答疑系统的完整工程实现
教育行业 AI 落地——个性化答疑系统的完整工程实现
适读人群:做教育 AI 或对知识图谱感兴趣的工程师
阅读时长:约 24 分钟
文章价值:教育 AI 的引导式设计 + 知识图谱 + RAG 完整实现
一个学生把我的 AI「教坏了」
做教育 AI 以来,最让我意外的一件事,是我们的系统曾经被一个初中生「玩坏了」。
他发现只要一直追问「给我直接说答案」,AI 会在第三四次追问后妥协,直接把答案给出来。然后他把这个「技巧」分享给了班上的同学。大概一个星期之内,系统里出现了大量的答案直给记录,老师发现后,把这个场景截图发给了我。
那段时间我每天盯着用户行为日志,发现这个问题比我想象的普遍。有相当一部分学生,不是来学习的,是来「抄作业」的。这不是道德问题,这是我的系统设计有问题——我把学生当成了有主动学习意愿的用户,但现实里的用户行为没那么理想。
这次事件让我重新审视了整个系统的设计。教育 AI 不是搜索引擎,不是问答系统,它的核心价值是辅助学习,不是替代思考。这个定位差一点,整个系统的设计就会走歪。
教育 AI 的特殊要求:和其他行业根本不同
不能直接给答案,但不能什么都不给
这是教育 AI 最难拿捏的平衡点。
直接给答案会让学生依赖系统,不利于真正的学习。但什么都不给,或者提示太模糊,学生会觉得 AI 没用,去用别的工具(比如直接搜索答案)。
我见过几种做法:
- 只给提示,绝对不给答案(用户体验差,留存率很低)
- 给过程步骤,不给最终结果(好一些,但学生容易只看最后一步)
- 引导式提问,帮学生自己推导出答案(最难实现,但效果最好)
我们最终选择了引导式提问,但加了一个「学生坚持要答案」的处理逻辑——可以给答案,但必须先给完整的推导过程,并且记录到学习记录里,老师可以看到(这个功能让大多数学生主动放弃了「要答案」这条路)。
知识是有结构的,不是一篇篇独立文章
这是教育 AI 和普通 RAG 最大的区别。
普通 RAG 把知识库当成一堆文档,语义检索找到相关的,塞给 LLM 就完了。教育场景不行——学生学一道题,可能需要理解这道题背后涉及的知识点体系,这道题是哪个知识点的应用,这个知识点有哪些前置知识,学会之后可以解决什么更难的问题。
没有知识结构,AI 可以帮学生做完这道题,但学生不知道自己学到了什么,下次遇到类似问题还是不会。
所以教育 AI 需要知识图谱,不只是文档库。
个性化是真需求,不是宣传口号
每个学生的知识掌握情况不同。对于一个已经掌握了基础概念的学生,从头讲概念是浪费;对于一个基础薄弱的学生,跳过前置知识直接讲方法是无效的。
真正的个性化,需要系统了解每个学生的学习历史,知道他掌握了什么、不掌握什么,然后针对性地给出适合他当前水平的辅导。
这不是简单的 RAG 能做到的,需要用户画像 + 知识图谱 + 检索增强的组合。
知识图谱的设计
教育知识图谱是这个系统里最重要的基础设施,也是最费工时的部分。
图谱结构
我们的知识图谱分三层:
课程层:学科、年级、册次(如「初中数学-八年级上册」)
知识点层:每个知识点有:名称、定义、重要程度、所属课程节点、标准掌握描述
题目/例题层:每道题关联一个或多个知识点,有难度标注、题型标注、易错点标注
节点之间的关系类型:
PREREQUISITE:前置关系(A 是 B 的前置知识)CONTAINED_IN:包含关系(A 是 B 的子知识点)SIMILAR_TO:相似关系(A 和 B 是同类知识点)APPLIES_TO:应用关系(题目 T 考查知识点 A)
这个图谱不是一次性建完的,我们采用的是「先专家构建核心框架,再数据驱动补充细节」的方式:
- 学科专家建基础知识点框架和关系
- 教材内容解析自动抽取题目关联
- 学生答题数据反馈补充易错点和难度校正
知识图谱在 RAG 里的作用
普通 RAG:用户问题 → 向量检索 → 相关文档 → LLM 答复
知识图谱增强 RAG:用户问题 → 识别考查知识点 → 图谱查询(前置知识+相关例题+常见错误)→ 向量检索(相关讲解内容)→ LLM 引导式答复
多了「知识点识别」和「图谱查询」两步,但这两步让 AI 的答复从「解决这道题」提升到「帮学生建立知识结构」。
系统架构
代码实现:引导式答疑 Prompt 设计 + 知识图谱查询
@Service
public class PersonalizedTutoringService {
private final ChatClient chatClient;
private final VectorStore knowledgeVectorStore;
private final KnowledgeGraphService knowledgeGraph;
private final StudentProfileRepository studentProfileRepo;
private final LearningRecordService learningRecordService;
// 引导式答疑的最大追问轮数(超过此轮数才会提供更直接的提示)
private static final int MAX_GUIDED_ROUNDS = 3;
public TutoringResponse tutor(TutoringRequest request) {
// 1. 加载学生画像
StudentProfile profile = studentProfileRepo.findByStudentId(request.getStudentId())
.orElse(StudentProfile.createDefault(request.getStudentId()));
// 2. 识别问题中的知识点
List<String> targetKnowledgePoints = identifyKnowledgePoints(request.getQuestion());
// 3. 知识图谱查询:获取结构化知识上下文
KnowledgeContext knowledgeContext = buildKnowledgeContext(
targetKnowledgePoints, profile
);
// 4. RAG检索相关讲解内容
List<Document> relevantContent = retrieveRelevantContent(
request.getQuestion(), targetKnowledgePoints
);
// 5. 判断是否是「反复追问要答案」的行为
boolean isAnswerSeeking = detectAnswerSeekingBehavior(
request.getStudentId(), request.getSessionId(), request.getQuestion()
);
// 6. 生成引导式答复
String response = generateGuidedResponse(
request, profile, knowledgeContext, relevantContent, isAnswerSeeking
);
// 7. 记录学习行为
learningRecordService.record(LearningRecord.builder()
.studentId(request.getStudentId())
.sessionId(request.getSessionId())
.question(request.getQuestion())
.response(response)
.knowledgePoints(targetKnowledgePoints)
.answerSeeking(isAnswerSeeking)
.timestamp(LocalDateTime.now())
.build());
return TutoringResponse.builder()
.response(response)
.knowledgePointsInvolved(targetKnowledgePoints)
.suggestedNextStudy(knowledgeContext.getSuggestedNextStudy())
.build();
}
private List<String> identifyKnowledgePoints(String question) {
// 用LLM从问题中提取涉及的知识点ID
String extractionPrompt = """
从以下学生问题中,识别涉及的数学/语文/物理等学科知识点。
返回JSON格式的知识点列表:{"knowledgePoints": ["知识点1", "知识点2"]}
只返回明确涉及的知识点,不要过度推断。
学生问题:%s
""".formatted(question);
String response = chatClient.prompt()
.user(extractionPrompt)
.call()
.content();
try {
String json = extractJsonFromResponse(response);
Map<String, Object> parsed = new ObjectMapper().readValue(json, Map.class);
return (List<String>) parsed.getOrDefault("knowledgePoints", List.of());
} catch (Exception e) {
log.warn("Failed to extract knowledge points, using empty list", e);
return List.of();
}
}
private KnowledgeContext buildKnowledgeContext(
List<String> knowledgePoints, StudentProfile profile) {
KnowledgeContext.Builder contextBuilder = KnowledgeContext.builder();
for (String kp : knowledgePoints) {
// 从知识图谱查询该知识点的前置知识
List<String> prerequisites = knowledgeGraph.getPrerequisites(kp);
// 判断学生是否掌握了前置知识
List<String> missingPrerequisites = prerequisites.stream()
.filter(prereq -> !profile.hasMastered(prereq))
.collect(Collectors.toList());
// 查询相关例题(按难度匹配学生水平)
List<ExampleProblem> examples = knowledgeGraph.getExamples(
kp,
profile.getCurrentLevel(kp),
3 // 最多3道例题
);
// 查询常见错误点
List<String> commonMistakes = knowledgeGraph.getCommonMistakes(kp);
// 查询掌握该知识点后的后续学习路径
List<String> nextStudyPoints = knowledgeGraph.getNextStudyPoints(kp);
contextBuilder.addKnowledgePointContext(KnowledgePointContext.builder()
.knowledgePoint(kp)
.missingPrerequisites(missingPrerequisites)
.relevantExamples(examples)
.commonMistakes(commonMistakes)
.studentMasteryLevel(profile.getMasteryLevel(kp))
.build());
contextBuilder.suggestedNextStudy(nextStudyPoints);
}
return contextBuilder.build();
}
private boolean detectAnswerSeekingBehavior(
String studentId, String sessionId, String currentQuestion) {
// 获取当前会话的历史交互
List<LearningRecord> sessionHistory = learningRecordService
.getSessionHistory(studentId, sessionId);
if (sessionHistory.isEmpty()) return false;
// 判断标准:最近3轮中,有2轮以上包含「直接给我答案」类似语义
long directAnswerRequests = sessionHistory.stream()
.skip(Math.max(0, sessionHistory.size() - 3))
.filter(record -> isDirectAnswerRequest(record.getQuestion()))
.count();
return directAnswerRequests >= 2 || isDirectAnswerRequest(currentQuestion);
}
private boolean isDirectAnswerRequest(String question) {
// 检测直接要答案的行为
List<String> answerSeekingPatterns = List.of(
"直接告诉我答案", "给我答案", "答案是什么", "直接说答案",
"就告诉我结果", "不用解释", "答案给我", "直接给答案"
);
return answerSeekingPatterns.stream()
.anyMatch(pattern -> question.contains(pattern));
}
private String generateGuidedResponse(
TutoringRequest request,
StudentProfile profile,
KnowledgeContext knowledgeContext,
List<Document> relevantContent,
boolean isAnswerSeeking) {
String systemPrompt = buildTutoringSystemPrompt(profile, isAnswerSeeking);
String userPrompt = buildTutoringUserPrompt(
request, knowledgeContext, relevantContent, isAnswerSeeking
);
return chatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content();
}
private String buildTutoringSystemPrompt(StudentProfile profile, boolean isAnswerSeeking) {
StringBuilder sb = new StringBuilder();
sb.append("""
你是一位耐心、专业的学习辅导老师,擅长引导学生通过思考自己找到答案。
你的辅导风格:
1. 不直接给出答案,而是通过提问和提示,帮助学生自己推导
2. 发现学生的思维误区时,用温和的方式指出,不批评
3. 适时给予鼓励,让学生有成就感
4. 解释要与学生的水平匹配,不要太难也不要太简单
""");
// 根据学生水平调整语言风格
sb.append("学生基本情况:\n");
sb.append(String.format("- 年级:%s\n", profile.getGrade()));
sb.append(String.format("- 整体学习水平:%s\n", profile.getOverallLevel()));
sb.append(String.format("- 偏好的解释风格:%s\n", profile.getPreferredExplanationStyle()));
if (isAnswerSeeking) {
sb.append("""
特别注意:该学生反复要求直接给答案。
处理方式:
- 明确告知AI辅导的目的是帮助学习,不是替代思考
- 提供更详细的解题步骤提示,但仍然不给最终答案
- 可以说"我注意到你已经问了好几次答案了,让我换个方式帮助你..."
- 向学生解释:掌握方法比得到答案更重要
""");
}
return sb.toString();
}
private String buildTutoringUserPrompt(
TutoringRequest request,
KnowledgeContext knowledgeContext,
List<Document> relevantContent,
boolean isAnswerSeeking) {
StringBuilder sb = new StringBuilder();
sb.append("## 学生提问\n");
sb.append(request.getQuestion()).append("\n\n");
// 如果有对话历史,加入
if (!request.getConversationHistory().isEmpty()) {
sb.append("## 本次对话历史\n");
request.getConversationHistory().forEach(msg -> {
sb.append(msg.getRole()).append(":").append(msg.getContent()).append("\n");
});
sb.append("\n");
}
// 知识图谱上下文
if (!knowledgeContext.getContexts().isEmpty()) {
sb.append("## 知识点上下文(辅导参考,不直接展示给学生)\n");
for (KnowledgePointContext kpc : knowledgeContext.getContexts()) {
sb.append(String.format("### 知识点:%s\n", kpc.getKnowledgePoint()));
sb.append(String.format("学生掌握程度:%s\n", kpc.getStudentMasteryLevel()));
if (!kpc.getMissingPrerequisites().isEmpty()) {
sb.append("⚠️ 缺少前置知识:\n");
kpc.getMissingPrerequisites()
.forEach(prereq -> sb.append("- ").append(prereq).append("\n"));
sb.append("→ 辅导时需要先补充这些前置知识\n");
}
if (!kpc.getCommonMistakes().isEmpty()) {
sb.append("常见错误点(重点关注):\n");
kpc.getCommonMistakes()
.forEach(mistake -> sb.append("- ").append(mistake).append("\n"));
}
}
sb.append("\n");
}
// 相关教材内容
if (!relevantContent.isEmpty()) {
sb.append("## 相关教材内容(作为解释依据)\n");
relevantContent.forEach(doc -> {
sb.append(doc.getContent()).append("\n---\n");
});
sb.append("\n");
}
sb.append("## 辅导要求\n");
if (isAnswerSeeking) {
sb.append("学生一直在要求直接给答案,请先温和地说明学习目的,然后提供分步骤的提示,引导学生自己找到答案。\n");
} else {
sb.append("请用引导式方式辅导这位学生,不要直接给出最终答案,而是通过提问和提示帮助他/她思考。\n");
}
return sb.toString();
}
}这段代码的核心设计点:
1. 行为检测机制
detectAnswerSeekingBehavior 会分析学生的历史行为,识别反复要答案的模式。这不是技术炫耀,是真实解决了我前面说的「学生把系统教坏」的问题。
2. 知识图谱在 Prompt 里的使用方式
注意我把知识图谱上下文标注为「辅导参考,不直接展示给学生」。这是告诉 LLM 这部分信息的用途——用来指导辅导策略,不是要原样复读给学生看。LLM 会据此调整辅导方式,比如发现学生缺少前置知识时,会先补充前置知识再讲题。
3. 前置知识检查是个重要设计
很多学生卡在一道题上,不是这道题本身难,是前置知识没掌握。如果系统发现学生缺少前置知识,直接辅导这道题是无效的。正确的做法是先补充前置知识,这在普通 RAG 系统里完全没有考虑,需要知识图谱来支撑。
学生画像的工程实现
个性化的基础是学生画像,画像的质量直接决定个性化的效果。
画像包含什么
@Entity
public class StudentProfile {
private String studentId;
private String grade; // 年级
private String overallLevel; // 整体水平(BASIC/INTERMEDIATE/ADVANCED)
private String preferredExplanationStyle; // 偏好解释方式
// 按知识点的掌握情况
@ElementCollection
private Map<String, MasteryLevel> knowledgeMastery;
// 学习行为特征
private int averageQuestionLength; // 平均问题长度(判断思考深度)
private double answerSeekingRatio; // 要答案行为占比(识别学习习惯)
private int consecutiveDays; // 连续学习天数
// 薄弱知识点列表(频繁出错的)
@ElementCollection
private List<String> weakKnowledgePoints;
// 最近学习的知识点(用于上下文连续性)
@ElementCollection
private List<String> recentStudyPoints;
}画像的动态更新
每次辅导结束后,系统根据学生的表现更新画像:
- 学生答对了:提升对应知识点的掌握度
- 学生反复问同一类问题:降低掌握度,加入薄弱知识点列表
- 学生主动追问深度问题(不是要答案,而是问「为什么」):记录为主动学习行为,提升个性化水平
这个更新逻辑不能太激进(一次答对就认为完全掌握),也不能太保守(需要多次证明才更新)。我们用的是加权滑动平均:每次交互给一个 0-1 的掌握度更新值,新状态 = 老状态 × 0.7 + 新值 × 0.3。
冷启动问题
新学生第一次使用,没有历史数据,怎么做个性化?
解决方案:开始使用前,做一个简短的诊断性测试(5-8 道题,覆盖核心知识点),快速建立初始画像。这个测试设计得像一个小游戏,不要太正式,学生接受度更高。
知识体系可视化:让学生看见自己的学习地图
除了答疑功能,我们还做了一个知识地图的可视化界面,把学生的知识图谱掌握情况直观展示出来。
绿色节点:已掌握;黄色节点:部分掌握;灰色节点:未学习;红色节点:薄弱知识点(需要复习)。
学生可以点击任意节点,直接进入相关知识点的辅导会话。这个功能上线后,学生的主动复习行为(不是因为作业,而是自己点进去学)提升了 3 倍。
让学习进度变得可见,是提升学习动力最有效的方法之一。这是个产品设计洞察,不是技术洞察,但对系统的使用效果影响很大。
教师端:让老师看见 AI 在做什么
学生在用 AI,老师要知道 AI 在教什么、教得怎么样。这不只是运营需要,也是学校采购 AI 产品的一个核心关注点——老师担心 AI 会取代自己的作用,担心学生学了歪东西。
我们给教师端设计了几个视图:
知识点掌握分布:班级里每个知识点的整体掌握情况,老师可以快速发现「大多数学生都在卡」的知识点,调整教学重点。
学生学习行为报告:每个学生的使用情况,包括提问次数、知识点覆盖、是否存在频繁要答案的行为。
AI 答复质量抽查:老师可以抽查 AI 对某道题的答复是否符合教学要求。这个功能在最开始很多老师并不信任 AI 时,起到了很大的信任建立作用。
教师端的设计,让 AI 成为老师的助手,而不是老师的竞争者。这个定位在推广阶段非常重要,直接影响了采购决策。
上线后的一些意外收获
系统上线六个月,有几个意外的发现:
一是学生提问质量在提升。 早期很多学生的问题是「这道题怎么做」,六个月后,更多的问题变成了「这道题我这么做是不是有问题」「这两道题的方法有什么区别」。这说明学生在 AI 引导下,思维方式在改变。
二是晚上 9-11 点是使用高峰。 这是学生做作业的时间。系统要能扛住这个峰值,并发设计要考虑到这个使用模式。
三是有些学生把 AI 当「树洞」用。 会跟 AI 说一些学习上的压力和困惑,不是问题,是情感宣泄。这是我完全没预料到的使用场景。我们在 Prompt 里加了情感支持的部分,但这个边界需要谨慎处理——AI 可以表达理解和鼓励,但不能做心理咨询,超出能力范围的情况要引导去找老师或心理老师。
总结:教育 AI 的工程本质
做了一年多教育 AI,我最大的收获是:教育行业 AI 落地的最大挑战,不是技术,是对「学习」这件事的理解。
技术工程师容易把教育 AI 做成「一个答案更准的搜索引擎」。但好的教育 AI 应该是「一个懂学生、有耐心、会引导的辅导老师」。这两者在技术架构上的差距,就是有没有知识图谱、有没有学生画像、有没有引导式的 Prompt 设计。
几个核心经验:
1. 知识图谱是教育 AI 的基础设施,不是加分项。 没有知识图谱,做出来的是问答系统,不是学习辅导系统。
2. 引导式设计要防御「答案寻求」行为。 学生会找捷径,系统要有检测和应对机制。
3. 个性化必须基于真实学情数据。 没有学情数据的「个性化」是假个性化。
4. 教师端和学生端同等重要。 老师信任 AI,才会推荐学生用。
如果你在教育行业做 AI,尤其是在做 K12 场景,欢迎交流,这个方向的经验积累还很少。
