fix: band-level windowed refine_layout + programmatic map_fields to prevent 91.5% content loss
Root cause: LLM receiving full 34k-char JRXML would regenerate from scratch
instead of modifying coordinates in-place, shrinking output to ~3k chars.
Solution (programmatic node control, not prompt engineering):
- New agent/jrxml_windower.py: decompose JRXML into header (never sent to
LLM) + individual bands. Split bands >4000 chars at element boundaries.
Reassemble with element count validation (>10% change = rollback).
- Rewrite refine_layout: per-band windowed LLM processing (~2-4k chars
each). LLM cannot "reimagine" the entire report.
- Rewrite map_fields: 100% programmatic regex $F{field_N} -> real name
replacement. Zero LLM calls, zero content loss.
- _sanitize_field_name: non-ASCII chars escaped to _uXXXX_ format for
valid JRXML identifiers.
- Tests: 48 new unit tests (windower 28 + map_fields 20). All passing.
Full suite 385 tests, zero regressions.
This commit is contained in:
@@ -44,12 +44,15 @@ cd frontend && npm run dev
|
||||
│ ├── 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 统一输入框(文本+文件拖拽/粘贴/芯片)
|
||||
│ │ └── SummaryCard.vue 结果摘要卡片(含耗时)
|
||||
│ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴/芯片,含 .jrxml)
|
||||
│ │ ├── SummaryCard.vue 结果摘要卡片(含耗时)
|
||||
│ │ ├── KbSelector.vue KB 下拉选择器(对话中切换知识库)
|
||||
│ │ └── KbManager.vue KB 管理面板(创建/上传/构建/删除)
|
||||
│ └── utils/format.ts 工具函数
|
||||
│
|
||||
▼ HTTP + SSE (Server-Sent Events)
|
||||
@@ -87,9 +90,16 @@ validation_service/ (FastAPI, 端口 8001) — 不变
|
||||
| `backend/annotation_detector.py` | 批注检测: 圈选(cv2 HoughCircles) + 箭头(HoughLinesP聚类) + OCR关联 + LLM格式化 | 中 |
|
||||
| `backend/embeddings.py` | 嵌入模型工厂 (HuggingFace/OpenAI) | 低 |
|
||||
| `backend/validation.py` | 验证服务 HTTP 客户端 | 低 |
|
||||
| `backend/session.py` | 会话 JSON 文件 CRUD | 低 |
|
||||
| `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_kb.py` | 知识库初始化/模型下载 | 低 |
|
||||
| `scripts/init_kb.py` | 旧 RAG 知识库初始化/模型下载 | 低 |
|
||||
| `scripts/init_default_kb.py` | 多租户默认 KB 初始化(默认用户 + 预置 KB) | 低 |
|
||||
| `app.py` | ~~旧 Streamlit UI~~(已由 api_server.py + frontend/ 替代) | 废弃 |
|
||||
|
||||
## 关键约定
|
||||
@@ -427,3 +437,115 @@ cd frontend && npx playwright test
|
||||
### 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 CRUD:create_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 # 构建 KB(chunk→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 安全解析 → 分离 header(field 声明/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 项)无回归。
|
||||
|
||||
Reference in New Issue
Block a user