From 15144c636ea0d71f629204f44a05c1003f3c10ac Mon Sep 17 00:00:00 2001 From: StudiousXiaoYu <1316356098@qq.com> Date: Tue, 16 Dec 2025 15:08:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E6=95=99=E6=A1=88=E7=94=9F=E6=88=90=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现基于 FastAPI 和 LazyLLM 的智能教案生成系统,包含以下核心功能: - 完整的项目目录结构和基础配置 - 教案生成 API 接口和页面路由 - 大模型调用与 Agent 编排逻辑 - 教案评审与打分功能 - 前端页面模板与交互实现 - 项目文档与使用说明 --- jiao-an-ai/README.md | 271 +++++++++ jiao-an-ai/app/__init__.py | 1 + .../app/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 135 bytes .../app/__pycache__/main.cpython-311.pyc | Bin 0 -> 1824 bytes jiao-an-ai/app/agents/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 142 bytes .../lesson_plan_agent.cpython-311.pyc | Bin 0 -> 16771 bytes jiao-an-ai/app/agents/lesson_plan_agent.py | 364 ++++++++++++ jiao-an-ai/app/api/__init__.py | 1 + .../api/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 139 bytes .../api/__pycache__/health.cpython-311.pyc | Bin 0 -> 574 bytes .../__pycache__/lesson_plans.cpython-311.pyc | Bin 0 -> 1956 bytes jiao-an-ai/app/api/health.py | 13 + jiao-an-ai/app/api/lesson_plans.py | 43 ++ jiao-an-ai/app/main.py | 36 ++ jiao-an-ai/app/models/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 142 bytes .../__pycache__/lesson_plan.cpython-311.pyc | Bin 0 -> 2791 bytes jiao-an-ai/app/models/lesson_plan.py | 60 ++ jiao-an-ai/app/services/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 144 bytes .../lesson_plan_service.cpython-311.pyc | Bin 0 -> 2064 bytes .../app/services/lesson_plan_service.py | 52 ++ jiao-an-ai/app/static/css/style.css | 3 + jiao-an-ai/app/templates/index.html | 335 +++++++++++ .../app/templates/lesson_plans_detail.html | 136 +++++ .../app/templates/lesson_plans_new.html | 537 ++++++++++++++++++ jiao-an-ai/app/views/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 141 bytes .../views/__pycache__/pages.cpython-311.pyc | Bin 0 -> 3139 bytes jiao-an-ai/app/views/pages.py | 60 ++ jiao-an-ai/config/__init__.py | 34 ++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1610 bytes jiao-an-ai/requirements.txt | 4 + jiao-an-ai/scripts/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 139 bytes ...13\347\273\215\346\226\207\346\241\243.md" | 285 ++++++++++ 37 files changed, 2240 insertions(+) create mode 100644 jiao-an-ai/README.md create mode 100644 jiao-an-ai/app/__init__.py create mode 100644 jiao-an-ai/app/__pycache__/__init__.cpython-311.pyc create mode 100644 jiao-an-ai/app/__pycache__/main.cpython-311.pyc create mode 100644 jiao-an-ai/app/agents/__init__.py create mode 100644 jiao-an-ai/app/agents/__pycache__/__init__.cpython-311.pyc create mode 100644 jiao-an-ai/app/agents/__pycache__/lesson_plan_agent.cpython-311.pyc create mode 100644 jiao-an-ai/app/agents/lesson_plan_agent.py create mode 100644 jiao-an-ai/app/api/__init__.py create mode 100644 jiao-an-ai/app/api/__pycache__/__init__.cpython-311.pyc create mode 100644 jiao-an-ai/app/api/__pycache__/health.cpython-311.pyc create mode 100644 jiao-an-ai/app/api/__pycache__/lesson_plans.cpython-311.pyc create mode 100644 jiao-an-ai/app/api/health.py create mode 100644 jiao-an-ai/app/api/lesson_plans.py create mode 100644 jiao-an-ai/app/main.py create mode 100644 jiao-an-ai/app/models/__init__.py create mode 100644 jiao-an-ai/app/models/__pycache__/__init__.cpython-311.pyc create mode 100644 jiao-an-ai/app/models/__pycache__/lesson_plan.cpython-311.pyc create mode 100644 jiao-an-ai/app/models/lesson_plan.py create mode 100644 jiao-an-ai/app/services/__init__.py create mode 100644 jiao-an-ai/app/services/__pycache__/__init__.cpython-311.pyc create mode 100644 jiao-an-ai/app/services/__pycache__/lesson_plan_service.cpython-311.pyc create mode 100644 jiao-an-ai/app/services/lesson_plan_service.py create mode 100644 jiao-an-ai/app/static/css/style.css create mode 100644 jiao-an-ai/app/templates/index.html create mode 100644 jiao-an-ai/app/templates/lesson_plans_detail.html create mode 100644 jiao-an-ai/app/templates/lesson_plans_new.html create mode 100644 jiao-an-ai/app/views/__init__.py create mode 100644 jiao-an-ai/app/views/__pycache__/__init__.cpython-311.pyc create mode 100644 jiao-an-ai/app/views/__pycache__/pages.cpython-311.pyc create mode 100644 jiao-an-ai/app/views/pages.py create mode 100644 jiao-an-ai/config/__init__.py create mode 100644 jiao-an-ai/config/__pycache__/__init__.cpython-311.pyc create mode 100644 jiao-an-ai/requirements.txt create mode 100644 jiao-an-ai/scripts/__init__.py create mode 100644 jiao-an-ai/scripts/__pycache__/__init__.cpython-311.pyc create mode 100644 "jiao-an-ai/\344\273\213\347\273\215\346\226\207\346\241\243.md" diff --git a/jiao-an-ai/README.md b/jiao-an-ai/README.md new file mode 100644 index 0000000..e512ec5 --- /dev/null +++ b/jiao-an-ai/README.md @@ -0,0 +1,271 @@ +# jiao-an-ai:基于 LazyLLM 的智能教案生成项目 + +## 1. 项目简介 + +`jiao-an-ai` 是一个基于 **LazyLLM** 和 **FastAPI** 搭建的「AI 智能教案」独立项目,目标是帮助一线教师显著降低备课成本。你可以把它当作一个单独的 Web 服务来部署,`lazyllm` 仅作为普通依赖使用。 + +项目聚焦以下几点: + +- 老师只需输入课程名称、学科、年级、课型等基本信息,以及少量教学设想; +- 系统基于大模型自动生成一份结构化教案(包含教学目标、重难点、教学流程、差异化教学、评估与作业等模块); +- 支持 Web 表单交互和 REST API 两种使用方式,方便后续扩展为完整教案工作台。 + +当前版本重点是跑通整体链路:**表单输入 → LazyLLM 调用在线模型 → 解析结构化结果 → 页面展示教案**。 + +--- + +## 2. 技术栈与整体架构 + +### 2.1 后端 + +- `Python 3.10–3.12`(推荐 3.11) +- `FastAPI`:提供 HTTP API 和简单页面渲染 +- `Uvicorn[standard]`:ASGI 服务器 +- `Pydantic v2`:请求 / 响应模型定义 +- `LazyLLM`:大模型调用与 Agent 编排框架(通过 pip 安装为依赖) + - 使用 `OnlineChatModule` 统一封装在线大模型(OpenAI、通义千问、豆包等) +- (可选)后续可接入数据库(如 SQLite + SQLAlchemy)用于教案持久化 + +### 2.2 前端 + +- 基于 FastAPI 的 Jinja2 模板渲染 +- 简单的 HTML + CSS(`app/static/css/style.css`) +- 阶段 1:服务端渲染(SSR)表单 + 教案预览页面 +- 阶段 2(可选):可在此基础上引入 React/Vue,直接调用后端 JSON API + +### 2.3 目录结构 + +项目根目录为 `jiao-an-ai/`: + +```text +jiao-an-ai/ + README.md 本说明文档 + app/ + main.py FastAPI 入口 + __init__.py + api/ REST API 路由 + __init__.py + health.py 健康检查接口 + lesson_plans.py 教案生成 API 接口 + views/ 页面路由(Jinja2 模板渲染) + __init__.py + pages.py 首页 / 新建教案 / 预览页面路由 + models/ Pydantic 模型 + __init__.py + lesson_plan.py 教案请求 / 响应数据结构 + services/ 业务服务层 + __init__.py + lesson_plan_service.py 教案生成服务封装 + agents/ 基于 LazyLLM 的 Agent 封装 + __init__.py + lesson_plan_agent.py 教案生成 Agent(调用大模型) + templates/ Jinja2 HTML 模板 + index.html 首页 + lesson_plans_new.html 新建教案表单页面 + lesson_plans_detail.html 教案预览页面 + static/ + css/ + style.css 全局样式 + config/ + __init__.py 模型配置(统一创建 OnlineChatModule 实例) + scripts/ + __init__.py 预留脚本目录(运行 / 初始化脚本) +``` + +--- + +## 3. 功能概览 + +### 3.1 教案生成核心流程 + +1. 教师在前端表单中填写课程信息: + - 课程名称(例如:初一语文《背影》) + - 学科、年级、课型 + - 课时(分钟) + - 教学目标、教学重点、教学难点、特殊要求(可选) +2. 后端根据输入构造提示词(Prompt),明确要求大模型以 **JSON 数组** 的形式输出教案各模块: + - 每个元素形如:`{"title": "模块标题", "content": "详细内容"}` + - 建议包含:教学目标、教学重难点、教学流程、差异化教学、评估与作业等模块 +3. 使用 LazyLLM 的 `OnlineChatModule` 调用线上大模型,得到原始字符串结果; +4. 尝试将模型输出解析为 JSON,并映射为内部的 `LessonPlanSection` 列表; +5. 将结构化教案在网页上按模块展示,便于教师查看和后续微调。 + +### 3.2 提供的页面与接口 + +#### 页面 + +- `GET /` + - 首页,展示项目简介和「创建新教案」入口。 +- `GET /lesson-plans/new` + - 新建教案表单页面。 +- `POST /lesson-plans/preview` + - 提交表单并渲染教案预览页面。 + +#### REST API + +- `GET /api/health` + - 健康检查接口,返回 `{"status": "ok"}`。 +- `POST /api/lesson-plans/generate` + - 请求体:`LessonPlanRequest`(JSON) + - 返回体:`LessonPlanResponse`,包含 `id`、`sections`、`meta`。 + - 方便后续前后端分离或第三方系统集成。 + +--- + +## 4. 环境与依赖 + +### 4.1 Python 与虚拟环境 + +- Python 版本:`>=3.10, <3.13`(建议 3.11) +- 强烈建议使用虚拟环境(如 `python -m venv venv`)隔离依赖: + +```bash +cd jiao-an-ai +python -m venv venv +venv\Scripts\activate +``` + +### 4.2 安装依赖 + +`jiao-an-ai` 作为一个独立项目运行时,仅需要通过 pip 安装必要依赖即可,例如: + +```bash +pip install "fastapi[standard]" uvicorn lazyllm pydantic +``` + +如需精细控制版本,可以在项目根目录维护自己的 `pyproject.toml` 或 `requirements.txt`,将 `lazyllm`、`fastapi`、`uvicorn`、`pydantic` 等依赖写入其中,然后: + +```bash +pip install -r requirements.txt +``` + +### 4.3 配置在线大模型 + +项目通过 LazyLLM 的 `OnlineChatModule` 调用在线大模型,配置入口在: + +- `jiao-an-ai/config/__init__.py` + +环境变量说明: + +- `JIAO_AN_AI_LLM_SOURCE` + - 大模型来源平台,默认为 `"openai"`。 + - 可根据 LazyLLM 支持的 source 进行设置,例如:`openai` / `qwen` / `doubao` / `kimi` / `glm` / `sensenova` 等。 +- `JIAO_AN_AI_LLM_MODEL` + - 使用的具体模型名称,可选。 + - 当未设置时,使用对应平台的默认模型。 + +除此之外,你还需要根据所选平台配置相应的 API Key(例如 `OPENAI_API_KEY` 等),具体环境变量名称请参考 LazyLLM 官方文档和各云厂商文档。 +**注意:不要在代码仓库中硬编码或打印任何 API Key 等敏感信息。** + +--- + +## 5. 启动与使用 + +### 5.1 启动步骤 + +1. 进入 `jiao-an-ai` 项目根目录并激活虚拟环境: + +```bash +cd path\to\jiao-an-ai +venv\Scripts\activate +``` + +2. 确保 `lazyllm`、`fastapi`、`uvicorn` 等依赖已安装(见上文)。 + +3. 配置大模型环境变量(示例,以 OpenAI 为例): + +```bash +set JIAO_AN_AI_LLM_SOURCE=openai +set JIAO_AN_AI_LLM_MODEL=gpt-4o-mini +set OPENAI_API_KEY=你的Key +``` + +4. 启动开发服务: + +```bash +py -m uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 +``` + +5. 在浏览器中访问: + +- 首页:`http://127.0.0.1:8000/` +- 新建教案:`http://127.0.0.1:8000/lesson-plans/new` + +### 5.2 示例表单填写建议 + +以语文课《背影》为例,可以这样填写: + +- 课程名称:`初一语文《背影》` +- 学科:`语文` +- 年级:`初一` +- 课型:`新授课` +- 课时:`45` +- 教学目标: + - `帮助学生理解文章中父爱的含蓄表达,学会从细节体会人物情感。` +- 教学重点: + - `抓住描写父亲“背影”的细节,体会情感。` +- 教学难点: + - `引导学生联系自身生活经验,进行情感共鸣。` +- 特殊要求: + - `课堂中至少安排一次小组讨论,鼓励学生主动发言。` + +提交后,系统会调用大模型生成结构化教案,并在预览页中按模块显示。 + +--- + +## 6. 核心模块说明 + +### 6.1 模型配置:`config/__init__.py` + +- `get_chat_module()`:使用 `OnlineChatModule` 封装在线大模型,并通过 `@lru_cache` 进行复用。 +- 支持通过环境变量动态切换大模型来源与型号,方便后续对比不同模型效果。 + +### 6.2 教案 Agent:`app/agents/lesson_plan_agent.py` + +- 核心职责: + - 将教师输入转换为清晰的中文提示词; + - 要求模型输出结构化 JSON; + - 解析 JSON 并转换为内部 `LessonPlanSection`。 +- 异常处理: + - 如果模型输出不能被正确解析为 JSON,返回一个包含基本信息和错误提示的保底教案,避免前端崩溃。 + +### 6.3 服务层:`app/services/lesson_plan_service.py` + +- `generate_lesson_plan`: + - 封装教案生成的业务流程; + - 负责生成教案 ID、填充 meta 信息; + - 是 API 层与 Agent 的中间层,后续可在此添加持久化、日志等能力。 + +### 6.4 API 与视图:`app/api` 与 `app/views` + +- API 层: + - `health.py`:提供 `/api/health` 健康检查接口。 + - `lesson_plans.py`:提供 `/api/lesson-plans/generate` 教案生成接口。 +- 视图层: + - `pages.py`: + - `/`:首页; + - `/lesson-plans/new`:新建教案表单; + - `/lesson-plans/preview`:表单提交后预览教案。 + +--- + +## 7. 后续规划与扩展方向 + +当前项目处于原型阶段,后续可以在此基础上扩展: + +1. **教案模板多样化** + - 按学科、学段、课型配置不同的提示词与模块结构; + - 支持学校自定义模板。 +2. **教案版本管理与持久化** + - 引入数据库(SQLite / MySQL / PostgreSQL)存储教案; + - 支持版本回溯、复制、基于历史教案二次生成。 +3. **差异化教学与分层作业** + - 明确学困生 / 资优生 / 特殊需要学生的教学策略; + - 生成分层作业与课堂即时检测题库。 +4. **与现有教务系统集成** + - 提供稳定的 REST API 或导出格式(如 Word / PDF / Markdown),便于导入学校教案系统。 +5. **效果评估与优化** + - 收集老师对生成教案的修改数据,反向优化提示词与工作流; + - 逐步从单模型调用演进到多 Agent 协作(如目标规划 Agent、活动设计 Agent、评估设计 Agent 等)。 + + diff --git a/jiao-an-ai/app/__init__.py b/jiao-an-ai/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jiao-an-ai/app/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..042bb2b0ec6c863067ba449cdaaa1b739924c28f GIT binary patch literal 135 zcmZ3^%ge<81lv~IXEFlm#~=<2fCNC`GaHbY&XB?o%%I8Wx00cV2_y)T`6ZQ=nV7Ge zn5UbV8IxF05ECDtnU`4-AFo$X`HRCQH$SB`C)KWq6{r+sSTR44_`uA_$oPQ)Miemv F#Q^0>8^8bn literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/__pycache__/main.cpython-311.pyc b/jiao-an-ai/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a30fd2fb142f553c9d78b22bb89fb651dd4871b7 GIT binary patch literal 1824 zcma)6-D?|15Z}|Sbh6Hp9QjLfO6pL%pctuk=#y*Qx``9yw!uVtD8eAFx^sN$eTco2 zi$wz0VyHv%;D?wRN=jbprr0fkzNJw5C)^o@h=YQmHKlI`(-%K=_N0@n&_efScYiap zGrK!8d*4N)5d`h*r+4KY7NOrMPVKE^k z#890_s5l6S^U#nOme|#R*tdy`!?4G2uXS-m8Wkg-2B;4*26y3=WHAZrl9iP2R~ixf z#i$rNi0@(88G5V1`T4xp(*u7|QJQh_vS6;k%-s+Sb746Lkk6ZfDVJ}^ievyAUy`+o zaJ3+*RYfp8QHba@QzCB2@K(Bgrmu8EL^5kcE4R1wCYlUL5IX(XONQAA7ec+4yNB+g zC+DSx!-un9Z=jY??l#U-ifAr_0r^i2j z^T#g_Jbj&etCcxOZ=Wa=sch5*j);k}oV2a@R`NbGHf2j>{S*aifLBkCs1NC@kiw z0#3jR)v7`3VEim|eRgFbm#GrH0;gP2g!)ECQPh=+Ea(|Q%LwucEM8FsS<6;8+(?;7 zP^2Z0kt8^v!8IPhV4=Umh)>*4KTKQKnwWcuM-K7G&OHZDHt?j4Cp)qiqgNa})xc9W zp6Up9?mBp)fhTM{0kSW1U*xvr#}|L7+o@S6HQPwd{+hIM4qj^DB^xjOh6f*)zs)$8 zrW=>0Z9L=PnFgK#(Bxy*(kUDVx#3cY9=cSzRp+L&-Y38JIzojs>*6)tFkM{LiAf)n zq6=br%;l=^R2BIXLaWE+R^h=4Rk@X>!8Vs|<#m<5-B!pGzNY;~W$jKY^f8E0793<- z86_NeC2{I{$k&`tE}_3RH%?XSvh#)}Mq>|G?JU+u{5359eR< zmk;^N4*y1j(iJP*j1KRHo#?m~X$~ZJ-*E=sw4x`W2ovAFd4gbibr?h31*`J8vZ+`s zc*gx(^+Psgv#BOdShwhRgyYuD%@4NO-SpFmXQqwk96Z;+a{yX&!XZ2c7GCuJ0Y`|v Aw*UYD literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/agents/__init__.py b/jiao-an-ai/app/agents/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jiao-an-ai/app/agents/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/agents/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/agents/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f5b7503cea54ee080f7b5cb50dd68a065850b1d GIT binary patch literal 142 zcmZ3^%ge<81h#AJGZ}&OV-N=hKms7}nGHxxXGmcPX3%8xTggzw1QG)=dU}j`w M{J;PsikN|70K4!WEdT%j literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/agents/__pycache__/lesson_plan_agent.cpython-311.pyc b/jiao-an-ai/app/agents/__pycache__/lesson_plan_agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d8818709b0d4958f14b6f678e541e3611cbb2c9 GIT binary patch literal 16771 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 G$^8$ayg2Cq literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/agents/lesson_plan_agent.py b/jiao-an-ai/app/agents/lesson_plan_agent.py new file mode 100644 index 0000000..c839f69 --- /dev/null +++ b/jiao-an-ai/app/agents/lesson_plan_agent.py @@ -0,0 +1,364 @@ +import json +from typing import Any, Iterable + +import lazyllm + +from app.models.lesson_plan import ( + LessonPlanRequest, + LessonPlanReviewResponse, + LessonPlanSection, + LessonPlanVariant, +) +from config import get_chat_module + + +def _build_prompt(data: LessonPlanRequest, profile: str | None = None) -> str: + """ + 构造教案生成提示词 + """ + base_info = ( + f"课程名称:{data.course_name}\n" + f"学科:{data.subject}\n" + f"年级:{data.grade}\n" + f"课型:{data.lesson_type}\n" + f"课时:{data.duration_minutes} 分钟\n" + ) + goals = data.teaching_goals or "" + key_points = data.key_points or "" + difficult_points = data.difficult_points or "" + requirements = data.special_requirements or "" + profile_hint = "" + if profile == "low": + profile_hint = ( + "当前班级整体基础较弱,部分学生学习信心不足,需要更多示例讲解、课堂练习和过程性支撑。\n" + "请在设计中增加铺垫、拆解步骤、同伴互助等环节,降低一次性认知负担。\n" + ) + elif profile == "high": + profile_hint = ( + "当前班级整体基础较好,学习能力较强,适合增加思维挑战和拓展任务。\n" + "请在设计中加入探究式活动、开放性问题和拓展阅读等内容,避免重复性机械练习。\n" + ) + elif profile == "normal": + profile_hint = ( + "当前班级学情整体均衡,既要保证基础知识落实,也要保留适当的思维提升与讨论空间。\n" + ) + extra_profile = "" + if profile_hint: + extra_profile = "【班级学情提示】\n" + profile_hint + return ( + "你是一名熟悉中国义务教育课程标准的资深教研员,请根据以下课程信息," + "生成一份教案的结构化框架。只需要输出 JSON,不要输出任何额外说明。\n\n" + "【课程信息】\n" + f"{base_info}" + f"教师给出的初步教学目标:{goals}\n" + f"教师给出的教学重点:{key_points}\n" + f"教师给出的教学难点:{difficult_points}\n" + f"教师的特殊要求:{requirements}\n" + f"{extra_profile}" + "【输出要求】\n" + "1. 严格输出一个 JSON 数组,每个元素为一个对象,形如:\n" + ' {"title": "模块标题", "content": "详细内容"}\n' + "2. 建议包含以下模块:教学目标、教学重难点、教学流程、差异化教学、评估与作业。\n" + "3. content 字段为字符串,可以包含换行。\n" + "4. 不要在 JSON 之外输出任何文字,例如解释、前后缀等。\n" + ) + + +def _build_stream_prompt(data: LessonPlanRequest) -> str: + """ + 构造用于流式 Markdown 预览的提示词 + """ + base_info = ( + f"课程名称:{data.course_name}\n" + f"学科:{data.subject}\n" + f"年级:{data.grade}\n" + f"课型:{data.lesson_type}\n" + f"课时:{data.duration_minutes} 分钟\n" + ) + goals = data.teaching_goals or "" + key_points = data.key_points or "" + difficult_points = data.difficult_points or "" + requirements = data.special_requirements or "" + return ( + "你是一名熟悉中国义务教育课程标准的资深教研员,请根据以下课程信息," + "生成一份可以直接用于上课的详细教案草案,输出格式为 Markdown。\n\n" + "【课程信息】\n" + f"{base_info}" + f"教师给出的初步教学目标:{goals}\n" + f"教师给出的教学重点:{key_points}\n" + f"教师给出的教学难点:{difficult_points}\n" + f"教师的特殊要求:{requirements}\n\n" + "【输出要求】\n" + "1. 只输出 Markdown 文本,不要输出 JSON、XML 或代码块标记。\n" + "2. 使用合适的标题层级组织内容,建议包含:教学目标、教学重难点、教学流程、差异化教学、课堂评价与作业等模块。\n" + "3. 内容条理清晰、段落分明,适合教师直接复制和微调。\n" + ) + + +def _build_review_prompt(data: LessonPlanRequest) -> str: + """ + 构造教案评审与打分提示词 + """ + base_info = ( + f"课程名称:{data.course_name}\n" + f"学科:{data.subject}\n" + f"年级:{data.grade}\n" + f"课型:{data.lesson_type}\n" + f"课时:{data.duration_minutes} 分钟\n" + ) + goals = data.teaching_goals or "" + key_points = data.key_points or "" + difficult_points = data.difficult_points or "" + requirements = data.special_requirements or "" + profiles = ", ".join(data.class_profiles or []) or "默认班级" + return ( + "你是一名熟悉中国义务教育课程标准的一线教研员,请基于下面这节课的整体设计意图," + "从教学目标匹配度、重难点落实、学情适配、课堂活动设计、评价与作业等维度做一次专业评审,并给出 0-100 分的综合得分。\n\n" + "【课程信息】\n" + f"{base_info}" + f"教师给出的初步教学目标:{goals}\n" + f"教师给出的教学重点:{key_points}\n" + f"教师给出的教学难点:{difficult_points}\n" + f"教师的特殊要求:{requirements}\n" + f"目标适配班级学情:{profiles}\n\n" + "【评审输出要求】\n" + "1. 请只返回一个 JSON 对象,不要输出任何额外解释或前后缀。\n" + "2. JSON 字段说明如下(字段名需保持一致):score、level、summary、strengths、improvements。\n" + "3. 其中 score 为 0-100 的整数,level 为“优秀/良好/合格/待优化”等等级描述," + "summary 为总体评价概述,strengths 为若干条亮点(可使用换行分隔)," + "improvements 为若干条有针对性的改进建议(可使用换行分隔)。\n" + ) + + +def _extract_json_text(raw: str) -> str | None: + """ + 从大模型原始输出中尽量提取可解析的 JSON 文本 + """ + text = (raw or "").strip() + if not text: + return None + + if text.startswith("```"): + lines = text.splitlines() + if lines and lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].startswith("```"): + lines = lines[:-1] + text = "\n".join(lines).strip() + + try: + json.loads(text) + return text + except Exception: + pass + + start = text.find("[") + end = text.rfind("]") + if start != -1 and end != -1 and end > start: + candidate = text[start : end + 1] + try: + json.loads(candidate) + return candidate + except Exception: + pass + + start = text.find("{") + end = text.rfind("}") + if start != -1 and end != -1 and end > start: + candidate = text[start : end + 1] + try: + obj = json.loads(candidate) + if isinstance(obj, dict): + return json.dumps([obj], ensure_ascii=False) + except Exception: + pass + + return None + + +def _generate_sections_from_prompt(prompt: str, data: LessonPlanRequest) -> list[LessonPlanSection]: + """ + 使用指定提示词调用大模型并解析为分节内容 + """ + try: + chat = get_chat_module() + raw = chat(prompt) + except Exception as exc: + raw = str(exc) + + try: + json_text = _extract_json_text(raw) + if json_text is None: + raise ValueError("no valid json text") + parsed: Any = json.loads(json_text) + sections: list[LessonPlanSection] = [] + if isinstance(parsed, list): + for item in parsed: + if not isinstance(item, dict): + continue + title = str(item.get("title", "")).strip() + content = str(item.get("content", "")).strip() + if not title and not content: + continue + sections.append(LessonPlanSection(title=title, content=content)) + if sections: + return sections + except Exception: + pass + + raw_text = (raw or "").strip() + if raw_text: + return [LessonPlanSection(title="AI 生成教案草案", content=raw_text)] + + overview = LessonPlanSection( + title="教学概述", + content=( + f"课程名称:{data.course_name};学科:{data.subject};" + f"年级:{data.grade};课型:{data.lesson_type};" + f"课时:{data.duration_minutes} 分钟。" + ), + ) + fallback_content = "AI 教案生成出现问题,请稍后重试或检查大模型配置。" + return [overview, LessonPlanSection(title="教案内容", content=fallback_content)] + + +def generate_lesson_plan_sections(data: LessonPlanRequest) -> list[LessonPlanSection]: + """ + 基于 LazyLLM 生成教案分节内容 + """ + prompt = _build_prompt(data) + return _generate_sections_from_prompt(prompt, data) + + +def _normalize_profiles(class_profiles: list[str] | None) -> list[str]: + """ + 归一化班级画像列表 + """ + if not class_profiles: + return ["normal"] + normalized: list[str] = [] + for p in class_profiles: + p = (p or "").strip().lower() + if not p: + continue + if p in {"low", "normal", "high"}: + normalized.append(p) + elif "困" in p or "差" in p: + normalized.append("low") + elif "优" in p or "拔高" in p: + normalized.append("high") + else: + normalized.append("normal") + if not normalized: + normalized.append("normal") + return list(dict.fromkeys(normalized)) + + +def _profile_label(profile: str) -> str: + """ + 将班级画像编码转换为展示标签 + """ + if profile == "low": + return "学困型班级教案" + if profile == "high": + return "拔高型班级教案" + return "标准班级教案" + + +def generate_lesson_plan_variants(data: LessonPlanRequest) -> list[LessonPlanVariant]: + """ + 生成按班级学情区分的多版本教案 + """ + profiles = _normalize_profiles(data.class_profiles) + variants: list[LessonPlanVariant] = [] + for profile in profiles: + prompt = _build_prompt(data, profile=profile) + sections = _generate_sections_from_prompt(prompt, data) + variants.append( + LessonPlanVariant( + profile=profile, + label=_profile_label(profile), + sections=sections, + ) + ) + return variants + + +def stream_lesson_plan_text(data: LessonPlanRequest) -> Iterable[str]: + """ + 流式生成教案文本内容 + """ + prompt = _build_stream_prompt(data) + chat = get_chat_module() + lazyllm.FileSystemQueue().clear() + queue = lazyllm.FileSystemQueue() + + with lazyllm.ThreadPoolExecutor(1) as executor: + future = executor.submit(chat, prompt) + while True: + value = queue.dequeue() + if value: + chunk = "".join(value) + if chunk: + yield chunk + elif future.done(): + break + _ = future.result() + lazyllm.FileSystemQueue().clear() + + +def review_lesson_plan(data: LessonPlanRequest) -> LessonPlanReviewResponse: + """ + 基于 LazyLLM 对教案进行评审与打分 + """ + prompt = _build_review_prompt(data) + try: + chat = get_chat_module() + raw = chat(prompt) + except Exception as exc: + raw = str(exc) + + text = (raw or "").strip() + if not text: + return LessonPlanReviewResponse( + score=0, + level="待优化", + summary="AI 评审出现问题,请稍后重试或检查大模型配置。", + strengths=None, + improvements=None, + ) + + try: + json_text = _extract_json_text(text) or text + parsed: Any = json.loads(json_text) + if isinstance(parsed, list) and parsed: + parsed = parsed[0] + if isinstance(parsed, dict): + score_raw = parsed.get("score", 0) + try: + score = int(score_raw) + except Exception: + score = 0 + level = str(parsed.get("level") or "").strip() or "待优化" + summary = str(parsed.get("summary") or "").strip() + strengths = str(parsed.get("strengths") or "").strip() + improvements = str(parsed.get("improvements") or "").strip() + if not summary: + summary = "AI 已完成评审,但未返回详细说明。" + return LessonPlanReviewResponse( + score=score, + level=level, + summary=summary, + strengths=strengths or None, + improvements=improvements or None, + ) + except Exception: + pass + + return LessonPlanReviewResponse( + score=0, + level="待优化", + summary=text, + strengths=None, + improvements=None, + ) diff --git a/jiao-an-ai/app/api/__init__.py b/jiao-an-ai/app/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jiao-an-ai/app/api/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/api/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4143002dfde506b48cc8760df1c7f2d66ad5ba5 GIT binary patch literal 139 zcmZ3^%ge<81UpvSXEFlm#~=<2fCNC`GaHbY&XB?o%%I8Wx00cV2_y)T`6ZW?nV7Ge zn5UbV8IxF00K}Ow@$s2?nI-Y@dIgogIBatBQ%ZAE?TT1|%0UJe^8<+w%#4hT9~fXn I5i?K>0DpHKTmS$7 literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/api/__pycache__/health.cpython-311.pyc b/jiao-an-ai/app/api/__pycache__/health.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13dd484e4334a5dbf14d6f98e83846433ced879b GIT binary patch literal 574 zcmY*W&r1S96n-tJ`0d=Zwc`Fc& z$a~PO|G=O_>@}3SZFK9@+clHk?)ToCZ{B1MBa;s(OQB; zv?P%?BrSDA+5!0P(H13hEK_<1yx4`_Z9NtdSTaK_Bge9gkmxb@i2~E;5YwoZy@K*% zf3ZXJ*o&`RzO+`WJA}d{d&QwdHTCM|8nLZf)h2n-%f3u(yH>qysMR&{PMz3}Hx^v# z0==o=+BKcL8=#ojBvnFHhg1yCRk47~8>58b!fQ6aL2#{+Evc4X>h3JCowk<>@Ys35 zlPyX%^y{AFsGD{pUh1C*K4oanm5N!@h~Y_^>ZqPfiBqT5^&|B`4H5c>&=2Rpyo4}h zm=k#a9ty_`RK)cs0h-7`>wNK5=Wg@Q&%f?8KkeN7_Tbhxd^wM43?TO~9f84Sbh(Gm zT|8G_xm2)d?J8M!%7)t5Dj0@YzN)LWf?6%8dRetB=z2NmXVKd7ruv=*6{-M|P5|{d zJ7W^eefVvoe-bE_yCb!d^X=rkE6pFm8`Ky*Qm4=rJ0t=tF)qRj@GvZb!$j(}M+MRF zo)9h!kVvdQ1MsdyKfizQ>#bviYMgM$b*BX3)vM=o>u;;nCeBjb*(jWaASO(r>1xH< zvPfQFp9MtOSAvd#>5593@7}5Zl&6M{Q2#f7G7-QMPHEcf}20buC?G^c$JR> z^FTzxQXl4LdJJy*R14?YIOhvdEW6LH_=t@!zRt&iVY{F1!~BW<`ER(VFR=5M+jyCs bKj)r4%Z{9LmtN!Jz%Ygt+{X>pc~t%d7<2Hk literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/api/health.py b/jiao-an-ai/app/api/health.py new file mode 100644 index 0000000..e5e50fc --- /dev/null +++ b/jiao-an-ai/app/api/health.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + + +router = APIRouter(prefix="/api") + + +@router.get("/health") +async def health_check() -> dict: + """ + 健康检查接口 + """ + return {"status": "ok"} + diff --git a/jiao-an-ai/app/api/lesson_plans.py b/jiao-an-ai/app/api/lesson_plans.py new file mode 100644 index 0000000..d02475c --- /dev/null +++ b/jiao-an-ai/app/api/lesson_plans.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter +from fastapi.responses import StreamingResponse + +from app.models.lesson_plan import ( + LessonPlanRequest, + LessonPlanResponse, + LessonPlanReviewResponse, +) +from app.services.lesson_plan_service import ( + generate_lesson_plan, + review_lesson_plan, + stream_lesson_plan, +) + + +router = APIRouter(prefix="/api/lesson-plans", tags=["lesson_plans"]) + + +@router.post("/generate", response_model=LessonPlanResponse) +async def generate_lesson_plan_api(data: LessonPlanRequest) -> LessonPlanResponse: + """ + 教案生成接口 + """ + return generate_lesson_plan(data) + + +@router.post("/generate-stream") +async def generate_lesson_plan_stream_api(data: LessonPlanRequest) -> StreamingResponse: + """ + 流式教案生成接口 + """ + return StreamingResponse( + stream_lesson_plan(data), + media_type="text/plain; charset=utf-8", + ) + + +@router.post("/review", response_model=LessonPlanReviewResponse) +async def review_lesson_plan_api(data: LessonPlanRequest) -> LessonPlanReviewResponse: + """ + 教案评审与打分接口 + """ + return review_lesson_plan(data) diff --git a/jiao-an-ai/app/main.py b/jiao-an-ai/app/main.py new file mode 100644 index 0000000..65d5f26 --- /dev/null +++ b/jiao-an-ai/app/main.py @@ -0,0 +1,36 @@ +from pathlib import Path + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from app.api.health import router as health_router +from app.api.lesson_plans import router as lesson_plans_router +from app.views.pages import router as pages_router + + +BASE_DIR = Path(__file__).resolve().parent +STATIC_DIR = BASE_DIR / "static" +TEMPLATES_DIR = BASE_DIR / "templates" + + +def create_app() -> FastAPI: + """ + 创建 FastAPI 应用实例 + """ + app = FastAPI() + app.include_router(health_router) + app.include_router(lesson_plans_router) + app.include_router(pages_router) + app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") + Jinja2Templates(directory=str(TEMPLATES_DIR)) + return app + + +app = create_app() + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) diff --git a/jiao-an-ai/app/models/__init__.py b/jiao-an-ai/app/models/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jiao-an-ai/app/models/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/models/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb37deb656d7a2799c00c88045447b651de195ab GIT binary patch literal 142 zcmZ3^%ge<81eR;;GZ}&OV-N=hKms7}nGHxxXGmcPX3%8xTggzw1QG!x``eu literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/models/__pycache__/lesson_plan.cpython-311.pyc b/jiao-an-ai/app/models/__pycache__/lesson_plan.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3276ca8ed84f038d3fc0fad0e3a9beadf45632d6 GIT binary patch literal 2791 zcma)7-*XdH6uz6>O*TIoXeme?#FnCIfS1r-CLNG6cmKsim8CAol!tfhW zm3*Jn;*0pcNQ*D(`=TwrnD2|RIHP@Tun4Q~j(a?QqtiXbI5&;=bj3K#PG%XmvpZmJ zv>&e8cb99|=a(-1Seq|Ay!(6Y*E6+S*Q-~*a!DtaGP4$EvZ17z6Xw~YCs@jM#92#G znG@69*UEM-!<>z3)>3TEG~~2qWNpT$orKMl)N#$2m1j*w=T7ufmXkB4X4o92YsZdh zsjO}{JiR=_QktU67KEr-EX{CUBBd*w%Nff&ronRXF_P>?Bg;-ymeZ!1)fw1vSw5Ln zbc_?8!m_NIDKLWEwww*JtQdxAyWw&4LI_q26tLn5T?h$;4FC?+HE#bzmcb^E-#GaC z%v*=XVSW7a8C^M*8`t&p%n3~~#}#8-(PorPW(MM5I-l_c$$-$iGdT-)f(x)k2>%Np z-||-v1VpJ$5G2yGxkP(mlxc6JYh!*YKXrPl5=-PK^OL71E8R%>?3TU~#Zjhxm4V^> zbm8sd(Bfo?4#OzZ;mQjG`N=}Cpe{yBbO1(~4pat5;Kzkj@!;Z#5(RstOh+msVK>=KniU5c%Bc?8sj z;%TqT2ls9-{cyHAfBxZ@XR7DVRqy=a)um`_whk4UG7TH*WO_7YZN$*J5qc0dA@m|_ zM%aqrM*loo+Zuo*-wm*CX5t(CBs9{3$?;x=Z&_Q2{lGkn5G=&P&CLrf!EGI?2=atl z<@r#tG{vy97^$~yFDxv5dwc2r&FbwNwJ*+9FWd)_Kq`MbFU;4j{NhO!_V>dH~3 zI}-P#=F_9n|A_V+tmaA8CSN}i2;1T22LRTUFVWpz%!!`TrlG$RSg&5mdWI({zI1I# zKLO?`lC}%!bz`Y1X)*$0N1I}+qThwR&Ww_Wou1ZC$up+GS$j!(t+IOY>+1bWt*tVk zsn#IG=+HE?m1#$s^K!;VW6vtHLsczhTeyo{b=#|U7zqTN!9NCASN2FeKarm}JyGdU zyAo}5l|6g&@64-SGH%gwVpoaog;9193@;1F>4>^PFzdFPMP!0ozcjeIokrudi})hEPL?r6@<3^ zf#F#Qd*S8^z`BWAZ7-|MzR_nY+xqi|=6T`$g;OQk52H-`D=!Y_rwW5bx(Gdc5JuTW zFuY{pn$u9nQYNQDztB=v5Io*N(h!-jycRD+#aFNhcKh a9-()oNo|P;Lv_*##oQzGuB<`1a{dR~iNdM? literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/models/lesson_plan.py b/jiao-an-ai/app/models/lesson_plan.py new file mode 100644 index 0000000..689e36d --- /dev/null +++ b/jiao-an-ai/app/models/lesson_plan.py @@ -0,0 +1,60 @@ +from pydantic import BaseModel + + +class LessonPlanRequest(BaseModel): + """ + 教案生成请求模型 + """ + + course_name: str + subject: str + grade: str + lesson_type: str + duration_minutes: int + teaching_goals: str | None = None + key_points: str | None = None + difficult_points: str | None = None + special_requirements: str | None = None + class_profiles: list[str] | None = None + + +class LessonPlanSection(BaseModel): + """ + 教案中的分节内容 + """ + + title: str + content: str + + +class LessonPlanVariant(BaseModel): + """ + 按班级学情区分的教案版本 + """ + + profile: str + label: str + sections: list[LessonPlanSection] + + +class LessonPlanResponse(BaseModel): + """ + 教案生成响应模型 + """ + + id: str + sections: list[LessonPlanSection] + meta: dict + variants: list[LessonPlanVariant] | None = None + + +class LessonPlanReviewResponse(BaseModel): + """ + 教案评审与打分结果模型 + """ + + score: int + level: str + summary: str + strengths: str | None = None + improvements: str | None = None diff --git a/jiao-an-ai/app/services/__init__.py b/jiao-an-ai/app/services/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jiao-an-ai/app/services/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/services/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be1fffbea62d787f134e657c1b1e83f3caa68ac5 GIT binary patch literal 144 zcmZ3^%ge<81lDWpGZ}&OV-N=hKms7}nGHxxXGmcPX3%8xTggzw1QGRX`fWlw(RG)dES6V%h@m+#N{ z{@(AqxzgF`M{q7a{$ld12cbVX(>%b7v-uHxokKd(v4Nue+ht(W6?I`wyA8J~MJ3Y{ z^_bqM&y=IGkV{5~>5uwNC90T#XaHm$;@7=IIW6hFGg7p(!MjNCp++EtKqrH)tDy@3 z-vPW=$8RFl{};b^T*XX2<`Al#GKea%{WHV@#vyTo*ml~AXAI4X+a%#6)0X`J^{hsd zn&sH6my+2edAHq`g>8ot&1{#&9rB)|x>?WbBE}mK;skjoOKgYrwu|fx#3Zc0UB>;N zAPtSAYmUaelsH*xC2*^7guFnz;j=jbQs_3G$2pX0U7$}mT-!J~v4YhbadPG|gmvp# z7%>>AxS6!F4zXeITuGhDtvQoS0uNSh4wl-Xu0N!cvK@Nk?^C8}`kUJk+wZ=%gz@q+KZGBp`{>DVYl<3+>_?ftn? z!!Tp1q?R7ltWhl)(=wTuP3UYgLF`zgy2i2y;aFyl_1$lq=paPlx!D&17EoPA-F*ww z6`xY}?W-f7=b1l3)Iw3Qv~9 zlY(uxxD_5KhYwal;lj~M&z{1*$`(HuDGxqbSG)ml9RVy%-3g+=!E>zuYlTUtFkO`K)&KliJcXaYe)rh(p%GB1@UeMA7tkFU z1s+|VC@I5bWwu0Lptu&V4=1&{H6{_E!uYI~$&3}CRm&Mxo zrAGFwfA>o;uE8KUbciRqT@dBVE?}H1wkvpSd8(wSWkoGYs;EHiru30QYyj4Mc45P!GOKyl}PUGiX89sM^5NZj_{K(G%JTpSv& wBSAd<>K1Vy9FwqG+&{+MtLx9+c=?Ct1$i$7B|Or&Hd1`*HLf0cAc%JT4b2n_4FCWD literal 0 HcmV?d00001 diff --git a/jiao-an-ai/app/services/lesson_plan_service.py b/jiao-an-ai/app/services/lesson_plan_service.py new file mode 100644 index 0000000..cdd02de --- /dev/null +++ b/jiao-an-ai/app/services/lesson_plan_service.py @@ -0,0 +1,52 @@ +import uuid + +from typing import Iterable + +from app.agents.lesson_plan_agent import ( + generate_lesson_plan_sections, + generate_lesson_plan_variants, + review_lesson_plan as review_lesson_plan_agent, + stream_lesson_plan_text, +) +from app.models.lesson_plan import ( + LessonPlanRequest, + LessonPlanResponse, + LessonPlanReviewResponse, +) + + +def generate_lesson_plan(data: LessonPlanRequest) -> LessonPlanResponse: + """ + 生成教案响应对象 + """ + variants = None + if data.class_profiles: + variants = generate_lesson_plan_variants(data) + if variants: + sections = variants[0].sections + else: + sections = generate_lesson_plan_sections(data) + else: + sections = generate_lesson_plan_sections(data) + meta = { + "course_name": data.course_name, + "subject": data.subject, + "grade": data.grade, + "lesson_type": data.lesson_type, + "duration_minutes": data.duration_minutes, + } + return LessonPlanResponse(id=str(uuid.uuid4()), sections=sections, meta=meta, variants=variants) + + +def stream_lesson_plan(data: LessonPlanRequest) -> Iterable[str]: + """ + 流式生成教案原始文本 + """ + return stream_lesson_plan_text(data) + + +def review_lesson_plan(data: LessonPlanRequest) -> LessonPlanReviewResponse: + """ + 评审教案并返回打分结果 + """ + return review_lesson_plan_agent(data) diff --git a/jiao-an-ai/app/static/css/style.css b/jiao-an-ai/app/static/css/style.css new file mode 100644 index 0000000..699a279 --- /dev/null +++ b/jiao-an-ai/app/static/css/style.css @@ -0,0 +1,3 @@ +body { + margin: 0; +} diff --git a/jiao-an-ai/app/templates/index.html b/jiao-an-ai/app/templates/index.html new file mode 100644 index 0000000..887e475 --- /dev/null +++ b/jiao-an-ai/app/templates/index.html @@ -0,0 +1,335 @@ + + + + + AI 智能教案 + + + + +
+ +
+ +
+ + +
+
+
+
+

+ + 每节课平均节省 60% 备课时间 +

+

+ 让 AI 帮你备课,而不是替你上课 +

+

+ AI 智能教案专注一件事:用最懂教学的方式,把你的想法快速整理成规范教案, + 帮你从搜索资料、排版格式中解放出来,把时间还给学生和课堂。 +

+
+ + 立即体验教案生成 + + + 无需安装软件 · 打开浏览器即可使用 + +
+
+
+
真正懂“教案”的结构
+

+ 按教学目标、重难点、教学流程、差异化、作业与评估等模块输出, + 贴合学校常用教案模板。 +

+
+
+
从“空白页焦虑”中解放
+

+ 你只需提供课程信息和大致思路,其余交给系统,让修改比从零写更轻松。 +

+
+
+
贴合真实课堂节奏
+

+ 支持设置课时长度和课型,生成的活动设计更容易直接搬到课堂上用。 +

+
+
+
+ + +
+
+
+
+ 一眼看懂的教案结构 +
+ + + 已为多节课堂生成 + +
+
+
+
+ 教学目标 +
+

+ 通过细读文本和课堂活动,引导学生理解文本情感, + 提升朗读表达与情感共鸣能力。 +

+
+
+
+ 教学重难点 +
+

+ 重点:抓住关键细节体会人物情感。难点:帮助学生联系自身生活经验, + 把“理解父爱”变成学生自己的表达。 +

+
+
+
+ 课堂流程示意 +
+

+ 情境导入 → 默读标记 → 小组讨论 → 全班交流 → 情感升华 → 课后延伸。 +

+
+
+
+
+ 备课时长:约 10 分钟 + + 支持任意学科 / 年级 +
+ + 去试一试 → + +
+
+
+
+
+ + +
+
+
+

+ 为什么老师愿意用 AI 智能教案? +

+

+ 我们只解决老师日常最真实的几个问题:时间不够、结构难想、课堂不好设计。 +

+
+
+
+
+ 减少“机械劳动”而不是“教学思考” +
+

+ 教师专注于确定教学目标和课堂方向;查资料、拆结构、润色表述等机械工作交给系统完成, + 避免被 PPT 和格式牵着走。 +

+
+
+
+ 教案一目了然,方便教研与共备 +
+

+ 输出结构统一、逻辑清晰的教案,便于与备课组共享、对照修改, + 大幅减少“看不懂别人教案”的沟通成本。 +

+
+
+
+ 充分保留教师个人风格 +
+

+ 系统给出的是“80% 完成度”的草案,你可以根据班级特点随时调整活动设计和提问方式, + 不会变成千篇一律的流水线课堂。 +

+
+
+
+
+ + +
+
+
+
+

+ 这些场景下,AI 智能教案特别好用 +

+
    +
  • + +
    +
    第一次教这节课
    +

    + 对教材内容不够熟悉时,先生成一份结构清晰的教案,帮助快速理清思路。 +

    +
    +
  • +
  • + +
    +
    准备公开课 / 观摩课
    +

    + 用系统先跑出一个「教研版」教案,再和同事一起打磨亮点和细节。 +

    +
    +
  • +
  • + +
    +
    跨班级、跨平行班备课
    +

    + 基于一份教案模板,快速调整目标和活动设计,生成适配不同班级的版本。 +

    +
    +
  • +
  • + +
    +
    需要分层作业与差异化教学
    +

    + 通过简单说明班级情况,让系统自动给出学困生、资优生的不同任务建议。 +

    +
    +
  • +
+
+ +
+
+ 一天的时间,怎样被重新分配? +
+
+
+ 传统模式:备课占用 + 2–3 小时 / 节 +
+
+
+
+
+ 使用 AI 智能教案后:备课占用 + 30–40 分钟 / 节 +
+
+
+
+
+

+ 省下来的时间,可以用在找练习、批改作业、设计更有趣的课堂活动, + 而不是和文档模板“斗争”。 +

+
+
+
+
+ + +
+
+
+

+ 老师们怎么评价? +

+

+ 下面是我们在打磨产品过程中,老师们最常给出的几句话反馈。 +

+
+
+
+

+ “以前写教案最怕打开空白 Word,现在只要先生成一个框架, + 我只需要改细节,反而更容易想清楚整节课怎么走。” +

+
+ —— 初中语文老师 +
+
+
+

+ “结构跟学校教案本子基本一致,复制过去稍微调一下就能交, + 不用再到处找模板抄格式了。” +

+
+ —— 高中英语老师 +
+
+
+

+ “我会把重要的课堂提问和活动改成自己的语言, + 但有这个底稿以后,备课不会再拖到半夜了。” +

+
+ —— 小学数学老师 +
+
+
+
+
+ + +
+
+
+
+
+

+ 用一节课时间,换一整学期更轻松的备课体验 +

+

+ 现在就试着用 AI 帮你准备下一节课的教案。先从一节课开始, + 感受一下“备课不再是熬夜”的状态。 +

+
+ +
+
+
+
+
+ +
+
+
AI 智能教案 · 帮老师把时间用在最值得的地方
+
仅做备课助手,不替代老师的判断与温度
+
+
+
+ + diff --git a/jiao-an-ai/app/templates/lesson_plans_detail.html b/jiao-an-ai/app/templates/lesson_plans_detail.html new file mode 100644 index 0000000..5f9a479 --- /dev/null +++ b/jiao-an-ai/app/templates/lesson_plans_detail.html @@ -0,0 +1,136 @@ + + + + + 教案预览 · AI 智能教案 + + + + +
+
+ +
+ +
+
+
+
+

+ 教案预览 +

+

+ 以下内容由 AI 根据你填写的信息生成,你可以直接复制到学校教案系统中并按需修改。 +

+
+ +
+ +
+
+
+
+ 基本信息 +
+
+ 课程与班级信息仅在本地展示,不会被保存到服务器。 +
+
+
+
+
+
课程
+
{{ lesson_plan.meta.course_name }}
+
+
+
学科
+
{{ lesson_plan.meta.subject }}
+
+
+
年级
+
{{ lesson_plan.meta.grade }}
+
+
+
课型
+
{{ lesson_plan.meta.lesson_type }}
+
+
+
课时
+
{{ lesson_plan.meta.duration_minutes }} 分钟
+
+
+
教案 ID
+
{{ lesson_plan.id }}
+
+
+
+ +
+ {% for section in lesson_plan.sections %} +
+

+ {{ section.title }} +

+
+{{ section.content }}
+                        
+
+ {% endfor %} +
+ +
+ + 返回修改输入 + +

+ 提示:你可以在教案系统中按模块粘贴或整体粘贴本页内容。 +

+
+
+
+
+ + + + diff --git a/jiao-an-ai/app/templates/lesson_plans_new.html b/jiao-an-ai/app/templates/lesson_plans_new.html new file mode 100644 index 0000000..0508428 --- /dev/null +++ b/jiao-an-ai/app/templates/lesson_plans_new.html @@ -0,0 +1,537 @@ + + + + + 新建教案 · AI 智能教案 + + + + + +
+
+ +
+ +
+
+
+
+

+ 填写课程信息 +

+

+ 建议尽量填写清晰的教学目标和重难点,AI 会在此基础上生成更贴合课堂的教案结构。 +

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ + + +
+

+ 同一堂课可同时生成学困版、标准版、拔高版多套教案方案。 +

+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + + 系统将生成结构化教案,你可以在下一步查看和复制。 + +
+
+ + +
+ +
+
+
+
+
+ 实时教案预览 +
+

+ 生成过程会以 Markdown 形式实时渲染,便于直接阅读和复制。 +

+
+ +
+
+
+
+
+
+
+
+ AI 教案评审与打分 +
+

+ 基于课程信息,从教学目标、学情适配等维度给出综合评价。 +

+
+ +
+
+ 生成教案后,可点击上方按钮让 AI 帮你做一次快速评审。 +
+ +
+
+
+
+
+
+ + + diff --git a/jiao-an-ai/app/views/__init__.py b/jiao-an-ai/app/views/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jiao-an-ai/app/views/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/views/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/views/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fc422a6d1ed506843b91487bf451b257f840834 GIT binary patch literal 141 zcmZ3^%ge<81SV_jGZ}&OV-N=hKms7}nGHxxXGmcPX3%8xTggzw1QG42!^+kPdWZ{ z@90DVT@b0!sz3`+sFKQni<|;OkzqdwK@zm-i=T=<9s>e791y@jVa0m8z{!iBGP_68 zq}>!~fvk4tW@qPS_rCpRs1E}HAA;ui_&MpH9)$i)C!IFbR_=Y~KL?S1nBA4Yj zsyk#y(V2A?`7B>_WnIkXl-)&7*28pO9w>UVUZ%TbUs1>kOn1xvVjvsfkON^32e&wc zZosgcHrUa6pIb1w(KRfEI;!Xy?x;u{SP>A)h9RmKMg)iZBj5D)&$ChRa`s2g#&ArF zcSH6lq7Qp+3R{4~4H$M4{n#Z2uxEi6gO~Y-A|<{z(%^rM6nZ355+ZeOqO&!7is45i z!8h?@r2k8Q5L@&=?D2ov7Q76}7>7zr3j7(;ki9EN9f1$Zf@x>~xcjdV473kh-b zvg@H+NQu$&Xlmac#cPT)`LlWT!j#kWo;W*mu3S|yG5KGVNzwG2!@sU#MFk)K%X6=s zIfs=>xujs256()ZrTkClaIqriRjhz1d}mma(qTf`+yz#}KK)f~8&_9(GU zuiEpnSCj}YsAaNzpF0Agsc@EH6(HU>;+t$)dV>C#-Q!~?W5iI|Gc;L-rnXX%&_8k8d_?0zgWpRZKrFG~0=WxkSM#7d^JY`R!5k^~o;;L775 z?!sRked|O1_!q+X=fb!q@BHePM?d;w-I&YJ%b z^7GY0;KALchA;7dPGSpv}}=om^&8nOLGC}{+v zR2iN!lH*2nlq#b?v7)|&`)dT^8!GCrew#qiN$oey$WyJzQ}xqE>cGw+J$M{?{iQF3 zn4S6h#Phc%o5E~MnAQ1NM)Rq(>8U`qmZdicy;Re)kXO`vMI!XN2`z-gP3_^fJ=3Y9 z3W47XWJWsncJT#nlL5MrfclgvmPITpnV#!l_?u&NQNiS*RDjCWvz%)i*$tTkx!i&z z<6MrGYo#n-goLZ)2~7htF!$msXV09Nd+~hk)XX{4si?%RoNi+Oxg|nyO#R{KZk=>Wqh1W!xBv(WflJau$&yn8E8oF@3+y9^zZKmdQ$K2 z2FmFD{WdzP_jd!G(EI!CP^`f>USA8Y2fqkSd>)!;hMs8A{^Ol+EA+FSH=Cgo^?>0| zul`aiXoc;=&537P6VK@W>85|W<)5y54R2(1_z&qnrt5CYjT{5NPrZ{`J*oNYsU~;0 z#T~XBJU7d2BrQar+PwYjm0vrNBh&S?g6$x>fBz1Pa;(K2qfuVtw5RENuW{Qo``IgL zl;d4bJ4*VUv>rIPGha_Pxffd83vj@pk%qc)a&38i*@z|^M>m{n)9celINpdg3Twmb z!$vaQbr>1jIHg6e%v_x@4vcFtP25afOBtgFHs-X0SF%^L#^aOP(QSV7*=x@llZP~Q z`_$&jwG}Hq!ky+UM5PgkSx&0l4#zAPv%66+Y$yu_IL9m*aK HTMLResponse: + """ + 渲染首页 + """ + return templates.TemplateResponse("index.html", {"request": request}) + + +@router.get("/lesson-plans/new", response_class=HTMLResponse) +async def new_lesson_plan(request: Request) -> HTMLResponse: + """ + 渲染新建教案页面 + """ + return templates.TemplateResponse("lesson_plans_new.html", {"request": request}) + + +@router.post("/lesson-plans/preview", response_class=HTMLResponse) +async def preview_lesson_plan( + request: Request, + course_name: str = Form(...), + subject: str = Form(...), + grade: str = Form(...), + lesson_type: str = Form(...), + duration_minutes: int = Form(...), + teaching_goals: str | None = Form(None), + key_points: str | None = Form(None), + difficult_points: str | None = Form(None), + special_requirements: str | None = Form(None), +) -> HTMLResponse: + """ + 处理表单并预览生成教案 + """ + lesson_request = LessonPlanRequest( + course_name=course_name, + subject=subject, + grade=grade, + lesson_type=lesson_type, + duration_minutes=duration_minutes, + teaching_goals=teaching_goals, + key_points=key_points, + difficult_points=difficult_points, + special_requirements=special_requirements, + ) + lesson_plan = generate_lesson_plan(lesson_request) + context = {"request": request, "lesson_plan": lesson_plan} + return templates.TemplateResponse("lesson_plans_detail.html", context) diff --git a/jiao-an-ai/config/__init__.py b/jiao-an-ai/config/__init__.py new file mode 100644 index 0000000..421134e --- /dev/null +++ b/jiao-an-ai/config/__init__.py @@ -0,0 +1,34 @@ +import os +from functools import lru_cache + +import lazyllm + + +@lru_cache(maxsize=1) +def get_chat_module() -> lazyllm.OnlineChatModule: + """ + 获取用于教案生成的在线对话模型 + """ + source = os.getenv("JIAO_AN_AI_LLM_SOURCE", "kimi") + model = os.getenv("JIAO_AN_AI_LLM_MODEL", "kimi-k2-turbo-preview") + base_url = os.getenv("JIAO_AN_AI_LLM_BASE_URL") + api_key = os.getenv("JIAO_AN_AI_LLM_API_KEY","sk-") + + if not base_url and model and "kimi" in model: + base_url = "https://api.moonshot.cn/v1" + if base_url and "moonshot.cn" in base_url: + after_scheme = base_url.split("://", 1)[-1] + if "/v1" not in after_scheme: + base_url = base_url.rstrip("/") + "/v1" + + params: dict = {"source": source, "stream": True} + if model: + params["model"] = model + if base_url: + params["base_url"] = base_url + if api_key: + params["api_key"] = api_key + + return lazyllm.OnlineChatModule(**params) + + diff --git a/jiao-an-ai/config/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/config/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73efa325f67cb99e8639e5c3f473d4b3726fba96 GIT binary patch literal 1610 zcmZ`(-A~(A6u-9PB#@Bs6#{52Ol3%@OHxP~YlWs|Z5W{hy3$H*6v=XP9WXd?YCGwO zLYs#^Fby$Ob%RdbmP!-L)Jf%ST3_}rI8q)WOGro?(7rLDz3{S~n+!UTIKKDXd(Qct z58vzKpBftK5m3hTvDj-u=nowgb}7B6d@}uQ3*OiQ#BnEVQ3`KnEA!D zP9gLHW{of_FtZS~MsXUOADPxccyXvg5f)QZsL%3-XeRn7qfS!NT#S#+2yaLj&{z`u z=cKS_GphOOs~_B7G{kWPD3)-bOK485|cgt zstfcfI5W1Kh{)t<)taLeGp2J?1}QYQF3>qz8OC)v8sraibm6Gd!-~m@>#^;4ENt&M z0xXztL(Y_;ly>kxyt*T6)!%WvX55ekHRMLvi5s8M3oKLxGsohlVX#0=Y>o!0xhgRr zF(9=NNvk6fYcxx4p-Q+_L^<|HYMETBD?J3t`fi}t%fD`a_08^+;-9aUcfNkQ^L%ml z$qze=E4$x5+J08tef9hH%imtV{BEcCeEa(^s~(1HEEJgJ0ux*y#Ep-Kxv9xpH?9N? z)0~(Pjn)&!@Z{Ctc-BR1p1A=Jd?t5gPf8IoqHy1Ep8$UnmYZj2jkCk%n>A?{l6 zPS&r^d8Yh>{^TVgBwZIGlkpGu@OU6H8Xk=grf!ap-X6=2+>1|KnjeUag%hLqGS{ZE zoim!2QZM^_d`k2tWI3tM$eK5n^kw?9w*Sdl1~3X~iZ}lNWf=e*P`;aev|(1|v=W04 zUDXtUPZ(@M#)4#6@A9g^r4`ArKx}SKnEx5|nGIT24fC|13CWDXswqj-phLh@Oc@r5 z&(2Fy!f2dKN@7yDGQ(?O8K)(ohwbDdbGhr>j3_Bm!1cg(f?Glhb#VdS5Rn1@` zx7Xc$aPjKpXdnbfD$fcrElT2hAVG9idH4K#-hVmnucE?gkZ+l<&221vlvV9~U z+;lcA&FdWlC8xja^yf!6!AaEHFX;X8l4H8;n9hH?Woy|(6nkNftuBS$VzA&V_$n>F zMQg?4+(Qg|aV1>1t+#q$RNP&w4<0`#2*uAzZeQ8$+n~#C|DwHOYh1JIwjLM+^6>G) zf?9H)FT2kdg%SWpx6bx#nDmxG82OQk#ZHoSS30{E&5QTHa8?shY;Wbvd!Wdcteq8$ zgFt#J@0L$-V912{j;)2Dfi>H$@O_n89A$_cn9#=XE zBj=~I64pi78DIz2;o9MTq`V6v`RS>1!1fr5qAI8^Uwta5OFurgkRxAx_RNU3blV}|t-4ywzk+n7=0.115.2,<1.0.0 +uvicorn[standard]>=0.23.2,<0.24.0 +lazyllm>=0.7.0,<0.8.0 +pydantic>=2.11.7,<3.0.0 diff --git a/jiao-an-ai/scripts/__init__.py b/jiao-an-ai/scripts/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jiao-an-ai/scripts/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/scripts/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/scripts/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4bbd383e6e10a3435f4f464ba73024fc86e75c6 GIT binary patch literal 139 zcmZ3^%ge<81a52WGZ}&OV-N=hKms7}nGHxxXGmcPX3%8xTggzw1QG