LangChain.js 完全开发手册(十七)实战综合项目三:个性化学习助手平台

第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 伦理框架和负责任开发实践
  • 开发安全审计和风险评估工具

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

相关推荐
mxpan5 小时前
从 0 到 1:用 Python 对接阿里云 DashScope,轻松实现 AI 对话功能
python·ai编程
飞哥数智坊7 小时前
Claude Code 网页版上线,让我更确信:AI 编程需要“少干预”
人工智能·ai编程·claude
后台开发者Ethan16 小时前
FastAPI之 Python的类型提示
python·fastapi·ai编程
bst@微胖子16 小时前
Langchain之Agent代理的使用
langchain
vipbic18 小时前
使用Cursor开发Strapi5插件bag-strapi-plugin
前端·ai编程·cursor
猫头虎20 小时前
openAI发布的AI浏览器:什么是Atlas?(含 ChatGPT 浏览功能)macOS 离线下载安装Atlas完整教程
人工智能·macos·chatgpt·langchain·prompt·aigc·agi
算家计算21 小时前
快手推出“工具+模型+平台”AI编程生态!大厂挤占AI赛道,中小企业如何突围?
人工智能·ai编程·资讯
孟健21 小时前
AI 浏览器大战打响:手把手白嫖一个月 Perplexity 会员(Comet 实操 5 步)
ai编程
该用户已不存在1 天前
Gemini CLI 扩展,把Nano Banana 搬到终端
前端·后端·ai编程