## 项目概述
- **名称**: 初中物理作业批改工作流
- **功能**: 上传多学生的作业图片和Word答案文件，自动识别学生答案、提取标准答案、精准批改并返回每个学生的批改结果JSON

### 数据结构（重要变更）
**输入参数**：
```json
{
  "student_homework": [
    {
      "student_id": 0,
      "student_name": "张三",
      "homework_images": [
        "图片URL1",
        "图片URL2"
      ]
    },
    {
      "student_id": 1,
      "student_name": "李四",
      "homework_images": [
        "图片URL3",
        "图片URL4"
      ]
    }
  ],
  "answer_doc_url": "答案文档URL（可选）",
  "subject": "physics",
  "comment_max_length": 100,
  "max_concurrent": 10
}
```

**输出结果**：
```json
{
  "student_results": [
    {
      "student_id": 0,
      "student_name": "张三",
      "total_images": 2,
      "image_results": [...],
      "overall_comment": "优秀！5题全部正确",
      "total_score": 15,
      "full_score": 15,
      "grade": "A+"
    }
  ]
}
```

### 节点清单
| 节点名 | 文件位置 | 类型 | 功能描述 | 分支逻辑 | 配置文件 |
|-------|---------|------|---------|---------|---------|
| doc_extract | `nodes/doc_extract_node.py` | agent | 从Word文件（.docx）提取题干和标准答案；如无URL则返回空列表 | - | `config/doc_extract_llm_cfg.json` |
| process_images | `nodes/process_images_node.py` | looparray | 循环调用子图处理每张作业图片，生成最终批改结果 | - | - |

**类型说明**: task(普通任务节点) / agent(大模型节点) / condition(条件分支) / looparray(列表循环) / loopcond(条件循环)

## 子图清单
| 子图名 | 文件位置 | 功能描述 | 被调用节点 |
|-------|---------|------|---------|
| single_image_subgraph | `graphs/loop_graph.py` | 处理单张图片的完整批改流程（预处理→识别批改→整合→包装） | process_images |

### 子图内部节点
| 节点名 | 文件位置 | 类型 | 功能描述 |
|-------|---------|------|---------|
| image_preprocess | `nodes/image_preprocess_node.py` | task | 下载图片、自动旋转（横向→纵向）、缩放到固定宽度1000px、上传对象存储 |
| recognize_and_correct | `nodes/recognize_and_correct_node.py` | agent | **一体化识别批改**：合并识别题目和批改为一次LLM调用 |
| result_merge | `nodes/result_merge_node.py` | task | 将识别结果和批改结果合并为最终批注 |
| wrap_result | `graphs/loop_graph.py` | task | 包装子图结果为SingleImageResult输出 |

## 技能使用
- 节点 `recognize_and_correct` 使用大语言模型技能（多模态，识别+批改合并）
  - 模型：`doubao-seed-2-0-pro-260215`（旗舰视觉模型，推理能力强，输出简洁）
- 节点 `doc_extract` 使用大语言模型技能
  - 模型：`doubao-seed-2-0-pro-260215`（旗舰模型，复杂推理能力强）
  - 使用 python-docx 解析 Word 文档
  - **缓存优化**：使用 `utils/cache_manager.py` 缓存解析结果，有效期30天

## 缓存机制（优化版 v2026-03-28）
- **缓存管理器**：`src/utils/cache_manager.py`
- **双层架构**：
  - 内存缓存：LRU淘汰，最大数量1000，快速访问
  - 文件缓存：持久化存储，进程重启后仍可用
- **缓存有效期**：30天（自动清理过期缓存）
- **缓存内容**：AI解析后的结构化数据（CorrectAnswer列表）
- **缓存键**：`{subject}:{answer_doc_url}`（MD5哈希）
  - **学科隔离**：相同URL在不同学科下不会冲突
  - 示例：`physics:https://example.com/answer.docx` 和 `math:https://example.com/answer.docx` 是不同的缓存
- **线程安全**：使用锁保护并发访问
- **异常安全**：文件缓存失败时自动降级为纯内存模式
- **统计功能**：`get_stats()` 返回缓存统计信息

### 性能优化与超时控制
- **图片下载超时**：30秒（单次），总时间不超过60秒
- **重试机制**：图片获取失败最多重试2次
- **单图片处理超时**：120秒（含LLM调用）
- **总任务超时**：120秒 × 图片数量
- **降级处理**：超时任务返回空结果，不影响其他任务
- **并发安全**：使用 `ThreadPoolExecutor` + 超时保护

## 等级标准配置（核心规则）
- **参数名**：`grade_standards`
- **核心规则**：**A+ 和 A 的首要条件是"全对"，与得分率无关**

### 等级判定逻辑（简化版）
| 等级 | 条件 | 说明 |
|------|------|------|
| A+ | 全对（错误数=0） | 所有题目都正确，与得分率无关 |
| A | （预留，全对时返回A+） | 答案全对 |
| B | 有错误，得分率≥80% | 有少量错误 |
| C | 有错误，得分率≥70% | 错误较多 |
| D | 有错误，得分率<70% | 错误很多 |

### 关键说明
- **全对 = A+**：只要所有题目都正确（incorrect_count == 0），就是A+
- **有错 = B/C/D**：有错误时，按得分率判断具体等级

### 示例
- ✅ 得分80分，错误0题 → A+（全对，得分率不重要）
- ✅ 得分95分，错误0题 → A+（全对）
- ❌ 得分95分，错误1题 → B（有错误，按得分率判断）
- ❌ 得分90分，错误2题 → B（有错误，按得分率判断）

### 配置示例
```json
{
  "grade_standards": {
    "A+": {"min_percentage": 95, "description": "优秀"},
    "A": {"min_percentage": 85, "description": "良好"},
    "B": {"min_percentage": 70, "description": "中等"}
  }
}
```

## 工作流程（多图片批改架构）

```
┌─────────────────────┐
│    doc_extract      │
│   (Word答案解析)     │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   process_images    │
│  (多图片循环处理)    │
│  生成最终批改结果   │
└─────────────────────┘
```

### 子图内部流程（处理单张图片 - 优化版）

```
┌─────────────────────┐
│  image_preprocess   │
│    (图像预处理)      │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│recognize_and_correct│  ← 合并节点：识别+批改一次完成
│  (一体化识别批改)    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   result_merge      │
│    (结果整合)        │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   wrap_result       │
│   (包装输出)         │
└─────────────────────┘
```

## 核心功能：多学生多图片批改机制

### 输入参数
- `student_homework`: 学生作业列表（List[StudentHomework]，支持多个学生）
  - 每个学生包含：
    - `student_id`: 学生ID（int）
    - `student_name`: 学生姓名（str，可选）
    - `homework_images`: 该学生的作业图片URL列表（List[str]，纯字符串数组）
- `answer_doc_url`: 正确答案Word文件的URL（.docx格式，**可选**）
- `subject`: 学科标识（str，**可选**，默认"physics"）
  - 用于缓存隔离，相同URL在不同学科下不会冲突
  - 支持值：physics、math、chinese、english 等
- `comment_max_length`: 评语最大字数（默认100字，**可选**）
- `max_concurrent`: 并行批改的最大数量（默认10，**可选**）
- `grade_standards`: 评价等级标准（**可选**，默认值如下）
  ```json
  {
    "A+": {"min_percentage": 95, "description": "答案全部正确，步骤完整规范，逻辑严谨；书写/格式整洁，无错别字、无遗漏；完成度100%，态度认真，质量上乘"},
    "A": {"min_percentage": 90, "description": "答案完全正确，无任何错误；步骤合理、格式规范，无原则性问题；完成度100%，满足全部要求"},
    "B": {"min_percentage": 80, "description": "存在少量非关键性错误，或步骤略有缺失；整体思路基本正确，仅细节、格式、计算等小问题；完成大部分内容，整体合格但不够严谨"},
    "C": {"min_percentage": 70, "description": "错误较多，部分核心题目作答错误；步骤不完整、逻辑不够清晰；完成度一般，有明显应付、漏答情况"},
    "D": {"min_percentage": 0, "description": "大面积错误，核心知识点未掌握；大量空白、敷衍、抄袭；未达到基本完成要求"}
  }
  ```

### 输出结果
- `student_results`: 各学生的批改结果列表（List[StudentResult]）
  - 每个学生包含：
    - `student_id`: 学生ID（int）
    - `student_name`: 学生姓名（str）
    - `total_images`: 该学生的总图片数
    - `image_results`: 该学生各图片的批改结果列表
    - `overall_comment`: 该学生的整体评价
    - `total_score`: 该学生的总分
    - `full_score`: 该学生的满分
    - `grade`: 该学生的等级评定

### 批改优先级（严格按照以下顺序）
1. **最优先**：使用Word文档中的标准答案批改
   - 当提供了`answer_doc_url`且在文档中找到对应题目时
   - 严格按照标准答案判断学生答案正误

2. **降级方案**：使用专业物理老师批改
   - 场景1：未提供`answer_doc_url`
   - 场景2：提供了URL但文档中未找到对应题目
   - 使用专业物理老师的经验自主判断答案正误

### 功能说明
1. **多图片支持**：可上传多张作业图片，系统会并行处理每张图片（并发数限制为3）
2. **Word答案提取**：从.docx文件中提取题干和标准答案
3. **子图循环处理**：使用子图封装单图片处理流程，主图调用子图处理每张图片
4. **批改结果JSON**：返回包含所有图片批改结果的结构化JSON
5. **智能降级**：无标准答案时自动切换到专业老师模式

## 优化记录
### 2026-03-28 缓存键加入学科标识（重要）
**问题**：相同URL在不同学科下会使用相同的缓存，导致答案解析结果冲突

**修复内容**：
1. **新增 `subject` 参数**：
   - 默认值：`physics`
   - 支持值：physics、math、chinese、english 等
   
2. **修改缓存键生成逻辑**：
   ```python
   # 修改前
   cache_key = answer_doc_url
   
   # 修改后
   cache_key = f"{subject}:{answer_doc_url}"
   ```

3. **缓存隔离效果**：
   - `physics:https://example.com/answer.docx`
   - `math:https://example.com/answer.docx`
   - 两个缓存完全独立，不会冲突

**效果**：
- 相同URL在不同学科下可以有不同的解析结果
- 缓存数据按学科隔离，更加灵活

### 2026-03-27 最终图片处理方案（重要）
**问题**：如何在不上传图片的前提下，保证AI识别准确？

**方案对比**：

| 方案 | 旋转缩放 | 上传 | AI访问 | 请求体积 | 选择 |
|------|---------|------|--------|---------|------|
| 方案1 | ✅ | base64编码 | Data URL | 大 | ❌ |
| 方案2 | ❌ | ❌ | 原始URL | 小 | ✅ |

**选择方案2的原因**：
1. **AI模型足够强大**：`doubao-seed-2-0-pro-260215` 可以处理各种尺寸和方向的图片
2. **坐标系统统一**：使用相对坐标（0-1000），自动适配任意尺寸
3. **最简单高效**：不需要base64编码，不需要上传，处理速度最快

**最终逻辑**：
```python
# 获取原始图片URL和尺寸
original_url = state.homework_image.url
width, height, dpi = get_image_info(original_url)

# 直接返回原始URL（不上传）
return ImagePreprocessOutput(
    image_url=original_url,
    image_info=ImageInfo(width=width, height=height, dpi=dpi)
)
```

**效果**：
- ✅ 不上传新图片到Coze
- ✅ 返回原始图片URL
- ✅ 坐标自动适配原始尺寸
- ✅ 处理速度最快

### 2026-03-27 移除图片上传功能（重要）
**问题**：系统自动上传处理后的图片到Coze对象存储，用户不需要这个功能

**原逻辑**：
1. 下载原始图片
2. 自动旋转（横向→纵向）
3. 缩放到固定宽度1000px
4. **上传到Coze对象存储**
5. 返回上传后的URL

**新逻辑**：
1. 获取原始图片URL
2. 获取图片尺寸信息
3. **直接返回原始URL和尺寸**（不上传）

**优化内容**：
- 移除图片旋转功能
- 移除图片缩放功能
- 移除图片上传功能
- 直接使用原始图片URL
- 坐标系统仍然使用相对坐标（0-1000），自动适配原始图片尺寸

**代码对比**：
```python
# 优化前
img = download_and_rotate(image_url)
img = resize_to_1000px(img)
new_url = upload_to_coze(img)  # 上传新图片
return ImagePreprocessOutput(image_url=new_url)

# 优化后
width, height, dpi = get_image_info(image_url)
return ImagePreprocessOutput(
    image_url=original_url,  # 直接返回原始URL
    image_info=ImageInfo(width=width, height=height, dpi=dpi)
)
```

**效果**：
- ✅ 不再上传新图片到Coze
- ✅ 返回原始图片URL
- ✅ 减少存储空间占用
- ✅ 提升处理速度

### 2026-03-27 坐标偏移量优化（重要）
**问题**：批改标记离学生答案太远，定位不够精准

**原因分析**：
- 原偏移量设置过大（30px、20px）
- 导致标记与答案视觉距离过远
- 影响批改结果的精准度

**优化方案**：
减小所有偏移量，让标记更贴近答案：

| 策略 | 原偏移 | 新偏移 | 说明 |
|------|--------|--------|------|
| 策略1（右侧空间>80px） | +30px | **+10px** | 紧贴答案框右侧 |
| 策略2（右侧空间40-80px） | -20px | **-10px** | 答案框右上角内部 |
| 策略3（右侧空间<40px） | +20px | **+10px** | 答案框左上角 |
| Y轴偏移 | 15px | **10px** | 顶部位置 |

**代码对比**：
```python
# 优化前
mark_x = answer_bbox[2] + 30  # 偏移过大

# 优化后
mark_x = answer_bbox[2] + 10  # 紧贴答案框
```

**效果**：
- ✅ 批改标记更贴近学生答案
- ✅ 视觉定位更精准
- ✅ 避免标记离答案太远的问题

### 2026-03-27 坐标边界严格限制（重要）
**问题**：标记坐标可能超过图片宽度，导致定位错误

**修复内容**：
1. **严格边界检查**：
   ```python
   # 确保x轴不超过图片宽度，y轴不超过图片高度
   mark_x = max(10, min(mark_x, image_info.width - 10))
   mark_y = max(10, min(mark_y, image_info.height - 10))
   ```

2. **边界优化**：
   - 边距从20px减少到10px，确保标记更接近边缘
   - 绝对不会超过图片宽度和高度
   - 保证批改标记始终在图片可视范围内

**效果**：标记坐标始终在图片范围内，不会出现越界问题

### 2026-03-27 空答案判定优化（重要）
**问题**：学生没有作答（空白）时，判定不够明确

**修复内容**：
在Prompt中新增"空答案处理"规则：
```
# ⚠️ 重要：空答案处理
- 如果学生没有作答（空白），必须判定为**incorrect**
- status字段填写"incorrect"
- score字段填写0
- comment字段填写"未作答"或"空白，无答案"
```

**示例**：
- 正确：`"status": "correct", "score": 10, "comment": "计算正确"`
- 错误：`"status": "incorrect", "score": 5, "comment": "单位错误"`
- 空答案：`"status": "incorrect", "score": 0, "comment": "未作答"`

**效果**：空答案统一判定为错误，得分0分，评语明确

### 2026-03-27 坐标定位精准度优化（重要）
**问题**：个别批改标记过于偏右，超出答题区域，甚至与相邻题目重叠

**原因分析**：
- 原逻辑固定在答案框右侧30px，未考虑右侧空间是否充足
- 当答案框本身靠右时，标记会超出合理范围

**优化方案**（三级策略）：
1. **策略1**：右侧空间充足（>100px）→ 标记在右侧（原有逻辑）
   ```python
   mark_x = answer_bbox[2] + 30
   mark_y = answer_bbox[1] + height * 0.5
   ```

2. **策略2**：右侧空间不足（50-100px）→ 标记在答案框右上角内部
   ```python
   mark_x = answer_bbox[2] - 20  # 内部
   mark_y = answer_bbox[1] + 15
   ```

3. **策略3**：右侧空间很小（<50px）→ 标记在答案框左上角
   ```python
   mark_x = answer_bbox[0] + 20  # 左侧
   mark_y = answer_bbox[1] + 15
   ```

**效果**：
- 批改标记始终在合理范围内
- 不会超出答题区域
- 不会与相邻题目重叠
- 视觉效果更精准

### 2026-03-27 完全并行架构优化（重要）
**问题**：原架构外层串行处理学生，内层并行处理图片，效率不高且可能有数据混乱风险

**修复内容**：
1. **完全并行架构**：
   - 所有学生的所有图片同时提交到线程池
   - 学生间+图片间完全并行，最大化效率
   - 使用 `(student_id, image_index, image_result)` 元组确保数据关联

2. **数据隔离机制**：
   - 结果按 `student_id` 分组存储
   - 每个学生的 `total_score`、`full_score`、`overall_comment`、`grade` 完全独立
   - 只使用该学生自己的 `image_results` 计算分数

3. **核心代码**：
   ```python
   # 返回元组：(student_id, image_index, image_result)
   return (student_id, idx, image_result)
   
   # 按student_id分组存储
   student_image_results[student_id][image_index] = image_result
   
   # 为每个学生独立计算结果
   for student in state.student_homework:
       image_results = student_image_results[student_id]
       # 只使用该学生的数据计算...
   ```

**效果**：
- 完全并行，效率最大化
- 数据严格隔离，不会混淆
- 学生A的数据绝不会出现在学生B的结果中

### 2026-03-27 输入参数格式优化（重要）
**问题**：`homework_images` 使用 `List[File]` 格式，用户输入不够简洁

**修复内容**：
1. **简化输入格式**：
   - `homework_images: List[File]` → `homework_images: List[str]`
   - 直接传入URL字符串数组，无需构造File对象
   - 代码内部自动将URL转换为File对象

2. **输入示例**：
   ```json
   {
     "student_homework": [
       {
         "student_id": 0,
         "homework_images": ["url1", "url2"]
       }
     ]
   }
   ```

**效果**：
- 用户输入更简洁
- 减少构造对象的复杂度
- 符合用户习惯

### 2026-03-27 多学生支持（重要变更）
**问题**：原架构只支持单个学生的多图片批改，无法区分不同学生

**修复内容**：
1. **数据结构重构**：
   - 输入参数：`homework_images` → `student_homework`（List[StudentHomework]）
   - 输出结果：`final_result` → `student_results`（List[StudentResult]）
   - 新增 `StudentHomework` 类型：包含 student_id 和 homework_images
   - 新增 `StudentResult` 类型：包含 student_id 和批改结果

2. **处理逻辑优化**：
   - 外层循环：遍历每个学生
   - 内层循环：并行处理该学生的所有图片
   - 每个学生独立计算分数、评语和等级

3. **返回结果独立**：
   - 每个学生有自己的 overall_comment、total_score、full_score、grade
   - 各学生的批改结果互不影响

**效果**：
- 支持批量批改多个学生的作业
- 每个学生的结果独立、清晰
- 符合实际教学场景需求

### 2026-03-27 Comment评语优化（重要）
**问题**：comment字段输出过于简单（仅"正确"/"错误"）或输出思考过程，不符合"精练评语"要求

**修复内容**：
1. **明确comment定义**：
   - **正确时**：简短说明为什么正确（如"根据称重法F浮=G-F示计算正确"）
   - **错误时**：指出错误原因并给出正确答案（如"应为1.2N，注意单位换算"）
   - **字数限制**：不超过comment_max_length字（默认100字）
   - **禁止**：不输出思考过程、不输出详细解析

2. **提供comment示例**：
   ```
   ✅ 正确：根据称重法F浮=G-F示计算正确
   ✅ 正确：浮力产生原因理解正确
   ✅ 错误：应为1.2N，根据F浮=ρ液gV排计算
   ✅ 错误：应选ACE，控制变量法应用错误
   
   ❌ 错误：正确（过于简单）
   ❌ 错误：根据...（思考过程）...所以正确（包含思考过程）
   ```

3. **参数传递优化**：
   - comment_max_length正确传递到Jinja2模板
   - LLM根据该参数生成符合长度要求的评语

**效果**：
- comment既简洁又有意义
- 正确时说明原因，错误时指出问题
- 符合comment_max_length限制
- 无思考过程，无详细解析

### 2026-03-27 JSON解析健壮性优化（重要）
**问题**：
- LLM输出JSON包含思考过程，导致格式错误
- JSON太长（11946字符），解析失败
- annotations为空，无法识别题目

**修复内容**：
1. **新增extract_complete_objects函数**：
   - 从包含思考过程的JSON中提取完整对象
   - 按对象边界逐个提取，不受思考过程干扰
   - 即使JSON格式错误，也能提取出有效数据

2. **新增clean_comment函数**：
   - 检测思考过程特征词（"不对"、"重新看"、"可能我"等）
   - 在思考过程开始处截断comment
   - 保留完整句子，确保结论清晰

3. **增加max_completion_tokens**：
   - 从8192增加到16384，避免JSON被截断
   - 确保完整输出所有题目

4. **优化Prompt**：
   - 明确要求"禁止输出思考过程"
   - comment只写结论："正确"或"错误，应为X"
   - 强调不要输出推理过程

**效果**：
- JSON解析成功率大幅提升
- 即使包含思考过程也能提取有效数据
- annotations不再为空
- comment简洁，无思考过程

### 2026-03-26 填空题拆分优化（重要）
**问题**：一道题有多个填空时，被合并成一个答案，批改标记无法精准定位

**修复内容**：
1. **优化Prompt**：
   - 明确要求：一道题有多个填空时，**每个空单独识别为一个题目**
   - 题号格式：\"3(1)第一空\"、\"3(1)第二空\"、\"4(2)第一空\"、\"4(2)第二空\"
   - 每个空单独批改，单独打分

2. **示例说明**：
   ```
   ❌ 错误：3(1) → "4、1"（合并）
   ✅ 正确：3(1)第一空 → "4"
           3(1)第二空 → "1"
   ```

3. **参数传递优化**：
   - comment_max_length参数正确传递到Jinja2模板
   - 确保LLM生成符合长度要求的comment

**效果**：
- 识别数量从9个增加到13个
- 每个填空都有独立的批改标记
- 批改标记精准定位到每个答案位置

### 2026-03-26 JSON解析优化（重要）
**问题**：LLM输出可能不完整（被max_completion_tokens截断），导致JSON解析失败

**修复内容**：
1. **新增fix_incomplete_json函数**：
   - 自动检测缺失的括号（}和]）
   - 自动补全缺失的括号，使JSON完整
   - 示例：`{"results": [{"id": 1}` → 自动补全为 `{"results": [{"id": 1}]}`

2. **增强JSON解析流程**：
   - 第一步：尝试直接解析
   - 第二步：尝试修复不完整的JSON（补全括号）
   - 第三步：尝试提取JSON对象
   - 第四步：尝试修复提取的JSON

3. **移除错误的截断逻辑**：
   - 不再在解析后截断comment（可能破坏转义字符）
   - 完全依赖LLM遵守comment_max_length限制
   - 通过Prompt明确要求LLM控制comment长度

4. **参数正确传递**：
   - comment_max_length参数正确传递到Prompt
   - LLM根据该参数生成符合长度的comment

**效果**：
- JSON解析成功率大幅提升
- 能够处理不完整的JSON输出
- comment长度由LLM控制，避免截断破坏格式

### 2026-03-26 识别优化：禁止标注实验装置图（重要）
**问题**：
1. 在实验装置图（如弹簧测力计、烧杯等）上标注了批改气泡
2. 坐标定位不够精准

**修复内容**：
1. **Prompt优化**：
   - 明确禁止标注实验装置图、示意图、电路图
   - 明确禁止标注图中标注的字母（如A、B、C、D、E、F、G）
   - 强调只标注学生手写答案

2. **工程规范优化**：
   - 从config文件读取sp和up（符合工程规范）
   - 使用Jinja2模板渲染Prompt
   - 代码中只保留动态部分构建（标准答案、图片尺寸等）

3. **识别流程优化**：
   - 找题号 → 找学生答案 → 框选答案 → 判断正误
   - 强调学生答案的特征：手写、填写空白处、计算结果

**效果**：不再误标注实验装置图，只标注学生手写答案

### 2026-03-26 新增并行数量控制参数
**优化前**：硬编码并发数限制为3，不够灵活
**优化后**：添加max_concurrent参数，默认值10，用户可自定义

**具体优化**：
1. **新增参数**：`max_concurrent`（可选，默认10）
2. **修改位置**：
   - `GraphInput.max_concurrent: Optional[int] = 10`
   - `GlobalState.max_concurrent: int = 10`
   - `ProcessImagesInput.max_concurrent: int`
3. **使用方式**：
   ```json
   {
     "homework_images": [...],
     "max_concurrent": 5  // 最多同时处理5张图片
   }
   ```

**效果**：用户可根据服务器性能和网络情况灵活调整并发数

### 2026-03-26 学科变更
**修改**：将所有"数学"改为"物理"
- 节点描述：数学作业 → 物理作业
- Prompt中的学科引用：数学 → 物理
- 配置文件说明更新

### 2026-03-25 多图片并行处理优化
**优化前**：多图片串行处理，总时间 = 单张图片时间 × 图片数量
**优化后**：多图片并行处理（并发数限制为3），总时间大幅缩短

**具体优化**：
1. **并行处理架构**：使用 `ThreadPoolExecutor` 并行调用子图处理每张图片
   - 最多同时处理3张图片
   - 结果按 `image_index` 正确排序，保证顺序一致性
2. **性能提升**：
   - 3张图片：时间减少约66%（从3份时间 → 1份时间）
   - 5张图片：时间减少约80%（从5份时间 → 约2份时间，分两批并行）
3. **质量保证**：
   - 每张图片独立处理，互不影响
   - 识别逻辑、批改逻辑完全相同，质量不受影响

### 2026-03-26 坐标定位修复（重要）
**问题**：坐标定位特别不准，批改标记位置错误
**原因**：Y坐标修正逻辑错误，导致坐标被错误缩放

**修复内容**：
1. **坐标系统重构**：从绝对坐标改为相对坐标（0-1000）系统
   - AI返回相对坐标（0-1000），(0,0)为图片左上角，(1000,1000)为右下角
   - 代码将相对坐标转换为绝对坐标：`绝对X = 相对X × width / 1000`，`绝对Y = 相对Y × height / 1000`

2. **Prompt优化**：
   - 明确要求AI返回相对坐标（0-1000）
   - 添加坐标系统说明和示例

3. **转换逻辑修正**：
   - 移除错误的Y坐标修正（`Y × height_ratio`）
   - 实现正确的相对坐标到绝对坐标转换

**效果**：坐标定位准确，批改标记位置正确

### 2026-03-26 题目和答案识别优化（重要）
**问题**：
1. 无法准确区分"题干"和"学生答案"
2. 批改气泡不在学生答案位置
3. 题干位置被误标注为答案

**修复内容**：
1. **Prompt重写**：
   - 明确定义"题干"和"学生答案"的区别
   - 强调只标注学生手写答案，不标注印刷体题干
   - 添加识别流程指导

2. **坐标定位优化**：
   - 自动计算mark_position：答案框右侧30像素，垂直居中
   - 添加边界检查，确保不超出图片范围
   - 不再依赖AI返回的mark_position（可能不准确）

3. **识别指导**：
   - 题号识别：如1、2、3、(1)、(2)等
   - 答案定位：学生手写内容（不是印刷体）
   - bbox框选：准确框选学生答案区域

**效果**：更准确地区分题干和答案，批改气泡位置更精准

### 2026-03-25 批改速度优化
**优化前**：每张图片需要3次LLM调用（识别+批改+整体评价）
**优化后**：每张图片只需1次LLM调用

**具体优化**：
1. **合并识别和批改**：将`homework_recognize`和`correction_judge`合并为`recognize_and_correct`节点
   - 识别题目、学生答案、坐标的同时进行批改
   - 减少一次LLM调用，速度提升约50%

2. **简化整体评价**：不再调用LLM生成整体评价
   - 使用规则直接生成评价内容
   - 根据得分率和错误数量生成个性化评语
   - 减少一次LLM调用

3. **子图节点精简**：从5个节点减少到4个节点
   - 移除：homework_recognize、correction_judge
   - 新增：recognize_and_correct（合并节点）
   - 保留：image_preprocess、result_merge、wrap_result

**效果**：
- LLM调用次数：每张图片从3次减少到1次
- 预计批改时间减少约60%

### 2026-03-25 新增输入参数控制
1. **新增 `comment_max_length` 参数**：控制评语最大字数，默认100字
2. **新增 `grade_standards` 参数**：自定义评价等级标准
   - 支持自定义各等级的最低得分率百分比
   - 支持自定义各等级的描述
   - 默认标准：A+(≥95%)、A(≥90%)、B(≥80%)、C(≥70%)、D(<70%)
3. **使用方式**：
   ```json
   {
     "homework_images": [...],
     "comment_max_length": 50,  // 评语最多50字
     "grade_standards": {
       "A+": {"min_percentage": 98, "description": "完美"},
       "A": {"min_percentage": 90, "description": "优秀"},
       ...
     }
   }
   ```

### 2026-03-25 评语优化与整体评价
1. **评语具体化**：批改评语要求具体说明对错原因
   - 正确时：说明为什么正确
   - 错误时：指出错误原因并给出正确答案
   - 部分正确时：说明哪些对了哪些错了
   - 字数限制：50字以内，最多不超过100字
   - 不要显示思考过程，只输出结果
2. **评语示例**：
   - 选择题正确：答案为B，与标准答案一致，正确。
   - 填空题错误：答案应为8+√7和8-√7，学生只写了一个，不完整。
   - 解答题正确：解题过程完整，步骤清晰，结果正确。
   - 计算题错误：计算过程有误，正确答案是m=2，建议检查移项步骤。
3. **整体评价**：根据所有批改内容自动生成简短的整体评价
   - 调用LLM生成个性化评价
   - 评价不超过50字
   - 包含主要问题或优点
   - 给出简短建议
4. **HTML报告优化**：在统计总览后显示整体评价区域

### 2026-03-25 自动旋转功能
1. **新增横向图片自动旋转**：如果上传的图片宽度大于高度（横向图片），系统会自动旋转-90度使其变为纵向
2. **旋转时机**：在图像预处理阶段，下载图片后、缩放前进行旋转
3. **旋转方向**：逆时针旋转90度（rotate(-90)），确保文字方向正确
4. **日志记录**：添加详细的旋转日志，便于调试

### 2026-03-25 多图片批改功能
1. **新增多图片支持**：从单图片批改升级为支持多图片批量批改
2. **新增子图架构**：创建 `loop_graph.py` 封装单图片处理流程
3. **新增循环节点**：创建 `process_images_node.py` 循环调用子图处理每张图片
4. **重构状态定义**：
   - `GraphInput.homework_image` → `homework_images: List[File]`
   - 新增 `SubgraphState`、`SubgraphInput`、`SubgraphOutput` 子图状态
   - 新增 `SingleImageResult` 单图片批改结果
   - 新增 `FinalResult.image_results` 多图片结果列表
5. **重构HTML生成**：支持生成包含所有图片批改标注的HTML报告
6. **优化主图编排**：简化为三节点线性流程（doc_extract → process_images → html_generate）

### 2026-03-25 双模式批改机制
1. **新增智能降级逻辑**：优先使用标准答案，无标准答案时自动切换专业老师模式
2. **修改state.py**：`answer_doc_url`改为可选字段，支持不提供答案URL的场景
3. **升级correction_judge_node**：实现题目分离逻辑，有标准答案和无标准答案分别处理
4. **更新Prompt**：批改节点支持两种模式（标准答案模式 + 专业老师模式）
5. **优化doc_extract_node**：无URL时返回空列表，不中断工作流

### 2026-03-25 Word答案解析功能
1. 新增 `doc_extract_node` 节点：从Word文件（.docx）提取题干和标准答案
2. 使用 python-docx 提取 Word 文档内容
3. 并行处理架构：图像识别与答案解析同时进行
4. 基于 Word 中的标准答案进行精准批改

### 2026-03-25 OCR识别能力优化
1. 问题：识别节点把学生答案中的"8"错认成"9"，导致误判
2. 优化识别节点prompt：增加OCR识别特别提示，强调区分8和9、6和0、1和7等相似字符
3. 效果：第7题正确识别为"8+√7，8-√7"，满分通过

### 2026-03-25 批改能力升级
1. 升级批改节点模型：`doubao-seed-1-6-vision-250815` → `doubao-seed-2-0-pro-260215`
2. 原因：较小模型对选择题判断准确率不足
3. 效果：选择题判断准确率大幅提升，推理过程更严谨

### 2026-03-24 重构（学习豆包APP方式）
1. 从8个节点简化为5个节点（现调整为子图+主图架构）
2. 采用一体化识别：AI识别answer_bbox，代码计算mark_position
3. 实现精准坐标计算，Y坐标与答案垂直中心完美对齐

## TODO
- 提供真实的多页作业图片进行完整流程测试
- 优化HTML报告的图片展示布局
- 支持PDF格式答案文档
