fix: MAX_RETRY 5 + rolling continuation + namespace-aware JRXML extraction

- MAX_RETRY: 3→5 (graph.py:35, nodes.py:25) with env override
- Rolling continuation: _generate_with_continuation() auto-detects
  truncated JRXML and sends anchor-based continuation, max 3 rounds
- JRXML extraction: regex/end-tag now namespace-prefix aware
  (ns0:jasperReport, ns:jasperReport, etc.)
- All 5 generation nodes refactored to use continuation helper
- Tests updated: scenario1 accepts ns-prefixed root, max_retry
  verifies graph termination
- stop_reason capture + WARNING log on max_tokens truncation
- Correction prompt now injects OCR context + layout schema
This commit is contained in:
2026-05-23 10:58:46 +08:00
parent 83e801a0b8
commit 1210b926c3
5 changed files with 187 additions and 50 deletions
+28 -3
View File
@@ -4,7 +4,7 @@
一个**本地桌面应用**,通过自然语言多轮对话帮助非技术用户创建 JasperReports 模板(JRXML 文件)。核心技术栈:Vue 3 前端 + FastAPI SSE 后端 + LangGraph 状态机 + LLM 生成/修改 + 自动验证修正循环。
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML → 自动验证 → 失败则自动修正(最多3次) → 重试耗尽后失败上下文自动注入下一轮 → 返回可编译的 JRXML 文件。
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML → 自动验证 → 失败则自动修正(最多5次) → 重试耗尽后失败上下文自动注入下一轮 → 返回可编译的 JRXML 文件。
## 启动命令
@@ -34,7 +34,7 @@ cd frontend && npm run dev
- **向量库**: ChromaDB 持久化在 `./db/chroma`
- **验证服务**: FastAPI `localhost:8001`
- **日志**: JSON 格式化,`logs/app.log` + `logs/llm.log`,中国时区 (UTC+8)
- **MAX_RETRY**: 3
- **MAX_RETRY**: 5
## 架构
@@ -233,7 +233,7 @@ validation_service/ (FastAPI, 端口 8001) — 不变
- **OCR 引擎**: 优先 PaddleOCR 2.9.x(精确识别,`pip install paddleocr`),回退 EasyOCR 1.7+。两者均未安装时仅返回图片元信息。PaddlePaddle 3.x 在 Windows 上有 ONEDNN bug,固定在 2.6.x。
- **OCR 字段提取**: `process_input` 自动检测上传图片,调用 `OcrExtractor` 提取常见中文字段(发票代码/号码/金额/日期等),提取结果自动注入 LLM 上下文。
- **会话持久化**: `session_id` 现已包含在 `save_session_node` 的持久化字段中,避免切换会话时因 `session_id` 丢失导致的无限 rerun bug。`create_session` 存盘前强制写入 `agent_state["session_id"] = sid``load_session_node` 不从磁盘覆盖 `session_id`。切换会话增加 `_last_switched_to` 哨兵防止重复触发。
- **MAX_RETRY**: 默认 3 次。重试耗尽后 `pending_failure_context` 记录失败信息,下次用户输入时自动注入。
- **MAX_RETRY**: 默认 5 次。重试耗尽后 `pending_failure_context` 记录失败信息,下次用户输入时自动注入。
- **验证最小内容检查**: 验证服务额外检查至少 1 个 `<band>` + 1 个 `<textField>``<staticText>`,拦截空壳 JRXML。
- **XLSX 支持 (v3)**: 需要 `openpyxl>=3.1.0`(已加入 requirements.txt)。表格按工作表逐行读取,单元格用 `|` 分隔。
- **粘贴功能限制**: 文件以 base64 编码在 sessionStorage 中传递,单文件上限 20MB。大文件建议使用 file_uploader 按钮。
@@ -360,3 +360,28 @@ cd frontend && npx playwright test
`backend/session.py``create_session()` 新增可选参数 `session_id: Optional[str] = None`
`api_server.py:507` 调用 `create_session(session_id=session_id)` 时之前会抛出 `TypeError`
## 更新 (v10 — 2026-05-23)
### 5-Fix — 生成可靠性全面加固
**问题诊断**: 上传车辆历史卡片图片后,`map_fields` 节点 LLM 返回 0 字符,导致 ~11,500 字符的骨架 JRXML 被空字符串覆盖,修正循环无法恢复,最终输出 934 字符的占位桩(与原始图片内容完全不符)。
**Fix 1 — 空响应保护**: 所有 5 个生成节点(`generate_skeleton`, `refine_layout`, `map_fields`, `modify_jrxml`, `correct_jrxml`)增加空响应守卫。LLM 返回空字符串时拒绝更新 `current_jrxml`,保留前一有效版本。
**Fix 2 — max_tokens 扩容**: `backend/llm.py``max_tokens` 从 4096 → 8192。MiniMax-M2.7 支持最大 131K 输出 token8192 在生成复杂 JRXML(通常 5000-15000 字符)时提供充裕空间。
**Fix 3 — 快照回退**: 5 个生成节点在 LLM 输出 JRXML 短于 200 字符时,回退到生成前的 `prev_jrxml` 版本,防止 LLM 输出无意义短文本污染状态。
**Fix 4 — 修正循环注入 OCR 上下文**: `correct_jrxml` 节点将 OCR 提取结果(`ocr_context`)和布局 schema`layout_schema_text`)注入修正 prompt。此前修正节点"盲修"——只看到 JRXML 和编译错误,不理解原始单据的字段结构和布局意图。
**Fix 5 — 滚动续写机制**: 当 LLM 输出因 `max_tokens` 限制被截断(JRXML 不以 `</jasperReport>` 结尾),自动发送续写请求(附最后 800 字符锚点),最多 3 轮(1 正常 + 2 续写)。
- `backend/llm.py``MiniMaxLLM.stream()` 捕获 `stop_reason``_LLMLoggingWrapper``max_tokens` 截断时记录 WARNING
- `agent/nodes.py` — 新增 `_generate_with_continuation()` 辅助函数,5 个生成节点全部重构使用
- `_extract_jrxml()` — 正则表达式支持命名空间前缀 JRXML(`<\w+:jasperReport`
- 内容去重:续写文本直接拼接,依赖 `_extract_jrxml` 提取完整 XML
**MAX_RETRY 调整**: 默认值从 3 → 5(环境变量 `MAX_RETRY`),配合续写机制确保复杂报表有充分修正机会。
**JRXML 提取命名空间兼容**: `_extract_jrxml()``_generate_with_continuation()` 的完整性检查统一支持 `</ns0:jasperReport>` 等命名空间前缀闭合标签。