diff --git a/AGENTS.md b/AGENTS.md index 7617f75..2c7c326 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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_url(MD5哈希) - **线程安全**:使用锁保护并发访问 - **异常安全**:文件缓存失败时自动降级为纯内存模式 - **统计功能**:`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对象不存在: ` 错误,原因是: +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' 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() # 检查数据大小 diff --git a/src/graphs/nodes/recognize_and_correct_node.py b/src/graphs/nodes/recognize_and_correct_node.py index 2af358f..cf7a836 100644 --- a/src/graphs/nodes/recognize_and_correct_node.py +++ b/src/graphs/nodes/recognize_and_correct_node.py @@ -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") + # 检查是否为 HTML(404 页面) + if b'