第17章:实战综合项目三:个性化学习助手平台
前言
大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!
🎯 本章学习目标
通过本章学习,您将:
- 构建一个完整的个性化学习助手平台,涵盖学习路径推荐、进度跟踪和智能评估
- 掌握知识图谱构建技术,实现知识点关联和依赖关系管理
- 实现自适应学习算法,根据学习者的能力和进度动态调整学习内容
- 集成 LangGraph 工作流,实现学习计划的自动生成和优化
- 搭建现代化的学习管理界面,支持多种学习模式和交互方式
- 掌握教育科技领域的 AI 应用架构设计和最佳实践
📋 项目概述
系统功能特性
🧠 智能学习路径规划
- 基于学习者能力评估的个性化路径推荐
- 知识点依赖关系分析和学习顺序优化
- 多维度学习目标设定和进度跟踪
- 学习难度自适应调整和挑战性任务推荐
📊 学习进度监控
- 实时学习数据收集和分析
- 学习效果评估和能力提升跟踪
- 学习习惯分析和优化建议
- 学习成果可视化和报告生成
🎯 智能内容推荐
- 基于学习历史的个性化内容推荐
- 多模态学习资源整合(文本、视频、音频、交互)
- 学习资源质量评估和筛选
- 学习内容难度匹配和适应性调整
🤖 AI 学习助手
- 智能问答和概念解释
- 学习难点识别和针对性辅导
- 学习策略建议和方法指导
- 学习动机激励和成就系统
🏗️ 系统架构设计
整体架构图
scss
┌─────────────────────────────────────────────────────────────┐
│ 前端层 (Next.js) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 学习界面 │ │ 进度跟踪 │ │ 知识图谱 │ │
│ │ 组件 │ │ 组件 │ │ 可视化 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ API 网关层 (Next.js API Routes) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 学习管理 │ │ 推荐系统 │ │ 评估系统 │ │
│ │ API │ │ API │ │ API │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 业务服务层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 知识图谱 │ │ 推荐引擎 │ │ LangGraph │ │
│ │ 服务 │ │ 服务 │ │ 工作流 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据存储层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 图数据库 │ │ 关系数据库 │ │ 向量数据库 │ │
│ │ (Neo4j) │ │ (PostgreSQL) │ │ (Chroma) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
技术栈选择
前端技术
- Next.js 14 (App Router)
- TypeScript
- D3.js (知识图谱可视化)
- React Query
- Tailwind CSS
后端技术
- Node.js + TypeScript
- LangChain.js
- LangGraph
- Neo4j (图数据库)
- Express.js
AI 模型服务
- OpenAI GPT-4/GPT-3.5-turbo
- OpenAI Embeddings
- Anthropic Claude
- 本地开源模型 (Ollama)
数据存储
- Neo4j (知识图谱)
- PostgreSQL (关系数据)
- Chroma (向量数据库)
- Redis (缓存)
推荐算法
- 协同过滤
- 内容推荐
- 深度学习推荐
- 知识图谱推荐
🚀 项目初始化
环境准备
bash
# 1. 创建项目目录
mkdir personalized-learning-platform
cd personalized-learning-platform
# 2. 初始化 Next.js 项目
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
# 3. 安装核心依赖
npm install @langchain/core @langchain/community @langchain/openai @langchain/langgraph
npm install @langchain/chroma @langchain/postgres
npm install neo4j-driver
npm install d3 @types/d3
npm install @prisma/client prisma
npm install redis ioredis
npm install zod react-hook-form @hookform/resolvers
npm install @tanstack/react-query
npm install lucide-react clsx tailwind-merge
# 4. 安装开发依赖
npm install -D @types/node tsx nodemon
npm install -D prisma
项目结构
bash
personalized-learning-platform/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── api/ # API 路由
│ │ │ ├── learning/ # 学习管理 API
│ │ │ ├── recommendation/ # 推荐系统 API
│ │ │ └── assessment/ # 评估系统 API
│ │ ├── learning/ # 学习页面
│ │ ├── progress/ # 进度跟踪页面
│ │ └── knowledge-graph/ # 知识图谱页面
│ ├── components/ # 可复用组件
│ │ ├── ui/ # 基础 UI 组件
│ │ ├── learning/ # 学习相关组件
│ │ └── visualization/ # 可视化组件
│ ├── lib/ # 工具库
│ │ ├── ai/ # AI 相关工具
│ │ ├── graph/ # 图数据库工具
│ │ ├── recommendation/ # 推荐算法
│ │ └── utils.ts # 通用工具
│ ├── services/ # 业务服务
│ │ ├── knowledge/ # 知识图谱服务
│ │ ├── recommendation/ # 推荐服务
│ │ ├── assessment/ # 评估服务
│ │ └── workflow/ # 工作流服务
│ └── types/ # TypeScript 类型定义
├── prisma/ # 数据库模式
├── docker/ # Docker 配置
├── docs/ # 项目文档
└── package.json
📄 数据库设计
Prisma 模式定义
prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
avatar String?
role UserRole @default(STUDENT)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
profiles LearningProfile[]
enrollments Enrollment[]
progress LearningProgress[]
assessments Assessment[]
interactions LearningInteraction[]
@@map("users")
}
model LearningProfile {
id String @id @default(cuid())
level String
interests String[]
goals String[]
preferences Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id])
@@map("learning_profiles")
}
model Course {
id String @id @default(cuid())
title String
description String?
level String
duration Int // 预计学习时长(分钟)
difficulty Int // 难度等级 1-5
tags String[]
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
enrollments Enrollment[]
modules Module[]
@@map("courses")
}
model Module {
id String @id @default(cuid())
title String
description String?
order Int
duration Int
difficulty Int
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
courseId String
course Course @relation(fields: [courseId], references: [id])
lessons Lesson[]
@@map("modules")
}
model Lesson {
id String @id @default(cuid())
title String
content String
type LessonType
duration Int
difficulty Int
order Int
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
moduleId String
module Module @relation(fields: [moduleId], references: [id])
progress LearningProgress[]
assessments Assessment[]
@@map("lessons")
}
model Enrollment {
id String @id @default(cuid())
status EnrollmentStatus @default(ACTIVE)
enrolledAt DateTime @default(now())
completedAt DateTime?
userId String
user User @relation(fields: [userId], references: [id])
courseId String
course Course @relation(fields: [courseId], references: [id])
@@map("enrollments")
}
model LearningProgress {
id String @id @default(cuid())
status ProgressStatus @default(NOT_STARTED)
progress Float @default(0) // 0-100
timeSpent Int @default(0) // 学习时长(分钟)
lastAccessed DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id])
lessonId String
lesson Lesson @relation(fields: [lessonId], references: [id])
@@map("learning_progress")
}
model Assessment {
id String @id @default(cuid())
type AssessmentType
score Float?
answers Json?
feedback String?
completedAt DateTime?
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id])
lessonId String?
lesson Lesson? @relation(fields: [lessonId], references: [id])
@@map("assessments")
}
model LearningInteraction {
id String @id @default(cuid())
type InteractionType
content String
metadata Json?
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id])
@@map("learning_interactions")
}
model KnowledgeNode {
id String @id @default(cuid())
title String
description String?
type NodeType
difficulty Int
prerequisites String[]
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
relationships KnowledgeRelationship[]
@@map("knowledge_nodes")
}
model KnowledgeRelationship {
id String @id @default(cuid())
type RelationshipType
weight Float @default(1.0)
createdAt DateTime @default(now())
fromNodeId String
fromNode KnowledgeNode @relation("FromNode", fields: [fromNodeId], references: [id])
toNodeId String
toNode KnowledgeNode @relation("ToNode", fields: [toNodeId], references: [id])
@@map("knowledge_relationships")
}
enum UserRole {
STUDENT
INSTRUCTOR
ADMIN
}
enum LessonType {
TEXT
VIDEO
AUDIO
INTERACTIVE
QUIZ
EXERCISE
}
enum EnrollmentStatus {
ACTIVE
COMPLETED
PAUSED
CANCELLED
}
enum ProgressStatus {
NOT_STARTED
IN_PROGRESS
COMPLETED
REVIEWING
}
enum AssessmentType {
PRETEST
POSTTEST
QUIZ
EXERCISE
PROJECT
}
enum InteractionType {
QUESTION
ANSWER
COMMENT
RATING
BOOKMARK
}
enum NodeType {
CONCEPT
SKILL
TOPIC
SUBJECT
}
enum RelationshipType {
PREREQUISITE
RELATED_TO
PART_OF
SIMILAR_TO
DEPENDS_ON
}
🔧 核心服务实现
知识图谱服务
typescript
// src/services/knowledge/knowledge-graph.ts
import neo4j, { Driver, Session } from 'neo4j-driver';
export interface KnowledgeNode {
id: string;
title: string;
description?: string;
type: string;
difficulty: number;
prerequisites: string[];
metadata?: any;
}
export interface KnowledgeRelationship {
id: string;
type: string;
weight: number;
fromNodeId: string;
toNodeId: string;
}
export interface LearningPath {
nodes: KnowledgeNode[];
relationships: KnowledgeRelationship[];
estimatedDuration: number;
difficulty: number;
}
export class KnowledgeGraphService {
private driver: Driver;
constructor() {
this.driver = neo4j.driver(
process.env.NEO4J_URI || 'bolt://localhost:7687',
neo4j.auth.basic(
process.env.NEO4J_USERNAME || 'neo4j',
process.env.NEO4J_PASSWORD || 'password'
)
);
}
async createNode(node: Omit<KnowledgeNode, 'id'>): Promise<KnowledgeNode> {
const session = this.driver.session();
try {
const result = await session.run(
`CREATE (n:KnowledgeNode {
id: randomUUID(),
title: $title,
description: $description,
type: $type,
difficulty: $difficulty,
prerequisites: $prerequisites,
metadata: $metadata,
createdAt: datetime()
})
RETURN n`,
{
title: node.title,
description: node.description,
type: node.type,
difficulty: node.difficulty,
prerequisites: node.prerequisites,
metadata: node.metadata || {}
}
);
const createdNode = result.records[0].get('n').properties;
return {
id: createdNode.id,
title: createdNode.title,
description: createdNode.description,
type: createdNode.type,
difficulty: createdNode.difficulty,
prerequisites: createdNode.prerequisites,
metadata: createdNode.metadata
};
} finally {
await session.close();
}
}
async createRelationship(
fromNodeId: string,
toNodeId: string,
type: string,
weight: number = 1.0
): Promise<KnowledgeRelationship> {
const session = this.driver.session();
try {
const result = await session.run(
`MATCH (from:KnowledgeNode {id: $fromNodeId})
MATCH (to:KnowledgeNode {id: $toNodeId})
CREATE (from)-[r:RELATIONSHIP {
id: randomUUID(),
type: $type,
weight: $weight,
createdAt: datetime()
}]->(to)
RETURN r`,
{
fromNodeId,
toNodeId,
type,
weight
}
);
const relationship = result.records[0].get('r').properties;
return {
id: relationship.id,
type: relationship.type,
weight: relationship.weight,
fromNodeId,
toNodeId
};
} finally {
await session.close();
}
}
async findLearningPath(
startNodeId: string,
endNodeId: string,
userLevel: number
): Promise<LearningPath> {
const session = this.driver.session();
try {
const result = await session.run(
`MATCH path = shortestPath((start:KnowledgeNode {id: $startNodeId})-[*]-(end:KnowledgeNode {id: $endNodeId}))
WHERE ALL(node in nodes(path) WHERE node.difficulty <= $userLevel + 1)
RETURN path`,
{
startNodeId,
endNodeId,
userLevel
}
);
if (result.records.length === 0) {
throw new Error('No learning path found');
}
const path = result.records[0].get('path');
const nodes = path.segments.map((segment: any) => ({
id: segment.start.properties.id,
title: segment.start.properties.title,
description: segment.start.properties.description,
type: segment.start.properties.type,
difficulty: segment.start.properties.difficulty,
prerequisites: segment.start.properties.prerequisites || [],
metadata: segment.start.properties.metadata
}));
// 添加最后一个节点
const lastNode = path.end;
nodes.push({
id: lastNode.properties.id,
title: lastNode.properties.title,
description: lastNode.properties.description,
type: lastNode.properties.type,
difficulty: lastNode.properties.difficulty,
prerequisites: lastNode.properties.prerequisites || [],
metadata: lastNode.properties.metadata
});
const relationships = path.segments.map((segment: any) => ({
id: segment.relationship.properties.id,
type: segment.relationship.properties.type,
weight: segment.relationship.properties.weight,
fromNodeId: segment.start.properties.id,
toNodeId: segment.end.properties.id
}));
const estimatedDuration = nodes.reduce((total, node) => total + (node.difficulty * 30), 0);
const difficulty = Math.max(...nodes.map(node => node.difficulty));
return {
nodes,
relationships,
estimatedDuration,
difficulty
};
} finally {
await session.close();
}
}
async getPrerequisites(nodeId: string): Promise<KnowledgeNode[]> {
const session = this.driver.session();
try {
const result = await session.run(
`MATCH (prereq:KnowledgeNode)-[r:PREREQUISITE]->(target:KnowledgeNode {id: $nodeId})
RETURN prereq
ORDER BY prereq.difficulty ASC`,
{ nodeId }
);
return result.records.map(record => {
const node = record.get('prereq').properties;
return {
id: node.id,
title: node.title,
description: node.description,
type: node.type,
difficulty: node.difficulty,
prerequisites: node.prerequisites || [],
metadata: node.metadata
};
});
} finally {
await session.close();
}
}
async getRelatedNodes(nodeId: string, limit: number = 10): Promise<KnowledgeNode[]> {
const session = this.driver.session();
try {
const result = await session.run(
`MATCH (target:KnowledgeNode {id: $nodeId})-[r]-(related:KnowledgeNode)
WHERE r.type IN ['RELATED_TO', 'SIMILAR_TO']
RETURN related, r.weight
ORDER BY r.weight DESC
LIMIT $limit`,
{ nodeId, limit }
);
return result.records.map(record => {
const node = record.get('related').properties;
return {
id: node.id,
title: node.title,
description: node.description,
type: node.type,
difficulty: node.difficulty,
prerequisites: node.prerequisites || [],
metadata: node.metadata
};
});
} finally {
await session.close();
}
}
async updateNodeDifficulty(nodeId: string, newDifficulty: number): Promise<void> {
const session = this.driver.session();
try {
await session.run(
`MATCH (n:KnowledgeNode {id: $nodeId})
SET n.difficulty = $newDifficulty, n.updatedAt = datetime()`,
{ nodeId, newDifficulty }
);
} finally {
await session.close();
}
}
async getKnowledgeGraphStats(): Promise<{
totalNodes: number;
totalRelationships: number;
nodeTypes: Record<string, number>;
relationshipTypes: Record<string, number>;
}> {
const session = this.driver.session();
try {
const [nodesResult, relationshipsResult, nodeTypesResult, relationshipTypesResult] = await Promise.all([
session.run('MATCH (n:KnowledgeNode) RETURN count(n) as count'),
session.run('MATCH ()-[r]->() RETURN count(r) as count'),
session.run('MATCH (n:KnowledgeNode) RETURN n.type as type, count(n) as count'),
session.run('MATCH ()-[r]->() RETURN r.type as type, count(r) as count')
]);
const totalNodes = nodesResult.records[0].get('count').toNumber();
const totalRelationships = relationshipsResult.records[0].get('count').toNumber();
const nodeTypes: Record<string, number> = {};
nodeTypesResult.records.forEach(record => {
nodeTypes[record.get('type')] = record.get('count').toNumber();
});
const relationshipTypes: Record<string, number> = {};
relationshipTypesResult.records.forEach(record => {
relationshipTypes[record.get('type')] = record.get('count').toNumber();
});
return {
totalNodes,
totalRelationships,
nodeTypes,
relationshipTypes
};
} finally {
await session.close();
}
}
async close(): Promise<void> {
await this.driver.close();
}
}
推荐引擎服务
typescript
// src/services/recommendation/recommendation-engine.ts
import { Chroma } from '@langchain/chroma';
import { OpenAIEmbeddings } from '@langchain/openai';
export interface RecommendationRequest {
userId: string;
userLevel: number;
interests: string[];
goals: string[];
completedLessons: string[];
preferences?: any;
}
export interface RecommendationResult {
courseId: string;
title: string;
description: string;
level: string;
difficulty: number;
estimatedDuration: number;
tags: string[];
score: number;
reason: string;
}
export interface LearningContent {
id: string;
title: string;
description: string;
content: string;
type: string;
difficulty: number;
tags: string[];
metadata: any;
}
export class RecommendationEngine {
private vectorStore: Chroma;
private embeddings: OpenAIEmbeddings;
constructor() {
this.embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: 'text-embedding-3-small'
});
this.vectorStore = new Chroma(this.embeddings, {
url: process.env.CHROMA_URL || 'http://localhost:8000',
collectionName: 'learning_content'
});
}
async recommendCourses(request: RecommendationRequest): Promise<RecommendationResult[]> {
// 1. 基于用户兴趣的内容推荐
const interestRecommendations = await this.recommendByInterests(request.interests);
// 2. 基于学习目标的内容推荐
const goalRecommendations = await this.recommendByGoals(request.goals);
// 3. 基于学习历史的协同过滤推荐
const collaborativeRecommendations = await this.recommendByCollaborativeFiltering(request.userId);
// 4. 基于知识图谱的推荐
const knowledgeGraphRecommendations = await this.recommendByKnowledgeGraph(request);
// 5. 合并和排序推荐结果
const allRecommendations = [
...interestRecommendations,
...goalRecommendations,
...collaborativeRecommendations,
...knowledgeGraphRecommendations
];
return this.mergeAndRankRecommendations(allRecommendations, request);
}
private async recommendByInterests(interests: string[]): Promise<RecommendationResult[]> {
if (interests.length === 0) return [];
const query = interests.join(' ');
const results = await this.vectorStore.similaritySearchWithScore(query, 10);
return results.map(([doc, score]) => ({
courseId: doc.metadata.courseId,
title: doc.metadata.title,
description: doc.metadata.description,
level: doc.metadata.level,
difficulty: doc.metadata.difficulty,
estimatedDuration: doc.metadata.estimatedDuration,
tags: doc.metadata.tags,
score: score,
reason: '基于您的兴趣推荐'
}));
}
private async recommendByGoals(goals: string[]): Promise<RecommendationResult[]> {
if (goals.length === 0) return [];
const query = goals.join(' ');
const results = await this.vectorStore.similaritySearchWithScore(query, 10);
return results.map(([doc, score]) => ({
courseId: doc.metadata.courseId,
title: doc.metadata.title,
description: doc.metadata.description,
level: doc.metadata.level,
difficulty: doc.metadata.difficulty,
estimatedDuration: doc.metadata.estimatedDuration,
tags: doc.metadata.tags,
score: score,
reason: '基于您的学习目标推荐'
}));
}
private async recommendByCollaborativeFiltering(userId: string): Promise<RecommendationResult[]> {
// 简化的协同过滤实现
// 在实际应用中,这里会查询数据库获取相似用户的学习历史
const similarUsers = await this.findSimilarUsers(userId);
const recommendedCourses = await this.getCoursesFromSimilarUsers(similarUsers);
return recommendedCourses.map(course => ({
courseId: course.id,
title: course.title,
description: course.description,
level: course.level,
difficulty: course.difficulty,
estimatedDuration: course.estimatedDuration,
tags: course.tags,
score: course.similarityScore,
reason: '相似用户也在学习'
}));
}
private async recommendByKnowledgeGraph(request: RecommendationRequest): Promise<RecommendationResult[]> {
// 基于知识图谱的推荐
// 分析用户已完成的课程,推荐相关的进阶内容
const completedTopics = await this.extractTopicsFromCompletedLessons(request.completedLessons);
const relatedTopics = await this.findRelatedTopics(completedTopics);
const recommendedCourses = await this.getCoursesByTopics(relatedTopics);
return recommendedCourses.map(course => ({
courseId: course.id,
title: course.title,
description: course.description,
level: course.level,
difficulty: course.difficulty,
estimatedDuration: course.estimatedDuration,
tags: course.tags,
score: course.relevanceScore,
reason: '基于知识关联推荐'
}));
}
private mergeAndRankRecommendations(
recommendations: RecommendationResult[],
request: RecommendationRequest
): RecommendationResult[] {
// 去重
const uniqueRecommendations = new Map<string, RecommendationResult>();
recommendations.forEach(rec => {
const existing = uniqueRecommendations.get(rec.courseId);
if (!existing || rec.score > existing.score) {
uniqueRecommendations.set(rec.courseId, rec);
}
});
// 计算综合分数
const rankedRecommendations = Array.from(uniqueRecommendations.values()).map(rec => {
let finalScore = rec.score;
// 根据用户水平调整分数
const levelMatch = this.calculateLevelMatch(rec.difficulty, request.userLevel);
finalScore *= levelMatch;
// 根据用户偏好调整分数
const preferenceMatch = this.calculatePreferenceMatch(rec, request.preferences);
finalScore *= preferenceMatch;
return {
...rec,
score: finalScore
};
});
// 排序并返回前20个推荐
return rankedRecommendations
.sort((a, b) => b.score - a.score)
.slice(0, 20);
}
private calculateLevelMatch(courseDifficulty: number, userLevel: number): number {
const diff = Math.abs(courseDifficulty - userLevel);
if (diff === 0) return 1.0;
if (diff === 1) return 0.8;
if (diff === 2) return 0.6;
return 0.3;
}
private calculatePreferenceMatch(course: RecommendationResult, preferences: any): number {
if (!preferences) return 1.0;
let match = 1.0;
// 检查时长偏好
if (preferences.maxDuration && course.estimatedDuration > preferences.maxDuration) {
match *= 0.5;
}
// 检查标签偏好
if (preferences.preferredTags) {
const tagMatch = course.tags.some(tag => preferences.preferredTags.includes(tag));
if (tagMatch) match *= 1.2;
}
return match;
}
private async findSimilarUsers(userId: string): Promise<string[]> {
// 简化的相似用户查找
// 在实际应用中,这里会使用更复杂的算法
return [];
}
private async getCoursesFromSimilarUsers(similarUsers: string[]): Promise<any[]> {
// 从相似用户获取课程推荐
return [];
}
private async extractTopicsFromCompletedLessons(completedLessons: string[]): Promise<string[]> {
// 从已完成的课程中提取主题
return [];
}
private async findRelatedTopics(topics: string[]): Promise<string[]> {
// 查找相关主题
return [];
}
private async getCoursesByTopics(topics: string[]): Promise<any[]> {
// 根据主题获取课程
return [];
}
async addLearningContent(content: LearningContent): Promise<void> {
await this.vectorStore.addDocuments([{
pageContent: content.content,
metadata: {
id: content.id,
title: content.title,
description: content.description,
type: content.type,
difficulty: content.difficulty,
tags: content.tags,
...content.metadata
}
}]);
}
async updateUserPreferences(userId: string, preferences: any): Promise<void> {
// 更新用户偏好,用于改进推荐
// 这里可以存储到数据库或缓存中
}
}
自适应学习工作流
typescript
// src/services/workflow/adaptive-learning-workflow.ts
import { StateGraph, END } from '@langchain/langgraph';
import { KnowledgeGraphService } from '../knowledge/knowledge-graph';
import { RecommendationEngine } from '../recommendation/recommendation-engine';
export interface AdaptiveLearningState {
userId: string;
currentLevel: number;
goals: string[];
interests: string[];
completedLessons: string[];
currentLesson?: string;
assessmentResults?: any[];
learningPath?: any[];
nextRecommendations?: any[];
status: 'assessing' | 'planning' | 'learning' | 'assessing_progress' | 'adapting' | 'completed' | 'failed';
progress: number;
error?: string;
}
export class AdaptiveLearningWorkflow {
private knowledgeGraph: KnowledgeGraphService;
private recommendationEngine: RecommendationEngine;
constructor() {
this.knowledgeGraph = new KnowledgeGraphService();
this.recommendationEngine = new RecommendationEngine();
}
async buildGraph() {
const workflow = new StateGraph<AdaptiveLearningState>({
channels: {
userId: { value: '' },
currentLevel: { value: 1 },
goals: { value: [] },
interests: { value: [] },
completedLessons: { value: [] },
currentLesson: { value: '' },
assessmentResults: { value: [] },
learningPath: { value: [] },
nextRecommendations: { value: [] },
status: { value: 'assessing' },
progress: { value: 0 },
error: { value: '' }
}
});
// 添加节点
workflow.addNode('assess', this.assessUserLevel.bind(this));
workflow.addNode('plan', this.planLearningPath.bind(this));
workflow.addNode('recommend', this.recommendContent.bind(this));
workflow.addNode('adapt', this.adaptLearningPath.bind(this));
workflow.addNode('complete', this.completeLearning.bind(this));
workflow.addNode('fail', this.handleFailure.bind(this));
// 添加边
workflow.addEdge('start', 'assess');
workflow.addConditionalEdges('assess', this.checkAssessmentResult.bind(this));
workflow.addConditionalEdges('plan', this.checkPlanningResult.bind(this));
workflow.addConditionalEdges('recommend', this.checkRecommendationResult.bind(this));
workflow.addConditionalEdges('adapt', this.checkAdaptationResult.bind(this));
workflow.addEdge('complete', END);
workflow.addEdge('fail', END);
return workflow.compile();
}
private async assessUserLevel(state: AdaptiveLearningState): Promise<Partial<AdaptiveLearningState>> {
try {
// 1. 分析用户已完成的课程
const completedAnalysis = await this.analyzeCompletedLessons(state.completedLessons);
// 2. 进行能力评估测试
const assessmentResults = await this.runAssessmentTests(state.userId);
// 3. 计算用户当前水平
const currentLevel = await this.calculateUserLevel(completedAnalysis, assessmentResults);
return {
status: 'planning',
currentLevel,
assessmentResults,
progress: 20
};
} catch (error) {
return {
status: 'failed',
error: error.message,
progress: 0
};
}
}
private async planLearningPath(state: AdaptiveLearningState): Promise<Partial<AdaptiveLearningState>> {
try {
// 1. 基于用户目标和当前水平规划学习路径
const learningPath = await this.knowledgeGraph.findLearningPath(
'beginner', // 起始节点
state.goals[0] || 'advanced', // 目标节点
state.currentLevel
);
// 2. 优化学习路径
const optimizedPath = await this.optimizeLearningPath(learningPath, state);
return {
status: 'learning',
learningPath: optimizedPath,
progress: 40
};
} catch (error) {
return {
status: 'failed',
error: error.message,
progress: 20
};
}
}
private async recommendContent(state: AdaptiveLearningState): Promise<Partial<AdaptiveLearningState>> {
try {
// 1. 基于当前学习状态推荐内容
const recommendations = await this.recommendationEngine.recommendCourses({
userId: state.userId,
userLevel: state.currentLevel,
interests: state.interests,
goals: state.goals,
completedLessons: state.completedLessons
});
// 2. 个性化调整推荐
const personalizedRecommendations = await this.personalizeRecommendations(recommendations, state);
return {
status: 'assessing_progress',
nextRecommendations: personalizedRecommendations,
progress: 60
};
} catch (error) {
return {
status: 'failed',
error: error.message,
progress: 40
};
}
}
private async adaptLearningPath(state: AdaptiveLearningState): Promise<Partial<AdaptiveLearningState>> {
try {
// 1. 分析学习进度和效果
const progressAnalysis = await this.analyzeLearningProgress(state);
// 2. 根据分析结果调整学习路径
const adaptedPath = await this.adjustLearningPath(state.learningPath, progressAnalysis);
// 3. 更新推荐内容
const updatedRecommendations = await this.updateRecommendations(state, progressAnalysis);
return {
status: 'learning',
learningPath: adaptedPath,
nextRecommendations: updatedRecommendations,
progress: 80
};
} catch (error) {
return {
status: 'failed',
error: error.message,
progress: 60
};
}
}
private async completeLearning(state: AdaptiveLearningState): Promise<Partial<AdaptiveLearningState>> {
return {
status: 'completed',
progress: 100
};
}
private async handleFailure(state: AdaptiveLearningState): Promise<Partial<AdaptiveLearningState>> {
return {
status: 'failed',
progress: 0
};
}
private checkAssessmentResult(state: AdaptiveLearningState): string {
return state.status === 'failed' ? 'fail' : 'plan';
}
private checkPlanningResult(state: AdaptiveLearningState): string {
return state.status === 'failed' ? 'fail' : 'recommend';
}
private checkRecommendationResult(state: AdaptiveLearningState): string {
return state.status === 'failed' ? 'fail' : 'adapt';
}
private checkAdaptationResult(state: AdaptiveLearningState): string {
if (state.status === 'failed') return 'fail';
if (state.progress >= 100) return 'complete';
return 'recommend'; // 继续推荐新内容
}
// 辅助方法
private async analyzeCompletedLessons(completedLessons: string[]): Promise<any> {
// 分析已完成的课程,提取学习模式和能力
return {};
}
private async runAssessmentTests(userId: string): Promise<any[]> {
// 运行能力评估测试
return [];
}
private async calculateUserLevel(completedAnalysis: any, assessmentResults: any[]): Promise<number> {
// 基于完成课程和测试结果计算用户水平
return 1;
}
private async optimizeLearningPath(learningPath: any, state: AdaptiveLearningState): Promise<any> {
// 优化学习路径,考虑用户偏好和学习风格
return learningPath;
}
private async personalizeRecommendations(recommendations: any[], state: AdaptiveLearningState): Promise<any[]> {
// 个性化调整推荐内容
return recommendations;
}
private async analyzeLearningProgress(state: AdaptiveLearningState): Promise<any> {
// 分析学习进度和效果
return {};
}
private async adjustLearningPath(learningPath: any, progressAnalysis: any): Promise<any> {
// 根据进度分析调整学习路径
return learningPath;
}
private async updateRecommendations(state: AdaptiveLearningState, progressAnalysis: any): Promise<any[]> {
// 更新推荐内容
return state.nextRecommendations || [];
}
}
🌐 Next.js API 路由实现
学习管理 API
typescript
// src/app/api/learning/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { AdaptiveLearningWorkflow } from '@/services/workflow/adaptive-learning-workflow';
import { prisma } from '@/lib/db';
export async function POST(request: NextRequest) {
try {
const { userId, goals, interests, currentLevel } = await request.json();
if (!userId || !goals || !interests) {
return NextResponse.json(
{ error: '缺少必要参数' },
{ status: 400 }
);
}
// 获取用户已完成课程
const completedLessons = await prisma.learningProgress.findMany({
where: {
userId,
status: 'COMPLETED'
},
select: {
lessonId: true
}
});
// 启动自适应学习工作流
const workflow = new AdaptiveLearningWorkflow();
const graph = await workflow.buildGraph();
const result = await graph.invoke({
userId,
currentLevel: currentLevel || 1,
goals,
interests,
completedLessons: completedLessons.map(c => c.lessonId),
status: 'assessing',
progress: 0
});
return NextResponse.json({
success: true,
learningPath: result.learningPath,
recommendations: result.nextRecommendations,
currentLevel: result.currentLevel
});
} catch (error) {
console.error('学习路径生成失败:', error);
return NextResponse.json(
{ error: '学习路径生成失败' },
{ status: 500 }
);
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get('userId');
if (!userId) {
return NextResponse.json(
{ error: '缺少用户ID' },
{ status: 400 }
);
}
// 获取用户学习进度
const progress = await prisma.learningProgress.findMany({
where: { userId },
include: {
lesson: {
include: {
module: {
include: {
course: true
}
}
}
}
}
});
// 计算学习统计
const stats = {
totalLessons: progress.length,
completedLessons: progress.filter(p => p.status === 'COMPLETED').length,
inProgressLessons: progress.filter(p => p.status === 'IN_PROGRESS').length,
totalTimeSpent: progress.reduce((total, p) => total + p.timeSpent, 0),
averageProgress: progress.reduce((avg, p) => avg + p.progress, 0) / progress.length
};
return NextResponse.json({
success: true,
progress,
stats
});
} catch (error) {
console.error('获取学习进度失败:', error);
return NextResponse.json(
{ error: '获取学习进度失败' },
{ status: 500 }
);
}
}
推荐系统 API
typescript
// src/app/api/recommendation/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { RecommendationEngine } from '@/services/recommendation/recommendation-engine';
export async function POST(request: NextRequest) {
try {
const { userId, userLevel, interests, goals, completedLessons, preferences } = await request.json();
if (!userId || !userLevel || !interests || !goals) {
return NextResponse.json(
{ error: '缺少必要参数' },
{ status: 400 }
);
}
const recommendationEngine = new RecommendationEngine();
const recommendations = await recommendationEngine.recommendCourses({
userId,
userLevel,
interests,
goals,
completedLessons,
preferences
});
return NextResponse.json({
success: true,
recommendations
});
} catch (error) {
console.error('推荐生成失败:', error);
return NextResponse.json(
{ error: '推荐生成失败' },
{ status: 500 }
);
}
}
知识图谱 API
typescript
// src/app/api/knowledge-graph/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { KnowledgeGraphService } from '@/services/knowledge/knowledge-graph';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const nodeId = searchParams.get('nodeId');
const action = searchParams.get('action');
const knowledgeGraph = new KnowledgeGraphService();
switch (action) {
case 'prerequisites':
if (!nodeId) {
return NextResponse.json(
{ error: '缺少节点ID' },
{ status: 400 }
);
}
const prerequisites = await knowledgeGraph.getPrerequisites(nodeId);
return NextResponse.json({
success: true,
prerequisites
});
case 'related':
if (!nodeId) {
return NextResponse.json(
{ error: '缺少节点ID' },
{ status: 400 }
);
}
const relatedNodes = await knowledgeGraph.getRelatedNodes(nodeId);
return NextResponse.json({
success: true,
relatedNodes
});
case 'path':
const startNodeId = searchParams.get('startNodeId');
const endNodeId = searchParams.get('endNodeId');
const userLevel = parseInt(searchParams.get('userLevel') || '1');
if (!startNodeId || !endNodeId) {
return NextResponse.json(
{ error: '缺少起始或目标节点ID' },
{ status: 400 }
);
}
const learningPath = await knowledgeGraph.findLearningPath(
startNodeId,
endNodeId,
userLevel
);
return NextResponse.json({
success: true,
learningPath
});
case 'stats':
const stats = await knowledgeGraph.getKnowledgeGraphStats();
return NextResponse.json({
success: true,
stats
});
default:
return NextResponse.json(
{ error: '无效的操作' },
{ status: 400 }
);
}
} catch (error) {
console.error('知识图谱操作失败:', error);
return NextResponse.json(
{ error: '知识图谱操作失败' },
{ status: 500 }
);
} finally {
// 注意:在实际应用中,应该使用连接池管理数据库连接
}
}
export async function POST(request: NextRequest) {
try {
const { action, data } = await request.json();
const knowledgeGraph = new KnowledgeGraphService();
switch (action) {
case 'create_node':
const node = await knowledgeGraph.createNode(data);
return NextResponse.json({
success: true,
node
});
case 'create_relationship':
const relationship = await knowledgeGraph.createRelationship(
data.fromNodeId,
data.toNodeId,
data.type,
data.weight
);
return NextResponse.json({
success: true,
relationship
});
case 'update_difficulty':
await knowledgeGraph.updateNodeDifficulty(data.nodeId, data.newDifficulty);
return NextResponse.json({
success: true
});
default:
return NextResponse.json(
{ error: '无效的操作' },
{ status: 400 }
);
}
} catch (error) {
console.error('知识图谱操作失败:', error);
return NextResponse.json(
{ error: '知识图谱操作失败' },
{ status: 500 }
);
}
}
评估系统 API
typescript
// src/app/api/assessment/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { prisma } from '@/lib/db';
export async function POST(request: NextRequest) {
try {
const { userId, lessonId, type, answers } = await request.json();
if (!userId || !type || !answers) {
return NextResponse.json(
{ error: '缺少必要参数' },
{ status: 400 }
);
}
// 使用 AI 评估答案
const model = new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: 'gpt-3.5-turbo',
temperature: 0.1
});
const promptTemplate = PromptTemplate.fromTemplate(`
请评估以下学习者的答案:
评估类型:{type}
学习者答案:{answers}
请从以下方面进行评估:
1. 准确性(0-100分)
2. 完整性(0-100分)
3. 理解深度(0-100分)
4. 创新性(0-100分)
请提供:
1. 总体评分(0-100分)
2. 详细反馈
3. 改进建议
4. 知识点掌握情况
请按照以下 JSON 格式输出:
{
"score": 85,
"accuracy": 90,
"completeness": 80,
"understanding": 85,
"creativity": 75,
"feedback": "答案整体正确,但缺少一些细节...",
"suggestions": ["建议加强...", "可以尝试..."],
"knowledgePoints": ["知识点1", "知识点2"]
}
`);
const chain = promptTemplate.pipe(model);
const response = await chain.invoke({
type,
answers: JSON.stringify(answers)
});
const assessmentResult = JSON.parse(response.content);
// 保存评估结果
const assessment = await prisma.assessment.create({
data: {
type: type.toUpperCase(),
score: assessmentResult.score,
answers: answers,
feedback: assessmentResult.feedback,
completedAt: new Date(),
userId,
lessonId
}
});
// 更新学习进度
if (lessonId) {
await prisma.learningProgress.upsert({
where: {
userId_lessonId: {
userId,
lessonId
}
},
update: {
progress: Math.max(assessmentResult.score, 0),
status: assessmentResult.score >= 70 ? 'COMPLETED' : 'IN_PROGRESS',
lastAccessed: new Date()
},
create: {
userId,
lessonId,
progress: assessmentResult.score,
status: assessmentResult.score >= 70 ? 'COMPLETED' : 'IN_PROGRESS',
timeSpent: 0
}
});
}
return NextResponse.json({
success: true,
assessment: {
id: assessment.id,
score: assessmentResult.score,
feedback: assessmentResult.feedback,
suggestions: assessmentResult.suggestions,
knowledgePoints: assessmentResult.knowledgePoints
}
});
} catch (error) {
console.error('评估失败:', error);
return NextResponse.json(
{ error: '评估失败' },
{ status: 500 }
);
}
}
🎨 前端界面实现
学习界面组件
tsx
// src/components/learning/LearningInterface.tsx
'use client';
import { useState, useEffect } from 'react';
import { Play, Pause, SkipForward, BookOpen, Target, Clock } from 'lucide-react';
interface LearningInterfaceProps {
lessonId: string;
userId: string;
}
interface Lesson {
id: string;
title: string;
content: string;
type: string;
duration: number;
difficulty: number;
}
interface LearningProgress {
progress: number;
timeSpent: number;
status: string;
}
export default function LearningInterface({ lessonId, userId }: LearningInterfaceProps) {
const [lesson, setLesson] = useState<Lesson | null>(null);
const [progress, setProgress] = useState<LearningProgress | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [answers, setAnswers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchLessonData();
}, [lessonId]);
const fetchLessonData = async () => {
try {
const response = await fetch(`/api/lessons/${lessonId}`);
const data = await response.json();
if (data.success) {
setLesson(data.lesson);
setProgress(data.progress);
}
} catch (error) {
console.error('获取课程数据失败:', error);
} finally {
setLoading(false);
}
};
const handlePlayPause = () => {
setIsPlaying(!isPlaying);
};
const handleAnswerSubmit = async (answers: any[]) => {
try {
const response = await fetch('/api/assessment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
lessonId,
type: 'quiz',
answers
}),
});
const data = await response.json();
if (data.success) {
setProgress(prev => ({
...prev!,
progress: data.assessment.score,
status: data.assessment.score >= 70 ? 'COMPLETED' : 'IN_PROGRESS'
}));
}
} catch (error) {
console.error('提交答案失败:', error);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
);
}
if (!lesson) {
return (
<div className="text-center py-8">
<p className="text-gray-500">课程不存在</p>
</div>
);
}
return (
<div className="max-w-4xl mx-auto p-6">
{/* 课程头部 */}
<div className="bg-white rounded-lg shadow-sm border p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold text-gray-900">{lesson.title}</h1>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 text-gray-600">
<Clock className="w-4 h-4" />
<span>{lesson.duration} 分钟</span>
</div>
<div className="flex items-center space-x-2 text-gray-600">
<Target className="w-4 h-4" />
<span>难度 {lesson.difficulty}/5</span>
</div>
</div>
</div>
{/* 进度条 */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">学习进度</span>
<span className="text-sm text-gray-500">{progress?.progress || 0}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress?.progress || 0}%` }}
></div>
</div>
</div>
{/* 播放控制 */}
<div className="flex items-center space-x-4">
<button
onClick={handlePlayPause}
className="flex items-center justify-center w-12 h-12 bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-colors"
>
{isPlaying ? <Pause className="w-6 h-6" /> : <Play className="w-6 h-6" />}
</button>
<div className="flex-1">
<div className="flex items-center justify-between text-sm text-gray-500 mb-1">
<span>{Math.floor(currentTime / 60)}:{(currentTime % 60).toString().padStart(2, '0')}</span>
<span>{Math.floor(lesson.duration)}:00</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1">
<div
className="bg-gray-400 h-1 rounded-full transition-all duration-300"
style={{ width: `${(currentTime / (lesson.duration * 60)) * 100}%` }}
></div>
</div>
</div>
<button className="flex items-center space-x-2 text-gray-600 hover:text-gray-800">
<SkipForward className="w-5 h-5" />
<span className="text-sm">下一课</span>
</button>
</div>
</div>
{/* 课程内容 */}
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="prose max-w-none">
<div dangerouslySetInnerHTML={{ __html: lesson.content }} />
</div>
</div>
{/* 交互组件 */}
{lesson.type === 'QUIZ' && (
<div className="mt-6 bg-white rounded-lg shadow-sm border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">测验</h3>
{/* 这里可以添加具体的测验组件 */}
</div>
)}
{lesson.type === 'EXERCISE' && (
<div className="mt-6 bg-white rounded-lg shadow-sm border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">练习</h3>
{/* 这里可以添加具体的练习组件 */}
</div>
)}
</div>
);
}
知识图谱可视化组件
tsx
// src/components/visualization/KnowledgeGraphVisualization.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
interface KnowledgeNode {
id: string;
title: string;
type: string;
difficulty: number;
x?: number;
y?: number;
}
interface KnowledgeRelationship {
id: string;
type: string;
weight: number;
fromNodeId: string;
toNodeId: string;
}
interface KnowledgeGraphVisualizationProps {
nodes: KnowledgeNode[];
relationships: KnowledgeRelationship[];
onNodeClick?: (node: KnowledgeNode) => void;
onNodeHover?: (node: KnowledgeNode) => void;
}
export default function KnowledgeGraphVisualization({
nodes,
relationships,
onNodeClick,
onNodeHover
}: KnowledgeGraphVisualizationProps) {
const svgRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
useEffect(() => {
if (!svgRef.current || nodes.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
const width = dimensions.width;
const height = dimensions.height;
// 创建力导向图
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(relationships).id((d: any) => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(30));
// 创建连接线
const link = svg.append('g')
.selectAll('line')
.data(relationships)
.enter().append('line')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', (d: any) => Math.sqrt(d.weight) * 2);
// 创建节点
const node = svg.append('g')
.selectAll('circle')
.data(nodes)
.enter().append('circle')
.attr('r', (d: any) => Math.max(10, d.difficulty * 3))
.attr('fill', (d: any) => {
const colors = {
'CONCEPT': '#3B82F6',
'SKILL': '#10B981',
'TOPIC': '#F59E0B',
'SUBJECT': '#EF4444'
};
return colors[d.type as keyof typeof colors] || '#6B7280';
})
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.style('cursor', 'pointer')
.on('click', (event, d) => {
onNodeClick?.(d);
})
.on('mouseover', (event, d) => {
onNodeHover?.(d);
d3.select(event.currentTarget)
.attr('stroke', '#000')
.attr('stroke-width', 3);
})
.on('mouseout', (event, d) => {
d3.select(event.currentTarget)
.attr('stroke', '#fff')
.attr('stroke-width', 2);
});
// 添加标签
const label = svg.append('g')
.selectAll('text')
.data(nodes)
.enter().append('text')
.text((d: any) => d.title)
.attr('font-size', '12px')
.attr('font-family', 'Arial, sans-serif')
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.style('pointer-events', 'none');
// 更新位置
simulation.on('tick', () => {
link
.attr('x1', (d: any) => d.source.x)
.attr('y1', (d: any) => d.source.y)
.attr('x2', (d: any) => d.target.x)
.attr('y2', (d: any) => d.target.y);
node
.attr('cx', (d: any) => d.x)
.attr('cy', (d: any) => d.y);
label
.attr('x', (d: any) => d.x)
.attr('y', (d: any) => d.y);
});
// 添加缩放功能
const zoom = d3.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
svg.selectAll('g').attr('transform', event.transform);
});
svg.call(zoom);
return () => {
simulation.stop();
};
}, [nodes, relationships, dimensions]);
const handleResize = () => {
if (svgRef.current) {
const rect = svgRef.current.getBoundingClientRect();
setDimensions({ width: rect.width, height: rect.height });
}
};
useEffect(() => {
window.addEventListener('resize', handleResize);
handleResize();
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div className="w-full h-full">
<svg
ref={svgRef}
width={dimensions.width}
height={dimensions.height}
className="border rounded-lg"
/>
{/* 图例 */}
<div className="mt-4 flex items-center space-x-4 text-sm">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span>概念</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span>技能</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
<span>主题</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<span>学科</span>
</div>
</div>
</div>
);
}
学习进度跟踪组件
tsx
// src/components/learning/ProgressTracker.tsx
'use client';
import { useState, useEffect } from 'react';
import { TrendingUp, Clock, Target, Award } from 'lucide-react';
interface ProgressTrackerProps {
userId: string;
}
interface LearningStats {
totalLessons: number;
completedLessons: number;
inProgressLessons: number;
totalTimeSpent: number;
averageProgress: number;
}
interface ProgressData {
date: string;
progress: number;
timeSpent: number;
}
export default function ProgressTracker({ userId }: ProgressTrackerProps) {
const [stats, setStats] = useState<LearningStats | null>(null);
const [progressData, setProgressData] = useState<ProgressData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProgressData();
}, [userId]);
const fetchProgressData = async () => {
try {
const response = await fetch(`/api/learning?userId=${userId}`);
const data = await response.json();
if (data.success) {
setStats(data.stats);
setProgressData(data.progressData || []);
}
} catch (error) {
console.error('获取进度数据失败:', error);
} finally {
setLoading(false);
}
};
const formatTime = (minutes: number): string => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}小时${mins}分钟`;
};
const getProgressColor = (progress: number): string => {
if (progress >= 80) return 'text-green-600';
if (progress >= 60) return 'text-yellow-600';
return 'text-red-600';
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
);
}
if (!stats) {
return (
<div className="text-center py-8">
<p className="text-gray-500">暂无学习数据</p>
</div>
);
}
return (
<div className="space-y-6">
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<Target className="w-6 h-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">总课程</p>
<p className="text-2xl font-bold text-gray-900">{stats.totalLessons}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<Award className="w-6 h-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">已完成</p>
<p className="text-2xl font-bold text-gray-900">{stats.completedLessons}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 rounded-lg">
<TrendingUp className="w-6 h-6 text-yellow-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">进行中</p>
<p className="text-2xl font-bold text-gray-900">{stats.inProgressLessons}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="flex items-center">
<div className="p-2 bg-purple-100 rounded-lg">
<Clock className="w-6 h-6 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">学习时长</p>
<p className="text-2xl font-bold text-gray-900">{formatTime(stats.totalTimeSpent)}</p>
</div>
</div>
</div>
</div>
{/* 进度概览 */}
<div className="bg-white rounded-lg shadow-sm border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">学习进度概览</h3>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">整体进度</span>
<span className={`text-sm font-medium ${getProgressColor(stats.averageProgress)}`}>
{stats.averageProgress.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${stats.averageProgress}%` }}
></div>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">完成率</span>
<span className={`text-sm font-medium ${getProgressColor((stats.completedLessons / stats.totalLessons) * 100)}`}>
{((stats.completedLessons / stats.totalLessons) * 100).toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(stats.completedLessons / stats.totalLessons) * 100}%` }}
></div>
</div>
</div>
</div>
</div>
{/* 学习趋势 */}
{progressData.length > 0 && (
<div className="bg-white rounded-lg shadow-sm border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">学习趋势</h3>
<div className="space-y-3">
{progressData.slice(-7).map((data, index) => (
<div key={index} className="flex items-center justify-between">
<span className="text-sm text-gray-600">{data.date}</span>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-500">{formatTime(data.timeSpent)}</span>
<div className="w-32 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${data.progress}%` }}
></div>
</div>
<span className="text-sm font-medium text-gray-700 w-12 text-right">
{data.progress.toFixed(0)}%
</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
🐳 Docker 部署配置
Docker Compose 配置
yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@postgres:5432/learning_platform
- NEO4J_URI=bolt://neo4j:7687
- NEO4J_USERNAME=neo4j
- NEO4J_PASSWORD=password
- OPENAI_API_KEY=${OPENAI_API_KEY}
- CHROMA_URL=http://chroma:8000
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- neo4j
- chroma
- redis
volumes:
- ./uploads:/app/uploads
postgres:
image: postgres:15
environment:
- POSTGRES_DB=learning_platform
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
neo4j:
image: neo4j:5.0
environment:
- NEO4J_AUTH=neo4j/password
- NEO4J_PLUGINS=["apoc"]
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
ports:
- "7474:7474"
- "7687:7687"
chroma:
image: chromadb/chroma:latest
ports:
- "8000:8000"
volumes:
- chroma_data:/chroma/chroma
environment:
- CHROMA_SERVER_HOST=0.0.0.0
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
volumes:
postgres_data:
neo4j_data:
neo4j_logs:
chroma_data:
redis_data:
📊 性能优化与监控
学习数据缓存
typescript
// src/lib/learning-cache.ts
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
export class LearningCache {
async getUserProgress(userId: string): Promise<any | null> {
try {
const cached = await redis.get(`user_progress:${userId}`);
return cached ? JSON.parse(cached) : null;
} catch (error) {
console.error('获取用户进度缓存失败:', error);
return null;
}
}
async setUserProgress(userId: string, progress: any, ttl: number = 1800): Promise<void> {
try {
await redis.setex(`user_progress:${userId}`, ttl, JSON.stringify(progress));
} catch (error) {
console.error('设置用户进度缓存失败:', error);
}
}
async getRecommendations(userId: string): Promise<any | null> {
try {
const cached = await redis.get(`recommendations:${userId}`);
return cached ? JSON.parse(cached) : null;
} catch (error) {
console.error('获取推荐缓存失败:', error);
return null;
}
}
async setRecommendations(userId: string, recommendations: any, ttl: number = 3600): Promise<void> {
try {
await redis.setex(`recommendations:${userId}`, ttl, JSON.stringify(recommendations));
} catch (error) {
console.error('设置推荐缓存失败:', error);
}
}
async getKnowledgeGraph(nodeId: string): Promise<any | null> {
try {
const cached = await redis.get(`knowledge_graph:${nodeId}`);
return cached ? JSON.parse(cached) : null;
} catch (error) {
console.error('获取知识图谱缓存失败:', error);
return null;
}
}
async setKnowledgeGraph(nodeId: string, graphData: any, ttl: number = 7200): Promise<void> {
try {
await redis.setex(`knowledge_graph:${nodeId}`, ttl, JSON.stringify(graphData));
} catch (error) {
console.error('设置知识图谱缓存失败:', error);
}
}
async invalidateUserCache(userId: string): Promise<void> {
try {
await redis.del(`user_progress:${userId}`);
await redis.del(`recommendations:${userId}`);
} catch (error) {
console.error('清除用户缓存失败:', error);
}
}
}
🧪 测试策略
知识图谱测试
typescript
// src/services/knowledge/__tests__/knowledge-graph.test.ts
import { KnowledgeGraphService } from '../knowledge-graph';
describe('KnowledgeGraphService', () => {
let knowledgeGraph: KnowledgeGraphService;
beforeEach(() => {
knowledgeGraph = new KnowledgeGraphService();
});
describe('createNode', () => {
it('should create a knowledge node correctly', async () => {
const nodeData = {
title: 'JavaScript Basics',
description: 'Fundamental concepts of JavaScript',
type: 'CONCEPT',
difficulty: 2,
prerequisites: [],
metadata: { tags: ['programming', 'javascript'] }
};
const node = await knowledgeGraph.createNode(nodeData);
expect(node.id).toBeDefined();
expect(node.title).toBe(nodeData.title);
expect(node.type).toBe(nodeData.type);
expect(node.difficulty).toBe(nodeData.difficulty);
});
});
describe('findLearningPath', () => {
it('should find a learning path between nodes', async () => {
// 创建测试节点
const startNode = await knowledgeGraph.createNode({
title: 'HTML Basics',
type: 'CONCEPT',
difficulty: 1,
prerequisites: []
});
const endNode = await knowledgeGraph.createNode({
title: 'Web Development',
type: 'SKILL',
difficulty: 3,
prerequisites: ['HTML Basics']
});
// 创建关系
await knowledgeGraph.createRelationship(
startNode.id,
endNode.id,
'PREREQUISITE',
1.0
);
const learningPath = await knowledgeGraph.findLearningPath(
startNode.id,
endNode.id,
1
);
expect(learningPath.nodes).toHaveLength(2);
expect(learningPath.relationships).toHaveLength(1);
expect(learningPath.estimatedDuration).toBeGreaterThan(0);
});
});
});
推荐引擎测试
typescript
// src/services/recommendation/__tests__/recommendation-engine.test.ts
import { RecommendationEngine } from '../recommendation-engine';
describe('RecommendationEngine', () => {
let recommendationEngine: RecommendationEngine;
beforeEach(() => {
recommendationEngine = new RecommendationEngine();
});
describe('recommendCourses', () => {
it('should recommend courses based on user interests', async () => {
const request = {
userId: 'test-user',
userLevel: 2,
interests: ['programming', 'web development'],
goals: ['become a frontend developer'],
completedLessons: [],
preferences: { maxDuration: 120 }
};
const recommendations = await recommendationEngine.recommendCourses(request);
expect(Array.isArray(recommendations)).toBe(true);
expect(recommendations.length).toBeGreaterThan(0);
recommendations.forEach(rec => {
expect(rec.courseId).toBeDefined();
expect(rec.title).toBeDefined();
expect(rec.score).toBeGreaterThan(0);
expect(rec.reason).toBeDefined();
});
});
});
});
📚 本章总结
通过本章学习,我们完成了:
✅ 系统架构设计
- 设计了完整的个性化学习助手平台架构
- 实现了知识图谱构建和智能推荐功能
- 集成了 LangGraph 工作流自适应学习系统
✅ 技术实现
- 使用 Neo4j 构建知识图谱和关系网络
- 基于 LangChain.js 构建智能推荐和评估系统
- 搭建了 Next.js 全栈应用和现代化学习界面
✅ 工程实践
- 配置了 Docker 容器化部署
- 实现了学习数据缓存和性能优化
- 建立了完整的测试体系
✅ 生产就绪
- 实现了自适应学习算法和个性化推荐
- 添加了学习进度监控和效果评估
- 提供了完整的部署和运维方案
🎯 下章预告
在下一章《AI 应用安全与伦理实践》中,我们将:
- 探讨 AI 应用的安全风险和防护策略
- 实现数据隐私保护和合规性管理
- 建立 AI 伦理框架和负责任开发实践
- 开发安全审计和风险评估工具
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!