nmdra/AutoMark
GitHub: nmdra/AutoMark
一个基于 LangGraph 和 Ollama 的本地隐私保护自动评分系统,解决离线环境下学生作业的自动化打分与反馈问题。
Stars: 1 | Forks: 0
# AutoMark
**学生作业自动评分多智能体系统**
一个基于 [LangGraph](https://github.com/langchain-ai/langgraph) 和 [Ollama](https://ollama.com) 构建的本地、隐私保护的自动评分系统。它使用一组专业化智能体对 `.txt` 或 `.pdf` 格式的学生提交作业进行评分,依据 JSON 量规输出结构化的 Markdown 反馈报告和评分表——整个过程完全在本地硬件上运行,不调用任何外部 API。
一个 FastAPI REST 封装层将评分流程暴露为 HTTP 服务,便于与其他工具或前端集成。
## 目录
- [架构](#architecture)
- [智能体](#agents)
- [工具](#tools)
- [项目结构](#project-structure)
- [前置条件](#prerequisites)
- [安装](#installation)
- [配置](#configuration)
- [用法](#usage)
- [REST API](#rest-api)
- [Docker 栈](#docker-stack)
- [Make 命令](#make-targets)
- [测试](#testing)
- [数据格式](#data-formats)
- [输出](#output)
- [故障排查](#troubleshooting)
## 架构
该流程是一个定向的 LangGraph `StateGraph`。入口点根据文件类型路由到相应的摄入智能体,随后依次经过分析、历史对比和报告生成阶段:
```
flowchart LR
detect["_detect"]
ingest["ingestion (txt)"]
pdf["pdf_ingestion"]
analysis["analysis"]
historical["historical"]
report["report"]
endnode["END"]
detect --> ingest
detect --> pdf
ingest --> analysis
pdf --> analysis
analysis --> historical
historical --> report
report --> endnode
analysis -. ingestion failed .-> report
```
| 步骤 | 智能体 | 职责 |
|---|---|---|
| 1 | **_detect** | 根据文件扩展名路由到正确的摄入智能体 |
| 2a | **Ingestion** | 验证并读取 `.txt` 提交;提取 `student_id` |
| 2b | **PDF Ingestion** | 通过 `pymupdf4llm` 将 `.pdf` 转换为 Markdown;使用 LLM 提取 `student_id` 与 `student_name` |
| 3 | **Analysis** | 使用 LLM 对每个量规标准评分;总分通过确定性方法计算 |
| 4 | **Historical** | 将结果保存到 SQLite;检索历史报告;生成进展洞察 |
| 5 | **Report** | 生成 Markdown 反馈报告和评分表 |
如果摄入失败(例如文件缺失),流程会直接短路到 `report`,生成不包含分数的最小化回退报告。
## 智能体
### 摄入智能体 (`agents/ingestion.py`)
验证两个输入文件路径非空、文件存在且非空、扩展名正确。读取纯文本提交并解析 JSON 量规。使用正则模式(`Student ID: <值>`)提取 `student_id`。设置 `ingestion_status` 为 `"success"` 或 `"failed"`。
### PDF 摄入智能体 (`agents/pdf_ingestion.py`)
验证 `.pdf` 提交路径,调用 `pymupdf4llm` 将 PDF 转换为 Markdown,并始终将该完整 Markdown 传递给下游评分/报告。默认使用轻量模型(`gemma3:1b-it-q4_K_M`)从紧凑的元数据提示中提取学生信息(`student_id`、`student_name`)。设置 `ingestion_status` 为 `"success"` 或 `"failed"`。
### 分析智能体 (`agents/analysis.py`)
调用分析模型(默认:`phi4-mini:3.8b-q4_K_M`)对每个量规标准评分。LLM 输出使用 Pydantic 模式(`RubricScores`)进行结构化。分数被钳制在 `[0, max_score]` 范围内,总分通过 `calculate_total_score` 确定性计算——LLM 不会被用于算术运算。
### 历史智能体 (`agents/historical.py`)
将当前评分结果保存到 SQLite 数据库,检索该学生的所有历史报告,并使用轻量模型在存在历史记录时生成简洁的进展洞察。同时将一份独立的性能分析报告写入磁盘。
### 报告智能体 (`agents/report.py`)
调用分析模型生成格式良好的 Markdown 反馈报告和评分表。如果 LLM 不可用,则回退到基于模板的报告。
## 工具
| 模块 | 函数 | 描述 |
|---|---|---|
| `tools/file_ops.py` | `read_text_file` | 读取 UTF-8 文本文件;将 `OSError` 包装为 `RuntimeError` |
| `tools/file_ops.py` | `read_json_file` | 读取并解析 JSON 文件;将 `OSError`/`JSONDecodeError` 包装为异常 |
| `tools/file_ops.py` | `validate_submission_files` | 检查存在性、大小和文件扩展名 |
| `tools/file_writer.py` | `write_feedback_report` | 将反馈报告写入磁盘,必要时创建父目录 |
| `tools/file_writer.py` | `write_analysis_report` | 将性能分析报告写入磁盘 |
| `tools/score_calculator.py` | `calculate_total_score` | 求和标准分数,计算百分比,分配字母等级 |
| `tools/logger.py` | `log_agent_action`, `log_model_call`, `timed_model_call` | 通过 `structlog` 输出结构化日志(JSON 文件 + 控制台) |
| `tools/db_manager.py` | `init_db` | 初始化 SQLite 学生结果数据库 |
| `tools/db_manager.py` | `save_report` | 保存学生评分结果 |
| `tools/db_manager.py` | `get_past_reports` | 检索学生所有历史评分结果 |
| `tools/pdf_processor.py` | `convert_pdf_to_markdown` | 使用 `pymupdf4llm` 将 PDF 转换为 Markdown 文本 |
**等级阈值:**
| 等级 | 百分比 |
|---|---|
| A | ≥ 90% |
| B | ≥ 75% |
| C | ≥ 60% |
| D | ≥ 50% |
| F | < 50% |
## 项目结构
```
.
├── data/
│ ├── rubric.json # Rubric definition (criteria + max scores)
│ ├── submission.txt # Sample plain-text student submission
│ └── students.db # SQLite database (created by `make init-db`)
├── src/mas/
│ ├── agents/
│ │ ├── ingestion.py # Plain-text ingestion + student ID extraction
│ │ ├── pdf_ingestion.py # PDF → Markdown ingestion + LLM detail extraction
│ │ ├── analysis.py # LLM-powered rubric scoring
│ │ ├── historical.py # Persist results + progression insights
│ │ └── report.py # Markdown feedback report + marking sheet
│ ├── tools/
│ │ ├── file_ops.py # File reading and validation helpers
│ │ ├── file_writer.py # Report and analysis file writers
│ │ ├── score_calculator.py
│ │ ├── logger.py
│ │ ├── db_manager.py # SQLite persistence helpers
│ │ └── pdf_processor.py # pymupdf4llm PDF-to-Markdown converter
│ ├── api.py # FastAPI REST wrapper
│ ├── config.py # Environment-based settings (python-dotenv)
│ ├── graph.py # LangGraph pipeline definition
│ ├── llm.py # Ollama LLM factory functions
│ └── state.py # Shared AgentState TypedDict
├── tests/
│ ├── test_tools.py # Unit tests for all tool modules (no LLM)
│ ├── test_ingestion.py # Ingestion agent tests
│ ├── test_pdf_ingestion.py# PDF ingestion agent tests (mocked LLM)
│ ├── test_analysis.py # Analysis agent tests (mocked LLM)
│ ├── test_historical.py # Historical agent tests (mocked LLM + DB)
│ ├── test_report.py # Report agent tests (mocked LLM)
│ ├── test_config.py # Settings / config tests
│ └── test_llm_judge.py # LLM-as-a-Judge tests via phi4-mini + requests
├── output/ # Generated reports (created on first run)
├── Dockerfile.api # Docker image for the FastAPI service
├── docker-compose.yml # Full stack: Ollama + AutoMark API
├── .env.example # Example environment variable file
├── Makefile
└── pyproject.toml
```
## 前置条件
- **Python 3.13+** 和 [uv](https://docs.astral.sh/uv/)(包管理器)
- **Docker** 与 Docker Compose(用于 Ollama 服务和/或完整栈)
- 至少 **4 GB** 空闲内存(用于 `phi4-mini:3.8b-q4_K_M` 模型)
- 可选:用于更快推理的 NVIDIA GPU
## 安装
**1. 克隆仓库**
```
git clone https://github.com/nmdra/AutoMark.git
cd AutoMark
```
**2. 安装 Python 依赖**
```
# 运行时 + 开发依赖项(包括 pytest 和 requests)
uv pip install -e ".[dev]"
```
**3. 配置环境变量**(可选)
```
cp .env.example .env
# 编辑 .env 以覆盖默认值(模型名称、路径、Ollama URL 等)
```
**4. 初始化学生数据库**
```
make init-db
```
**5. 启动 Ollama 服务**
```
# CPU(默认)
make start
# GPU(NVIDIA)
make start-gpu
```
**6. 拉取模型**
```
make pull-model
```
这会预先将默认分析模型拉取到 Ollama 容器中。首次下载约需数 GB,仅需执行一次(数据持久化在 `~/.ollama` 卷中)。
## 配置
设置从环境变量(或项目根目录的 `.env` 文件)加载,所有值均有合理默认值。
AutoMark 默认使用两个本地 Ollama 模型角色:
- **分析模型**:用于评分与报告生成(`AUTOMARK_ANALYSIS_MODEL_NAME`)
- **轻量模型**:用于 PDF 元数据提取与历史洞察(`AUTOMARK_LIGHT_MODEL_NAME`)
| 变量 | 默认值 | 描述 |
|---|---|---|
| `AUTOMARK_ANALYSIS_MODEL_NAME` | `phi4-mini:3.8b-q4_K_M` | 用于量规评分与报告生成的 Ollama 模型标识符 |
| `AUTOMARK_MODEL_NAME` | `phi4-mini:3.8b-q4_K_M` | 遗留别名(已弃用);分析模型未设置时作为后备,启用分析模型后忽略 |
| `AUTOMARK_LIGHT_MODEL_NAME` | `gemma3:1b-it-q4_K_M` | 轻量,用于提取/洞察任务 |
| `AUTOMARK_OLLAMA_BASE_URL` | `http://localhost:11434` | Ollama HTTP API 基础 URL |
| `AUTOMARK_DB_PATH` | `data/students.db` | SQLite 数据库路径 |
| `AUTOMARK_LOG_FILE` | `agent_trace.log` | JSON 代理跟踪日志路径 |
| `AUTOMARK_OUTPUT_PATH` | `output/feedback_report.md` | 默认反馈报告路径 |
| `AUTOMARK_MARKING_SHEET_PATH` | `output/marking_sheet.md` | 默认评分表路径 |
| `AUTOMARK_ANALYSIS_REPORT_PATH` | `output/analysis_report.md` | 默认分析报告路径 |
| `AUTOMARK_DATA_BASE_DIR` | `<项目根>/data` | 提交/量规文件基础目录(API 路径遍历防护) |
| `AUTOMARK_NUM_CTX` | `4096` | Ollama 上下文窗口大小 |
| `AUTOMARK_NUM_PREDICT` | `512` | 每个模型响应的最大生成令牌数 |
| `AUTOMARK_LLM_REPORT_ENABLED` | `true` | 设为 `false` 以跳过 LLM 报告生成,改用确定性模板 |
| `AUTOMARK_SUBMISSION_MAX_CHARS` | `8000` | 发送至分析模型的提交最大字符数 |
| `AUTOMARK_MIN_REPORTS_FOR_INSIGHTS` | `1` | 生成进展洞察所需的最少历史报告数 |
| `AUTOMARK_PDF_REGEX_FAST_PATH_ENABLED` | `true` | 设为 `false` 以强制使用基于模型的提取而非正则快速路径 |
| `AUTOMARK_JOB_WORKER_CONCURRENCY` | `2` | 异步批处理任务的线程数 |
| `AUTOMARK_JOB_QUEUE_MAX_SIZE` | `100` | 内存中排队的最大批处理任务数 |
| `AUTOMARK_JOB_MAX_RETRIES` | `1` | 每个批处理项的默认重试次数 |
| `AUTOMARK_BATCH_MAX_ITEMS` | `100` | 每个批处理请求接受的最大项目数 |
| `AUTOMARK_JOB_RETENTION_DAYS` | `30` | 建议的已完成任务保留周期 |
| `AUTOMARK_EXPORT_MAX_BYTES` | `10485760` | 最大允许导出的 CSV/JSON/PDF 大小(字节) |
为提升并行报告/洞察生成的吞吐量,可在 Ollama 服务上设置 `OLLAMA_NUM_PARALLEL>=2`。
## 用法
### REST API
启动开发 API 服务器(连接到 `localhost:11434` 处的 Ollama):
```
make api
```
交互式 API 文档位于 [http://localhost:8000/docs](http://localhost:8000/docs)。
**端点:**
| 方法 | 路径 | 描述 |
|---|---|---|
| `GET` | `/health` | 活跃性检查 |
| `POST` | `/grade` | 运行评分管道 |
| `POST` | `/grade/batch` | 提交异步批处理评分任务 |
| `GET` | `/jobs` | 列出异步任务(支持状态、限制、偏移) |
| `GET` | `/jobs/{job_id}` | 获取完整任务状态及每项结果 |
| `POST` | `/jobs/{job_id}/cancel` | 取消排队/运行中的任务 |
| `POST` | `/jobs/{job_id}/exports/{format}` | 为已完成任务生成 CSV/JSON/PDF 导出 |
| `GET` | `/jobs/{job_id}/exports/{format}` | 下载先前生成的导出文件 |
| `GET` | `/sessions/{session_id}/logs` | 检索指定会话的跟踪日志条目 |
**示例评分请求:**
```
curl -X POST http://localhost:8000/grade \
-H "Content-Type: application/json" \
-d '{"submission_path": "submission.txt", "rubric_path": "rubric.json"}'
```
两个路径均相对于 `AUTOMARK_DATA_BASE_DIR`(默认:`data/`)解析,并阻止路径遍历。
**示例批处理请求:**
```
curl -X POST http://localhost:8000/grade/batch \
-H "Content-Type: application/json" \
-d '{
"items": [
{"submission_path": "submission.txt", "rubric_path": "rubric.json", "correlation_id": "s1"},
{"submission_path": "submission2.txt", "rubric_path": "rubric.json", "correlation_id": "s2"}
],
"max_retries": 1
}'
```
随后轮询 `GET /jobs/{job_id}` 获取状态/进度,并使用
`POST /jobs/{job_id}/exports/{format}`(`csv`、`json` 或 `pdf`)构建可下载工件。
**示例响应(缩写):**
```
{
"session_id": "abc123",
"student_id": "IT21000001",
"student_name": "",
"total_score": 17.0,
"percentage": 85.0,
"grade": "B",
"summary": "A well-structured submission...",
"criteria": [...],
"feedback_report": "## Feedback\n...",
"output_filepath": "output/20240101_120000_..._feedback_report.md",
"marking_sheet_path": "output/20240101_120000_..._marking_sheet.md"
}
```
### Docker 栈
使用 Docker Compose 运行完整栈(Ollama + AutoMark API):
```
make docker-up
```
这将构建 API 镜像并启动 Ollama 与 AutoMark API 容器。API 在 `http://localhost:8000` 可用。停止命令:
```
make docker-down
```
## Make 命令
| 目标 | 描述 |
|---|---|
| `make start` | 启动 Ollama(CPU Docker 配置文件) |
| `make start-gpu` | 启动 Ollama(NVIDIA GPU Docker 配置文件) |
| `make stop` | 停止所有 Ollama 容器 |
| `make pull-model` | 将默认分析模型拉取到 Ollama 容器 |
| `make init-db` | 在 `data/students.db` 初始化 SQLite 学生数据库 |
| `make api` | 在端口 8000 启动 FastAPI 开发服务器 |
| `make docker-up` | 构建并启动完整 Docker 栈(Ollama + API) |
| `make docker-down` | 停止完整 Docker 栈 |
| `make test` | 运行完整的 pytest 测试套件 |
| `make logs` | 跟踪 Ollama 容器日志 |
| `make clean` | 移除容器、卷、`__pycache__` 和生成的输出 |
## 测试
```
make test
# 或直接:
uv run pytest tests/ -v
```
测试套件分为三层:
### 工具单元测试 (`test_tools.py`)
快速、确定性的测试,覆盖所有工具模块——无需 LLM 或网络。测试成功路径、错误处理、边界情况、文件 I/O、等级阈值、日志格式和时间戳有效性。
### 代理集成测试
每个智能体都有独立的测试文件。LLM 调用通过 `unittest.mock` 模拟,因此这些测试无需 Ollama 即可即时运行:
| 文件 | 测试智能体 |
|---|---|
| `test_ingestion.py` | 摄入——文件验证、学生 ID 提取、错误处理 |
| `test_pdf_ingestion.py` | PDF 摄入——PDF 转换、LLM 提取、回退行为 |
| `test_analysis.py` | 分析——评分、分数钳制、LLM 回退、等级逻辑 |
| `test_historical.py` | 历史——数据库持久化、历史报告检索、进展洞察生成 |
| `test_report.py` | 报告——文件写入、LLM 回退、覆盖行为 |
| `test_config.py` | 配置——环境变量加载、默认值 |
### LLM 作为裁判 (`test_llm_judge.py`)
使用 `requests` 库直接调用 `phi4-mini`(通过 Ollama REST API `/api/generate`),询问给定分配分数是否公平。模型被提示仅以 **YES** 或 **NO** 响应。
包含三个测试:
- 对高质量提交给予高分——期望 `YES`
- 对同一提交给予零分——期望 `NO`
- 响应格式断言(必须恰好为 `YES` 或 `NO`)
当 Ollama 未运行或未拉取模型时,这些测试会自动跳过。
## 数据格式
### 提交
支持的格式:
- **纯文本**(`.txt`)——UTF-8 编码。可选地在任意行包含 `Student ID: <值>` 以自动提取 ID。
- **PDF**(`.pdf`)——自动转换为 Markdown。LLM 尝试从封面页提取 `student_id` 与 `student_name`。
### 量规(`data/rubric.json`)
```
{
"module": "CTSE – IT4080",
"assignment": "Cloud Technology Fundamentals",
"total_marks": 20,
"criteria": [
{
"id": "C1",
"name": "Definition of Containerisation",
"description": "Accurately defines containerisation and distinguishes it from virtual machines.",
"common_mistakes": ["missing_answer", "out_of_context"],
"max_score": 5
}
]
}
```
必需字段:`total_marks`(整数)、`criteria`(数组)。每个标准需包含 `id`、`name`、`description` 和 `max_score`。
可选字段:`common_mistakes`(),用于记录预期错误,例如 `missing_answer` 和 `out_of_context`。
## 输出
成功运行后,以下文件将写入 `output/` 目录。通过 REST API 调用时,文件名会包含时间戳、学生姓名和学号以避免冲突:
| 文件 | 描述 |
|---|---|
| `*_feedback_report.md` | 总体摘要、每标准分数与理由、改进建议 |
| `*_marking_sheet.md` | 紧凑的评分表,包含每标准常见错误标签及常见错误汇总 |
| `*_analysis_report.md` | 历史性能分析及进展洞察 |
一个追加式的结构化 JSON 跟踪日志(通过 `structlog`)会写入项目根目录的 `agent_trace.log`,在每次运行时记录。
## 故障排查
**Ollama 无法访问**
确保容器正在运行(`docker ps`)且端口已绑定:`curl http://localhost:11434/api/tags`
**模型未找到**
运行 `make pull-model`。首次拉取需要约 2.5 GB 磁盘空间。
**LLM 返回格式错误的 JSON(分析智能体)**
该智能体会捕获异常并将所有标准分数回退为零,同时在最终状态中添加 `error` 字段。请查看 `agent_trace.log` 获取详细信息。
**未检测到 GPU**
确保已安装 [NVIDIA 容器工具包](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html),并使用 `make start-gpu` 启动。
**数据库未初始化**
在首次运行评分前执行 `make init-db` 以创建 `data/students.db`。
标签:AI辅助评估, AI风险缓解, AV绕过, FastAPI, JSON评分标准, LangGraph, LLM评估, Markdown报告, Ollama, PDF解析, PyRIT, REST API, 作业批改, 多智能体系统, 学生作业评估, 教育技术, 文本解析, 文档批改, 无API调用, 有状态工作流, 本地AI, 本地运行, 状态图, 离线批改, 管道处理, 结构化反馈, 网络安全, 自动批改, 自动评分, 自托管, 请求拦截, 逆向工具, 隐私保护