Skip to content

Repository files navigation

Automatic Grading System — 布林代數 / 數位邏輯自動批改系統

一套以 YOLO 區域偵測 + 視覺-LLM OCR + 思考型 LLM 批改 為核心的自動批改系統,支援:

  • Moodle 作業批改流程(從 CSV 成績表配對學生)
  • 獨立紙本考試批改流程(從 xlsx 名單配對學生)
  • 多後端 LLM:OpenAI / Gemini / 本地 vLLM
  • 多 Agent 架構:布林代數 / 心得報告 / 紙本考卷,各自獨立可註冊

本文件主要說明 exam_agent(紙本考卷批改)boolean_local(布林函數批改,本地 vLLM 兩階段) 兩個 Agent,並完整解釋系統中兩種資料夾命名格式的差異(這是這套系統最容易踩坑的地方)。

📁 另有架構優化版:modular/

modular/ 底下是 exam_agent模組化重構版本——把 prompt 抽到 prompts/、 設定抽到 configs/、模型呼叫抽到 agents/backends/ModelBackend 介面。

它的批改行為經過逐項驗證,與本目錄的部署版一致: prompt 逐位元組相同、token/thinking 設定相同、對 69 份真實批改紀錄重播後 報告與 JSON 解析結果完全相同(python modular/test_parity.py,27 項檢查)。

但它尚未實際部署過——從未對真實 vLLM 端點跑完一輪。 本目錄的單檔版才是實際批出過成績的那一份。 詳見 modular/README.md


目錄

  1. 系統架構總覽
  2. Agent 介紹
  3. 兩種資料夾格式 — 重要踩坑點
  4. 典型執行流程
  5. vLLM 端點互動式選擇(boolean_local 限定)
  6. 常見問題排查

系統架構總覽

┌────────────────────────────────────────────────────────────────────┐
│                      integrated_workflow.py                        │
│                      (整合工作流程的主進入點)                        │
└──────┬─────────────────────────────────────────────────────────────┘
       │
       ├─► 步驟 1: PDF 處理 + 學號提取 (所有需要處理的考卷皆放在 Extractd_PDF)
       │   ├─ Moodle 模式 → complete_pdf_processing_system.py
       │   │                  (PDFProcessingSystem,讀 Moodle格式 CSV)
       │   └─ Exam   模式 → exam_pdf_processing_system.py
       │                      (ExamPDFProcessingSystem,讀一般課程格式 xlsx)
       │
       ├─► 步驟 2: 將 PDF 處理之輸出資料夾複製到各 Agent 的 student/ 目錄
       │
       └─► 步驟 3: 呼叫 Agent 批改
           ├─ exam_agent     (考卷批改)
           ├─ boolean_local  (布林函數批改,本地 vLLM)
           ├─ boolean_gemini (布林函數批改,Gemini)
           ├─ essay          (心得報告批改)
           └─ ...

Agent 註冊機制(agent_registry.py):
  agents/ 目錄下的所有 .py 在系統啟動時自動掃描並註冊。

進入點選擇

進入點 用途
integrated_workflow.py 建議的主進入點。一條龍跑完 PDF 處理 → 結構轉換 → 批改 → CSV 匯出
main.py 只跑批改階段(假設 student/ 目錄已備好)
run_agent.py 單一 Agent 獨立執行,不經過 registry

啟動參數速覽

python integrated_workflow.py -h

# 列出所有可用 Agent
python integrated_workflow.py --list-agents

# Moodle 模式(從 CSV)
python integrated_workflow.py <agent_id> <assignment_id>

# Exam 模式(從 xlsx)
python integrated_workflow.py <agent_id> <assignment_id> --exam

# 跳過 PDF 處理("CSV名稱"的學號處理輸出資料夾 已準備好時)
python integrated_workflow.py <agent_id> <assignment_id> --skip-pdf

# 強制重新生成標準答案
python integrated_workflow.py <agent_id> <assignment_id> --force-regenerate

Agent 介紹

exam_agent — 紙本考卷批改

設計目的:批改大規模紙本考試(例如期中考、隨堂考),題目中可能包含手寫公式、數字計算、文字敘述。

流程

學生考卷 PDF/PNG
      │
      ├─[1]──► YOLO 區域偵測 (~99% mAP50)
      │       擷取 header(學號區) + calculation(作答區)
      │
      ├─[2]──► 視覺 LLM OCR
      │       辨識手寫內容,輸出 LaTeX/JSON
      │
      ├─[3]──► 思考型 LLM 批改
      │       對照標準答案,給每題分數 + 評語
      │
      └─[4]──► 輸出 CSV 成績表 + 個別評分檔

重點特性

  • 多端點動態切換:可在多個 vLLM 服務(不同 port / 不同模型)間選擇
  • max_model_len 動態解析:啟動時打 /v1/models 取得真實模型 ID 跟最大 context,自動計算最佳 max_tokensthinking_budget
  • 思考模式自動切換:max_model_len > 100K 時自動開啟 thinking,否則用 enable_thinking=False 加速
  • 思考預算兩種方案可選:fixed(固定 4096)或 ratio(max_tokens × 0.8,思考:回答 ≈ 4:1);後者為實測準確度最高的組合(與 boolean_local 同一套公式),詳見下方
  • 完整截斷診斷:當 finish_reason=length 時,精確指出是 thinking 用完還是 content 用完,並依當前方案給出對應的調整建議

思考預算(thinking budget)方案

max_tokens 一律 = max_model_len × 0.7,兩案只差在思考額度怎麼算:

方案 thinking_budget 特性
fixed THINKING_BUDGET = 4096(寫死,不隨 context 放大) 思考短、回答空間大,單題較快
ratio int(max_tokens × THINKING_BUDGET_RATIO)(0.8) 實測批改準確度較 fixed 高約 3%;代價是思考變長、單題 latency 上升,較容易吃到 PER_REQUEST_TIMEOUT
class ExamGradingAgent(BaseGradingAgent):
    MAX_TOKENS_RATIO = 0.7                # max_tokens = max_model_len × 0.7
    MAX_TOKENS_FALLBACK = 8192            # 抓不到 max_model_len 時的備援值
    THINKING_CONTEXT_THRESHOLD = 100000   # max_model_len > 此值才開思考
    THINKING_BUDGET = 4096                # 方案 fixed 用
    THINKING_BUDGET_RATIO = 0.8           # 方案 ratio 用(思考:回答 ≈ 4:1)
    THINKING_BUDGET_MODE = "fixed"        # 程式化建構的預設(互動式選單預設 ratio)

三種選法(任選一種,皆會保證 budget < max_tokens,答案一定有空間可吐):

# 1) 互動式:run_grading 沒指定 endpoints 時會跳選單,直接 Enter = ratio
# 2) 自動化:kwargs 指定
agent.run_grading("midterm", endpoints=[...], thinking_budget_mode="ratio")
# 3) 建構時指定 / 事後切換
agent = ExamGradingAgent(thinking_budget_mode="ratio")
agent.set_thinking_budget_mode("fixed")

實際採用的方案會寫進 <assignment_id>_exam_grade.jsonmeta.token_config,方便事後比對兩案的準確度差異。

啟動範例

# 紙本考試流程(從 xlsx 名單)
python integrated_workflow.py exam midterm --exam

執行時會跳出互動式選單,讓你選擇要用哪個 vLLM 端點:

可選的 vLLM 端點(掃描中...):
  1) http://192.168.1.175:8000/v1
      ✅ 線上  模型: Qwen/Qwen3.5-35B-A3B-NVFP4  max_model_len=131072
  2) http://192.168.1.175:8001/v1
      ✅ 線上  模型: Qwen/Qwen3.6-35B-A3B-NVFP4  max_model_len=32768
  3) http://192.168.1.175:8080/v1
      ❌ 離線或無模型

請選擇端點 [1/2/3, 預設 1]: 1

boolean_local — 布林函數批改(本地 vLLM 兩階段)

設計目的:在不依賴雲端 API 的情況下,用本地 vLLM 進行布林代數作業批改。透過兩階段架構讓「視覺辨識能力」跟「批改思考能力」可以分別評估與分別配置

兩階段架構

學生手寫布林代數作業 (圖片)
        │
        ▼
┌───────────────────────────────────┐
│  Stage 1: OCR (視覺 LLM)          │  ← 用視覺模型(如 Qwen3-VL)
│  - 辨識手寫 LaTeX                  │     僅在這階段引入「外援模型」
│  - 標註學生用的定理代號             │
└───────────────────────────────────┘
        │
        ▼ (純文字 LaTeX)
┌───────────────────────────────────┐
│  Stage 2: 結構化 + 批改 (思考 LLM) │  ← 受測批改模型
│  - 解析題號分組                    │     完整評估其端到端能力
│  - 對照標準答案                    │
│  - 給分 + 評語                     │
└───────────────────────────────────┘
        │
        ▼
   評分 JSON + 評語

為什麼設計理念

核心原則:讓評估只反映「批改模型本身的能力」

許多批改模型(例如純文字大型語言模型)不具備視覺能力,無法直接看圖。如果讓每個批改模型自己做 OCR,則:

  • 視覺能力強的模型「分數」會虛高(其實是 OCR 準度差異)
  • 視覺能力弱或沒有的模型直接被排除在比較之外

因此這套系統把 OCR 強制外包給統一的視覺模型(干擾),所有受測批改模型拿到相同品質的 OCR 輸出,後續的結構化解析、定理判斷、給分推理,全部歸入批改模型自身的表現。這樣才能公平比較不同批改模型的能力。

具體分工:

階段 任務 歸類 程式碼中的 stage 參數
Stage 1 視覺 OCR 外援(干擾源) stage="ocr"
Stage 2a 結構化解析(把 OCR 結果整理成 JSON) 批改模型能力的一部分 stage="grading"
Stage 2b 對照標準答案、給分 批改模型核心能力 stage="grading"

註:Stage 2a 雖然名為「結構化解析」,看似 OCR 後處理,但本質是「理解題號分組、推測用了什麼定理」,屬於 reasoning 任務,應算進批改模型的能力範圍。

兩階段可以指向不同端點

boolean_local 啟動時會跳出兩次選單,讓你分別決定:

  1. OCR 階段用哪個 vLLM 端點(通常選掛載視覺模型的)
  2. 批改階段用哪個 vLLM 端點(可以選不同的純文字推理模型)

兩階段可以指向 同一個 端點(同模型一條龍),也可以分開(各自用最適合的模型)。

配置參數(類別常數)

class BooleanAgentLocal(BaseGradingAgent):
    PORT_OPTIONS = [
        ("1", "http://192.168.1.175:8000/v1"),
        ("2", "http://192.168.1.175:8001/v1"),
        ("3", "http://192.168.1.175:8080/v1"),
    ]
    DEFAULT_URL = "http://192.168.1.175:8001/v1"
    
    MAX_TOKENS_RATIO = 0.7          # max_tokens = max_model_len × 0.7
    MAX_TOKENS_FALLBACK = 8192      # 無法解析時的保守值
    THINKING_CONTEXT_THRESHOLD = 100000   # max_model_len > 此值才開思考
    THINKING_BUDGET_RATIO = 0.8     # thinking_budget = max_tokens × 0.8

啟動範例

# 互動式(每次啟動跳選單)
python integrated_workflow.py boolean_local 1.1

# 批次模式(跳過選單,用 __init__ 預設端點)
set BOOLEAN_LOCAL_SKIP_PROMPT=1  # Windows
export BOOLEAN_LOCAL_SKIP_PROMPT=1  # Linux/macOS
python integrated_workflow.py boolean_local 1.1

兩種資料夾格式 — 重要踩坑點

⚠️ 這是這套系統最容易出錯的地方。系統支援兩種不同的學生資料夾命名格式,不能混用

格式 A:Moodle 模式

PDFProcessingSystem(complete_pdf_processing_system.py)從 CSV 成績表 產生。

命名格式:

{姓名}_{Participant編號}_assignsubmission_file

實例:

適筠 劉_0015_assignsubmission_file
郁婕 温_0019_assignsubmission_file
Claire Lee_001_assignsubmission_file

特徵:

  • 姓名在前,編號(Moodle Participant 序號,不是學號)在後
  • 編號通常是 3-4 位數
  • 從 Moodle 「Grade-課程名.csv」(Moodle 匯出的標準格式)中讀取

格式 B:Exam 模式

ExamPDFProcessingSystem(exam_pdf_processing_system.py)從 xlsx 名單 產生。

命名格式:

{學號}_{姓名}_assignsubmission_file

實例:

1100331_張家維_assignsubmission_file
1103730_陳威宇_assignsubmission_file

特徵:

  • 學號在前,姓名在後(跟 Moodle 順序完全相反)
  • 學號是學校的 7 位學號
  • 從 xlsx(欄位:「學號」、「姓名」)中讀取

兩種模式的對照表

項目 Moodle 模式 Exam 模式
觸發指令 (預設) --exam
處理類別 PDFProcessingSystem ExamPDFProcessingSystem
名單來源 Grades-*.csv *.xlsx
資料夾格式 姓名_編號_assignsubmission_file 學號_姓名_assignsubmission_file
「編號」含義 Moodle Participant 序號 (無此概念)
輸出資料夾名 CSV 檔名 去掉 Grades- 前綴 xlsx 檔名 完整保留

已知不一致 ⚠️

output_folder 處理方式不同:

# Moodle (complete_pdf_processing_system.py 第 1174 行)
self.output_folder = os.path.splitext(...)[0].replace('Grades-', '')
# Grades-DLD-TEST.csv → DLD-TEST  ← 砍掉前綴

# Exam (exam_pdf_processing_system.py 第 293-294 行)
base_name = os.path.splitext(os.path.basename(self.xlsx_file_path))[0]
self.output_folder = base_name
# Grades-DLD-TEST.xlsx → Grades-DLD-TEST  ← 保留原名

如果你的 csv 跟 xlsx 同名(Grades-DLD-TEST.csv vs Grades-DLD-TEST.xlsx),輸出資料夾名稱會不一樣。建議統一檔名規範,或明確記住自己用的是哪個模式。

base_agent._parse_student_info 的解析限制 ⚠️

base_agent.py 第 102-128 行的解析邏輯永遠假設「第一段是姓名、第二段是學號」:

clean_name = folder_name.replace('_assignsubmission_file', '')
parts = clean_name.split('_')

if len(parts) >= 2:
    name = parts[0]              # 永遠是第一段
    student_number = parts[1]    # 永遠是第二段

對兩種格式的影響:

模式 parts[0] (name) parts[1] (student_number) 評估
Moodle Claire Lee 001 ⚠️ 姓名對,但 student_number 抓到 Participant 編號(非真正學號)
Exam 1100331 張家維 完全顛倒 — 學號被當姓名,姓名被當學號

Exam 模式跑下去,匯出的 CSV 成績表會出現「學號欄塞姓名、姓名欄塞學號」的情況,需要手動修正 base_agent._parse_student_info 或在匯出時加判斷。

建議的判斷邏輯(尚未實作)

# 偽碼:自動偵測格式
if parts[0].isdigit() and len(parts[0]) in (6, 7):
    # Exam 格式:學號在前
    name = parts[1]
    student_number = parts[0]
else:
    # Moodle 格式:姓名在前
    name = parts[0]
    student_number = parts[1]

如果你要長期同時支援兩種模式,強烈建議先做這個修補,再大規模跑批改。


典型執行流程

案例 1:Moodle 課程作業(布林函數第 1.1 題)

# 1. 工作目錄下放:
#    - Grades-EEB111.csv          (Moodle 匯出的成績表)
#    - icp01-B_1.pdf, icp01-B_2.pdf, ...  (學生繳交的 PDF)

# 2. 啟動流程
python integrated_workflow.py boolean_local 1.1

預期流程:

=== 整合工作流程開始 ===
步驟1: 執行PDF處理和學號提取
----------------------------------------
✓ 使用 Moodle 模式(CSV 成績表)
=== 步驟 2.1: 設定學號提取器 ===
1. gpt-4o-mini ...
6. Qwen3.6-35B-A3B (vLLM Qwen3.6 本地推論)
選擇 (1-6, 默認1): 6
✓ 學號提取器建立成功
... (學號提取進行中)

步驟2: 轉換資料夾結構 → base/boolean_local/student/
已複製學生資料夾: Claire Lee_001_assignsubmission_file
... (103 個學生)

步驟3: 執行 BOOLEAN_LOCAL Agent 自動批改
----------------------------------------
可選的 vLLM 端點(掃描中...):
  ...
請選擇 [Stage1-OCR] 要使用的端點 [1/2/3, 預設 1]: 1
請選擇 [Stage2-批改] 要使用的端點 [1/2/3, 預設 1]: 2

開始執行 boolean_local 的批改流程...
[LaTeX 提取] 檢查學生作業...
    → [Stage1-OCR] http://...:8000/v1 (Qwen3-VL-30B) ...
    ✓ 回傳 856 字元
    → [Stage2-批改] http://...:8001/v1 (Qwen3-30B) 🧠 思考 ...
    ✓ 回傳 2384 字元
...

CSV 成績表已匯出到: base/reports/boolean_local_1.1_grades_YYYYMMDD_HHMMSS.csv

案例 2:紙本期中考(獨立考試模式)

# 1. 工作目錄下放:
#    - Grades-DLD-MIDTERM.xlsx     (學生名單 xlsx,欄位:學號、姓名)
#    - midterm-001.pdf, midterm-002.pdf, ...  (掃描的考卷)

# 2. 啟動流程
python integrated_workflow.py exam midterm --exam

注意:--exam 旗標必加,否則會走 Moodle 模式,程式會去找 csv 而不是 xlsx。


vLLM 端點互動式選擇(boolean_local 限定)

boolean_local 在批改流程開始時會lazy 觸發互動選單(實際觸發點是第一次呼叫 load_standard_answer)。

觸發機制

agent_registry 註冊階段
  └─ 只 call get_agent_info()
  └─ 不會跳出選單  ✅

run_grading 開始
  └─ load_standard_answer() 開頭呼叫 _ensure_runtime_configured()
     ├─ 旗標未設 → 跳出選單,儲存使用者選擇
     └─ 旗標已設 → 直接跳過

跳過互動的方法

方法 適用場景
設環境變數 BOOLEAN_LOCAL_SKIP_PROMPT=1 批次跑、cron 排程
__init__ 明確傳入 ocr_endpointgrading_endpoint 程式內部呼叫
stdin 沒有 TTY(EOFError) CI/CD pipeline

為什麼設計成 lazy 觸發?

直接在 __init__ 觸發互動會踩到一個陷阱:agent_registry 在註冊階段會建一個 temp instance 拿 get_agent_info(),如果 __init__ 跳選單,程式還沒進入批改流程就會卡住問你。lazy 機制確保只有真的要批改時才會問。


常見問題排查

Q1:跑 --exam 但程式說找不到 xlsx

確認:

  1. xlsx 檔案是否在當前工作目錄(不是 agent base/ 目錄)
  2. xlsx 是不是用 ~$ 開頭(Excel 暫存檔會被過濾)
  3. xlsx 中是否有「學號」跟「姓名」兩個欄位

Q2:資料夾複製過去後,base_agent 印「無法解析資料夾名稱」

通常是資料夾名稱沒有 _assignsubmission_file 後綴,或格式分段不足 2 段。檢查 base/{agent_id}/student/ 下實際資料夾名。

Q3:批改結果中學號跟姓名顛倒

你用了 Exam 模式,但 base_agent._parse_student_info 的解析假設是 Moodle 格式。參考兩種資料夾格式那節,加上自動偵測邏輯。

Q4:boolean_local 跑批改時打到 LM Studio (port 1234) 而不是 vLLM

互動選單沒被觸發 — 確認:

  • BooleanAgentLocal.__init__ 結尾有設 self._runtime_configured = (...)
  • _ensure_runtime_configured() 方法存在
  • load_standard_answer() 開頭有 call self._ensure_runtime_configured()

詳細排查見 boolean_agent_local.py 開發筆記。

Q5:vLLM 報 'NoneType' object has no attribute 'extract_all_student_ids'

學號提取器初始化失敗。常見原因:

  • setup_extractor() 中沒設 self.extractor = ...(歷史 bug,應已修復)
  • OPENAI_API_KEY 環境變數未設定
  • 選了選項 5 (LM Studio) 但 LM Studio 沒啟動 — 目前選項 5 已停用,請改選其他

Q6:批改回傳被截斷(finish_reason=length)

兩種狀況:

  • content 為空 + 偵測到 thinking → thinking 用完了還沒開始寫答案。調高 THINKING_BUDGET_RATIO,或在 vLLM 啟動時提高 --max-model-len
  • content 有東西但被切掉 → 答案本身太長。調高 MAX_TOKENS_RATIO

環境需求

  • Python 3.10+
  • vLLM 服務(at least 1 個運行中)
  • 套件:requests, Pillow, pandas, openpyxl, pdf2image, ultralytics (YOLO)
  • (可選)OpenAI/Gemini API keys for cloud LLM backends

檔案結構速查

automatic_grading_sys/
├─ integrated_workflow.py             ← 主進入點
├─ main.py                            ← 純批改進入點
├─ run_agent.py                       ← 單一 Agent 進入點
├─ complete_pdf_processing_system.py  ← Moodle PDF 處理
├─ exam_pdf_processing_system.py      ← Exam PDF 處理
├─ base_agent.py                      ← Agent 抽象基類
├─ agent_registry.py                  ← Agent 動態註冊
├─ agents/
│  ├─ boolean_agent.py
│  ├─ boolean_agent_local.py          ← 本地 vLLM 兩階段
│  ├─ boolean_agent_gemini.py
│  ├─ essay_agent.py
│  ├─ exam_agent.py                   ← 紙本考卷批改
│  └─ NS_agent_local.py
└─ base/                              ← 各 Agent 的資料目錄
   ├─ boolean_local/
   │  ├─ student/                     ← 學生作業
   │  └─ teacher/                     ← 標準答案
   ├─ exam/
   │  ├─ student/
   │  └─ teacher/
   └─ reports/                        ← CSV 成績匯出

開發者註記

  • 系統設計為多 Agent 可擴充:新增批改類型只需在 agents/ 下新增繼承 BaseGradingAgent 的類,registry 會自動掃描
  • 雲端跟本地後端走同樣的 OpenAI 相容 API,差別只在 base_url
  • 大規模批次跑請設 BOOLEAN_LOCAL_SKIP_PROMPT=1 避免被互動選單卡住
  • 兩種資料夾格式不一致是已知議題,長期應在 _parse_student_info 加自動偵測

Last updated: 2026-06

About

Automated grading system combining YOLO region detection, vision-LLM OCR, and LLM-based grading - supports Moodle assignments and paper exams.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages