Trong 3 năm xây dựng hệ thống gợi ý cho nền tảng edtech phục vụ 500K+ học sinh, tôi đã triển khai và vận hành nhiều phiên bản recommendation engine. Bài viết này là bản tổng hợp chi tiết về kiến trúc, code production-ready, và những bài học xương máu khi xây dựng 学生画像构建系统 (hệ thống xây dựng hồ sơ học sinh) cho nền tảng giáo dục AI.

Tại sao Student Profiling quan trọng trong EdTech

Mỗi học sinh có cách học khác nhau — người tiếp thu qua hình ảnh, người cần nhiều ví dụ thực tế, người học nhanh nhưng mau quên. Hệ thống gợi ý hiệu quả không chỉ đơn thuần lọc nội dung theo độ khó, mà phải hiểu sâu hành vi và nhu cầu của từng cá nhân.

Kiến trúc Tổng quan

┌─────────────────────────────────────────────────────────────────────┐
│                    STUDENT PROFILING ARCHITECTURE                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌──────────┐    ┌──────────────┐    ┌─────────────────────────┐    │
│  │  Web/App │───▶│ API Gateway  │───▶│ Student Profile Service │    │
│  │  Client  │    │  (Rate Limit)│    │  - Behavior Tracker     │    │
│  └──────────┘    └──────────────┘    │  - Learning Style Calc  │    │
│                                      │  - Knowledge Graph      │    │
│                                      └───────────┬─────────────┘    │
│                                                  │                   │
│                          ┌────────────────────────┼───────────────┐   │
│                          ▼                        ▼               ▼   │
│                  ┌──────────────┐      ┌──────────────┐  ┌─────────┐ │
│                  │  Vector DB   │      │  Analytics   │  │  Cache  │ │
│                  │ (Pinecone/   │      │   Pipeline    │  │ (Redis) │ │
│                  │  Milvus)     │      │              │  │         │ │
│                  └──────────────┘      └──────────────┘  └─────────┘ │
│                                                                      │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │                  RECOMMENDATION ENGINE                          │ │
│  │  ┌─────────┐  ┌──────────┐  ┌───────────┐  ┌────────────────┐  │ │
│  │  │ Filter  │─▶│  Rerank  │─▶│  Context  │─▶│  Final Output  │  │ │
│  │  │  Layer  │  │  Layer   │  │  Aware    │  │   + Logging    │  │ │
│  │  └─────────┘  └──────────┘  └───────────┘  └────────────────┘  │ │
│  └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

Triển khai Student Profile Service

1. Cấu trúc dữ liệu Student Profile

# models/student_profile.py
from pydantic import BaseModel, Field
from typing import List, Dict, Optional
from datetime import datetime
from enum import Enum

class LearningStyle(str, Enum):
    VISUAL = "visual"           # Học qua hình ảnh, biểu đồ
    AUDITORY = "auditory"       # Học qua âm thanh, ghi chép
    KINESTHETIC = "kinesthetic" # Học qua thực hành, trải nghiệm
    READING = "reading"         # Học qua văn bản, sách giáo khoa

class MasteryLevel(str, Enum):
    NOVICE = "novice"
    BEGINNER = "beginner"
    INTERMEDIATE = "intermediate"
    ADVANCED = "advanced"
    EXPERT = "expert"

class StudentProfile(BaseModel):
    student_id: str = Field(..., description="Định danh học sinh")
    
    # Demographics & Context
    grade_level: int = Field(..., ge=1, le=12)
    subjects: List[str] = Field(default_factory=list)
    enrollment_date: datetime
    
    # Learning Style Analysis (0.0 - 1.0)
    learning_style_scores: Dict[LearningStyle, float] = Field(
        default_factory=lambda: {style: 0.0 for style in LearningStyle}
    )
    dominant_learning_style: LearningStyle
    
    # Knowledge Graph
    knowledge_graph: Dict[str, Dict[str, float]] = Field(
        default_factory=dict,
        description="subject -> {concept: mastery_score}"
    )
    
    # Behavioral Metrics
    engagement_metrics: Dict[str, float] = Field(default_factory=dict)
    avg_session_duration: float = 0.0
    sessions_per_week: int = 0
    completion_rate: float = 0.0
    
    # Performance Analytics
    strength_areas: List[str] = Field(default_factory=list)
    improvement_areas: List[str] = Field(default_factory=list)
    recent_performance_trend: str  # "improving", "stable", "declining"
    
    # AI-Generated Insights
    ai_insights: Optional[Dict[str, str]] = Field(default_factory=None)
    
    # Timestamps
    last_activity: datetime
    profile_last_updated: datetime
    
    class Config:
        use_enum_values = True

2. AI-Powered Profile Analysis với HolySheep

Điểm mấu chốt của hệ thống là khả năng phân tích hành vi học tập và sinh ra insights chính xác. Tôi sử dụng HolySheep AI vì chi phí chỉ $0.42/MTok (so với $8 của GPT-4.1), phù hợp cho việc xử lý hàng triệu hồ sơ học sinh mà không lo về chi phí.

# services/profile_analysis.py
import httpx
import json
from typing import List, Dict, Optional
from datetime import datetime
import asyncio
from dataclasses import dataclass

@dataclass
class AnalysisResult:
    learning_style: str
    strengths: List[str]
    weaknesses: List[str]
    recommended_content_types: List[str]
    engagement_insights: str
    suggested_difficulty_adjustment: float
    confidence_score: float

class HolySheepProfileAnalyzer:
    """
    Sử dụng HolySheep AI để phân tích hồ sơ học sinh
    Chi phí: $0.42/MTok — tiết kiệm 85%+ so với OpenAI
    Độ trễ trung bình: <50ms với cấu hình optimized
    """
    
    BASE_URL = "https://api.holysheep.ai/v1"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.client = httpx.AsyncClient(
            timeout=30.0,
            limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
        )
    
    async def analyze_student_behavior(
        self,
        student_id: str,
        interaction_history: List[Dict],
        performance_data: Dict
    ) -> AnalysisResult:
        """
        Phân tích toàn diện hành vi học sinh
        """
        # Xây dựng prompt cho AI analysis
        analysis_prompt = self._build_analysis_prompt(
            interaction_history,
            performance_data
        )
        
        # Gọi HolySheep API — chi phí cực thấp
        response = await self._call_holysheep(analysis_prompt)
        
        # Parse và trả về structured result
        return self._parse_analysis_response(response)
    
    def _build_analysis_prompt(
        self,
        interactions: List[Dict],
        performance: Dict
    ) -> str:
        """
        Xây dựng prompt chi tiết cho việc phân tích học sinh
        """
        # Format interaction data
        interaction_summary = self._summarize_interactions(interactions)
        
        prompt = f"""Bạn là chuyên gia phân tích giáo dục. Phân tích hồ sơ học sinh sau:

Dữ liệu tương tác (30 ngày gần nhất):

{interaction_summary}

Dữ liệu hiệu suất:

- Điểm trung bình: {performance.get('avg_score', 'N/A')} - Tỷ lệ hoàn thành bài tập: {performance.get('completion_rate', 'N/A')}% - Số giờ học/tuần: {performance.get('hours_per_week', 'N/A')} - Các môn học yếu: {', '.join(performance.get('weak_subjects', []))} - Các môn học mạnh: {', '.join(performance.get('strong_subjects', []))}

Yêu cầu phân tích:

1. Xác định phong cách học tập chính (visual/auditory/kinesthetic/reading) 2. Liệt kê 3-5 điểm mạnh cần phát huy 3. Liệt kê 3-5 điểm cần cải thiện 4. Đề xuất loại nội dung phù hợp nhất (video/bài tập/sơ đồ/casestudy) 5. Mức độ điều chỉnh độ khó (-1.0 đến +1.0) 6. Insights về mức độ tương tác và động lực Trả lời theo format JSON: {{ "learning_style": "string", "strengths": ["string"], "weaknesses": ["string"], "recommended_content_types": ["string"], "engagement_insights": "string", "difficulty_adjustment": number, "confidence": number }}""" return prompt async def _call_holysheep(self, prompt: str) -> Dict: """ Gọi HolySheep API với retry logic và error handling """ payload = { "model": "deepseek-v3.2", # Model rẻ nhất, hiệu năng tốt "messages": [ {"role": "system", "content": "Bạn là chuyên gia giáo dục AI."}, {"role": "user", "content": prompt} ], "temperature": 0.3, # Low temperature cho analysis "max_tokens": 500, "stream": False } headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" } # Retry với exponential backoff for attempt in range(3): try: response = await self.client.post( f"{self.BASE_URL}/chat/completions", json=payload, headers=headers ) if response.status_code == 200: return response.json() elif response.status_code == 429: # Rate limit - wait và retry await asyncio.sleep(2 ** attempt) continue else: raise Exception(f"API Error: {response.status_code}") except httpx.TimeoutException: if attempt == 2: raise await asyncio.sleep(1) raise Exception("Failed after 3 attempts") def _summarize_interactions(self, interactions: List[Dict]) -> str: """Tóm tắt dữ liệu tương tác để giảm token usage""" if not interactions: return "Không có dữ liệu tương tác" content_types = {} time_distribution = {"morning": 0, "afternoon": 0, "evening": 0} avg_duration = 0 for interaction in interactions: content_type = interaction.get("content_type", "unknown") content_types[content_type] = content_types.get(content_type, 0) + 1 hour = interaction.get("hour", 12) if 6 <= hour < 12: time_distribution["morning"] += 1 elif 12 <= hour < 18: time_distribution["afternoon"] += 1 else: time_distribution["evening"] += 1 avg_duration += interaction.get("duration_seconds", 0) avg_duration = avg_duration / len(interactions) if interactions else 0 summary = f""" - Tổng tương tác: {len(interactions)} - Phân bố nội dung: {content_types} - Thời gian học: Sáng {time_distribution['morning']}, Chiều {time_distribution['afternoon']}, Tối {time_distribution['evening']} - Thời lượng trung bình: {avg_duration:.0f} giây """ return summary def _parse_analysis_response(self, response: Dict) -> AnalysisResult: """Parse AI response thành structured result""" content = response["choices"][0]["message"]["content"] # Extract JSON từ response try: # Try direct parse data = json.loads(content) except json.JSONDecodeError: # Extract from markdown code block import re json_match = re.search(r'``(?:json)?\s*([\s\S]*?)``', content) if json_match: data = json.loads(json_match.group(1)) else: # Try to find JSON pattern data = json.loads(content.strip()) return AnalysisResult( learning_style=data["learning_style"], strengths=data["strengths"], weaknesses=data["weaknesses"], recommended_content_types=data["recommended_content_types"], engagement_insights=data["engagement_insights"], suggested_difficulty_adjustment=data["difficulty_adjustment"], confidence_score=data["confidence"] ) async def close(self): await self.client.aclose()

Ví dụ sử dụng

async def main(): analyzer = HolySheepProfileAnalyzer(api_key="YOUR_HOLYSHEEP_API_KEY") interactions = [ {"content_type": "video", "duration_seconds": 1800, "hour": 19}, {"content_type": "quiz", "duration_seconds": 300, "hour": 19}, {"content_type": "video", "duration_seconds": 2100, "hour": 20}, ] performance = { "avg_score": 7.5, "completion_rate": 85, "hours_per_week": 10, "weak_subjects": ["Toán", "Vật lý"], "strong_subjects": ["Ngữ văn", "Lịch sử"] } result = await analyzer.analyze_student_behavior( student_id="STU001", interaction_history=interactions, performance_data=performance ) print(f"Learning Style: {result.learning_style}") print(f"Strengths: {result.strengths}") print(f"Difficulty Adjustment: {result.suggested_difficulty_adjustment}") await analyzer.close() if __name__ == "__main__": asyncio.run(main())

3. Knowledge Graph Construction

# services/knowledge_graph.py
from typing import Dict, List, Set, Tuple
from collections import defaultdict
import numpy as np

class KnowledgeGraph:
    """
    Xây dựng knowledge graph cho mỗi học sinh
    Theo dõi mối quan hệ giữa các khái niệm và độ thành thạo
    """
    
    def __init__(self):
        # adjacency: concept -> {related_concept: weight}
        self.adjacency: Dict[str, Dict[str, float]] = defaultdict(dict)
        # mastery: concept -> mastery_score (0.0 - 1.0)
        self.mastery: Dict[str, float] = {}
        # prerequisites: concept -> set of prerequisite concepts
        self.prerequisites: Dict[str, Set[str]] = defaultdict(set)
    
    def add_concept(
        self,
        concept: str,
        mastery_score: float,
        prerequisites: List[str] = None
    ):
        """Thêm một khái niệm vào knowledge graph"""
        self.mastery[concept] = max(0.0, min(1.0, mastery_score))
        
        if prerequisites:
            for prereq in prerequisites:
                self.prerequisites[concept].add(prereq)
                # Cập nhật adjacency với weight dựa trên mastery
                if prereq in self.mastery:
                    weight = (self.mastery[prereq] + self.mastery[concept]) / 2
                    self.adjacency[concept][prereq] = weight
                    self.adjacency[prereq][concept] = weight
    
    def update_mastery(self, concept: str, new_score: float):
        """Cập nhật độ thành thạo và propagate changes"""
        old_score = self.mastery.get(concept, 0.0)
        self.mastery[concept] = max(0.0, min(1.0, new_score))
        
        # Propagate to dependent concepts
        self._propagate_mastery_change(concept, old_score, new_score)
    
    def _propagate_mastery_change(
        self,
        concept: str,
        old_score: float,
        new_score: float
    ):
        """Propagate thay đổi mastery đến các concepts liên quan"""
        change = new_score - old_score
        
        # Tìm tất cả concepts phụ thuộc vào concept này
        dependents = [
            c for c, prereqs in self.prerequisites.items()
            if concept in prereqs
        ]
        
        for dependent in dependents:
            # Giảm impact theo khoảng cách
            adjustment = change * 0.3  # Decay factor
            new_mastery = self.mastery[dependent] + adjustment
            self.mastery[dependent] = max(0.0, min(1.0, new_mastery))
    
    def get_learning_path(
        self,
        target_concepts: List[str],
        max_concepts: int = 10
    ) -> List[Tuple[str, float]]:
        """
        Tính toán lộ trình học tập tối ưu
        Ưu tiên các concepts có prerequisite chưa thành thạo
        """
        learning_path = []
        covered = set()
        
        for target in target_concepts:
            if len(learning_path) >= max_concepts:
                break
            
            # Tìm prerequisites chưa cover
            path_segments = self._find_optimal_path(target, covered)
            
            for concept, priority in path_segments:
                if concept not in covered and len(learning_path) < max_concepts:
                    learning_path.append((concept, priority))
                    covered.add(concept)
        
        # Sort theo priority (cao -> thấp)
        learning_path.sort(key=lambda x: x[1], reverse=True)
        
        return learning_path[:max_concepts]
    
    def _find_optimal_path(
        self,
        concept: str,
        covered: Set[str]
    ) -> List[Tuple[str, float]]:
        """Tìm đường đi tối ưu đến một concept"""
        path = []
        
        # Thêm prerequisites trước
        for prereq in self.prerequisites.get(concept, []):
            if prereq not in covered:
                mastery = self.mastery.get(prereq, 0.0)
                priority = (1.0 - mastery) * 0.8  # Ưu tiên concept chưa thành thạo
                path.append((prereq, priority))
        
        # Thêm concept chính
        mastery = self.mastery.get(concept, 0.0)
        priority = (1.0 - mastery) * 0.9
        path.append((concept, priority))
        
        return path
    
    def get_gaps_analysis(self) -> Dict[str, List[str]]:
        """
        Phân tích các lỗ hổng kiến thức
        Trả về: {concept: [missing_prerequisites]}
        """
        gaps = {}
        
        for concept, prereqs in self.prerequisites.items():
            missing = []
            for prereq in prereqs:
                mastery = self.mastery.get(prereq, 0.0)
                if mastery < 0.6:  # Ngưỡng "đã thành thạo"
                    missing.append(prereq)
            
            if missing:
                gaps[concept] = missing
        
        return gaps
    
    def to_vector(self, all_concepts: List[str]) -> np.ndarray:
        """
        Chuyển knowledge graph thành vector để so sánh/học
        """
        return np.array([
            self.mastery.get(concept, 0.0)
            for concept in all_concepts
        ])
    
    def similarity(self, other: 'KnowledgeGraph') -> float:
        """Tính cosine similarity với một knowledge graph khác"""
        all_concepts = set(self.mastery.keys()) | set(other.mastery.keys())
        
        if not all_concepts:
            return 0.0
        
        v1 = self.to_vector(list(all_concepts))
        v2 = other.to_vector(list(all_concepts))
        
        # Cosine similarity
        dot_product = np.dot(v1, v2)
        norm_product = np.linalg.norm(v1) * np.linalg.norm(v2)
        
        if norm_product == 0:
            return 0.0
        
        return dot_product / norm_product


Ví dụ sử dụng

def demo(): kg = KnowledgeGraph() # Thêm concepts với prerequisites (chủ đề Toán) kg.add_concept("Pythagorean Theorem", 0.7, prerequisites=["Triangle Basics", "Square Root"]) kg.add_concept("Triangle Basics", 0.9, prerequisites=[]) kg.add_concept("Square Root", 0.6, prerequisites=["Basic Arithmetic"]) kg.add_concept("Basic Arithmetic", 0.95, prerequisites=[]) # Get learning path path = kg.get_learning_path( target_concepts=["Pythagorean Theorem", "Trigonometry Basics"], max_concepts=5 ) print("Learning Path:") for concept, priority in path: print(f" - {concept} (priority: {priority:.2f})") # Analyze gaps gaps = kg.get_gaps_analysis() print(f"\nKnowledge Gaps: {gaps}") # Similarity với student khác kg2 = KnowledgeGraph() kg2.add_concept("Pythagorean Theorem", 0.3) kg2.add_concept("Triangle Basics", 0.5) print(f"\nSimilarity with student 2: {kg.similarity(kg2):.2%}") if __name__ == "__main__": demo()

Recommendation Engine Implementation

Hybrid Filtering với Collaborative và Content-based

# services/recommendation_engine.py
from typing import List, Dict, Optional, Tuple
import numpy as np
from dataclasses import dataclass
import redis.asyncio as redis
import json
from datetime import datetime

@dataclass
class RecommendedItem:
    item_id: str
    score: float
    reasons: List[str]
    content_type: str
    difficulty: float
    estimated_time: int  # minutes

class RecommendationEngine:
    """
    Hybrid Recommendation Engine kết hợp:
    1. Content-based filtering (dựa trên knowledge graph)
    2. Collaborative filtering (dựa trên similar students)
    3. Context-aware re-ranking
    """
    
    def __init__(
        self,
        redis_client: redis.Redis,
        knowledge_graph_service,
        analyzer: HolySheepProfileAnalyzer
    ):
        self.redis = redis_client
        self.kg_service = knowledge_graph_service
        self.analyzer = analyzer
        
        # Weights cho hybrid scoring
        self.weights = {
            "content": 0.4,
            "collaborative": 0.3,
            "context": 0.2,
            "diversity": 0.1
        }
    
    async def get_recommendations(
        self,
        student_id: str,
        num_items: int = 10,
        exclude_viewed: bool = True
    ) -> List[RecommendedItem]:
        """
        Lấy danh sách gợi ý cá nhân hóa cho học sinh
        """
        # 1. Load student profile từ cache
        profile = await self._load_profile(student_id)
        if not profile:
            # Fallback to cold start strategy
            return await self._cold_start_recommendations(num_items)
        
        # 2. Get candidate items từ các nguồn
        candidates = await self._generate_candidates(profile, num_items * 3)
        
        # 3. Calculate scores
        scored_items = []
        for item in candidates:
            content_score = self._content_based_score(item, profile)
            collab_score = await self._collaborative_score(item, student_id)
            context_score = self._context_score(item, profile)
            
            # Hybrid score
            hybrid_score = (
                self.weights["content"] * content_score +
                self.weights["collaborative"] * collab_score +
                self.weights["context"] * context_score
            )
            
            scored_items.append((item, hybrid_score))
        
        # 4. Rerank với diversity
        reranked = self._rerank_with_diversity(
            scored_items,
            profile,
            num_items
        )
        
        # 5. Log recommendation
        await self._log_recommendation(student_id, reranked)
        
        return reranked
    
    async def _load_profile(self, student_id: str) -> Optional[Dict]:
        """Load profile từ Redis cache"""
        cache_key = f"profile:{student_id}"
        data = await self.redis.get(cache_key)
        
        if data:
            return json.loads(data)
        return None
    
    async def _generate_candidates(
        self,
        profile: Dict,
        num_candidates: int
    ) -> List[Dict]:
        """
        Generate candidate items từ content catalog
        Lọc theo:
        - Subject alignment
        - Difficulty range
        - Content type preference
        """
        # 1. Get learning path từ knowledge graph
        target_concepts = profile.get("improvement_areas", [])
        learning_path = self.kg_service.get_learning_path(
            target_concepts,
            max_concepts=10
        )
        
        # 2. Query content catalog
        # (Implementation depends on your content DB)
        candidates = []
        
        # Filter: difficulty within range
        current_level = profile.get("knowledge_level", 0.5)
        difficulty_range = 0.2  # +/- 20%
        
        for concept, _ in learning_path:
            # Query contents matching this concept
            contents = await self._query_content_by_concept(
                concept,
                difficulty_min=current_level - difficulty_range,
                difficulty_max=current_level + difficulty_range,
                limit=num_candidates // len(learning_path)
            )
            candidates.extend(contents)
        
        return candidates
    
    def _content_based_score(
        self,
        item: Dict,
        profile: Dict
    ) -> float:
        """
        Content-based scoring:
        - Subject match
        - Difficulty match
        - Content type preference
        """
        score = 0.0
        
        # Subject match
        if item["subject"] in profile.get("subjects", []):
            score += 0.3
        
        # Difficulty match
        item_diff = item.get("difficulty", 0.5)
        profile_level = profile.get("knowledge_level", 0.5)
        difficulty_gap = abs(item_diff - profile_level)
        score += 0.3 * (1 - difficulty_gap)  # Closer = higher score
        
        # Content type preference
        preferred_types = profile.get("preferred_content_types", [])
        if item["content_type"] in preferred_types:
            score += 0.2
        
        # Novelty bonus (recommend things not yet learned)
        if item.get("mastery_required", 1.0) > 0.7:
            score += 0.2
        
        return min(score, 1.0)
    
    async def _collaborative_score(
        self,
        item: Dict,
        student_id: str
    ) -> float:
        """
        Collaborative filtering score:
        Dựa trên hành vi của similar students
        """
        # Get similar students
        similar_key = f"similar_students:{student_id}"
        similar_ids = await self.redis.smembers(similar_key)
        
        if not similar_ids:
            return 0.5  # Default score
        
        # Count how many similar students interacted with this item
        positive_count = 0
        total_count = 0
        
        for sim_id in similar_ids:
            interaction_key = f"interactions:{sim_id}"
            interacted = await self.redis.sismember(
                interaction_key,
                item["item_id"]
            )
            
            if interacted:
                total_count += 1
                # Check if they rated it positively
                rating_key = f"rating:{sim_id}:{item['item_id']}"
                rating = await self.redis.get(rating_key)
                if rating and float(rating) >= 4.0:
                    positive_count += 1
        
        if total_count == 0:
            return 0.5
        
        return positive_count / total_count
    
    def _context_score(
        self,
        item: Dict,
        profile: Dict
    ) -> float:
        """
        Context-aware scoring:
        - Time of day
        - Session duration
        - Recent performance
        """
        score = 1.0
        
        # Time of day preference
        current_hour = datetime.now().hour
        item_time_pref = item.get("optimal_time", {})
        
        if item_time_pref:
            if current_hour in item_time_pref.get("morning", []):
                score *= 1.2
            elif current_hour in item_time_pref.get("evening", []):
                score *= 0.9  # Penalize if preferred morning content
        
        # Performance trend adjustment
        trend = profile.get("recent_performance_trend", "stable")
        if trend == "declining" and item.get("difficulty", 0.5) > 0.6:
            score *= 0.7  # Reduce difficulty items for struggling students
        elif trend == "improving" and item.get("difficulty", 0.5) > 0.8:
            score *= 1.3  # Challenge students doing well
        
        return min(score, 1.0)
    
    def _rerank_with_diversity(
        self,
        items: List[Tuple[Dict, float]],
        profile: Dict,
        num_items: int
    ) -> List[RecommendedItem]:
        """
        Rerank với MMR (Maximal Marginal Relevance)
        Đảm bảo diversity trong recommendations
        """
        if not items:
            return []
        
        # Sort by score
        items.sort(key=lambda x: x[1], reverse=True)
        
        selected = []
        selected_content_types = set()
        max_same_type = num_items // 2  # Max 50% cùng content type
        
        for item, score in items:
            content_type = item["content_type"]
            
            # Check diversity constraint
            same_type_count = sum(
                1 for s in selected
                if s.content_type == content_type
            )
            
            if same_type_count >= max_same_type:
                continue
            
            # MMR calculation
            mmr_score = (
                0.5 * score -
                0.5 * self._max_similarity_to_selected(item, selected)
            )
            
            if len(selected) < num_items:
                selected.append(RecommendedItem(
                    item_id=item["item_id"],
                    score=mmr_score,
                    reasons=self._generate_reasons(item, profile),
                    content_type=content_type,
                    difficulty=item.get("difficulty", 0.5),
                    estimated_time=item.get("estimated_time", 15)
                ))
        
        return selected
    
    def _max_similarity_to_selected(
        self,
        item: Dict,
        selected: List[RecommendedItem]
    ) -> float:
        """Tính max similarity với items đã chọn"""
        if not selected:
            return 0.0
        
        max_sim = 0.0
        for sel in selected:
            # Similarity based on subject và difficulty
            subj_sim = 1.0 if item["subject"] == sel.content_type else 0.0
            diff_sim = 1.0 - abs(
                item.get("difficulty", 0.5) - sel.difficulty
            )
            
            sim = 0.7 * subj_sim + 0.3 * diff_sim
            max_sim = max(max_sim, sim)
        
        return max_sim
    
    def _generate_reasons(
        self,
        item: Dict,
        profile: Dict
    ) -> List[str]:
        """Generate human-readable reasons cho recommendation"""
        reasons = []
        
        if item["subject"] in profile.get("weak_subjects", []):
            reasons.append("Cải thiện điểm yếu")
        
        if item.get("difficulty", 0.5) < profile.get("knowledge_level", 0.5):
            reasons.append("Ôn tập kiến thức cơ bản")