一个基于 LangGraph + LangChain 构建的考研数学(数学一 / 二 / 三)AI 辅导 Agent。它不只是"问答",而是把备考流程中真正重复、琐碎、容易断档的环节做成了可持久化的工具链:讲知识、批解答、出错题、查薄弱点、排计划、按遗忘曲线催复习。
- 🧠 Agent 核心:LangGraph 状态图 + 12 个工具,带滑动窗口记忆与 Postgres 检查点持久化
- 📐 先画图再讲解:涉及函数 / 曲线 / 曲面 / 积分区域时强制调用绘图工具,支持切线、极值点、积分区域填充等标注
- 📝 闭环错题本:错题自动进入艾宾浩斯遗忘曲线(8 个复习节点),答对进阶、答错回退
- 📊 学情驱动:按错误率聚合薄弱知识点,反向驱动出题与复习计划
- ⏰ 每日推送:定时生成「今日学习任务 + 到期错题 + 推荐练习」简报
- 🖼 多模态:支持拍照上传手写解答、题目截图,直接批改或提取题目
| 能力 | 说明 |
|---|---|
| 知识教学 | 按「定义 / 定理 → 适用条件 → 典型例题 → 常见误区」讲解,并区分数一 / 二 / 三的考纲差异 |
| 解题批改 | 接收文字或 LaTeX 题目,输出分步解答(每步附原理);支持上传手写解答逐行批改、模拟阅卷给分 |
| 出题练习 | 按薄弱点生成练习题并标注难度;支持真题检索(联网);支持基于原题生成变式题 |
| 错题本管理 | 记录错题、按错误类型分类(概念错误 / 计算失误 / 方法不当 / 格式不规范)、汇总薄弱点 |
| 学习规划 | 根据目标分数、每日可用时长、三科进度生成周计划,并支持按完成率动态调整 |
| 间隔复习 | 基于艾宾浩斯遗忘曲线自动安排复习节点,到点提醒并驱动回顾 |
| 每日推送 | 可配置推送时间,自动生成含今日任务与到期错题的简报 |
| 可视化讲解 | 一元函数曲线 / 二元函数曲面绘制,支持积分区域涂色、关键点、切线、辅助线、箭头标注 |
┌─────────────────────────────────────────────┐
│ FastAPI 服务 │
│ POST /run POST /stream_run (SSE) │
│ POST /chat POST /upload/image │
│ POST /cancel/{id} GET /health │
└───────────────────────┬─────────────────────┘
│
┌─────────▼──────────┐
│ GraphService │ ← 运行 / 流式 / 取消 / 单节点调试
└─────────┬──────────┘
│
┌───────────────▼────────────────┐
│ MathGuide Agent (LangGraph) │
│ system prompt + 12 tools │
│ 记忆:滑动窗口最近 20 轮对话 │
└───────────────┬────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────┐ ┌────────────┐ ┌──────────────┐
│ 工具层 │─────▶│ 工作流层 │────▶│ 数据层 │
│ tools/ │ │ workflows/ │ │ PostgreSQL │
│ 12 个 │ │ 5 个内核 │ │ + Supabase │
└─────────┘ └────────────┘ │ + S3 对象存储 │
└──────────────┘
分层职责
src/tools/—— Agent 的工具封装层(LangChain@tool),负责参数校验、用户身份解析与结果格式化src/workflows/—— 业务内核,不依赖 Agent 框架,可独立调用或替换为远程 APIsrc/storage/—— 持久化:会话检查点、业务数据、图片对象存储src/main.py—— HTTP 服务与调度器(APScheduler 每分钟检查一次到期推送)
这套分层刻意让 workflows/ 与 tools/ 解耦:业务逻辑可以脱离 Agent 单独测试,也便于把工作流换成独立微服务。
| 工具 | 用途 | 主要参数 |
|---|---|---|
plot_function |
绘制一元函数曲线 / 二元函数曲面并上传,返回 Markdown 图片链接 | expression、var_type、x_range、annotations |
analyze_math_image |
多模态分析数学图片:提取题目 / 批改手写解答 / 分析图形 / OCR | image_urls、analysis_type |
error_notebook_workflow |
错题本管理:记录 / 列表 / 标记掌握 / 薄弱点分析 | operation、question、error_type、topic |
study_planner_workflow |
复习计划:生成 / 查看 / 调整 | operation、target_score、daily_hours、math_type |
spaced_review_workflow |
间隔复习:查询到期 / 开始复习 / 提交结果 / 统计 | operation、error_id、correct |
daily_push_workflow |
每日推送配置与简报生成 | operation、push_time、recommended_problems |
exam_analysis_workflow |
历年高频考点分析、考频与分值分布 | math_type、chapter |
socratic_guide |
苏格拉底式提问引导,不给答案只给提示 | problem、student_input |
variant_generator |
基于原题生成变式题(条件微调 / 结论翻转 / 数形结合 / 数值变换 / 推广引申) | original_problem、difficulty、variant_count、variant_types |
formula_handbook |
公式推导、几何意义、使用条件、默写训练 | formula_name、include_proof、include_geometry、include_quiz |
web_search |
联网检索真题、考纲、考情数据 | query |
get_current_time |
获取当前北京时间(避免引用对话历史中的旧时间) | — |
关于三类"提示词工具":
socratic_guide、variant_generator、formula_handbook并不执行计算或查询,而是根据参数拼装出一段结构化的教学指令返回给模型,由模型据此生成引导话术、变式题或公式讲解。它们本质是可控的 prompt 模板,好处是让"引导而不给答案""变式出题""公式推导"这些行为稳定复现,而不依赖模型自由发挥。相对地,
exam_analysis_workflow的考频数据来自workflows/exam_analysis.py中的内置知识库,并非实时查询,数据时效性以仓库内的静态内容为准。
绘图策略:概念讲解时只画简洁函数图像;只有涉及具体题目(习题 / 真题 / 例题)时才附加标注。这条规则写在 system prompt 里,用来避免图像被无意义的标注淹没。
复习节点间隔(天):0 → 1 → 2 → 4 → 7 → 15 → 30 → 90,共 8 个阶段。
新错题入库 → 阶段 0(当天立即到期)
│
答对 ────┴──── 答错
│ │
stage + 1 stage − 2(下限为 0)
│ │
按下个间隔排期 按新阶段间隔排期
│
stage 达到 8 → 标记「长期掌握」
review_schedule字段保存完整的 8 节点时间线,review_stage记录当前阶段,next_review_at驱动到期查询- 答错回退 2 个阶段(
max(0, stage - 2)),这是该实现相对简单「答错归零」的差异点 - 需要连续答对 8 次才算"毕业",理论上限约 149 天
排期时间归一化:下次复习时间统一由 _normalize_next_review() 计算——先取当前 UTC 时间换算到用户当地时区,落在当天的 REVIEW_ANCHOR_HOUR 点整(默认早 8 点),再累加间隔天数;若算出的时间已早于当前时刻,则顺延到下一个锚点,因此写入的一定是未来时间。
- 锚点小时与时区可配置:
REVIEW_ANCHOR_HOUR(默认8)、REVIEW_TIMEZONE(默认Asia/Shanghai) - 阶段 0(刚记下的错题)语义为「立即到期」,入库后马上出现在待复习列表
- 当天内的快速重练直接按
当前时刻 + RETRY_INTERVAL_HOURS(默认 1 小时)计算,不套用每日锚点
修复背景:此前的实现直接用
datetime.now(timezone.utc).replace(hour=8),锚点实际是 UTC 08:00 = 北京 16:00,导致下午 4 点之后录入的错题立刻显示「已到期」,而清晨录入的要等一整天。详见「开发者须知:已修复的缺陷」。
答错回退的时间行为已统一:PostgreSQL 与内存兜底两条分支现在都走同一个 helper,退回阶段 0 时均排期 +1 小时(此前分别是 +1 小时 与 +1 天)。
错题按 topic 聚合,统计每个知识点的错题数、占比、主要错误类型与已掌握数;错误率 > 20% 的知识点会被主动标为优先复习建议,并可直接用于驱动出题。
输入目标分数、每日可用时长、数学类型与高数 / 线代 / 概率三科当前进度,生成周维度的阶段计划;后续可传入 completed_ratio 动态调整节奏。
APScheduler 每分钟轮询一次 push_config 表,把当前 UTC 时间换算到用户时区后与设定的 HH:MM 比较,命中即生成简报,内容包含:今日学习任务、到期错题复习清单、以及基于薄弱点生成的推荐练习。简报落在 daily_briefings 表,重启不丢失。
✅ 此处曾有一个已修复的缺陷。 原先
daily_push.py读取的键名与上游实际返回的不匹配(计划侧读current_stage/plan_summary,实为plan.weeks/plan.summary;复习侧读items,实为due_errors),且都带.get(..., 默认值),因此不报错但简报长期显示兜底值。现已修正为读取真实字段,并新增_pick_current_week()依据end_date正确挑选当前周。
结合历年考频数据,输出各章节的考频、平均分值、失分率与复习优先级。
.
├── src/
│ ├── main.py # FastAPI 入口:/run /stream_run /chat /upload /cancel
│ ├── upload_server.py # 独立图片上传服务(默认 :5001)
│ ├── agents/agent.py # MathGuide Agent 定义(模型 + 工具 + 记忆)
│ ├── tools/ # 12 个 Agent 工具封装
│ ├── workflows/ # 5 个业务内核(错题本 / 计划 / 间隔复习 / 考点 / 推送)
│ ├── storage/
│ │ ├── memory/memory_saver.py # LangGraph 检查点(Postgres,失败降级内存)
│ │ ├── database/ # SQLAlchemy 模型与 Supabase 客户端
│ │ └── s3/s3_storage.py # 对象存储(图片上传 + 签名 URL)
│ └── utils/ # 用户身份识别、存储助手、文件工具
├── config/agent_llm_config.json # 模型参数 + system prompt
├── scripts/ # setup / local_run / http_run / pack
├── assets/wqy-microhei.ttc # 绘图用中文字体(随仓库分发)
├── .env.example # 环境变量模板
├── LICENSE # MIT
└── pyproject.toml # 依赖(uv 管理)
- Python 3.12+
- 推荐使用 uv 管理依赖
- 运行脚本为 bash(Linux / macOS / WSL / Git Bash)。Windows 原生环境可直接调用
python src/main.py,见下方说明。
uv sync若
uv.lock与pyproject.toml不同步(例如你修改过依赖),先执行uv lock重新生成锁文件。
复制模板并填入真实值:
cp .env.example .env.env 已被 .gitignore 排除,不会提交。关键变量:
# ── 模型服务(必填)──
# 兼容 OpenAI 协议的模型服务地址
COZE_INTEGRATION_MODEL_BASE_URL=https://api.openai.com/v1
COZE_WORKLOAD_IDENTITY_API_KEY=your-api-key
# ── 项目根目录(可选)──
# 不设置时由 src/agents/agent.py 的位置自动推导,通常无需填写
# COZE_WORKSPACE_PATH=
# ── 业务数据库(可选,不配置则错题本降级为内存存储)──
COZE_SUPABASE_URL=https://your-project.supabase.co
COZE_SUPABASE_ANON_KEY=your-anon-key
COZE_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# ── 会话记忆(可选,不配置则降级为 MemorySaver)──
PGDATABASE_URL=postgresql://user:pass@localhost:5432/mathguide
# ── 对象存储(可选,不配置则绘图功能不可用)──
COZE_BUCKET_NAME=your-bucket
COZE_BUCKET_ENDPOINT_URL=https://your-s3-endpoint
⚠️ 变量名带有COZE_前缀是历史原因;除coze_workload_identity的凭据拉取逻辑外,这些值都可以直接填自己的服务地址。
需要在 PostgreSQL 中创建以下表(结构见 src/storage/database/shared/model.py):
| 表名 | 用途 |
|---|---|
error_notebook |
错题记录、错误类型、复习阶段与时间线 |
study_plans |
用户复习计划(JSONB 存储完整计划) |
push_config |
每日推送配置(时间、时区、开关) |
daily_briefings |
已生成的每日简报 |
memory.* |
LangGraph 检查点表,首次启动由代码自动创建 |
error_notebook 建表示例:
CREATE TABLE error_notebook (
id serial PRIMARY KEY,
session_id varchar(128) NOT NULL, -- 用户会话 ID
question text NOT NULL,
user_answer text NOT NULL,
correct_answer text NOT NULL,
error_type varchar(32) NOT NULL, -- 概念错误/计算失误/方法不当/格式不规范
topic varchar(128) NOT NULL,
review_count integer NOT NULL,
mastered boolean NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
review_stage integer NOT NULL DEFAULT 0,
analysis text,
next_review_at timestamptz,
review_schedule jsonb
);
CREATE INDEX error_notebook_session_idx ON error_notebook (session_id);
CREATE INDEX error_notebook_next_review_idx ON error_notebook (next_review_at);
CREATE INDEX error_notebook_topic_idx ON error_notebook (topic);# 启动 HTTP 服务(默认端口 8000)
bash scripts/http_run.sh -p 8000
# 命令行跑一次完整对话
bash scripts/local_run.sh -m flow -i '{"text": "什么是导数?"}'
# 调试单个节点
bash scripts/local_run.sh -m node -n node_name -i '{"text": "测试"}'Windows 原生环境:仓库内脚本均为 bash,Windows 下建议使用 WSL / Git Bash。 若不想装 bash,可直接调用 Python 入口(行为一致):
# 启动 HTTP 服务
python src/main.py -m http -p 8000
# 跑一次对话
python src/main.py -m flow -i '{"text": "什么是导数?"}'
# 独立图片上传服务
python src/upload_server.pycurl http://localhost:8000/health
# 期望输出: {"status":"ok","message":"Service is running"}| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/run |
同步执行一次对话,返回完整结果 |
POST |
/stream_run |
流式执行(SSE),支持 x-run-id 与 x-workflow-stream-mode 头 |
POST |
/chat |
多图 + 文本一次提交(multipart/form-data) |
POST |
/upload/image |
上传单张图片,返回可分析 URL |
POST |
/cancel/{run_id} |
取消指定运行中的任务 |
POST |
/node_run/{node_id} |
单独运行某个图节点(调试用) |
POST |
/v1/chat/completions |
OpenAI 兼容接口 |
GET |
/health |
健康检查 |
GET |
/graph_parameter |
获取图的输入 / 输出 Schema |
示例
# 文本答疑
curl -X POST http://localhost:8000/run \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{"text": "求 y=x² 在 x=1 处的切线方程"}'
# 上传手写解答并批改
curl -X POST http://localhost:8000/chat \
-F "text=帮我批改这道题" \
-F "images=@solution.jpg"所有用户数据(错题本、复习计划、间隔复习进度)都按会话隔离,身份识别逻辑在 src/utils/user_auth.py:
- 从
Authorization: Bearer <token>(或X-Api-Key)中取出 API Token - 对 Token 做 SHA-256 哈希,取前 16 位作为
user_id(形如u_1a2b3c...),不落明文 - 同一 Token 跨所有会话共享同一份数据;不同 Token 之间完全隔离
- 请求未带 Token 时降级使用
run_id,保证向后兼容
实现上用 contextvars 而非 threading.local(),保证在异步环境跨 await 点和线程边界都能正确传递。
| 数据 | 存储位置 | 降级策略 |
|---|---|---|
| 对话记忆 | LangGraph 检查点(memory schema) |
Postgres 不可用时降级为进程内 MemorySaver |
| 错题 / 计划 / 推送配置 | PostgreSQL / Supabase | 不可用时降级为进程内字典 |
| 生成的函数图像 | S3 兼容对象存储 | 无降级,上传失败则绘图工具返回错误 |
对话记忆使用滑动窗口,只保留最近 20 轮(40 条消息),避免长对话下上下文无限膨胀。
memory_saver.py会在首次连接时自动创建memoryschema 与检查点表,无需手工建表。
模型配置位于 config/agent_llm_config.json:
| 字段 | 说明 |
|---|---|
model |
模型名称 |
temperature |
采样温度(默认 0.3,偏严谨) |
top_p / max_tokens |
采样与长度限制 |
reasoning_effort |
推理强度 |
thinking_type |
是否开启思考模式 |
sp |
完整的 system prompt,定义角色、能力、工具调用规则与行为约束 |
把 system prompt 单独放在 JSON 里,是为了不写代码就能调整教学风格与工具调用策略。
本项目的接口与 SDK 依赖深度绑定 Coze 平台(coze_coding_utils、coze_coding_dev_sdk、coze_workload_identity):
- 联网搜索与多模态识图直接使用平台的
SearchClient/LLMClient - 图片签名 URL 走平台的
/sign-url代理(携带x-storage-token),不是标准 S3 预签名 - 数据库与存储凭据可通过
coze_workload_identity拉取(也可以直接用环境变量注入)
要脱离平台独立部署,需要替换搜索、识图、对象存储签名这三处实现。这也是本项目当前最主要的开源化障碍。
- Agent 的图形(
src/graphs/)为空,实际以 Agent 模式运行(见src/agents/agent.py);/graph_parameter在 Agent 模式下返回空 Schema。 exam_analysis_workflow的考频数据来自workflows/exam_analysis.py中的内置知识库,并非实时查询,时效性以仓库内静态内容为准。error_notebook、study_plans两表的列名叫session_id,但实际存的是从 API Token 派生的user_id(见「用户身份与数据隔离」)。属历史命名遗留。
- 未配置数据库时,错题本 / 复习计划 / 推送配置会降级为进程内内存存储,服务重启即丢失。生产部署请务必配置
COZE_SUPABASE_URL与PGDATABASE_URL。 - AI 生成内容可能存在错误,数学结论请以教材与真题解析为准。
以下问题在开源整理阶段已修复,此处记录以便追溯(若你从更早的版本升级,请注意行为变化):
| # | 问题 | 修复方式 |
|---|---|---|
| 1 | 复习排期时区错误:锚点用 datetime.now(timezone.utc).replace(hour=8),实际是 UTC 8:00 = 北京 16:00,导致下午 4 点后录入的错题立刻显示"已到期" |
新增 _normalize_next_review(),按用户当地时区归一化到 REVIEW_ANCHOR_HOUR(默认早 8 点),并保证返回值一定在未来;时区可用 REVIEW_TIMEZONE / REVIEW_ANCHOR_HOUR 配置 |
| 2 | DB 与内存两分支行为不一致:答错退回阶段 0 时,Postgres 分支排 +1 小时、内存分支排 +1 天 |
两条分支统一走 _normalize_next_review(),快速重练固定 +1 小时(RETRY_INTERVAL_HOURS) |
| 3 | add_error 崩溃:内存分支对已是字符串的 next_review 调用 .isoformat(),抛 AttributeError |
统一按 ISO 字符串处理 |
| 4 | 每日推送简报显示兜底值:daily_push.py 读取的键名与上游实际返回不匹配(计划侧读 current_stage/plan_summary,实为 plan.weeks/plan.summary;复习侧读 items,实为 due_errors) |
修正为读取真实字段;新增 _pick_current_week() 正确挑选当前周;待复习项改用 review_stage/review_count 字段 |
| 5 | upload_to_storage 缺失:upload_server.py 引用了不存在的函数,ImportError 被静默吞掉,上传接口永远降级返回本地路径 |
在 utils/storage_helper.py 中实现该函数(含对象 key 字符集清洗,满足 S3 命名规范) |
| 6 | 硬编码路径:/tmp、/workspace/projects 在 Windows / 本地环境不可用 |
统一改用 tempfile.gettempdir();workspace 默认值由 agent.py 位置自动推导 |
| 7 | 硬编码平台标识:原代码内置了某个具体的对象存储桶名作为默认值,配置中也写死了平台内测模型名 | 桶名改为必须显式配置(缺失时给出清晰报错);模型名改为占位符 YOUR_MODEL_NAME,请在 config/agent_llm_config.json 中填入你自己的模型 |
| 8 | 依赖声明不完整:httpx/supabase/postgrest/numpy/requests 被直接 import 却未声明;pycairo/dbus-python/PyGObject 是 Linux 专用且无任何引用 |
补齐缺失依赖,移除无用的 Linux 专用依赖;项目名由 vibe-coding 更正为 mathguide |
| 9 | 中文字体只探测 Linux 路径 | 增加 macOS / Windows 字体候选路径 |
本项目基于 MIT License 开源,版权归「项目贡献者」所有。
Copyright (c) 2026 项目贡献者