Files
agent_jrxml/CLAUDE.md
T
panda 4e14334030 fix: per-node max_tokens + validation 502 guard + correct_jrxml output validity
- backend/llm.py: per-node max_tokens via get_llm(max_tokens=N), LLM_MAX_TOKENS env var (default 8192)
- agent/nodes.py: 5 generation nodes use max_tokens=32768, generate_skeleton retries at 65536
- agent/nodes.py: fix ns:field regex (<field → <[\w:]*field) to handle namespace prefixes
- agent/nodes.py: fix correct_jrxml never writing back to state["current_jrxml"]
- agent/nodes.py: correct_jrxml rejects non-JRXML output (no <jasperReport tag)
- agent/nodes.py: _strip_continuation_wrapper strips markdown/prefixes from continuation rounds
- agent/nodes.py: _extract_jrxml iterates multiple markdown code blocks, skips fragments
- agent/graph.py: route_after_validate skips correction loop when service_unavailable
- agent/graph.py: route_after_save skips validation for empty JRXML
- backend/validation.py: returns service_unavailable: True for ConnectError and HTTP 5xx
- Docs: CLAUDE.md v14 changelog, README.md LLM_MAX_TOKENS, .env.example LLM_MAX_TOKENS
2026-05-24 15:20:25 +08:00

594 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — JRXML 生成代理
## 项目概述
一个**本地桌面应用**,通过自然语言多轮对话帮助非技术用户创建 JasperReports 模板(JRXML 文件)。核心技术栈:Vue 3 前端 + FastAPI SSE 后端 + LangGraph 状态机 + LLM 生成/修改 + 自动验证修正循环。
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML → 自动验证 → 失败则自动修正(最多5次) → 重试耗尽后失败上下文自动注入下一轮 → 返回可编译的 JRXML 文件。
## 启动命令
**方式 1 — 一键启动(Windows)**:双击 `start.bat`,自动打开三个窗口分别运行验证服务、后端 API、前端开发服务器。停止用 `stop.bat`
**方式 2 — 手动启动**
```bash
# 终端 1 — 验证服务(必须先启动)
python -m uvicorn validation_service.main:app --port 8001 --host 0.0.0.0
# 终端 2 — 后端 APISSE + REST
python -m uvicorn api_server:app --port 8000 --host 0.0.0.0
# 终端 3 — 前端开发服务器
cd frontend && npm run dev
```
浏览器打开 `http://localhost:5173`
## 当前配置(.env
- **OCR**: PaddleOCR(精确识别首选,ppocr-v4)→ EasyOCR(回退,ch_sim+en),两者均未安装时仅返回图片元信息
- **LLM**: `cloud` / `anthropic` → MiniMax Anthropic 兼容 API (`MiniMax-M2.7`)
- Base URL: `https://api.minimaxi.com/anthropic`
- 认证: Anthropic SDK 自动读取 `ANTHROPIC_API_KEY`fallback `OPENAI_API_KEY`
- **嵌入模型**: `local` / `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`
- **向量库**: ChromaDB 持久化在 `./db/chroma`
- **验证服务**: FastAPI `localhost:8001`
- **日志**: JSON 格式化,`logs/app.log` + `logs/llm.log`,中国时区 (UTC+8)
- **MAX_RETRY**: 5
## 架构
```
前端 (Vue 3 + Vite, 端口 5173)
│ src/
│ ├── api/client.ts SSE 客户端 + fetch 封装
│ ├── stores/chat.ts Pinia: 消息/流式/节点进度
│ ├── stores/session.ts Pinia: 会话管理
│ ├── stores/kb.ts Pinia: KB 状态管理(多租户知识库)
│ ├── components/
│ │ ├── Sidebar.vue 会话列表 + 下载 + 历史版本
│ │ ├── ChatMessages.vue 消息列表渲染
│ │ ├── ProcessSection.vue 过程折叠区(替代 StreamingMessage + NodeProgress
│ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴/芯片,含 .jrxml)
│ │ ├── SummaryCard.vue 结果摘要卡片(含耗时)
│ │ ├── KbSelector.vue KB 下拉选择器(对话中切换知识库)
│ │ └── KbManager.vue KB 管理面板(创建/上传/构建/删除)
│ └── utils/format.ts 工具函数
▼ HTTP + SSE (Server-Sent Events)
api_server.py (FastAPI, 端口 8000)
│ POST /api/sessions/{id}/chat → SSE 流式响应
│ CRUD /api/sessions/... → 会话管理
│ POST /api/upload → 文件上传
│ GET /api/download/... → JRXML 下载
│ GET /api/health, /api/config
│ 包装 LangGraph Agent(不变)──► agent/
validation_service/ (FastAPI, 端口 8001) — 不变
```
## 关键文件映射
| 文件 | 职责 | 修改频率 |
|------|------|---------|
| `api_server.py` | FastAPI SSE 后端,REST API + 流式推送 | **高** |
| `frontend/src/` | Vue 3 聊天 UI(替代旧 app.py | **高** |
| `agent/state.py` | AgentState 类型定义(~28 字段) | 低 |
| `agent/nodes.py` | 18 个工作流节点 + 流式生成 + 错误记录 | **高** |
| `agent/graph.py` | 状态图编译 + 路由函数 + node_start 回调 | 中 |
| `prompts/loader.py` | Prompt 加载器(从 .md 文件热重载) | 低 |
| `prompts/*.md` | 10 个独立 Prompt 模板 | **高** |
| `backend/llm.py` | LLM 工厂,统一 `_BaseLLM` 接口(invoke + stream+ `_LLMLoggingWrapper` | 中 |
| `backend/logger.py` | 集中日志模块:JSON 格式化 + trace_id + 独立 llm.log | 低 |
| `backend/rag_adapter.py` | RAGSearcher 单例,语义搜索接口 | 中 |
| `backend/error_kb.py` | ErrorKB — 错误指纹去重 + ChromaDB 持久化 + 语义检索 | 中 |
| `backend/file_parser.py` | 文件解析: PDF/DOCX/XLSX/XLS/DOC/图片(EasyOCR→PaddleOCR回退)/文本 | 中 |
| `backend/layout_analyzer.py` | A4模板分析: 比例检测/EasyOCR→PaddleOCR元素提取/行分组/JRXML行匹配/布局schema提取 | 中 |
| `backend/ocr_extractor.py` | OCR字段精确提取: 4策略(exact→kv_pair→regex→table_match) + 置信度 | 中 |
| `backend/annotation_detector.py` | 批注检测: 圈选(cv2 HoughCircles) + 箭头(HoughLinesP聚类) + OCR关联 + LLM格式化 | 中 |
| `backend/embeddings.py` | 嵌入模型工厂 (HuggingFace/OpenAI) | 低 |
| `backend/validation.py` | 验证服务 HTTP 客户端 | 低 |
| `backend/session.py` | 会话 JSON 文件 CRUD(含 kb_id | 低 |
| `backend/kb_manager.py` | 用户+知识库 CRUD(多租户,原子 JSON 持久化) | 中 |
| `backend/kb_searcher.py` | 知识库隔离搜索 + 模板检索(per-KB ChromaDB | 中 |
| `backend/kb_parser.py` | KB 解析管道:文件解析→字段提取→chunk切割→向量嵌入 | 中 |
| `backend/field_matcher.py` | OCR↔KB 字段匹配:Embedding 粗筛 + LLM 精确确认 | 中 |
| `agent/datasource.py` | 数据源模式解析:$P{{xxx}} 参数 vs JDBC 直连 | 低 |
| `agent/jrxml_windower.py` | JRXML Band 级窗口化引擎:拆解/切分/重组/元素计数校验 | 中 |
| `validation_service/main.py` | FastAPI 验证服务 | 低 |
| `scripts/init_default_kb.py` | 多租户默认 KB 初始化(默认用户 + 预置 KB) | 低 |
## 关键约定
1. **LLM 调用接口**: 所有节点通过 `get_llm().invoke(prompt)` 同步调用,或用 `get_llm().stream(prompt)` 流式调用。三个后端(Anthropic/OpenAI/Ollama)通过 `_BaseLLM` 统一接口。
2. **流式生成**: generate/modify_jrxml/correct_jrxml 使用 `get_stream_writer()` 发射自定义事件,UI 通过 `stream_mode=["updates", "custom"]` 捕获逐字输出。
3. **JRXML 提取**: `_extract_jrxml()` 处理 LLM 响应 —— 去掉 markdown 代码块标记,提取 XML 内容。
4. **状态持久化**: 每个会话存为 `sessions/{session_id}.json`LangGraph 节点间通过 AgentState dict 传递。
5. **Token 计数**: 使用 `tiktoken` (gpt-4o encoder) 估算,不管实际模型是什么。
6. **RAG 子模块**: `rag/` 是一个独立的 git submodule,其内部的生成产物 (`models/`, `embeddings/`, `chroma_db/`, `jrxml_source_chunks/`) 不在 git 中。
## Prompt 模板位置
所有 Prompt 在 `prompts/` 目录,`.md` 文件可直接编辑,无需重启应用:
| 文件 | 用途 |
|------|------|
| `prompts/intent_classify.md` | 8 分类意图识别 |
| `prompts/initial_generation.md` | 首次生成 JRXML |
| `prompts/modification.md` | 修改现有 JRXML |
| `prompts/correction.md` | 自动修正错误 |
| `prompts/explain_error.md` | 错误转人话 |
| `prompts/compression.md` | 对话压缩摘要 |
| `prompts/consult.md` | 咨询解答 |
| `prompts/skeleton_generation.md` | 分层生成-骨架 |
| `prompts/refine_layout.md` | 分层生成-精调 |
| `prompts/field_mapping.md` | 分层生成-字段映射 |
## 新增功能 (v2)
### 流式输出 + 节点平铺
- LLM 生成时逐字展示 XML(不再是空白等待)
- 节点以"处理过程"折叠区展开,不相互覆盖
- 完成后自动折叠,展示总结卡片
### 错误自增长知识库
- `backend/error_kb.py` — ChromaDB 集合 `jrxml_error_cases`
- 错误指纹去重(标准化 + MD5):相同结构错误不重复录入
- 记录内容:错误信息 + 修正前后 JRXML + 修正 prompt + 工具链
- `retrieve` 节点自动注入历史修正案例
- 流程:correct_jrxml 保存 last_error_case → validate 通过时自动入库
### 文件上传
- **对话区域上传(v3**: `st.file_uploader` 位于聊天输入框上方,支持图片/PDF/DOCX/XLSX/文本
- **粘贴/拖拽(v3**: 全局 paste/drop 事件监听 + `sessionStorage` + 轮询桥接组件,Ctrl+V 粘贴或拖拽文件到页面任意位置
- **文件预览芯片(v3)**: 上传后显示在对话区域,可逐文件移除(自动清理临时文件)
- 侧边栏多文件上传(可逐文件移除,向后兼容保留)
- 支持: PDF(pdfplumber+PIL) / DOCX(python-docx) / XLSX(openpyxl, v3) / 图片(PIL+EasyOCR优先→PaddleOCR回退) / 纯文本
- 上传文本自动注入下一条消息前缀
- 根据 `can_use_vision()` 判断是否走原生多模态(当前 MiniMax 不支持)
### 对话区域文件粘贴/拖拽技术方案(v3)
- `st.html()` 注入全局 paste/drop/dragover 监听器 → 文件转 base64 → 写入 `sessionStorage`
- `components.html(height=0)` 桥接组件每 800ms 轮询 `sessionStorage``Streamlit.setComponentValue` 回传 Python
- Python 解码 base64 → 临时文件 → `parse_file` + `analyze_layout` 双层 OCR 解析
- 上限:单文件 20MB,单次最多 10 个文件
### A4 模板识别
- `backend/layout_analyzer.py` — 三种处理路径:
- **完整 A4**: 比例匹配 + OCR 元素 → 全量布局描述
- **行片段 + 有现有报表**: 行匹配到 JRXML section → 定位修改
- **行片段 + 无现有报表**: 按 A4 模板生成完整报表
- PaddleOCR(可选安装)提供精确元素位置/字号
- 行分组:Y 轴容差自动聚类;行匹配:文本相似度搜索 JRXML band
### 会话历史下载
- `AgentState.jrxml_versions` 追踪每次生成/修改的版本
- 侧边栏"历史版本"折叠区,每版本独立下载按钮
### 预览修复
- `route_after_save` 新增意图判断:预览/导出跳过验证直通 finalize
### Ctrl+C 修复
- JS 注入拦截 Streamlit 裸 `c` 键清缓存,保留 Ctrl+C 复制
### 结构化日志系统
- `backend/logger.py` — JSON 格式化 + trace_id + 国际时区
- `_LLMLoggingWrapper` — 包装所有 LLM 后端,记录完整 prompt/response
- `@log_node` / `@_log_route` — 装饰器自动记录节点和路由
- 日志分离: `logs/app.log` (业务) + `logs/llm.log` (AI 调用)
## 新增功能 (v3/v4)
### OCR 单据字段精确提取 (v3)
- `backend/ocr_extractor.py` — 4 策略优先级提取: exact_match → kv_pair → regex → table_match
- PaddleOCR 首次识别后将原始结果(含所有文本元素 + bbox坐标)持久化
- `_format_ocr_context()` — 将 OCR 结果(字段 + 原始元素坐标)格式化为 LLM prompt 注入
- OCR 结果在 `modify_jrxml``generate` 节点中自动注入 prompt
- `process_input` 节点在上传图片时自动触发 OCR 字段提取
- 结果持久化到会话文件(`save_session_node` / `load_session_node`
### 多模态聊天输入 + 多格式文件 (v4)
- `app.py``st.chat_input` 替换为 `st_multimodal_chatinput`(支持 Ctrl+V 粘贴 + 拖拽 + 文件按钮)
- `_process_uploaded_file()` — 提取共享文件处理逻辑(侧边栏 + 聊天共用,消除 ~70 行重复代码)
- 新增文件格式支持: XLSX (openpyxl)、XLS (xlrd)、DOC (olefile)
- 剪贴板粘贴文件通过 base64 解码 + MIME type → 扩展名推断
- 侧边栏上传器类型列表中新增 xlsx/xls/doc
### 批注检测 (v4)
- `backend/annotation_detector.py` — 识别用户在手写单据上的圈选和箭头标记
- **圆圈检测**: 红色通道增强 → HoughCircles → 圆形度验证
- **箭头检测**: Canny边缘 → HoughLinesP → 线段方向聚类 → 端点边缘密度判定方向
- **OCR 关联**: 批注与附近 OCR 文本元素关联(15% 图片尺寸内)
- **LLM 注入**: `format_annotation_context()` 将批注结果格式化为中文提示
- `process_input` 节点在 OCR 提取后自动运行批注检测
- `annotation_result` 字段持久化到 AgentState + 会话文件
### OCR 上下文提示增强 (v3/v4)
- `prompts/modification.md` — 新增 `{ocr_context}` 占位符
- `modify_jrxml` 节点 — 将 OCR 上下文注入 modification prompt
- OCR 上下文包含: 结构化字段、全部文本元素(含坐标)、批注检测结果
## 新增功能 (v5)
### 分层精确生成
- 解决 A4 报表图片 OCR 元素过多(数百个)导致 LLM prompt 超长的问题
- **3 阶段管线**(仅对 `initial_generation` + 有布局 schema 时触发):
1. `generate_skeleton` — 压缩的布局 schema → 骨架 JRXML (`$F{field_N}` 占位)
2. `refine_layout` — 采样坐标(表头+首行数据+末行)→ 像素级位置精调
3. `map_fields` — OCR 字段名 → 替换占位符
- `backend/layout_analyzer.py` — 新增 `extract_layout_schema()`: 列聚类 + 区域分类 + schema_text
- `agent/graph.py` — 新增 `route_after_retrieve()`: 有 schema 走 3 阶段,无 schema 走原有 1-shot
- `prompts/` — 新增 `skeleton_generation.md`, `refine_layout.md`, `field_mapping.md`
- 文本请求和所有其他意图零行为变更
## 已知注意点
- **环境变量优先级**: `backend/llm.py` 使用 `load_dotenv(override=True)` 确保 `.env` 值**始终覆盖**系统环境变量。曾因系统级 `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` 覆盖 `.env` 中的 MiniMax URL,导致 401 认证失败。新增 LLM 相关环境变量时,必须在 `.env` 中显式设置 `ANTHROPIC_*` 变量(而非仅设 `OPENAI_*` fallback),否则残留的系统环境变量会污染请求目标。
- **Anthropic SDK**: 使用原始 `anthropic` 包(非 `langchain-anthropic`),因为需要直连 MiniMax 兼容端点。API Key 优先读 `ANTHROPIC_API_KEY`fallback `OPENAI_API_KEY`。Anthropic SDK 会自动将 key 放入 `x-api-key` header。
- **MiniMax 模型名称**: `MiniMax-M2.7`(不是 `minimax-2.7`),大小写敏感。
- **Streamlit headless**: Windows 下必须设 `STREAMLIT_SERVER_HEADLESS=true` 跳过邮箱采集提示。
- **日志分析**: 通过 `trace_id` 字段可追踪一次请求的全链路。LLM 调用日志在 `logs/llm.log`,包含完整 prompt 和 response(各截断 10000 字符)。
- **验证服务结构检查**: 字段引用一致性 (`$F{field}` vs `<field>` 声明)、SQL SELECT 存在性、pageWidth/pageHeight/name 属性。
- **XSD 校验可选**: 需要 `validation_service/schemas/jasperreport_7_0_6.xsd` 存在。
- **rag 子模块**: 内部有独立的管线脚本(`batch_chunker.py``embed_chunks.py``import_to_chroma.py`),通常不需要在主项目中运行。
- **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**: 默认 5 次。重试耗尽后 `pending_failure_context` 记录失败信息,下次用户输入时自动注入。
- **验证最小内容检查**: 验证服务额外检查至少 1 个 `<band>` + 1 个 `<textField>``<staticText>`,拦截空壳 JRXML。
- **XLSX 支持 (v3)**: 需要 `openpyxl>=3.1.0`(已加入 requirements.txt)。表格按工作表逐行读取,单元格用 `|` 分隔。
- **粘贴功能限制**: 文件以 base64 编码在 sessionStorage 中传递,单文件上限 20MB。大文件建议使用 file_uploader 按钮。
- **torchvision**: `transformers` 库的懒加载需要 `torchvision`,已作为依赖安装。
- **opencv-python-headless**: 批注检测(圈选/箭头)依赖,通过 `pip install -r requirements.txt` 安装。
- **st-multimodal-chatinput**: Streamlit 聊天输入增强组件,替代 `st.chat_input`,支持粘贴/拖拽文件。返回 base64 编码文件内容。
- **xlwt**: 仅在测试中使用(生成 .xls 测试文件)。
- **分层精确生成**: 3 阶段管线仅在 `layout_schema.total_rows > 0` 时触发。文本请求和 `modify_report` 等意图不受影响,走原有 `generate` 节点。中间阶段(骨架/精调)跳过验证,只有最终 mapped 结果进入 `validate`
## 新增功能 (v6)
### 5-Issue Fix — 图片解析 Bug + 前端功能补全
**Fix 1 — 图片后缀 dot 缺失**: `file_parser.py` 后缀规范化(`"jpg"``".jpg"`),`api_server.py` 使用 `Path.suffix` 替代 `rsplit`。所有图片上传之前均因后缀不匹配回退到文本解析器,OCR/布局分析从未实际触发。
**Fix 2 — Vue 前端功能补全**:
- `ProcessSection.vue` 替代 `StreamingMessage.vue` + `NodeProgress.vue`,使用 `<details>`/`<summary>` 原生可折叠区域
- `Sidebar.vue` 新增历史版本下载列表(`jrxml_versions` 索引下载)
- `UnifiedInput.vue` 已集成文件拖拽/粘贴/芯片/移除(v5 已完成)
**Fix 3 — OCR 两层日志**: `agent/nodes.py` 新增 `_log_ocr_layers()``[内容层]` OCR 文本+字段提取,`[位置层]` 布局 schema 列×行+区域分类,`[合并]` 管线选择(3阶段 vs 单阶段)
**Fix 4 — 全过程流式输出+自动折叠**:
- `api_server.py` `node_start` 事件携带 `step_index`
- `chat.ts` 新增 `ProcessSection[]` 模型:per-section stream routing、完成自动折叠、运行中自动展开
- `ProcessSection.vue` 渲染步骤编号/标签/耗时/内容(XML 代码高亮)
**Fix 5 — 消息耗时显示**: `api_server.py` `agent_complete` 事件新增 `total_duration_ms``SummaryCard.vue` 显示总耗时,`chat.ts` 暴露 `lastDurationMs` + `formatDuration()`
## 已安装的 Claude Code 插件/Skills
| 插件 | 来源 | 关键 Skill |
|------|------|-----------|
| `superpowers` | `obra/superpowers-marketplace` | `tdd-workflow`(红-绿-重构)、`verification-loop`(修复验证)、`systematic-debugging`(根因分析) |
| `example-skills` | `anthropics/skills` | `webapp-testing`Playwright E2E 浏览器自动化)、`skill-creator` |
**测试工作流**:需求澄清 → TDD 红-绿-重构 → `webapp-testing` 浏览器验证 → `verification-loop` 确认 → 提交。
**E2E 测试前置条件**Chrome 已安装 (`C:\Program Files\Google\Chrome\Application\chrome.exe`)Playwright MCP Bridge 扩展需手动安装。
## 更新 (v7 — 2026-05-22)
### 会话持久化 & 多轮对话记忆修复
**原子写入** (`backend/session.py`): `save_session` 改用 tempfile + os.replace 原子写入,防止进程崩溃时 JSON 截断导致会话损坏。
**graph.stream 状态修复** (`api_server.py`): LangGraph 的 `graph.stream()`
只产出事件,不修改传入的 `agent_state``_run_graph_sync` 改为手动收集每个节点的
返回 dict 并 `agent_state.update()`,确保 done 事件到达时 agent_state 已是完整状态。
此修复解决了第二次请求时 `current_jrxml` 为空、导致多轮对话"失忆"的问题。
**save_session 调用时机**: 从 `stream_and_save` 末尾移至 `_sse_generator` 中 done 分支
yield `agent_complete` 之前),消除前端 `refreshFromApi()` 的竞态。
### OCR 管线打通
**uploaded_file_path 传递** (`api_server.py`): `_process_files` 返回的 `uploaded_paths`
注入 `agent_state["uploaded_file_path"]`,使 `process_input` 节点的 `OcrExtractor` 字段
精确提取和 `annotation_detector` 批注检测得以触发。此前 `uploaded_file_path` 始终为空,
第二层 OCR 从未执行。
### 前端体验改进
**下载区常驻** (`Sidebar.vue`): 下载区域始终可见,无文件时显示灰色"暂无下载文件",
生成完成后自动出现下载链接。
**侧边栏自动刷新** (`stores/session.ts`, `App.vue`): 新增 `refreshFromApi()` 方法,
`agent_complete` 后自动从 API 重新加载会话状态,下载按钮无需手动刷新即可出现。
**节点进度完整展示** (`api_server.py`): 移除 `node_complete` 事件的 SKIP_NODES 过滤,
所有节点(包括加载会话等内部节点)的 start/complete 事件均正常发送,前端可看到
完整流转(running → done)。
### modification_request 宽松化
原有 `status == "pass"` 条件去除:只要 `current_jrxml` 存在即设置
`user_modification_request`,确保修改意图的请求能携带完整上下文。
## 更新 (v8 — 2026-05-22)
### Prompt 花括号转义修复
**问题**: `skeleton_generation.md``$F{field_1}` 是给 LLM 看的占位字段名指令,
但 Python `.format()``{field_1}` 当作格式化占位符,因缺少对应 kwarg 抛出 `KeyError: 'field_1'`
所有图片上传触发的 `generate_skeleton` 节点均因此崩溃。
**修复**: 3 个 prompt 文件中 6 处 `{field_N}` / `{...}` 转义为 `{{field_N}}` / `{{...}}`
- `prompts/skeleton_generation.md``$F{field_1}``$F{{field_1}}`
- `prompts/field_mapping.md` — 4 处
- `prompts/refine_layout.md` — 1 处
Python 将 `{{` 输出为字面量 `{`LLM 看到的内容不变。
## 更新 (v9 — 2026-05-22)
### 测试基础设施全面补齐
**单元测试** (76 测试):
- `tests/test_session.py` — 27 测试:会话 CRUD、原子写入、唯一 ID、损坏 JSON 跳过
- `tests/test_error_kb.py` — 24 测试:指纹去重、关键词提取(中/英/JRXML)、ErrorKB CRUD、搜索、统计
- `tests/test_agent.py` — 5 个软断言强化为严格断言(`status`/`current_jrxml` 存在性检查)
- 已有测试:`test_ocr_extraction.py`49)、`test_layered_generation.py`19)、`test_validation.py`6)、`test_file_parser_formats.py`4)、`test_annotation_detector.py`7)、`test_e2e_ocr.py`3
**集成测试** (25 测试, `tests/test_api_integration.py`):
- FastAPI TestClient 全覆盖:健康检查、配置、会话 CRUD、文件上传、下载、Chat SSE、安全边界(路径穿越/非法 JSON/大 payload
- Mock LangGraph graph 避免真实 LLM 调用
**E2E 测试** (8 测试, `frontend/tests/e2e/main-flows.spec.ts`):
- Playwright 浏览器自动化:页面加载、侧边栏、会话管理、聊天流程、输入 UX
- 全量 API Mock`page.route`)无需后端运行
- 配置: `frontend/playwright.config.ts`, `npm run test:e2e`
**运行测试**:
```bash
# 全部单元+集成测试
cd D:\Idea Project\jaspersoft && python -m pytest tests/ -v
# 仅 E2E(需要前端 dev server
cd frontend && npx playwright test
```
### Bug 修复: create_session 参数缺失
`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>` 等命名空间前缀闭合标签。
## 更新 (v11 — 2026-05-23)
### Java 渲染管线 + 像素级对比
**目标**: 将 JRXML 渲染为 PNG 图片,与用户上传的原始图片进行 SSIM(结构相似性)像素级对比。
**Java 依赖** (`lib/java/`):
| JAR | 用途 |
|-----|------|
| `jasperreports-6.21.0.jar` (5.8MB) | 核心库,**必须用 6.x**7.x 仅支持 Jackson XML 格式) |
| `commons-digester-2.1.jar` | XML 解析(6.x 使用 Digester 2.x |
| `commons-logging-1.3.5.jar`, `commons-collections4-4.5.0.jar`, `commons-beanutils-1.10.1.jar`, `commons-lang3-3.17.0.jar` | 基础依赖 |
| `itext-2.1.7.jar` | PDF 生成 |
| `jfreechart-1.5.5.jar` | 图表 |
| `ecj-3.38.0.jar` | Eclipse JDT 编译器(报表表达式编译) |
**Java 工具** (`lib/java/`):
| 文件 | 用途 |
|------|------|
| `JrxmlRenderer.java` | JRXML → PNG 渲染器 |
| `JrxmlDebug.java` | 诊断:SAX/JRXmlLoader/compile 三层测试 |
| `JrxmlGen.java` | 参考:程序化构建 JasperDesign → 序列化为 XML |
**Python 渲染封装** (`agent/nodes.py`):
- `_render_jrxml_to_png(jrxml, output_path, scale)` — 调用 Java `JrxmlRenderer`
- `_compute_pixel_similarity(rendered_png, reference_image)` — OpenCV + scikit-image SSIM 对比
**像素对比流程**: validate 节点 XSD 通过 → 有 `uploaded_file_path` → Java 渲染 → SSIM 对比 → SSIM < 0.4 且 diff > 60% → 标记 fail → 注入 correct_jrxml 修正上下文
**手动渲染**: `java -cp ".;jasperreports-6.21.0.jar;..." JrxmlRenderer input.jrxml output.png 2.0`
### 内容保真度 + 修正去重 (v10 补充)
- `_check_ocr_fidelity(jrxml, state)` — OCR 字段名/元素数/列数三重检查
- `correct_jrxml` 去重检测:输入输出相同 → `retry_count += 2`
- `prompts/correction.md` — 一次只修复第1个错误 + 输出不可与输入相同 + 命名空间严格指定
- `prompts/skeleton_generation.md`, `prompts/modification.md` — 明确命名空间约束
### consult_answer 前端显示修复
- `api_server.py``agent_complete` SSE 事件新增 `consult_answer` 字段
## 更新 (v12 — 2026-05-23)
### 多租户知识库系统
**核心架构**:用户自行维护多套知识库,每套 KB 拥有独立的文件存储、JSON 元数据和 ChromaDB 向量集合。会话可绑定不同 KB,LLM 基于 KB 中的字段定义和 JRXML 模板生成报表。
**存储架构**
```
kb_data/
├── users.json # 用户注册表
└── {user_id}/
├── profile.json
└── {kb_id}/
├── meta.json # KB 元数据 + 字段定义 + 模板索引
├── raw/ # 原始上传文件
├── chunks.json # RAG chunks(含 JRXML 模板完整文本)
└── chroma/ # KB 专属 ChromaDB
```
**新增后端模块**
| 文件 | 职责 |
|------|------|
| `backend/kb_manager.py` | 用户+KB CRUDcreate_user/list_users/create_kb/list_kbs/get_kb/delete_kb/update_kb_meta/get_kb_raw_dir/get_kb_chroma_path。原子 JSON 写入(tempfile + os.replace |
| `backend/kb_parser.py` | KB 解析管道:`parse_jrxml_fields()` XML 提取参数/字段/查询 → `process_file_for_kb()` 处理多种格式(jrxml/zip/tar/pdf/docx/xlsx/md`chunk_file_results()` 切割 → `build_kb_from_files()` 全管线(parse→chunk→embed→update meta |
| `backend/kb_searcher.py` | `KBChromaSearcher` 类:per-KB ChromaDB 懒连接。`search()` 语义搜索、`search_templates()` 仅搜索 JRXML 模板 chunk、`add_chunks()` 向量写入。全局 searcher 缓存 `_searchers: dict` |
| `backend/field_matcher.py` | OCR↔KB 字段匹配:1) Embedding 粗筛(余弦相似度 top-3)2) LLM 精确确认。返回 `{"工单号": "billNo", ...}` 映射 |
| `agent/datasource.py` | 数据源模式:`resolve_datasource_mode()` 检测用户意图 → `"parameter"`(默认 $P{xxx})或 `"jdbc"`(SQL 直连)。未配置 DB 时生成反问消息 |
| `scripts/init_default_kb.py` | 默认 KB 初始化:创建默认用户 → 解析 `rag/jrxml_source/` 下的 17 个 JRXML + 16 个 MD → chunk + embed → ChromaDB |
**新增 API 端点**api_server.py):
```
POST /api/users # 创建用户
GET /api/users # 用户列表
GET /api/users/{user_id} # 用户详情
DELETE /api/users/{user_id} # 删除用户
GET /api/users/{user_id}/kbs # KB 列表
POST /api/users/{user_id}/kbs # 创建 KB
GET /api/kbs/{kb_id} # KB 详情
DELETE /api/kbs/{kb_id} # 删除 KB
POST /api/kbs/{kb_id}/upload # 上传文件到 KB
POST /api/kbs/{kb_id}/build # 构建 KBchunk→embed
GET /api/kbs/{kb_id}/status # KB 状态
GET /api/kbs/{kb_id}/fields # KB 字段+模板列表
GET /api/kbs/{kb_id}/search?q=&type= # KB 语义搜索
PUT /api/sessions/{session_id}/kb # 绑定会话-KB
GET /api/sessions/{session_id}/kb # 获取会话绑定的 KB
```
**三条模板获取路径**
1. **管理页预上传**:用户上传文件到 KB → 解析管道 → chunks + ChromaDB → 对话选择 KB → retrieve 节点从 KB 检索
2. **对话框即时上传**:用户拖入 `.jrxml``_parse_jrxml_file()` → 注入 `agent_state["uploaded_template_jrxml"]` → 生成节点直接使用该模板
3. **口头引用模板**:用户说"根据标准结算单模板" → `_detect_template_intent()` 正则匹配 → `retrieve()` 在 KB 中搜索模板 → 注入 `kb_template_jrxml`
**模板上下文注入**:所有 6 个生成节点(generate/generate_skeleton/refine_layout/map_fields/modify_jrxml/correct_jrxml)通过 `_build_template_context(state)` 获取模板上下文,优先级:聊天上传 > KB 检索 > KB 字段定义。6 个 prompt 模板全部新增 `{template_context}` 占位符。
**前端新增**
| 文件 | 职责 |
|------|------|
| `stores/kb.ts` | Pinia store:用户列表、KB 列表、当前选择、字段/模板缓存、CRUD 操作、会话绑定 |
| `components/KbSelector.vue` | 对话顶部 KB 下拉选择器 + 管理按钮 |
| `components/KbManager.vue` | 模态面板:创建 KB、上传文件(支持 .jrxml/.md/.xlsx/.docx/.pdf/.csv/.zip 等)、构建、删除 |
**API Server 增强**
- `_process_files()` 检测 `.jrxml` 文件 → 提取参数/字段/查询/页面尺寸 → 注入 `uploaded_template_jrxml` + `uploaded_template_params`
- `agent/state.py` 新增 10 个字段:`kb_id`, `kb_fields`, `kb_field_mapping`, `uploaded_template_jrxml`, `uploaded_template_params`, `kb_template_jrxml`, `kb_template_name`, `datasource_mode`, `db_config`
**字段匹配管线**`_match_ocr_to_kb` → 尚未集成到节点):OCR 提取中文字段名 → `match_ocr_to_kb()` 两阶段匹配 → 结果为 `{"工单号": "billNo"}``format_field_mapping_context()` 注入 prompt → LLM 使用 `$P{billNo}` 而非 `$P{工单号}`
## 更新 (v13 — 2026-05-24)
### 3 阶段管道内容丢失修复 — Band 级窗口化 + 程序化字段映射
**问题**`generate_skeleton` 生成 34k 字符骨架 JRXML → `refine_layout` 将完整 34k 发给 LLM → LLM 重新生成简化版(~3k 字符,丢失 91.5%)。`map_fields` 同样存在字段映射时内容丢失问题。
**根因**:LLM 看到完整 JRXML 时倾向于"重新生成"而非"在原基础上修改坐标/字段名"。提示词调控无法可靠解决。
**修复方案**(按用户要求 — 程序化节点控制,不靠 LLM 提示词):
#### `refine_layout`Band 级窗口化 LLM 精调
新增 `agent/jrxml_windower.py` — JRXML 拆解/切分/重组引擎:
| 函数 | 用途 |
|------|------|
| `decompose_jrxml()` | ET 安全解析 → 分离 headerfield 声明/queryString 等,不发给 LLM+ 所有 band |
| `split_band_into_windows()` | 超过 4000 字符的 band 在元素闭合标签处切分为多个窗口 |
| `reassemble_band_windows()` | 合并同一 band 的多个窗口结果 |
| `reassemble_jrxml()` | header + 所有修改后 band + footer → 完整 JRXML |
| `count_elements()` | 正则计数 textField/staticText/field(兼容命名空间前缀) |
| `validate_element_count()` | 校验元素数变化,>10% 回退到前一版本 |
**LLM 每次只看到 ~2-4k 字符片段**,无法"重写整个报表"。header 部分完全不发给 LLM,原样保留。
#### `map_fields`:完全程序化替换(零 LLM 调用)
`_programmatic_map_fields()` — 纯正则替换 `$F{field_N}` → OCR 真实字段名,100% 确定性。
`_sanitize_field_name()` — 非 ASCII 字符(中文/日文)转义为 `_uXXXX_` Unicode 码点格式,确保 JRXML 合法。
#### 新增测试
| 文件 | 用例数 | 覆盖 |
|------|--------|------|
| `tests/test_jrxml_windower.py` | 28 | 拆解/往返重组/窗口切分/元素计数/命名空间/多 section 多 band |
| `tests/test_programmatic_map_fields.py` | 20 | 字段声明替换/引用替换/中文转义/坐标保留/部分映射/空字段跳过 |
完整测试套件(385 项)无回归。
## 更新 (v14 — 2026-05-24)
### max_tokens per-node + 修正循环死锁修复
**问题 A — max_tokens 自限**: `backend/llm.py` 硬编码 `max_tokens=8192`。MiniMax M2.7 的 reasoning token 吃光 8192 输出预算后骨架生成为空(0 个可见字符)。其他节点(correct_jrxml/modify_jrxml)输入 68K+ 字符时输出也被截断。
**问题 B — ns:field 命名空间前缀正则失配**: `_programmatic_map_fields()` 正则 `<field\b` 匹配不到 `<ns0:field name="field_1">`,导致字段声明保持占位符但引用被替换为 OCR 字段名,校验报"used in expressions but not declared"。
**问题 C — 验证服务 502 修正死循环**: 验证服务(port 8001)未启动时,`validate_jrxml()` 返回 502。错误消息被当作 JRXML 校验错误送入 `explain_error → correct_jrxml`LLM 尝试"修复"网络错误产出 HTML/markdown 等垃圾,循环 5 轮直到 retry_count 耗尽。
**问题 D — correct_jrxml 从未写回 current_jrxml**: 修正后的 JRXML 只写入 `conversation_history`,从不更新 `state["current_jrxml"]`,导致每轮 validate 看到同一份原始 JRXML,修正完全无效。这是 5 轮 jrxml_length 始终 4441 不变的根本原因。
**修复方案**:
#### 1. per-node max_tokens`backend/llm.py` + `agent/nodes.py`
- `get_llm(caller, max_tokens=None)` — 新增可选 `max_tokens` 参数,透传到 `_build_raw_llm`
- `MiniMaxLLM.__init__()` — 存储 `self._max_tokens`
- `LLM_MAX_TOKENS` 环境变量覆盖默认 8192
- 5 个生成节点 max_tokens 提升到 32768`generate`, `generate_skeleton`, `refine_layout`, `modify_jrxml`, `correct_jrxml`
- `generate_skeleton` 空响应自动重试(max_tokens=65536
#### 2. ns:field 正则修复(`agent/nodes.py:548`
- `<field\b``<[\w:]*field\b` 兼容 `<ns0:field>`, `<field>` 等所有命名空间前缀
#### 3. 验证服务不可用防护
- `backend/validation.py` — 区分 ConnectError/HTTPStatusError(5xx):返回 `service_unavailable: True`
- `agent/nodes.py:validate` — 透传 `state["service_unavailable"]`
- `agent/graph.py:route_after_validate``service_unavailable` 时直接 `finalize`,不进入修正循环
#### 4. correct_jrxml 输出合法性守卫
- 新增 JRXML 有效性检查:输出不含 `<jasperReport` 且不含 `<?xml` 时,回退到前一版本
- **Bug 修复**: `state["current_jrxml"] = jrxml` 写回修正结果
#### 5. 连续输出提取增强
- `_strip_continuation_wrapper()` — 剥离续写响应中 LLM 重新添加的 markdown 代码块和自然语言前缀
- `_extract_jrxml()` — 逐一检查多个 markdown 代码块,跳过非 JRXML 片段
- `_generate_with_continuation()` — 续写轮次自动应用 `_strip_continuation_wrapper`
#### 新增环境变量
| 变量 | 描述 | 默认值 |
|------|------|--------|
| `LLM_MAX_TOKENS` | 默认 max_tokens(各节点可覆盖) | 8192 |