Skip to main content
Glama
path_planning.py5.55 kB
"""路径规划模块(规则版),基于先修与掌握度推荐下一步。""" from __future__ import annotations from collections import deque from typing import Dict, List import json from pathlib import Path import database from schemas import PathRequest, PathResponse # 测度论与泛函分析知识点先修图(默认配置) DEFAULT_ADJ: Dict[str, List[str]] = { # 测度论基础部分 "集合论基础": ["外测度", "可测集"], "外测度": ["可测集", "测度"], "可测集": ["测度", "可测函数"], "测度": ["可测函数", "Lebesgue积分"], "可测函数": ["Lebesgue积分"], "Lebesgue积分": ["乘积测度", "Fubini定理", "Lp空间"], "乘积测度": ["Fubini定理"], "Fubini定理": [], "Lp空间": ["泛函分析基础"], # 泛函分析部分 "度量空间": ["赋范空间", "Banach空间"], "赋范空间": ["Banach空间", "Hilbert空间"], "Banach空间": ["线性算子", "对偶空间"], "Hilbert空间": ["线性算子", "对偶空间"], "线性算子": ["紧算子", "谱理论"], "对偶空间": ["弱拓扑"], "弱拓扑": [], "紧算子": ["谱理论"], "谱理论": [], "泛函分析基础": ["度量空间"], } def load_adj() -> Dict[str, List[str]]: """ 优先从外部 JSON 配置加载先修图,文件不存在时回退到 DEFAULT_ADJ。 JSON 文件示例(knowledge_graph.json): { "集合论基础": ["外测度", "可测集"], "外测度": ["可测集", "测度"], "...": ["..."] } """ cfg_path = Path("knowledge_graph.json") if cfg_path.exists(): try: with cfg_path.open("r", encoding="utf-8") as f: data = json.load(f) # 确保 value 全部是 list return {k: list(v) for k, v in data.items()} except Exception: # 若解析失败,退回默认配置,避免整个服务挂掉 return DEFAULT_ADJ return DEFAULT_ADJ ADJ: Dict[str, List[str]] = load_adj() def _level_from_mastery(mastery: float) -> str: if mastery >= 0.85: return "稳定掌握" if mastery >= 0.65: return "发展中" return "高风险" def _priority(m: float) -> int: """数值越大表示越优先推荐""" if m < 0.65: # 高风险 return 3 if m < 0.85: # 发展中 return 2 return 1 # 稳定掌握,只偶尔复习 def plan(payload: PathRequest) -> PathResponse: """ 在拓扑排序的基础上,按“风险优先”选择推荐知识点: - 仍然用先修图 ADJ 做拓扑遍历(保证不会越级学习); - 在每一层可学习的节点中: * 先按风险等级优先级排序(高风险 > 发展中 > 稳定掌握); * 同等级内按掌握度缺口排序(越不会越优先); - 只有 mastery < payload.threshold 的节点才会被加入推荐列表。 """ # 1. 计算入度(拓扑排序用) indegree: Dict[str, int] = {k: 0 for k in ADJ} for prereq, next_list in ADJ.items(): for nxt in next_list: indegree[nxt] = indegree.get(nxt, 0) + 1 # 2. 获取掌握度:优先用请求里给的,其次用 student_id 从“数据库”里取 if payload.mastery: mastery = payload.mastery elif payload.student_id: mastery = database.dump_mastery(payload.student_id) else: mastery = {} def get_mastery(name: str) -> float: return float(mastery.get(name, 0.0)) # 3. 初始化:所有入度为 0 的点作为起点 queue = deque([k for k, deg in indegree.items() if deg == 0]) visited: set[str] = set() recommended: List[str] = [] # 4. 分层拓扑 + 风险优先 while queue and len(recommended) < payload.max_recommend: # 当前这一层的所有可处理节点 current_layer = list(queue) queue.clear() # 对当前层打分:(priority, gap, name) scored_layer = [] for node in current_layer: if node in visited: continue m = get_mastery(node) priority = _priority(m) # 高风险 3 > 发展中 2 > 稳定掌握 1 gap = 1.0 - m # 掌握度缺口,越大说明越不会 scored_layer.append((priority, gap, node)) # 按优先级从高到低、缺口从大到小排序 scored_layer.sort(key=lambda x: (-x[0], -x[1])) next_queue = deque() # 依次处理这一层的节点 for priority, gap, node in scored_layer: if node in visited: continue visited.add(node) m = get_mastery(node) # 是否加入推荐:仍然用 threshold 控制“需要学/复习”的点 if m < payload.threshold and len(recommended) < payload.max_recommend: recommended.append(node) # 不管推没推荐,都视为前置已处理,推进后继节点的入度 for nxt in ADJ.get(node, []): indegree[nxt] -= 1 if indegree[nxt] == 0 and nxt not in visited: next_queue.append(nxt) if len(recommended) >= payload.max_recommend: break # 下一轮从 next_queue 开始 queue = next_queue return PathResponse( request_id=payload.request_id, recommended_path=recommended, model_version="rule-0.2-risk-first", )

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Eiadrian/educational-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server