8.4 KiB
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 实际运行结果:
# 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):
score = round(field_coverage * 0.5 + element_coverage * 0.5, 3)
降级条件(nodes.py:1294):
if fidelity["score"] < 0.5:
state["status"] = "fail"
4. 已确认的根因
根因 1 — 评分逻辑设计错误(P0)
问题:field_coverage 将英文字段名和中文字段名做交集比对,在普通生成场景下永远为 0。
原因:
- LLM 生成英文字段名(
print_date、vehicle_plate)是正确的设计选择 - OCR 提取器硬套发票模板,提取中文字段名(
发票代码、合计金额) - 两者来自完全不同的命名体系,不可能匹配
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
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 问题参考 |