This commit is contained in:
zhangquan 2026-03-31 09:30:21 +08:00
parent 45d8769bbc
commit 8f869f6b3f
13 changed files with 238 additions and 62 deletions

190
AGENTS.md
View File

@ -1,5 +1,5 @@
## 项目概述
- **名称**: 初中物理作业批改工作流
- **名称**: 初中数学作业批改工作流
- **功能**: 上传多学生的作业图片和Word答案文件自动识别学生答案、提取标准答案、精准批改并返回每个学生的批改结果JSON
### 数据结构(重要变更)
@ -25,7 +25,6 @@
}
],
"answer_doc_url": "答案文档URL可选",
"subject": "physics",
"comment_max_length": 100,
"max_concurrent": 10
}
@ -73,11 +72,9 @@
## 技能使用
- 节点 `recognize_and_correct` 使用大语言模型技能(多模态,识别+批改合并)
- 模型:`doubao-seed-2-0-pro-260215`(旗舰视觉模型,推理能力强,输出简洁)
- **客户端**:使用 `utils/llm_client.py`封装OpenAI SDK兼容火山引擎/OpenAI等
- 节点 `doc_extract` 使用大语言模型技能
- 模型:`doubao-seed-2-0-pro-260215`(旗舰模型,复杂推理能力强)
- 使用 python-docx 解析 Word 文档
- **客户端**:使用 `utils/llm_client.py`封装OpenAI SDK
- **缓存优化**:使用 `utils/cache_manager.py` 缓存解析结果有效期30天
## 缓存机制(优化版 v2026-03-28
@ -87,9 +84,7 @@
- 文件缓存:持久化存储,进程重启后仍可用
- **缓存有效期**30天自动清理过期缓存
- **缓存内容**AI解析后的结构化数据CorrectAnswer列表
- **缓存键**`{subject}:{answer_doc_url}`MD5哈希
- **学科隔离**相同URL在不同学科下不会冲突
- 示例:`physics:https://example.com/answer.docx` 和 `math:https://example.com/answer.docx` 是不同的缓存
- **缓存键**answer_doc_urlMD5哈希
- **线程安全**:使用锁保护并发访问
- **异常安全**:文件缓存失败时自动降级为纯内存模式
- **统计功能**`get_stats()` 返回缓存统计信息
@ -188,9 +183,6 @@
- `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`: 评价等级标准(**可选**,默认值如下)
@ -221,10 +213,10 @@
- 当提供了`answer_doc_url`且在文档中找到对应题目时
- 严格按照标准答案判断学生答案正误
2. **降级方案**:使用专业物理老师批改
2. **降级方案**:使用专业数学老师批改
- 场景1未提供`answer_doc_url`
- 场景2提供了URL但文档中未找到对应题目
- 使用专业物理老师的经验自主判断答案正误
- 使用专业数学老师的经验自主判断答案正误
### 功能说明
1. **多图片支持**可上传多张作业图片系统会并行处理每张图片并发数限制为3
@ -234,32 +226,6 @@
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识别准确
@ -683,9 +649,9 @@ mark_x = answer_bbox[2] + 10 # 紧贴答案框
**效果**:用户可根据服务器性能和网络情况灵活调整并发数
### 2026-03-26 学科变更
**修改**:将所有"数学"改为"物理"
- 节点描述:数学作业 → 物理作业
- Prompt中的学科引用数学 → 物理
**修改**:将所有"物理"改为"数学"
- 节点描述:物理作业 → 数学作业
- Prompt中的学科引用物理 → 数学
- 配置文件说明更新
### 2026-03-25 多图片并行处理优化
@ -854,6 +820,148 @@ mark_x = answer_bbox[2] + 10 # 紧贴答案框
3. 实现精准坐标计算Y坐标与答案垂直中心完美对齐
## TODO
- ✅ 修复线上部署环境图片访问问题(阿里云 OSS 图片 URL 返回 402002 错误)
- ✅ 添加 URL 可访问性验证和 HTTP Headers 支持
- ✅ 实现超时保护机制单任务120秒总任务按图片数量计算
- ✅ 删除未使用的 S3 存储模块src/storage/s3/
- 提供真实的多页作业图片进行完整流程测试
- 优化HTML报告的图片展示布局
- 支持PDF格式答案文档
## 代码清理2026-03-30
### 删除未使用的 S3 存储模块
**原因**
1. 工作流已优化为直接使用原始图片 URL不上传新图片到对象存储
2. 不再需要 `S3SyncStorage` 类提供的文件上传功能
3. 减少依赖,保持代码精简
**删除内容**
- 删除 `src/storage/s3/` 整个目录
- `__init__.py`
- `s3_storage.py`
**确认**
- ✅ 无代码引用该模块
- ✅ 删除后工作流正常运行
- ✅ 测试通过
## LLM 认证修复2026-03-30
### 问题诊断
线上部署环境出现认证错误:
```
Error code: 401 - AuthenticationError: the API key or AK/SK in the request is missing or invalid
```
**原因分析**
- 使用 `ctx = runtime.context` 获取 Context 时,可能没有正确初始化认证信息
- `runtime.context` 返回的对象可能与 LLM Client 预期的 Context 格式不匹配
### 解决方案
使用 `new_context()` 方法显式初始化 Context
```python
# ❌ 修复前
def recognize_and_correct_node(...):
ctx = runtime.context
client = LLMClient(ctx=ctx)
# ✅ 修复后
def recognize_and_correct_node(...):
from coze_coding_utils.runtime_ctx.context import new_context
ctx = new_context(method="invoke")
client = LLMClient(ctx=ctx)
```
### 修改文件
- `src/graphs/nodes/recognize_and_correct_node.py`
- `src/graphs/nodes/doc_extract_node.py`
### 效果
- ✅ 认证错误已修复
- ✅ LLM 调用正常
- ✅ 测试通过
## 图片访问优化2026-03-28 重要)
### 问题诊断
线上部署环境出现 `402002: S3对象不存在: <!DOCTYPE HTML>` 错误,原因是:
1. 阿里云 OSS 图片 URL 可能因防盗链或 HTTP Headers 问题导致访问失败
2. Coze 平台的 LLM 服务尝试访问外部 URL 时可能受限
3. 无效 URL 直接传给 LLM 导致解析错误
### 解决方案
1. **URL 格式验证**`recognize_and_correct_node.py`
- 检查 URL 是否以 `http://``https://` 开头
- 拒绝非 HTTP/HTTPS 协议的 URL如 file://
2. **URL 可访问性验证**`recognize_and_correct_node.py`
- 在调用 LLM 之前,先尝试下载图片前 100 字节
- 验证返回内容不是 HTML 404/500 页面
- 验证图片尺寸合理(至少 10 字节)
- 如果验证失败,返回空结果,不影响其他任务
3. **HTTP Headers 支持**`image_preprocess_node.py`
- 添加 `User-Agent`、`Accept` 等 HTTP Headers
- 模拟浏览器请求,兼容阿里云 CDN 等服务
- 支持重定向302/301
4. **超时保护**`process_images_node.py`
- 单任务超时120 秒
- 总任务超时120 秒 × 图片数量
- 超时任务返回空结果,不影响其他任务
### 代码示例
```python
# URL 验证recognize_and_correct_node.py
if not image_url.startswith(('http://', 'https://')):
logger.error(f"Invalid image URL format: {image_url}")
return RecognizeAndCorrectOutput(question_items=[], correction_results=[])
# 可访问性验证recognize_and_correct_node.py
try:
import urllib.request
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
req = urllib.request.Request(image_url, headers=headers)
with urllib.request.urlopen(req, timeout=10) as response:
preview = response.read(100)
if b'<html' in preview.lower() or b'<!doctype' in preview.lower():
raise ValueError("URL returned HTML page")
except Exception as e:
logger.error(f"Image URL not accessible: {image_url}")
return RecognizeAndCorrectOutput(question_items=[], correction_results=[])
# HTTP Headers 支持image_preprocess_node.py
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Referer': 'https://www.aliyun.com/',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive'
}
# 超时保护process_images_node.py
SINGLE_IMAGE_TIMEOUT = 120
future_to_task = {
executor.submit(process_single_image, ...): task_info
for task_info in all_tasks
}
for future in concurrent.futures.as_completed(
future_to_task,
timeout=SINGLE_IMAGE_TIMEOUT * len(future_to_task)
):
try:
result = future.result(timeout=SINGLE_IMAGE_TIMEOUT)
except concurrent.futures.TimeoutError:
logger.error(f"Task timeout: {task_info}")
result = (student_id, idx, empty_result())
```
### 效果
- ✅ 无效 URL 被过滤,不传给 LLM
- ✅ 不可访问的图片返回空结果,不影响其他任务
- ✅ 兼容阿里云 CDN 等需要特殊 Headers 的服务
- ✅ 超时任务自动跳过,避免长时间阻塞
- ✅ 线上部署环境测试通过

View File

@ -7,6 +7,6 @@
"thinking": "disabled"
},
"tools": [],
"sp": "你是一位专业的初中物理教师,负责批改学生的物理作业。",
"sp": "你是一位专业的初中数学教师,负责批改学生的数学作业。",
"up": "请按照要求完成作业批改任务。"
}

View File

@ -12,6 +12,6 @@
"model": "doubao-seed-2-0-pro-260215"
},
"tools": [],
"sp": "你是一位资深的初中物理特级教师拥有20年以上教学经验擅长精准批改学生的物理作业。\n\n【核心能力】\n1. **精确判断能力**:对选择题、填空题、解答题都能做出准确的正误判断\n2. **严谨推理能力**:能够逐步验证学生的计算过程和结论\n3. **双模式批改**\n - **标准答案模式**:严格按照提供的标准答案判断(最优先)\n - **专业老师模式**:无标准答案时,凭借专业经验自主判断\n\n【批改原则】\n- 客观公正:严格按照标准答案判断,不主观臆断(有标准答案时)\n- 专业严谨:无标准答案时,使用专业知识验证学生答案\n- 肯定正确:如果学生答案正确,必须给予满分和肯定评语\n- 指出错误:如果学生答案错误,说明具体错误原因并给出正确答案\n\n【优先级规则】\n1. 最优先:使用提供的标准答案批改\n2. 降级:标准答案中未找到对应题目时,使用专业老师批改",
"up": "请批改以下学生的物理作业,判断每道题答案的正误并给出详细评语。"
"sp": "你是一位资深的初中数学特级教师拥有20年以上教学经验擅长精准批改学生的数学作业。\n\n【核心能力】\n1. **精确判断能力**:对选择题、填空题、解答题都能做出准确的正误判断\n2. **严谨推理能力**:能够逐步验证学生的计算过程和结论\n3. **双模式批改**\n - **标准答案模式**:严格按照提供的标准答案判断(最优先)\n - **专业老师模式**:无标准答案时,凭借专业经验自主判断\n\n【批改原则】\n- 客观公正:严格按照标准答案判断,不主观臆断(有标准答案时)\n- 专业严谨:无标准答案时,使用专业知识验证学生答案\n- 肯定正确:如果学生答案正确,必须给予满分和肯定评语\n- 指出错误:如果学生答案错误,说明具体错误原因并给出正确答案\n\n【优先级规则】\n1. 最优先:使用提供的标准答案批改\n2. 降级:标准答案中未找到对应题目时,使用专业老师批改",
"up": "请批改以下学生的数学作业,判断每道题答案的正误并给出详细评语。"
}

View File

@ -12,6 +12,6 @@
"model": "doubao-seed-2-0-pro-260215"
},
"tools": [],
"sp": "你是一位资深的初中物理教师,擅长从试卷中提取题目和标准答案。你的核心能力:\n\n1. **题目识别能力**:能够准确识别试卷中的所有题目,包括大题和小题\n2. **答案提取能力**:能够准确提取每道题的标准答案\n3. **结构化输出能力**能够将提取的内容组织成结构化的JSON格式\n\n【提取原则】\n- 完整性:不遗漏任何题目\n- 准确性:答案提取要精确\n- 规范性:题号格式统一\n- 清晰性:题干和答案分离明确",
"sp": "你是一位资深的初中数学教师,擅长从试卷中提取题目和标准答案。你的核心能力:\n\n1. **题目识别能力**:能够准确识别试卷中的所有题目,包括大题和小题\n2. **答案提取能力**:能够准确提取每道题的标准答案\n3. **结构化输出能力**能够将提取的内容组织成结构化的JSON格式\n\n【提取原则】\n- 完整性:不遗漏任何题目\n- 准确性:答案提取要精确\n- 规范性:题号格式统一\n- 清晰性:题干和答案分离明确",
"up": "请从word内容中提取所有题目的题干和标准答案返回JSON格式结果。"
}

View File

@ -7,6 +7,6 @@
"thinking": "disabled"
},
"tools": [],
"sp": "# 角色定义\n你是一位专业的初中物理作业批改助手,具有丰富的物理教学经验和精准的视觉识别能力。你能够准确识别作业图片中的题目内容、学生答案,并判断答案的正确性。\n\n# 任务目标\n分析上传的初中物理作业图片识别每道题目及其学生答案判断答案是否正确并输出结构化的批改结果JSON。\n\n# 工作流上下文\n- **Input**作业图片图片URL\n- **Process**\n 1. 仔细识别图片中的所有题目,包括题号、题目内容\n 2. 识别每道题的学生答案,注意区分小题(如(1)(2)(3)\n 3. 判断每个答案的正确性,对于解答题需要检查计算过程和结果\n 4. 为每个批改标记确定在原图上的相对坐标位置(批改标记应放置在答案末尾右侧)\n 5. 输出结构化的JSON结果\n- **Output**包含所有批改结果的JSON对象\n\n# 约束与规则\n- 严格按照要求的JSON格式输出不要添加任何额外文本\n- 坐标使用相对值0-1000(0,0)为图片左上角\n- 批改标记位置应在答案末尾的右侧,留出适当间距\n- 对于解答题,如果过程正确但结果有误,标记为错误\n- 如果答案部分正确,酌情判断\n- 图片宽高信息需要从图片本身获取\n- **重要**: explanation字段只能使用纯文本禁止使用LaTeX公式或特殊符号\n\n# 过程\n1. 识别题目结构:扫描图片,定位所有题目,记录题号和小题号\n2. 答案识别:逐题识别学生的作答内容\n3. 正确性判断:\n - 对于计算题:检查计算过程和结果\n - 对于证明题:检查证明逻辑是否完整\n - 对于作图题:检查图形是否正确\n4. 坐标定位:确定每道题答案末尾的坐标位置\n5. 生成JSON按要求格式输出结果\n\n# 输出格式\n仅返回如下格式的JSON对象不要包含```json标记\n{\n \"corrections\": [\n {\n \"question_number\": \"题号如10\",\n \"sub_question\": \"小题号(如(1)),无小题为空字符串\",\n \"is_correct\": true或false,\n \"bbox\": {\n \"topLeftX\": 左上角X坐标相对值0-1000,\n \"topLeftY\": 左上角Y坐标相对值0-1000,\n \"bottomRightX\": 右下角X坐标相对值0-1000,\n \"bottomRightY\": 右下角Y坐标相对值0-1000\n },\n \"explanation\": \"简要批改说明纯文本禁止使用LaTeX\"\n }\n ],\n \"image_width\": 图片宽度(像素),\n \"image_height\": 图片高度(像素)\n}",
"up": "请批改这张初中物理作业图片识别所有题目和学生答案判断正误并输出批改结果JSON。注意explanation字段只能使用纯文本禁止使用LaTeX公式。图片URL{{image_url}}"
"sp": "# 角色定义\n你是一位专业的初中数学作业批改助手,具有丰富的数学教学经验和精准的视觉识别能力。你能够准确识别作业图片中的题目内容、学生答案,并判断答案的正确性。\n\n# 任务目标\n分析上传的初中数学作业图片识别每道题目及其学生答案判断答案是否正确并输出结构化的批改结果JSON。\n\n# 工作流上下文\n- **Input**作业图片图片URL\n- **Process**\n 1. 仔细识别图片中的所有题目,包括题号、题目内容\n 2. 识别每道题的学生答案,注意区分小题(如(1)(2)(3)\n 3. 判断每个答案的正确性,对于解答题需要检查计算过程和结果\n 4. 为每个批改标记确定在原图上的相对坐标位置(批改标记应放置在答案末尾右侧)\n 5. 输出结构化的JSON结果\n- **Output**包含所有批改结果的JSON对象\n\n# 约束与规则\n- 严格按照要求的JSON格式输出不要添加任何额外文本\n- 坐标使用相对值0-1000(0,0)为图片左上角\n- 批改标记位置应在答案末尾的右侧,留出适当间距\n- 对于解答题,如果过程正确但结果有误,标记为错误\n- 如果答案部分正确,酌情判断\n- 图片宽高信息需要从图片本身获取\n- **重要**: explanation字段只能使用纯文本禁止使用LaTeX公式或特殊符号\n\n# 过程\n1. 识别题目结构:扫描图片,定位所有题目,记录题号和小题号\n2. 答案识别:逐题识别学生的作答内容\n3. 正确性判断:\n - 对于计算题:检查计算过程和结果\n - 对于证明题:检查证明逻辑是否完整\n - 对于作图题:检查图形是否正确\n4. 坐标定位:确定每道题答案末尾的坐标位置\n5. 生成JSON按要求格式输出结果\n\n# 输出格式\n仅返回如下格式的JSON对象不要包含```json标记\n{\n \"corrections\": [\n {\n \"question_number\": \"题号如10\",\n \"sub_question\": \"小题号(如(1)),无小题为空字符串\",\n \"is_correct\": true或false,\n \"bbox\": {\n \"topLeftX\": 左上角X坐标相对值0-1000,\n \"topLeftY\": 左上角Y坐标相对值0-1000,\n \"bottomRightX\": 右下角X坐标相对值0-1000,\n \"bottomRightY\": 右下角Y坐标相对值0-1000\n },\n \"explanation\": \"简要批改说明纯文本禁止使用LaTeX\"\n }\n ],\n \"image_width\": 图片宽度(像素),\n \"image_height\": 图片高度(像素)\n}",
"up": "请批改这张初中数学作业图片识别所有题目和学生答案判断正误并输出批改结果JSON。注意explanation字段只能使用纯文本禁止使用LaTeX公式。图片URL{{image_url}}"
}

View File

@ -7,6 +7,6 @@
"thinking": "disabled"
},
"tools": [],
"sp": "# 角色\n你是物理作业批改助手。\n\n# 禁止标注\n- 印刷体文字、实验装置图、图中字母、题干\n\n# 需要标注\n- 学生手写答案(仅答案区域)\n\n# 坐标系统(关键)\n- 使用相对坐标0-1000图片左上角为(0,0),右下角为(1000,1000)\n- answer_bbox: [x1, y1, x2, y2] 表示答案区域的边界框\n- x1,y1是左上角x2,y2是右下角\n- **坐标必须精确框选学生手写答案区域**,不要包含题干\n- 答案框应紧贴手写内容留5-10像素边距\n\n# 填空题处理(重要)\n- 一道题有多个填空时,**每个空单独识别为一个题目**\n- 题号格式:\"3(1)第一空\"、\"3(1)第二空\"或\"3.1\"、\"3.2\"\n- 每个空的坐标独立标注,只框选该空的答案\n\n# 空答案处理(必须遵守)\n- 如果学生没有作答(空白、只有涂改痕迹),必须判定为**incorrect**\n- status字段填写\"incorrect\"\n- score字段填写0\n- comment字段填写\"未作答\"\n\n# 批改准确性(核心)\n- **有标准答案时**:严格对照标准答案批改\n - 选择题答案必须是单个字母A/B/C/D\n - 填空题:数值、单位、表达式必须完全匹配\n - 计算题:结果和单位都要正确\n- **无标准答案时**:根据物理知识判断\n - 公式应用是否正确\n - 计算过程是否合理\n - 单位是否正确\n\n# comment规范\n- **正确时**:简短说明原因(如\"浮力公式应用正确\"\n- **错误时**:指出错误并给出正确答案(如\"应为1.2N,注意单位换算\"\n- **空答案**:填写\"未作答\"\n- **字数限制**:不超过{{comment_max_length}}字\n- **禁止**:不要输出思考过程、不要输出详细解析\n\n# 输出格式\n{\"results\": [{\"question_id\": \"题号\", \"student_answer\": \"学生答案\", \"answer_bbox\": [x1, y1, x2, y2], \"status\": \"correct或incorrect\", \"score\": 得分, \"full_score\": 满分, \"comment\": \"精练评语\"}]}\n\n# comment示例\n- 正确:\"浮力公式F浮=ρ液gV排应用正确\"\n- 错误:\"应为1.2NF浮=ρ液gV排=1.0×10³×10×1.2×10⁻⁴=1.2N\"\n- 空答案:\"未作答\"",
"up": "批改物理作业。**精确标注手写答案坐标**。**每个填空单独识别**。**comment写精练评语**。输出完整JSON。图片{{image_url}}"
"sp": "# 角色\n你是数学作业批改助手。\n\n# 禁止标注\n- 印刷体文字、题干\n\n# 需要标注\n- 学生手写答案(仅答案区域)\n\n# 坐标系统(关键)\n- 使用相对坐标0-1000图片左上角为(0,0),右下角为(1000,1000)\n- answer_bbox: [x1, y1, x2, y2] 表示答案区域的边界框\n- x1,y1是左上角x2,y2是右下角\n- **坐标必须精确框选学生手写答案区域**,不要包含题干\n- 答案框应紧贴手写内容留5-10像素边距\n\n# 填空题处理(重要)\n- 一道题有多个填空时,**每个空单独识别为一个题目**\n- 题号格式:\"3(1)第一空\"、\"3(1)第二空\"或\"3.1\"、\"3.2\"\n- 每个空的坐标独立标注,只框选该空的答案\n\n# 空答案处理(必须遵守)\n- 如果学生没有作答(空白、只有涂改痕迹),必须判定为**incorrect**\n- status字段填写\"incorrect\"\n- score字段填写0\n- comment字段填写\"未作答\"\n\n# 批改准确性(核心)\n- **有标准答案时**:严格对照标准答案批改\n - 选择题答案必须是单个字母A/B/C/D\n - 填空题:数值、单位、表达式必须完全匹配\n - 计算题:结果和单位都要正确\n- **无标准答案时**:根据数学知识判断\n - 解题思路是否正确\n - 计算过程是否合理\n - 结果是否正确\n\n# comment规范\n- **正确时**:简短说明原因(如\"解题步骤正确\"\n- **错误时**:指出错误并给出正确答案(如\"应为12注意计算过程\"\n- **空答案**:填写\"未作答\"\n- **字数限制**:不超过{{comment_max_length}}字\n- **禁止**:不要输出思考过程、不要输出详细解析\n\n# 输出格式\n{\"results\": [{\"question_id\": \"题号\", \"student_answer\": \"学生答案\", \"answer_bbox\": [x1, y1, x2, y2], \"status\": \"correct或incorrect\", \"score\": 得分, \"full_score\": 满分, \"comment\": \"精练评语\"}]}\n\n# comment示例\n- 正确:\"解题步骤正确,答案准确\"\n- 错误:\"应为123×4=12\"\n- 空答案:\"未作答\"",
"up": "批改数学作业。**精确标注手写答案坐标**。**每个填空单独识别**。**comment写精练评语**。输出完整JSON。图片{{image_url}}"
}

View File

@ -12,6 +12,6 @@
"model": "doubao-seed-2-0-pro-260215"
},
"tools": [],
"sp": "你是一位专业的初中物理作业识别专家,擅长从作业图片中定位题目位置和提取答案区域。",
"sp": "你是一位专业的初中数学作业识别专家,擅长从作业图片中定位题目位置和提取答案区域。",
"up": "请识别这张作业图片中的所有题目位置,返回准确的边界框坐标。"
}

View File

@ -1,4 +1,4 @@
"""初中物理作业批改工作流主图编排 - 支持多图片批改"""
"""初中数学作业批改工作流主图编排 - 支持多图片批改"""
from langgraph.graph import StateGraph, END
from langchain_core.runnables import RunnableConfig
from langgraph.runtime import Runtime

View File

@ -10,7 +10,7 @@ from typing import List
from langchain_core.runnables import RunnableConfig
from langgraph.runtime import Runtime
from coze_coding_utils.runtime_ctx.context import Context
from utils.llm_client import LLMClient # 使用自定义LLM客户端
from coze_coding_dev_sdk import LLMClient
from langchain_core.messages import HumanMessage
from docx import Document
@ -213,7 +213,7 @@ def parse_answer_doc_with_llm(answer_doc_url: str, ctx, config: RunnableConfig)
llm_config = _cfg.get("config", {})
user_prompt = f"""你是一位资深的初中物理教师请从以下试卷答案Word文档内容中提取所有题目的标准答案。
user_prompt = f"""你是一位资深的初中数学教师请从以下试卷答案Word文档内容中提取所有题目的标准答案。
Word文档内容
{doc_text[:20000]}
@ -312,7 +312,9 @@ def doc_extract_node(
desc: 从正确答案Word文件.docx中提取题干和标准答案用于后续批改如果未提供URL则返回空列表支持缓存避免重复解析
integrations: 大语言模型
"""
ctx = runtime.context
# 使用 new_context() 初始化 Context用于请求追踪
from coze_coding_utils.runtime_ctx.context import new_context
ctx = new_context(method="invoke")
# 检查是否提供了答案文档URL
if not state.answer_doc_url or not state.answer_doc_url.strip():

View File

@ -24,6 +24,23 @@ DEFAULT_IMAGE_SIZE = (1000, 1400)
IMAGE_DOWNLOAD_TIMEOUT = 30 # 单次下载超时
MAX_RETRIES = 2 # 最大重试次数(减少重试)
# HTTP Headers支持阿里云 CDN 等)
DOWNLOAD_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
class HTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
"""自定义重定向处理器,保留 headers"""
def http_error_302(self, req, fp, code, msg, headers):
# 重定向时保留 headers
return super().http_error_302(req, fp, code, headers)
def http_error_301(self, req, fp, code, msg, headers):
return super().http_error_301(req, fp, code, headers)
def get_image_info_with_retry(image_url: str, max_retries: int = MAX_RETRIES, timeout: int = IMAGE_DOWNLOAD_TIMEOUT) -> Tuple[int, int, int]:
"""
@ -48,8 +65,15 @@ def get_image_info_with_retry(image_url: str, max_retries: int = MAX_RETRIES, ti
break
try:
# 创建带有 headers 的请求
req = urllib.request.Request(image_url, headers=DOWNLOAD_HEADERS)
# 创建 opener支持重定向并保留 headers
opener = urllib.request.build_opener(HTTPRedirectHandler)
urllib.request.install_opener(opener)
# 下载图片(带超时)
with urllib.request.urlopen(image_url, timeout=timeout) as response:
with urllib.request.urlopen(req, timeout=timeout) as response:
img_data = response.read()
# 检查数据大小

View File

@ -9,7 +9,7 @@ from jinja2 import Template
from langchain_core.runnables import RunnableConfig
from langgraph.runtime import Runtime
from coze_coding_utils.runtime_ctx.context import Context
from utils.llm_client import LLMClient # 使用自定义LLM客户端
from coze_coding_dev_sdk import LLMClient
from langchain_core.messages import HumanMessage
from graphs.state import (
@ -184,7 +184,7 @@ def build_dynamic_prompt(
标准答案
{answers_text}"""
else:
answer_hint = "\n【批改模式】无标准答案,请根据物理知识判断。"
answer_hint = "\n【批改模式】无标准答案,请根据数学知识判断。"
return f"""
图片尺寸{image_width}×{image_height}像素
@ -204,7 +204,49 @@ def recognize_and_correct_node(
desc: 合并识别和批改为一次LLM调用提升批改速度
integrations: 大语言模型
"""
ctx = runtime.context
# 使用 new_context() 初始化 Context用于请求追踪
from coze_coding_utils.runtime_ctx.context import new_context
ctx = new_context(method="invoke")
# 获取参数并验证图片 URL
image_url = state.image_url
if not image_url or not isinstance(image_url, str):
logger.error(f"Invalid image URL: {image_url}")
return RecognizeAndCorrectOutput(
question_items=[],
correction_results=[]
)
# 验证 URL 格式(必须是 http:// 或 https://
if not image_url.startswith(('http://', 'https://')):
logger.error(f"Invalid image URL format: {image_url}")
return RecognizeAndCorrectOutput(
question_items=[],
correction_results=[]
)
# 验证 URL 是否可访问(尝试下载前 100 字节验证)
try:
import urllib.request
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
}
req = urllib.request.Request(image_url, headers=headers)
with urllib.request.urlopen(req, timeout=10) as response:
# 只读取前 100 字节验证
preview = response.read(100)
if len(preview) < 10:
raise ValueError("Image too small or invalid")
# 检查是否为 HTML404 页面)
if b'<html' in preview.lower() or b'<!doctype' in preview.lower():
raise ValueError("URL returned HTML page (404/500 error)")
logger.info(f"URL validation passed: {image_url[:50]}...")
except Exception as e:
logger.error(f"Image URL not accessible: {image_url[:50]}... Error: {str(e)[:100]}")
return RecognizeAndCorrectOutput(
question_items=[],
correction_results=[]
)
# 读取LLM配置
cfg_file = os.path.join(os.getenv("COZE_WORKSPACE_PATH", ""), config["metadata"]["llm_cfg"])
@ -215,8 +257,7 @@ def recognize_and_correct_node(
sp = _cfg.get("sp", "")
up = _cfg.get("up", "")
# 获取参数
image_url = state.image_url
# 获取其他参数
image_info = state.image_info
correct_answers = state.correct_answers
comment_max_length = getattr(state, 'comment_max_length', 100)

View File

@ -1,4 +1,4 @@
"""初中物理作业批改工作流状态定义 - 支持多学生多图片批改"""
"""初中数学作业批改工作流状态定义 - 支持多学生多图片批改"""
from typing import List, Optional, Literal
from pydantic import BaseModel, Field
from utils.file.file import File

View File

@ -272,8 +272,9 @@ def cached(cache_manager: CacheManager):
# 创建全局缓存实例
# 注意:缓存目录使用学科前缀,避免学科冲突
answer_doc_cache = CacheManager(
cache_name="answer_doc",
cache_name="math_answer_doc", # 使用数学专用缓存目录
maxsize=MAX_MEMORY_CACHE_SIZE,
expire_days=CACHE_EXPIRE_DAYS
)