AI Math Tutor v3:题目知识点自动分类

添加 功能

  • 每次解析题目时,自动归类到某个知识点

  • 统计每个学生在各知识点下:

    • 做题次数
    • 错题次数
    • 正确次数
    • 错误率

1)修改 backend/app/models.py

新增模型

ini 复制代码
class StudentKnowledgeStat(Base):
    __tablename__ = "student_knowledge_stat"

    id = Column(Integer, primary_key=True, index=True)
    student_id = Column(Integer, ForeignKey("student.id"), nullable=False, index=True)
    knowledge_name = Column(Text, nullable=False, index=True)
    total_count = Column(Integer, nullable=False, default=0)
    wrong_count = Column(Integer, nullable=False, default=0)
    correct_count = Column(Integer, nullable=False, default=0)

2)修改 backend/app/schemas.py

新增结构

python 复制代码
class KnowledgeGraphItem(BaseModel):
    knowledge_name: str
    total_count: int
    wrong_count: int
    correct_count: int
    wrong_rate: float


class KnowledgeGraphResponse(BaseModel):
    student_id: int
    items: List[KnowledgeGraphItem]

3)新增 backend/app/knowledge_graph_service.py

ini 复制代码
import json
from sqlalchemy.orm import Session
from app.models import StudentKnowledgeStat, QuestionHistory


def _normalize_knowledge_points(knowledge_points: list[str]) -> list[str]:
    result = []
    seen = set()

    for item in knowledge_points:
        name = (item or "").strip()
        if not name or name in seen:
            continue
        seen.add(name)
        result.append(name)

    return result


def update_student_knowledge_stat(
    db: Session,
    student_id: int,
    knowledge_points: list[str],
    is_wrong: bool,
):
    kp_list = _normalize_knowledge_points(knowledge_points)

    for kp in kp_list:
        row = (
            db.query(StudentKnowledgeStat)
            .filter(
                StudentKnowledgeStat.student_id == student_id,
                StudentKnowledgeStat.knowledge_name == kp,
            )
            .first()
        )

        if not row:
            row = StudentKnowledgeStat(
                student_id=student_id,
                knowledge_name=kp,
                total_count=0,
                wrong_count=0,
                correct_count=0,
            )
            db.add(row)
            db.flush()

        row.total_count += 1
        if is_wrong:
            row.wrong_count += 1
        else:
            row.correct_count += 1

    db.commit()


def rebuild_student_knowledge_stat(db: Session, student_id: int):
    db.query(StudentKnowledgeStat).filter(
        StudentKnowledgeStat.student_id == student_id
    ).delete()

    rows = (
        db.query(QuestionHistory)
        .filter(QuestionHistory.student_id == student_id)
        .order_by(QuestionHistory.id.asc())
        .all()
    )

    for row in rows:
        try:
            knowledge_points = json.loads(row.knowledge_points or "[]")
        except Exception:
            knowledge_points = []

        update_student_knowledge_stat(
            db=db,
            student_id=student_id,
            knowledge_points=knowledge_points,
            is_wrong=row.is_wrong,
        )


def get_student_knowledge_graph(db: Session, student_id: int):
    rows = (
        db.query(StudentKnowledgeStat)
        .filter(StudentKnowledgeStat.student_id == student_id)
        .order_by(
            StudentKnowledgeStat.wrong_count.desc(),
            StudentKnowledgeStat.total_count.desc(),
            StudentKnowledgeStat.knowledge_name.asc(),
        )
        .all()
    )

    items = []
    for row in rows:
        wrong_rate = round((row.wrong_count / row.total_count * 100), 2) if row.total_count > 0 else 0.0
        items.append({
            "knowledge_name": row.knowledge_name,
            "total_count": row.total_count,
            "wrong_count": row.wrong_count,
            "correct_count": row.correct_count,
            "wrong_rate": wrong_rate,
        })

    return {
        "student_id": student_id,
        "items": items,
    }

4)修改 backend/app/main.py

补充 import

javascript 复制代码
from app.knowledge_graph_service import (
    update_student_knowledge_stat,
    rebuild_student_knowledge_stat,
    get_student_knowledge_graph,
)
from app.schemas import KnowledgeGraphResponse

/api/solve 里,追加

ini 复制代码
update_student_knowledge_stat(
    db=db,
    student_id=student_id,
    knowledge_points=result.get("knowledge_points", []),
    is_wrong=False,
)

/api/solve-image 里,追加

ini 复制代码
update_student_knowledge_stat(
    db=db,
    student_id=student_id,
    knowledge_points=result.get("knowledge_points", []),
    is_wrong=False,
)

/api/history/{question_id}/wrong

ini 复制代码
@app.patch("/api/history/{question_id}/wrong", response_model=HistoryItem)
def mark_wrong(question_id: int, body: MarkWrongRequest, db: Session = Depends(get_db)):
    row = db.query(QuestionHistory).filter(QuestionHistory.id == question_id).first()
    if not row:
        raise HTTPException(status_code=404, detail="记录不存在")

    row.is_wrong = body.is_wrong
    db.commit()
    db.refresh(row)

    rebuild_student_knowledge_stat(db, row.student_id)

    return HistoryItem(
        id=row.id,
        question=row.question,
        answer=row.answer,
        steps=json.loads(row.steps),
        knowledge_points=json.loads(row.knowledge_points),
        similar_question=row.similar_question,
        is_wrong=row.is_wrong,
        matched_knowledge=json.loads(row.matched_knowledge or "[]"),
    )

新增两个接口

less 复制代码
@app.get("/api/knowledge-graph", response_model=KnowledgeGraphResponse)
def get_knowledge_graph(
    student_id: int = Query(1),
    db: Session = Depends(get_db),
):
    try:
        result = get_student_knowledge_graph(db, student_id)
        return KnowledgeGraphResponse(**result)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/knowledge-graph/rebuild")
def rebuild_knowledge_graph(
    student_id: int = Query(1),
    db: Session = Depends(get_db),
):
    try:
        rebuild_student_knowledge_stat(db, student_id)
        return {"message": "知识图谱重建成功"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

5)修改 frontend/src/api/math.ts

新增类型

typescript 复制代码
export interface KnowledgeGraphItem {
  knowledge_name: string
  total_count: number
  wrong_count: number
  correct_count: number
  wrong_rate: number
}

export interface KnowledgeGraphResponse {
  student_id: number
  items: KnowledgeGraphItem[]
}

新增接口

javascript 复制代码
export function getKnowledgeGraph(student_id: number) {
  return request.get<KnowledgeGraphResponse>('/api/knowledge-graph', {
    params: { student_id },
  })
}

export function rebuildKnowledgeGraph(student_id: number) {
  return request.post('/api/knowledge-graph/rebuild', null, {
    params: { student_id },
  })
}

6)修改 frontend/src/App.vue

补充 import

bash 复制代码
getKnowledgeGraph,
rebuildKnowledgeGraph,
type KnowledgeGraphResponse,

修改 activeTab 类型

csharp 复制代码
const activeTab = ref<'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'>('solve')

新增状态

csharp 复制代码
const graphLoading = ref(false)
const knowledgeGraph = ref<KnowledgeGraphResponse | null>(null)

新增方法

typescript 复制代码
const loadKnowledgeGraph = async () => {
  graphLoading.value = true
  try {
    const { data } = await getKnowledgeGraph(currentStudentId.value)
    knowledgeGraph.value = data
  } catch (error: any) {
    console.error('加载知识图谱失败:', error)
    alert(error?.response?.data?.detail || '加载知识图谱失败')
  } finally {
    graphLoading.value = false
  }
}

const switchToGraph = async () => {
  activeTab.value = 'graph'
  await loadKnowledgeGraph()
}

const handleRebuildKnowledgeGraph = async () => {
  try {
    await rebuildKnowledgeGraph(currentStudentId.value)
    await loadKnowledgeGraph()
    alert('知识图谱重建成功')
  } catch (error: any) {
    console.error('重建知识图谱失败:', error)
    alert(error?.response?.data?.detail || '重建知识图谱失败')
  }
}

handleSubmit

scss 复制代码
await loadKnowledgeGraph()

handleImageChange

scss 复制代码
await loadKnowledgeGraph()

toggleWrong

scss 复制代码
await loadKnowledgeGraph()

refreshAllStudentData

scss 复制代码
const refreshAllStudentData = async () => {
  await Promise.all([
    loadHistory(),
    loadWrongList(),
    loadReport(),
    loadStudySuggestion(),
    loadKnowledgeGraph(),
  ])
}

7)修改 src/components/TabNav.vue

css 复制代码
defineProps<{
  activeTab: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'
}>()

defineEmits<{
  (e: 'change', value: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'): void
}>()

tabList 里新增

css 复制代码
{ label: '知识图谱', value: 'graph' },

8)修改 src/App.vue

handleTabChange

csharp 复制代码
const handleTabChange = async (
  tab: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'
) => {
  activeTab.value = tab

  if (tab === 'history') {
    await loadHistory()
  } else if (tab === 'wrong') {
    await loadWrongList()
  } else if (tab === 'report') {
    await loadReport()
  } else if (tab === 'suggestion') {
    await loadStudySuggestion()
  } else if (tab === 'graph') {
    await loadKnowledgeGraph()
  }
}

template 里新增一个

xml 复制代码
<template v-else-if="activeTab === 'graph'">
  <div v-if="graphLoading" class="empty">知识图谱加载中...</div>

  <div v-else-if="knowledgeGraph" class="report-panel">
    <div class="result-card">
      <div class="card-header">
        <h2>学生知识图谱</h2>
        <button class="retry-btn" @click="handleRebuildKnowledgeGraph">
          重建图谱
        </button>
      </div>

      <div v-if="knowledgeGraph.items.length === 0" class="empty">
        暂无知识图谱数据
      </div>

      <div v-else class="graph-list">
        <div v-for="(item, index) in knowledgeGraph.items" :key="index" class="graph-item">
          <div class="graph-header">
            <strong>{{ item.knowledge_name }}</strong>
            <span class="weak-rate">错误率 {{ item.wrong_rate }}%</span>
          </div>

          <div class="graph-meta">
            共练习 {{ item.total_count }} 次 / 正确 {{ item.correct_count }} 次 / 错误 {{ item.wrong_count }} 次
          </div>

          <div class="graph-bar">
            <div
              class="graph-bar-inner"
              :style="{ width: `${Math.min(item.wrong_rate, 100)}%` }"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

9)补充 src/App.vue 样式

css 复制代码
.graph-list {
  margin-top: 12px;
}

.graph-item {
  padding: 16px 0;
  border-bottom: 1px solid #eee;
}

.graph-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.graph-meta {
  color: #666;
  font-size: 14px;
  margin-bottom: 10px;
}

.graph-bar {
  height: 10px;
  background: #f0f0f0;
  border-radius: 999px;
  overflow: hidden;
}

.graph-bar-inner {
  height: 100%;
  background: linear-gradient(90deg, #f0a020, #d03050);
  border-radius: 999px;
}

10)初始化补一行

onMounted 改成

scss 复制代码
onMounted(async () => {
  await loadStudents()
  await refreshAllStudentData()
})

11)注意

改了表结构,开发阶段建议你删库重建(删库跑路):

bash 复制代码
cd backend
rm math_tutor.db

然后重启后端:

lua 复制代码
uvicorn app.main:app --reload --port 8000

前端:

arduino 复制代码
npm run dev

相关推荐
tiany5242 小时前
养虾记录-如何配置多agent和多个飞书机器人独立对话
java·前端·飞书
2201_756206342 小时前
1111111
开发语言·python
我命由我123452 小时前
Element Plus - 在 el-select 的每个选项右侧添加按钮
前端·javascript·vue.js·前端框架·ecmascript·html5·js
与虾牵手2 小时前
Python asyncio 踩坑实录:5 个让我加班到凌晨的坑 🕳️
python
bryant_meng2 小时前
【AIGC】《A Quick 80-Minute Guide to Large Language Models》
人工智能·计算机视觉·语言模型·llm·aigc
weixin199701080162 小时前
衣联网商品详情页前端性能优化实战
前端·性能优化
技术钻石流2 小时前
面向“传统程序员”的端到端 10x Vibe Coding 指南(大型需求) - 从面向业务开发转向面向“Agent 员工”开发
前端·后端·ai编程
codingWhat2 小时前
Electron 入门实战:用一个加法计算器吃透 Electron 核心概念
前端·javascript·electron
魔道不误砍柴功2 小时前
Java Function 高级使用技巧:从工程实战中来
java·开发语言·python