当前版本:v0.2.4 前端纯内联 CSS-in-JS + Glass Morphism + Framer Motion + KaTeX。后端 FastAPI + SQLite + AsyncOpenAI 异步并行。 v0.2.4 新增:根目录
npm run dev一行启动、后端端口 8000→8001→... 鲁棒 fallback、共享scripts/lib/ports.js、5 个 v0.2.3 漏修的 module-level import bug 真落地、仓库瘦身(移除 tests/ + pytest.ini + requirements-test.txt)。
AI_Homeworkgrading/
├── README.md · PRD.md · ARCHITECTURE.md
├── .env.example
├── docker-compose.yml # 后端一键起;data/uploads/fonts 卷挂载
├── .dockerignore
├── .github/workflows/ci.yml # 后端 ruff / 前端 lint+tsc+build
├── scripts/
│ ├── lib/ports.js # 共享 findFreePort(start,end) 端口扫描
│ ├── dev.js # 并行启动前后端(端口鲁棒,TOCTOU retry)
│ ├── dev-api.js # 仅启动后端(端口鲁棒)
│ └── download_chinese_font.py # 一键拉取 Noto Sans SC OTF
├── frontend/src/
│ ├── main.tsx / App.tsx # 入口 + 15 条路由
│ ├── theme.ts # 设计 Token(含玻璃态色值)
│ ├── constants.ts # 题型标签映射
│ ├── index.css # 全局 CSS + @keyframes
│ ├── types/index.ts # 12+ TS 接口
│ ├── api/client.ts # 30+ API 方法
│ ├── motion/index.tsx # 8 个动画组件(含 TiltCard)
│ ├── components/ # 11 个组件
│ │ ├── CleanContent.tsx # 内容清洗 + JSON 检测 + KaTeX
│ │ ├── MathRenderer.tsx # LaTeX 公式 KaTeX 渲染
│ │ ├── ConfidenceBadge.tsx # 置信度三色徽章
│ │ ├── SocraticFeedback.tsx # 苏格拉底三级展开
│ │ ├── GradingResult.tsx # 单题批改卡片
│ │ ├── ImageUploader.tsx # 图片上传
│ │ ├── MarkdownRenderer.tsx # Markdown 渲染
│ │ ├── KnowledgeGraph.tsx # 知识树可视化
│ │ ├── MasteryBar.tsx # 掌握度进度条
│ │ ├── StyleIndicator.tsx # 教师偏差徽章
│ │ ├── SimilarQuestionCard.tsx # 变式题卡片
│ │ ├── TiltCard (in motion) # 3D 鼠标跟随
│ │ └── CountUp (in pages) # 数字递增动画
│ └── pages/
│ ├── Home.tsx # 玻璃态首页
│ ├── teacher/ (8 pages) # 看板/作业/复核/队列/风格/分析
│ └── student/ (6 pages) # 看板/作业/提交/结果/订正/错题本
└── backend/
├── Dockerfile # python:3.11-slim + /api/health healthcheck
├── main.py # FastAPI 入口(6 个路由,v0.2.4)
├── config.py / database.py # 配置 + 9 表 + PRAGMA 自动迁移 + db_session()
├── models.py # 30+ Pydantic 模型(含 CorrectItem)
├── utils.py # extract_json(括号计数状态机)+ 文件上传校验
├── fonts/ # 中文字体目录(gitignore;缺字时回退 Helvetica + WARN)
├── routers/ # 6 个路由模块(30+ 端点)
│ ├── assignments.py # 作业 CRUD + CSV 导出 + 编辑
│ ├── submissions.py # 提交管理(multipart + 逐题图片)
│ ├── grading.py # 异步并行批改引擎(OCR 任务独立连接)
│ ├── dashboard.py # 看板 + 知识图谱 + 班级分析 + 教师风格(用 db_session)
│ ├── error_book.py # 错题本(6 端点,用 db_session)
│ └── pdf_export.py # 家长报告 PDF 路由
├── services/ # 10 个服务
│ ├── ai_client.py # AsyncOpenAI(文本+多模态)
│ ├── ocr.py # DashScope 多模态 OCR
│ ├── grader.py # 规则 + AI 批改(参考答案守卫)
│ ├── feedback.py # 苏格拉底引导反馈
│ ├── knowledge_graph.py # 知识点提取 + 依赖图 + BFS 根因(v0.2.3 消除 N+1)
│ ├── teacher_style.py # Welford 偏差追踪 + 修正
│ ├── error_book.py # 错题本 service
│ ├── question_generator.py # 举一反三变式题
│ ├── class_analytics.py # 热力图 + 趋势 + 对比
│ └── pdf_export.py # fpdf2 A4 报告(v0.2.3 嵌入 CJK 字体)
└── prompts/ # 5 套 Prompt 模板
教师创建/编辑作业 ──► POST/PUT /api/assignments
学生逐题作答+拍照 ──► POST /api/submissions (multipart + JSON)
教师触发批改 ──► POST /api/submissions/:id/grade
├─ 参考答案空值守卫 → 无答案直接拒绝,提示补充
├─ OCR(异步并行)→ UPDATE answers
├─ 规则引擎(客观题)→ 本地比对
├─ AI 批改(主观题,异步并行)→ DashScope(基于参考答案锚定)
├─ 条件判分 → 无分值仅判对错,有分值 0-N 范围评分
├─ 知识点提取 → knowledge_points 表
└─ 掌握度更新 → student_mastery 表 (EMA)
教师复核 ──► PUT /api/answers/:id/override → 风格追踪
学生查看 ◄── GET /api/submissions/:id → CleanContent + KaTeX 渲染
错题自动收录 → error_book 表
学生错题本 ◄── GET /api/error-book → LLM 举一反三
| 表 | 关键字段 | 用途 |
|---|---|---|
| assignments | title, subject, class_name, teacher_name, status | 作业 |
| questions | type, content, reference_answer, points, knowledge_points_json | 题目(含知识点缓存) |
| submissions | student_name, status, image_url | 提交 |
| answers | student_answer, is_correct, ai_score, score, teacher_override | 答案(AI 分+教师终分分离) |
| knowledge_points | name, subject, parent_id (self FK) | 知识点树 |
| question_knowledge_points | question_id, knowledge_point_id | 题目-知识点 N:M |
| student_mastery | student_name, knowledge_point_id, mastery_score (EMA) | 掌握度 |
| teacher_style_profile | teacher_name, question_type, avg_bias (Welford) | 教师风格 |
| error_book | student_name, answer_id, knowledge_points_json | 错题本 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST/GET/PUT/DELETE | /api/assignments[/{id}] |
CRUD + 编辑 |
| GET | /api/assignments/{id}/export |
CSV 导出 |
| POST | /api/submissions | multipart 提交 |
| GET | /api/submissions[/{id}] | 列表/详情 |
| POST | /api/submissions/{id}/grade | 触发批改(异步,含参考答案守卫) |
| POST | /api/assignments/{id}/grade-all | 批量批改(异步并行) |
| PUT | /api/answers/{id}/override | 教师覆写(风格追踪) |
| POST | /api/submissions/{id}/correct | 学生订正 |
| POST | /api/ocr/question | 题目图片 OCR |
| POST | /api/ocr/reference-answer | 参考答案图片 OCR |
| GET | /api/ocr/health | v0.2.3 真实 ping DashScope,返回状态与截断回复 |
| GET | /api/dashboard/teacher\|student | 看板统计 |
| GET | /api/dashboard/knowledge-graph/{subject} | 知识树 |
| GET | /api/dashboard/student/{name}/diagnosis | 根因诊断 |
| GET | /api/dashboard/teacher-style/{name} | 风格报告 |
| GET | /api/dashboard/class-overview\|knowledge-heatmap\|trends | 班级分析 |
| GET/POST | /api/error-book[/{id}/review\|similar-question\|sync\|stats] | 错题本 |
| GET | /api/reports/student/{name} | PDF 报告 |
正则无法处理嵌套 JSON。改用字符级遍历 + 深度计数 + 字符串追踪 + 转义处理:
- 自动剥离 markdown 代码围栏
- 跟踪
in_string跳过字符串内花括号 - 跟踪
escape跳过\"/\\ - 失败时从下一个
{重试(容忍前面有集合符号) - 自动修复 AI 常见的尾部逗号
{"a": 1,}
MathRenderer 组件用正则匹配 $...$(行内)和 $$...$$(块级),将公式交给 KaTeX 渲染为 HTML。接入 CleanContent → 题目内容、学生答案、AI 反馈、错题本统一获得数学渲染。
所有用户可见文本在渲染前经过 CleanContent:
- 检测 raw JSON wrapper(OCR 失败残留)→ 提取真实内容
- 交给 MathRenderer 渲染 LaTeX 公式
- 普通文本原样输出
- 玻璃态卡片:
rgba(255,255,255,0.78)+backdrop-filter: blur(20px)+ 半透明边框 - TiltCard:
useMotionValue追踪鼠标位置 →useTransform计算 3D 旋转角 - CountUp:
requestAnimationFrame+ easeOutCubic 缓动,数字从 0 递增
grader.py:16-23 在批改入口检查 reference_answer,为空则直接短路返回,不调用 AI。确保所有批改都有参考答案作为比较基准,消除 AI 臆断。
build_grading_prompt()根据max_points动态生成指令:>0 时给分值范围,<=0 时告诉 AI 不评分- AI 返回后 score 二次校验:
max_points > 0 else 0 - 无分值 feedback 前加
[未计分]前缀
- OCR 并行:
asyncio.gather(*ocr_tasks) - 逐题批改并行:
asyncio.gather(*grade_tasks) - 批量提交并行:
asyncio.gather(*sub_tasks) - N×30s 串行 → ~30s 总耗时
database.py 新增 @contextmanager db_session(commit=True),封装 get_db() + commit/rollback/close。消除之前 get_db() + 手写 close() 容易漏关连接、或异常路径不 rollback 的隐患。dashboard.py / error_book.py 已切换;_ocr_and_update 内部独立打开连接,避免 asyncio.gather 中多个 OCR 任务争抢同一 SQLite 连接导致的锁竞争。
compute_root_causes() 之前对每个 BFS 访问到的 KP 节点都执行一次 SELECT id, name, parent_id FROM knowledge_points WHERE id = ?。改为:先用一次查询把全部 KP 拉到内存(kp_info: {id: (name, parent_id)}),再在 Python 中完成 BFS。复杂度从 O(visited × 1 查询) 降到 O(2 查询)。knowledge_points 表规模按课程知识点数量级(百级),无界增长风险。
fpdf2 内置 Helvetica 是 Latin-1,中文会被 errors="replace" 替换成 ?。services/pdf_export.py 改为探测 backend/fonts/ 下的 NotoSansSC-{Regular.otf,Regular.ttf,CJKsc-Regular.otf,SourceHanSansSC-Regular.otf} 之一,命中则 pdf.add_font(family, fname=path, uni=True) 嵌入并全文走 CJK 字体;未命中时记录 WARNING 并回退到 helvetica(家长报告里的中文会显示成 ?,但不阻塞导出)。一键拉取脚本:python scripts/download_chinese_font.py,下载到 backend/fonts/NotoSansSC-Regular.otf。字体二进制被 .gitignore 排除。
pip install -r backend/requirements.txt
cd frontend && npm install
cp .env.example .env # 设置 DASHSCOPE_API_KEY
python scripts/download_chinese_font.py # 可选:让 PDF 中文正常
cd frontend && npm run dev # :5173 (web) + :8000 (api via concurrently)cp .env.example .env
docker compose up --build # 后端 :8000详见 backend/Dockerfile(python:3.11-slim + /api/health 健康检查)与 docker-compose.yml(./backend/{data,uploads,fonts} 三个卷挂载,SQLite + 上传图片 + 字体持久化)。前端仍用 npm run dev 本地起,便于热更新。
scripts/lib/ports.js导出findFreePort(start, end)在[start, end]范围扫,默认 8000-8099dev.js/dev-api.js的startApi在[API_PORT_START, API_PORT_END]内循环,单次 spawn uvicorn 后等 2.5s 早退事件,若 bind 失败则换下一个端口重试,最多 3 次- 前端通过
VITE_API_PORT环境变量跟着后端实际端口走,无需改任何配置 - 范围起点可通过
API_PORT=9000 npm run dev覆盖
启动
- 根目录
package.json加setup/dev/dev:api/dev:web脚本,npm run dev前后端并行起 scripts/lib/ports.js抽findFreePort(start, end)共享端口扫描,默认范围 8000-8099dev.jsstartApi()包裹 retry:uvicorn 启动 2.5s 内早退则换下一个端口,最多 3 次- 日志显式标注 fallback:
API → http://localhost:8001 (8000 busy)
Bug 修复(v0.2.3 章节声称但未真正落地)
backend/database.py缺from contextlib import contextmanager——db_session在模块加载时 NameError,FastAPI 进程无法启动backend/services/grader.py缺import logging—— AI 主观题返回非 JSON 时logger.warningNameErrorbackend/services/teacher_style.py缺import math—— 任何一次教师覆写math.sqrtNameErrordatabase.py重复定义db_session,删冗余
仓库瘦身
- 移除
tests/目录(4 个历史 pytest 用例 + conftest + 本次新增的 1 个 import 回归用例) - 移除
pytest.ini/requirements-test.txt(孤立配置) - CI 移除 pytest 步骤 + requirements-test.txt 安装,保留 ruff / 前端 lint+tsc+build
Bug 修复
- 全图 OCR 结果按
question_number误写为question_id:改为用sort_order映射回真实主键;per-question image 多结果时打 WARNING 后取首条 - 掌握度更新使用 SELECT 旧值(常为
NULL):改为并行结果result["is_correct"] /api/ocr/test硬编码ready:改为/api/ocr/health真实 ping DashScope
PDF 中文:注册 Noto Sans SC TTF/OTF;字体下载脚本 scripts/download_chinese_font.py;.gitignore 排除二进制
工程化:backend/Dockerfile + docker-compose.yml + .dockerignore;.github/workflows/ci.yml(Python 3.11 + Node 20,含 cache);pytest.ini + tests/(27 用例)
架构优化
db_session()上下文管理器;_ocr_and_update改用独立连接,消除asyncio.gather共享连接锁竞争compute_root_causesN+1 → 2 次查询 + 内存 BFS
代码卫生:grader.py / teacher_style.py 顶部 import;CorrectRequest.answers: list → list[CorrectItem];删除 GradingStyle.tsx 重复 TYPE_LABELS;删除 SubmitPage.tsx 后端未用的 has_image 字段
文档:版本号、6 个 router / 11 组件 / 9 表 / 5 prompt 等数字全部对齐代码
- 参考答案锚定批改 + 条件判分 + 答案图片 OCR;CleanContent / MathRenderer 内容清洗层
- 详见 README v0.2.2 章节
文档版本: v0.2.4 | 更新日期: 2026-06-04