207 lines
8.4 KiB
Markdown
207 lines
8.4 KiB
Markdown
# jaspersoft 五轮修正失败问题 — 现象文档
|
||
|
||
## 1. 问题概述
|
||
|
||
**触发场景**:用户上传一张车历卡(维修结算单)图片,系统通过 OCR 识别并生成 JRXML 报表模板。
|
||
|
||
**现象**:5 次自动修正循环全部失败,系统提示"[内容保真度不足] 得分 0.00/1.0",最终无法生成可用 JRXML。
|
||
|
||
**测试会话**:`sessions/6d39a91e11c54f02bb70a62d856ea2d4.json`(2026-05-24 15:14)
|
||
|
||
---
|
||
|
||
## 2. 完整流程追踪
|
||
|
||
### 2.1 用户输入
|
||
|
||
- 上传文件:`e1113725c20fc4ec39bc9e4ab0caa6b2.jpg`(车历卡,1357×1920 RGB)
|
||
- 用户输入:空(仅上传文件)
|
||
|
||
### 2.2 系统处理流程
|
||
|
||
```
|
||
上传图片
|
||
→ process_input 节点(OCR 字段提取 + 布局分析)
|
||
→ layout_analyzer(34 行 × 1 列,A4 纵向)
|
||
→ ocr_extractor(4 策略提取)
|
||
→ classify_intent(= initial_generation)
|
||
→ retrieve
|
||
→ route_after_retrieve(有 layout_schema,走 generate_skeleton)
|
||
→ generate_skeleton(生成 ~34k 字符骨架 JRXML)
|
||
→ refine_layout(Band 级窗口化精调)
|
||
→ map_fields(程序化字段替换 $F{field_N} → 真实字段名)
|
||
→ validate(validate 节点)
|
||
→ XSD 验证:✅ 通过
|
||
→ OCR 保真度检查:❌ score=0.41 < 0.5 → 降级为 fail
|
||
→ error_msg = "[内容保真度不足] 得分 0.41/1.0。元素覆盖不足:JRXML 仅有 142 个文本元素,OCR 源有 173 个文本元素(覆盖率 82%)。JRXML 中未声明任何字段,但 OCR 提取了结构化字段数据"
|
||
→ 5 次 correct_jrxml 修正循环均失败
|
||
→ 状态:fail(MAX_RETRY=5 耗尽)
|
||
```
|
||
|
||
### 2.3 最终状态
|
||
|
||
```
|
||
status: fail
|
||
retry_count: 5
|
||
error_msg: "[内容保真度不足] 得分 0.00/1.0。JRXML 仅有 0 个文本元素,OCR 源有 173 个文本元素(覆盖率 0%)"
|
||
```
|
||
|
||
⚠️ **重要矛盾**:`current_jrxml`(24391 字符,142 个 textField)与 `error_msg`("0 个文本元素",score=0.00)存在矛盾。
|
||
|
||
验证服务审计(`validate-service-audit`)指出:"6d39a91e has 0 text elements causing score=0.00"——说明在**触发 fail 的那个时间点**,JRXML 确实只有 0 个文本元素。
|
||
|
||
但 session 文件最终保存的 `current_jrxml` 是 24391 字符版本。`jrxml_versions` 最后一条记录的 `jrxml` 才是触发失败的真实版本。
|
||
|
||
**结论**:`current_jrxml` 在 5 次修正过程中被逐步侵蚀,最终版本是某个接近空壳的状态。`jrxml_versions[-1]` 中的 `jrxml` 才是真正的失败版本。
|
||
|
||
---
|
||
|
||
## 3. 关键数据对比
|
||
|
||
### 3.1 JRXML 实际内容
|
||
|
||
session `6d39a91e` 的 `current_jrxml`:
|
||
- 长度:24391 字符
|
||
- `<textField` 标签数:284(含自闭合标签)
|
||
- 完整 textField 元素:142 个
|
||
- `<staticText>` 元素:0 个
|
||
- `<field>` 声明:63 个
|
||
- pageWidth/pageHeight:595×842(A4)
|
||
- namespace:无 ns0: 前缀(✅ 已消除)
|
||
|
||
### 3.2 OCR 数据
|
||
|
||
layout_schema:
|
||
- total_rows: 34
|
||
- total_columns: 1
|
||
- 总文本元素:173 个
|
||
|
||
ocr_extraction_result:
|
||
- total_elements: 173
|
||
- fields 数组:18 个字段,全部是**发票模板字段**
|
||
- 不含税金额、价税合计、单价、发票代码、发票号码、合计金额、开票日期、总金额、数量、日期、校验码、税率、税额、规格型号、货物名称、购买方名称、金额、销售方名称
|
||
- 正确匹配率:3/18(17%)
|
||
|
||
### 3.3 评分数据
|
||
|
||
`_check_ocr_fidelity` 实际运行结果:
|
||
|
||
```python
|
||
# element_coverage 计算
|
||
textField_count = 142 # 完整元素(非标签数)
|
||
staticText_count = 0
|
||
total_jrxml_elements = 142
|
||
ocr_text_count = 173
|
||
element_coverage = min(142/173, 1.0) = 0.82
|
||
|
||
# field_coverage 计算
|
||
jrxml_fields = {"print_date", "repair_number", ..., "vehicle_plate", ...} # 63 个英文字段
|
||
ocr_field_names = {"发票代码", "发票号码", "合计金额", ...} # 18 个中文字段
|
||
matched = jrxml_fields ∩ ocr_field_names = ∅
|
||
field_coverage = 0/18 = 0.0
|
||
|
||
# score 计算
|
||
score = 0.0 * 0.5 + 0.82 * 0.5 = 0.41
|
||
```
|
||
|
||
**评分公式**(`nodes.py:1251`):
|
||
```python
|
||
score = round(field_coverage * 0.5 + element_coverage * 0.5, 3)
|
||
```
|
||
|
||
**降级条件**(`nodes.py:1294`):
|
||
```python
|
||
if fidelity["score"] < 0.5:
|
||
state["status"] = "fail"
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 已确认的根因
|
||
|
||
### 根因 1 — 评分逻辑设计错误(P0)
|
||
|
||
**问题**:`field_coverage` 将英文字段名和中文字段名做交集比对,在普通生成场景下永远为 0。
|
||
|
||
**原因**:
|
||
1. LLM 生成英文字段名(`print_date`、`vehicle_plate`)是正确的设计选择
|
||
2. OCR 提取器硬套发票模板,提取中文字段名(`发票代码`、`合计金额`)
|
||
3. 两者来自完全不同的命名体系,不可能匹配
|
||
4. `field_coverage=0` 是**预期行为**,而非错误
|
||
|
||
**修复方向**:评分公式改为只依赖 `element_coverage`,`field_coverage` 作为信息提示而非降级条件。
|
||
|
||
### 根因 2 — OCR 字段提取器无文档类型区分(P0)
|
||
|
||
**问题**:`backend/ocr_extractor.py` 对所有单据使用同一套发票字段模板(14 个字段)。
|
||
|
||
**现象**:
|
||
- 车历卡被当作发票处理
|
||
- 手机号 `13516727312` 被 6 个字段复用(发票代码/校验码/价税合计/单价/税率/不含税金额)
|
||
- 字段名错配:发票号码→"服务套餐"、总金额→"钣金"、数量→"零件等级"
|
||
|
||
### 根因 3 — namespace 修复指令是条件触发(P1)
|
||
|
||
**位置**:`prompts/correction.md` 第 11 行
|
||
|
||
**问题**:namespace 修复指令只在错误消息**包含 "namespace" 关键词**时才激活,是条件触发而非无条件指令。
|
||
|
||
**现象**:
|
||
- ecd592 session 所有 5 次修正的实际错误类型是"字段未声明"(`字段 'u53d1_u7968...' 在表达式中使用但未声明`),**不包含 "namespace" 关键词**
|
||
- 因此 namespace 修复指令从未被激活
|
||
- ns0: 前缀在前两次修正中持续存在,直到第 3 次才被 LLM 自发消除
|
||
- 最终 `current_jrxml`(ecd592)仍有 `<ns0:jasperReport>`
|
||
|
||
**修复方向**:
|
||
- `prompts/correction.md`:改为无条件指令(检查 JRXML 是否包含 `ns0:`,而非依赖错误类型)
|
||
- `prompts/initial_generation.md` + `skeleton_generation.md`:添加删除 ns0: 前缀的无条件指令
|
||
|
||
### 根因 4 — 正则 `\w+` 不支持中文(低优先级)
|
||
|
||
**位置**:`nodes.py:1211`
|
||
```python
|
||
jrxml_fields = set(re.findall(r'<field name="(\w+)"', jrxml))
|
||
```
|
||
|
||
**问题**:`\w` 匹配 `[a-zA-Z0-9_]`,不匹配中文。如果 JRXML 使用中文字段名,正则返回 0 个匹配。
|
||
|
||
---
|
||
|
||
## 5. 验证服务的角色
|
||
|
||
**文件**:`validation_service/main.py`
|
||
|
||
- XSD 验证:通过(✅)
|
||
- 结构检查:字段声明一致性、SQL SELECT 存在性、pageWidth/pageHeight
|
||
- **结论**:XSD 验证通过,`correct_jrxml` 的 ns0: 消除也生效——真正导致 fail 的是 OCR 保真度评分
|
||
|
||
---
|
||
|
||
## 6. 现象描述的矛盾点(需进一步排查)
|
||
|
||
session `6d39a91e` 中存在数字矛盾:
|
||
|
||
| 指标 | session 中的值 | 矛盾说明 |
|
||
|---|---|---|
|
||
| 最终 `current_jrxml` | 24391 字符,142 个 textField | 这是最后一次修正后保存的最终版本 |
|
||
| `jrxml_versions[-1].jrxml` | 触发 fail 的真实版本 | 审计团队确认"6d39a91e has 0 text elements causing score=0.00" |
|
||
| `error_msg` score | "0.00/1.0" | 对应 `jrxml_versions[-1]`,而非 `current_jrxml` |
|
||
|
||
**核心矛盾已解决**:`current_jrxml`(24391 字符)是**最终状态**(修正耗尽后最后一次保存的版本),而触发 5 次 fail 降级的是 `jrxml_versions` 中各版本的 JRXML——这些版本在修正循环中被逐步侵蚀,最终版本 `jrxml_versions[-1]` 只有 0 个文本元素(score=0.00)。
|
||
|
||
`fidelity-check-audit` 用 Python 分析的是最终保存的 `current_jrxml`(score=0.5),而 `validate-service-audit` 分析的是 `jrxml_versions[-1]`(score=0.00)。**两者分析的不是同一个时间点的 JRXML**。
|
||
|
||
---
|
||
|
||
## 7. 相关文件清单
|
||
|
||
| 文件 | 职责 | 备注 |
|
||
|---|---|---|
|
||
| `agent/nodes.py:1171-1257` | `_check_ocr_fidelity` | 评分逻辑(有 bug) |
|
||
| `agent/nodes.py:1260-1350` | `validate` 节点 | 调用保真度检查 |
|
||
| `agent/nodes.py:1382-1461` | `correct_jrxml` | 修正循环 |
|
||
| `backend/ocr_extractor.py` | OCR 字段提取 | 无文档类型区分 |
|
||
| `prompts/correction.md` | 修正 prompt | namespace 触发受限 |
|
||
| `validation_service/main.py` | 验证服务 | XSD 通过 |
|
||
| `sessions/6d39a91e11c54f02bb70a62d856ea2d4.json` | 测试会话 | 主测试数据 |
|
||
| `sessions/ecd5921838004ab3bc4a1ef6ebd673d1.json` | 历史会话 | namespace 问题参考 | |