41 KiB
项目概述
- 名称: 初中数学作业批改工作流
- 功能: 上传多学生的作业图片和Word答案文件,自动识别学生答案、提取标准答案、精准批改并返回每个学生的批改结果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(可选)",
"comment_max_length": 100,
"max_concurrent": 10
}
输出结果:
{
"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列表)
- 缓存键:answer_doc_url(MD5哈希)
- 线程安全:使用锁保护并发访问
- 异常安全:文件缓存失败时自动降级为纯内存模式
- 统计功能:
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(有错误,按得分率判断)
配置示例
{
"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格式,可选)comment_max_length: 评语最大字数(默认100字,可选)max_concurrent: 并行批改的最大数量(默认10,可选)grade_standards: 评价等级标准(可选,默认值如下){ "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: 该学生的等级评定
- 每个学生包含:
批改优先级(严格按照以下顺序)
-
最优先:使用Word文档中的标准答案批改
- 当提供了
answer_doc_url且在文档中找到对应题目时 - 严格按照标准答案判断学生答案正误
- 当提供了
-
降级方案:使用专业数学老师批改
- 场景1:未提供
answer_doc_url - 场景2:提供了URL但文档中未找到对应题目
- 使用专业数学老师的经验自主判断答案正误
- 场景1:未提供
功能说明
- 多图片支持:可上传多张作业图片,系统会并行处理每张图片(并发数限制为3)
- Word答案提取:从.docx文件中提取题干和标准答案
- 子图循环处理:使用子图封装单图片处理流程,主图调用子图处理每张图片
- 批改结果JSON:返回包含所有图片批改结果的结构化JSON
- 智能降级:无标准答案时自动切换到专业老师模式
优化记录
2026-03-27 最终图片处理方案(重要)
问题:如何在不上传图片的前提下,保证AI识别准确?
方案对比:
| 方案 | 旋转缩放 | 上传 | AI访问 | 请求体积 | 选择 |
|---|---|---|---|---|---|
| 方案1 | ✅ | base64编码 | Data URL | 大 | ❌ |
| 方案2 | ❌ | ❌ | 原始URL | 小 | ✅ |
选择方案2的原因:
- AI模型足够强大:
doubao-seed-2-0-pro-260215可以处理各种尺寸和方向的图片 - 坐标系统统一:使用相对坐标(0-1000),自动适配任意尺寸
- 最简单高效:不需要base64编码,不需要上传,处理速度最快
最终逻辑:
# 获取原始图片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对象存储,用户不需要这个功能
原逻辑:
- 下载原始图片
- 自动旋转(横向→纵向)
- 缩放到固定宽度1000px
- 上传到Coze对象存储
- 返回上传后的URL
新逻辑:
- 获取原始图片URL
- 获取图片尺寸信息
- 直接返回原始URL和尺寸(不上传)
优化内容:
- 移除图片旋转功能
- 移除图片缩放功能
- 移除图片上传功能
- 直接使用原始图片URL
- 坐标系统仍然使用相对坐标(0-1000),自动适配原始图片尺寸
代码对比:
# 优化前
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 | 顶部位置 |
代码对比:
# 优化前
mark_x = answer_bbox[2] + 30 # 偏移过大
# 优化后
mark_x = answer_bbox[2] + 10 # 紧贴答案框
效果:
- ✅ 批改标记更贴近学生答案
- ✅ 视觉定位更精准
- ✅ 避免标记离答案太远的问题
2026-03-27 坐标边界严格限制(重要)
问题:标记坐标可能超过图片宽度,导致定位错误
修复内容:
-
严格边界检查:
# 确保x轴不超过图片宽度,y轴不超过图片高度 mark_x = max(10, min(mark_x, image_info.width - 10)) mark_y = max(10, min(mark_y, image_info.height - 10)) -
边界优化:
- 边距从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:右侧空间充足(>100px)→ 标记在右侧(原有逻辑)
mark_x = answer_bbox[2] + 30 mark_y = answer_bbox[1] + height * 0.5 -
策略2:右侧空间不足(50-100px)→ 标记在答案框右上角内部
mark_x = answer_bbox[2] - 20 # 内部 mark_y = answer_bbox[1] + 15 -
策略3:右侧空间很小(<50px)→ 标记在答案框左上角
mark_x = answer_bbox[0] + 20 # 左侧 mark_y = answer_bbox[1] + 15
效果:
- 批改标记始终在合理范围内
- 不会超出答题区域
- 不会与相邻题目重叠
- 视觉效果更精准
2026-03-27 完全并行架构优化(重要)
问题:原架构外层串行处理学生,内层并行处理图片,效率不高且可能有数据混乱风险
修复内容:
-
完全并行架构:
- 所有学生的所有图片同时提交到线程池
- 学生间+图片间完全并行,最大化效率
- 使用
(student_id, image_index, image_result)元组确保数据关联
-
数据隔离机制:
- 结果按
student_id分组存储 - 每个学生的
total_score、full_score、overall_comment、grade完全独立 - 只使用该学生自己的
image_results计算分数
- 结果按
-
核心代码:
# 返回元组:(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] 格式,用户输入不够简洁
修复内容:
-
简化输入格式:
homework_images: List[File]→homework_images: List[str]- 直接传入URL字符串数组,无需构造File对象
- 代码内部自动将URL转换为File对象
-
输入示例:
{ "student_homework": [ { "student_id": 0, "homework_images": ["url1", "url2"] } ] }
效果:
- 用户输入更简洁
- 减少构造对象的复杂度
- 符合用户习惯
2026-03-27 多学生支持(重要变更)
问题:原架构只支持单个学生的多图片批改,无法区分不同学生
修复内容:
-
数据结构重构:
- 输入参数:
homework_images→student_homework(List[StudentHomework]) - 输出结果:
final_result→student_results(List[StudentResult]) - 新增
StudentHomework类型:包含 student_id 和 homework_images - 新增
StudentResult类型:包含 student_id 和批改结果
- 输入参数:
-
处理逻辑优化:
- 外层循环:遍历每个学生
- 内层循环:并行处理该学生的所有图片
- 每个学生独立计算分数、评语和等级
-
返回结果独立:
- 每个学生有自己的 overall_comment、total_score、full_score、grade
- 各学生的批改结果互不影响
效果:
- 支持批量批改多个学生的作业
- 每个学生的结果独立、清晰
- 符合实际教学场景需求
2026-03-27 Comment评语优化(重要)
问题:comment字段输出过于简单(仅"正确"/"错误")或输出思考过程,不符合"精练评语"要求
修复内容:
-
明确comment定义:
- 正确时:简短说明为什么正确(如"根据称重法F浮=G-F示计算正确")
- 错误时:指出错误原因并给出正确答案(如"应为1.2N,注意单位换算")
- 字数限制:不超过comment_max_length字(默认100字)
- 禁止:不输出思考过程、不输出详细解析
-
提供comment示例:
✅ 正确:根据称重法F浮=G-F示计算正确 ✅ 正确:浮力产生原因理解正确 ✅ 错误:应为1.2N,根据F浮=ρ液gV排计算 ✅ 错误:应选ACE,控制变量法应用错误 ❌ 错误:正确(过于简单) ❌ 错误:根据...(思考过程)...所以正确(包含思考过程) -
参数传递优化:
- comment_max_length正确传递到Jinja2模板
- LLM根据该参数生成符合长度要求的评语
效果:
- comment既简洁又有意义
- 正确时说明原因,错误时指出问题
- 符合comment_max_length限制
- 无思考过程,无详细解析
2026-03-27 JSON解析健壮性优化(重要)
问题:
- LLM输出JSON包含思考过程,导致格式错误
- JSON太长(11946字符),解析失败
- annotations为空,无法识别题目
修复内容:
-
新增extract_complete_objects函数:
- 从包含思考过程的JSON中提取完整对象
- 按对象边界逐个提取,不受思考过程干扰
- 即使JSON格式错误,也能提取出有效数据
-
新增clean_comment函数:
- 检测思考过程特征词("不对"、"重新看"、"可能我"等)
- 在思考过程开始处截断comment
- 保留完整句子,确保结论清晰
-
增加max_completion_tokens:
- 从8192增加到16384,避免JSON被截断
- 确保完整输出所有题目
-
优化Prompt:
- 明确要求"禁止输出思考过程"
- comment只写结论:"正确"或"错误,应为X"
- 强调不要输出推理过程
效果:
- JSON解析成功率大幅提升
- 即使包含思考过程也能提取有效数据
- annotations不再为空
- comment简洁,无思考过程
2026-03-26 填空题拆分优化(重要)
问题:一道题有多个填空时,被合并成一个答案,批改标记无法精准定位
修复内容:
-
优化Prompt:
- 明确要求:一道题有多个填空时,每个空单独识别为一个题目
- 题号格式:"3(1)第一空"、"3(1)第二空"、"4(2)第一空"、"4(2)第二空"
- 每个空单独批改,单独打分
-
示例说明:
❌ 错误:3(1) → "4、1"(合并) ✅ 正确:3(1)第一空 → "4" 3(1)第二空 → "1" -
参数传递优化:
- comment_max_length参数正确传递到Jinja2模板
- 确保LLM生成符合长度要求的comment
效果:
- 识别数量从9个增加到13个
- 每个填空都有独立的批改标记
- 批改标记精准定位到每个答案位置
2026-03-26 JSON解析优化(重要)
问题:LLM输出可能不完整(被max_completion_tokens截断),导致JSON解析失败
修复内容:
-
新增fix_incomplete_json函数:
- 自动检测缺失的括号(}和])
- 自动补全缺失的括号,使JSON完整
- 示例:
{"results": [{"id": 1}→ 自动补全为{"results": [{"id": 1}]}
-
增强JSON解析流程:
- 第一步:尝试直接解析
- 第二步:尝试修复不完整的JSON(补全括号)
- 第三步:尝试提取JSON对象
- 第四步:尝试修复提取的JSON
-
移除错误的截断逻辑:
- 不再在解析后截断comment(可能破坏转义字符)
- 完全依赖LLM遵守comment_max_length限制
- 通过Prompt明确要求LLM控制comment长度
-
参数正确传递:
- comment_max_length参数正确传递到Prompt
- LLM根据该参数生成符合长度的comment
效果:
- JSON解析成功率大幅提升
- 能够处理不完整的JSON输出
- comment长度由LLM控制,避免截断破坏格式
2026-03-26 识别优化:禁止标注实验装置图(重要)
问题:
- 在实验装置图(如弹簧测力计、烧杯等)上标注了批改气泡
- 坐标定位不够精准
修复内容:
-
Prompt优化:
- 明确禁止标注实验装置图、示意图、电路图
- 明确禁止标注图中标注的字母(如A、B、C、D、E、F、G)
- 强调只标注学生手写答案
-
工程规范优化:
- 从config文件读取sp和up(符合工程规范)
- 使用Jinja2模板渲染Prompt
- 代码中只保留动态部分构建(标准答案、图片尺寸等)
-
识别流程优化:
- 找题号 → 找学生答案 → 框选答案 → 判断正误
- 强调学生答案的特征:手写、填写空白处、计算结果
效果:不再误标注实验装置图,只标注学生手写答案
2026-03-26 新增并行数量控制参数
优化前:硬编码并发数限制为3,不够灵活 优化后:添加max_concurrent参数,默认值10,用户可自定义
具体优化:
- 新增参数:
max_concurrent(可选,默认10) - 修改位置:
GraphInput.max_concurrent: Optional[int] = 10GlobalState.max_concurrent: int = 10ProcessImagesInput.max_concurrent: int
- 使用方式:
{ "homework_images": [...], "max_concurrent": 5 // 最多同时处理5张图片 }
效果:用户可根据服务器性能和网络情况灵活调整并发数
2026-03-26 学科变更
修改:将所有"物理"改为"数学"
- 节点描述:物理作业 → 数学作业
- Prompt中的学科引用:物理 → 数学
- 配置文件说明更新
2026-03-25 多图片并行处理优化
优化前:多图片串行处理,总时间 = 单张图片时间 × 图片数量 优化后:多图片并行处理(并发数限制为3),总时间大幅缩短
具体优化:
- 并行处理架构:使用
ThreadPoolExecutor并行调用子图处理每张图片- 最多同时处理3张图片
- 结果按
image_index正确排序,保证顺序一致性
- 性能提升:
- 3张图片:时间减少约66%(从3份时间 → 1份时间)
- 5张图片:时间减少约80%(从5份时间 → 约2份时间,分两批并行)
- 质量保证:
- 每张图片独立处理,互不影响
- 识别逻辑、批改逻辑完全相同,质量不受影响
2026-03-26 坐标定位修复(重要)
问题:坐标定位特别不准,批改标记位置错误 原因:Y坐标修正逻辑错误,导致坐标被错误缩放
修复内容:
-
坐标系统重构:从绝对坐标改为相对坐标(0-1000)系统
- AI返回相对坐标(0-1000),(0,0)为图片左上角,(1000,1000)为右下角
- 代码将相对坐标转换为绝对坐标:
绝对X = 相对X × width / 1000,绝对Y = 相对Y × height / 1000
-
Prompt优化:
- 明确要求AI返回相对坐标(0-1000)
- 添加坐标系统说明和示例
-
转换逻辑修正:
- 移除错误的Y坐标修正(
Y × height_ratio) - 实现正确的相对坐标到绝对坐标转换
- 移除错误的Y坐标修正(
效果:坐标定位准确,批改标记位置正确
2026-03-26 题目和答案识别优化(重要)
问题:
- 无法准确区分"题干"和"学生答案"
- 批改气泡不在学生答案位置
- 题干位置被误标注为答案
修复内容:
-
Prompt重写:
- 明确定义"题干"和"学生答案"的区别
- 强调只标注学生手写答案,不标注印刷体题干
- 添加识别流程指导
-
坐标定位优化:
- 自动计算mark_position:答案框右侧30像素,垂直居中
- 添加边界检查,确保不超出图片范围
- 不再依赖AI返回的mark_position(可能不准确)
-
识别指导:
- 题号识别:如1、2、3、(1)、(2)等
- 答案定位:学生手写内容(不是印刷体)
- bbox框选:准确框选学生答案区域
效果:更准确地区分题干和答案,批改气泡位置更精准
2026-03-25 批改速度优化
优化前:每张图片需要3次LLM调用(识别+批改+整体评价) 优化后:每张图片只需1次LLM调用
具体优化:
-
合并识别和批改:将
homework_recognize和correction_judge合并为recognize_and_correct节点- 识别题目、学生答案、坐标的同时进行批改
- 减少一次LLM调用,速度提升约50%
-
简化整体评价:不再调用LLM生成整体评价
- 使用规则直接生成评价内容
- 根据得分率和错误数量生成个性化评语
- 减少一次LLM调用
-
子图节点精简:从5个节点减少到4个节点
- 移除:homework_recognize、correction_judge
- 新增:recognize_and_correct(合并节点)
- 保留:image_preprocess、result_merge、wrap_result
效果:
- LLM调用次数:每张图片从3次减少到1次
- 预计批改时间减少约60%
2026-03-25 新增输入参数控制
- 新增
comment_max_length参数:控制评语最大字数,默认100字 - 新增
grade_standards参数:自定义评价等级标准- 支持自定义各等级的最低得分率百分比
- 支持自定义各等级的描述
- 默认标准:A+(≥95%)、A(≥90%)、B(≥80%)、C(≥70%)、D(<70%)
- 使用方式:
{ "homework_images": [...], "comment_max_length": 50, // 评语最多50字 "grade_standards": { "A+": {"min_percentage": 98, "description": "完美"}, "A": {"min_percentage": 90, "description": "优秀"}, ... } }
2026-03-25 评语优化与整体评价
- 评语具体化:批改评语要求具体说明对错原因
- 正确时:说明为什么正确
- 错误时:指出错误原因并给出正确答案
- 部分正确时:说明哪些对了哪些错了
- 字数限制:50字以内,最多不超过100字
- 不要显示思考过程,只输出结果
- 评语示例:
- 选择题正确:答案为B,与标准答案一致,正确。
- 填空题错误:答案应为8+√7和8-√7,学生只写了一个,不完整。
- 解答题正确:解题过程完整,步骤清晰,结果正确。
- 计算题错误:计算过程有误,正确答案是m=2,建议检查移项步骤。
- 整体评价:根据所有批改内容自动生成简短的整体评价
- 调用LLM生成个性化评价
- 评价不超过50字
- 包含主要问题或优点
- 给出简短建议
- HTML报告优化:在统计总览后显示整体评价区域
2026-03-25 自动旋转功能
- 新增横向图片自动旋转:如果上传的图片宽度大于高度(横向图片),系统会自动旋转-90度使其变为纵向
- 旋转时机:在图像预处理阶段,下载图片后、缩放前进行旋转
- 旋转方向:逆时针旋转90度(rotate(-90)),确保文字方向正确
- 日志记录:添加详细的旋转日志,便于调试
2026-03-25 多图片批改功能
- 新增多图片支持:从单图片批改升级为支持多图片批量批改
- 新增子图架构:创建
loop_graph.py封装单图片处理流程 - 新增循环节点:创建
process_images_node.py循环调用子图处理每张图片 - 重构状态定义:
GraphInput.homework_image→homework_images: List[File]- 新增
SubgraphState、SubgraphInput、SubgraphOutput子图状态 - 新增
SingleImageResult单图片批改结果 - 新增
FinalResult.image_results多图片结果列表
- 重构HTML生成:支持生成包含所有图片批改标注的HTML报告
- 优化主图编排:简化为三节点线性流程(doc_extract → process_images → html_generate)
2026-03-25 双模式批改机制
- 新增智能降级逻辑:优先使用标准答案,无标准答案时自动切换专业老师模式
- 修改state.py:
answer_doc_url改为可选字段,支持不提供答案URL的场景 - 升级correction_judge_node:实现题目分离逻辑,有标准答案和无标准答案分别处理
- 更新Prompt:批改节点支持两种模式(标准答案模式 + 专业老师模式)
- 优化doc_extract_node:无URL时返回空列表,不中断工作流
2026-03-25 Word答案解析功能
- 新增
doc_extract_node节点:从Word文件(.docx)提取题干和标准答案 - 使用 python-docx 提取 Word 文档内容
- 并行处理架构:图像识别与答案解析同时进行
- 基于 Word 中的标准答案进行精准批改
2026-03-25 OCR识别能力优化
- 问题:识别节点把学生答案中的"8"错认成"9",导致误判
- 优化识别节点prompt:增加OCR识别特别提示,强调区分8和9、6和0、1和7等相似字符
- 效果:第7题正确识别为"8+√7,8-√7",满分通过
2026-03-25 批改能力升级
- 升级批改节点模型:
doubao-seed-1-6-vision-250815→doubao-seed-2-0-pro-260215 - 原因:较小模型对选择题判断准确率不足
- 效果:选择题判断准确率大幅提升,推理过程更严谨
2026-03-24 重构(学习豆包APP方式)
- 从8个节点简化为5个节点(现调整为子图+主图架构)
- 采用一体化识别:AI识别answer_bbox,代码计算mark_position
- 实现精准坐标计算,Y坐标与答案垂直中心完美对齐
TODO
- ✅ 修复线上部署环境图片访问问题(阿里云 OSS 图片 URL 返回 402002 错误)
- ✅ 添加 URL 可访问性验证和 HTTP Headers 支持
- ✅ 实现超时保护机制(单任务120秒,总任务按图片数量计算)
- ✅ 删除未使用的 S3 存储模块(src/storage/s3/)
- ✅ 修复 LLM 认证错误,使用 new_context() 初始化 Context
- ✅ 修复 Docker 环境空请求体错误,添加请求体验证
- 提供真实的多页作业图片进行完整流程测试
- 优化HTML报告的图片展示布局
- 支持PDF格式答案文档
代码清理(2026-03-30)
删除未使用的 S3 存储模块
原因:
- 工作流已优化为直接使用原始图片 URL,不上传新图片到对象存储
- 不再需要
S3SyncStorage类提供的文件上传功能 - 减少依赖,保持代码精简
删除内容:
- 删除
src/storage/s3/整个目录__init__.pys3_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:
# ❌ 修复前
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.pysrc/graphs/nodes/doc_extract_node.py
效果
- ✅ 认证错误已修复
- ✅ LLM 调用正常
- ✅ 测试通过
图片访问优化(2026-03-28 重要)
问题诊断
线上部署环境出现 402002: S3对象不存在: <!DOCTYPE HTML> 错误,原因是:
- 阿里云 OSS 图片 URL 可能因防盗链或 HTTP Headers 问题导致访问失败
- Coze 平台的 LLM 服务尝试访问外部 URL 时可能受限
- 无效 URL 直接传给 LLM 导致解析错误
解决方案
-
URL 格式验证(
recognize_and_correct_node.py):- 检查 URL 是否以
http://或https://开头 - 拒绝非 HTTP/HTTPS 协议的 URL(如 file://)
- 检查 URL 是否以
-
URL 可访问性验证(
recognize_and_correct_node.py):- 在调用 LLM 之前,先尝试下载图片前 100 字节
- 验证返回内容不是 HTML 404/500 页面
- 验证图片尺寸合理(至少 10 字节)
- 如果验证失败,返回空结果,不影响其他任务
-
HTTP Headers 支持(
image_preprocess_node.py):- 添加
User-Agent、Accept等 HTTP Headers - 模拟浏览器请求,兼容阿里云 CDN 等服务
- 支持重定向(302/301)
- 添加
-
超时保护(
process_images_node.py):- 单任务超时:120 秒
- 总任务超时:120 秒 × 图片数量
- 超时任务返回空结果,不影响其他任务
代码示例
# 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 的服务
- ✅ 超时任务自动跳过,避免长时间阻塞
- ✅ 线上部署环境测试通过
Docker 环境请求验证修复(2026-03-30)
问题诊断
在 Docker 环境中运行时报错:
Invalid JSON format: Expecting value: line 1 column 1 (char 0)
原因分析:
- 请求体为空(例如,使用 GET 请求而不是 POST)
- Content-Type 不是 application/json
- 负载均衡器或代理可能过滤了请求体
解决方案
在 http_run 和 http_stream_run 函数中添加请求体验证:
# 检查请求体是否为空
if not body_text or body_text.strip() == "":
logger.error(f"Empty request body for run_id={ctx.run_id}")
raise HTTPException(
status_code=400,
detail="Request body is empty. Please provide a valid JSON payload."
)
# 检查 Content-Type
content_type = request.headers.get("content-type", "")
if "application/json" not in content_type.lower():
logger.error(f"Invalid Content-Type: {content_type}")
raise HTTPException(
status_code=400,
detail=f"Content-Type must be 'application/json', got: {content_type}"
)
修改文件
src/main.pyhttp_run函数:添加请求体验证http_stream_run函数:添加请求体验证
正确的请求格式
使用 curl:
curl -X POST http://localhost:8000/stream_run \
-H "Content-Type: application/json" \
-d '{
"student_homework": [
{
"student_id": 1,
"student_name": "张三",
"homework_images": ["https://example.com/image.jpg"]
}
],
"answer_doc_url": "",
"comment_max_length": 50,
"max_concurrent": 5
}'
使用 Python requests:
import requests
url = "http://localhost:8000/stream_run"
headers = {"Content-Type": "application/json"}
data = {
"student_homework": [
{
"student_id": 1,
"student_name": "张三",
"homework_images": ["https://example.com/image.jpg"]
}
],
"answer_doc_url": "",
"comment_max_length": 50,
"max_concurrent": 5
}
response = requests.post(url, json=data, headers=headers)
print(response.json())
注意事项:
- ✅ 必须使用 POST 请求
- ✅ Content-Type 必须是
application/json - ✅ 请求体必须是非空的 JSON 字符串
- ❌ 不要使用 GET 请求
- ❌ 不要发送空请求体
- ❌ 不要省略 Content-Type 头
效果
- ✅ 空请求体返回友好的错误信息
- ✅ 错误的 Content-Type 返回明确的错误提示
- ✅ 测试通过
Docker 环境 LLM 调用 404 错误诊断(2026-03-31)
问题诊断
在 Docker 环境部署时,LLM 调用返回 404 错误:
HTTP Request: POST https://api.coze.cn/chat/completions HTTP/1.1 404 Not Found
根本原因:错误的 API 端点(缺少 /v1/ 前缀)
- ❌ 错误端点:
https://api.coze.cn/chat/completions - ✅ 正确端点:
https://api.coze.cn/v1/chat/completions
解决方案
方案 1:更新代码和 SDK(推荐)
# 在 Docker 容器中执行
cd /workspace/projects
git pull
pip install --upgrade coze-coding-dev-sdk
docker restart <container_id>
方案 2:配置环境变量
export COZE_API_ENDPOINT="https://api.coze.cn/v1"
诊断工具
创建以下脚本帮助诊断问题:
scripts/test_llm_api.py:测试不同的 LLM API 端点scripts/diagnose_docker_env.py:综合诊断 Docker 环境docs/fix_llm_404_error.md:详细的修复指南
验证修复
修复后运行测试:
python scripts/test_llm_api.py
# 应该看到所有端点测试成功
修改文件
- 创建:
scripts/test_llm_api.py - 创建:
scripts/diagnose_docker_env.py - 创建:
docs/fix_llm_404_error.md - 更新:
AGENTS.md(本文档)
关键检查点
- ✅ SDK 版本是否最新(
pip show coze-coding-dev-sdk) - ✅ 代码是否包含
new_context()(检查关键节点文件) - ✅ 文件修改时间是否为最近(确保代码已更新)
- ✅ LLM 调用测试是否通过(
test_llm_api.py)