Files
agent_jrxml/docs/conversation-scenarios.md
T
panda e362f530ea chore: remove 13 stale files and clean up project structure
Removed:
- app.py (deprecated Streamlit UI, replaced by api_server.py + frontend/)
- start_agent_jrxml.py (old launcher, replaced by start.py)
- test_reorder.py, e2e_test.py (ad-hoc/outdated test scripts)
- ocr_raw_positions.json (debug output)
- ARCHITECTURE.md, CODE_GUIDE.md, RAG_INTEGRATION.md, ROADMAP.md (superseded by CLAUDE.md)
- EVALUATION_REPORT.md (auto-generated)
- scripts/init_kb.py (replaced by init_default_kb.py)
- validation_service/validate.bat (redundant, start.py covers it)
- sessions/*.json (34 test session files, already gitignored)

Updated:
- CLAUDE.md: removed stale file entries from key mapping table
- README.md: updated init script reference and removed validate.bat
- .gitignore: removed EVALUATION_REPORT.md entry
2026-05-24 09:07:15 +08:00

587 lines
26 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.
# 对话场景遍历文档
> 从 `agent/graph.py` 状态图递归遍历生成,覆盖所有用户意图 → 节点路径 → 退出条件。
> 最后更新: 2026-05-24
---
## 状态图总览
```
┌──────────────────────────────────────────────────┐
│ 修正循环 (最多 MAX_RETRY=5 次) │
│ ┌─────────┐ ┌──────────────┐ ┌────────┐ │
│ │ validate │───→│ explain_error│───→│correct │ │
│ └────┬─────┘ └──────────────┘ │_jrxml │ │
│ │ pass └───┬────┘ │
│ ▼ │ │
│ ┌─────────┐ retry<5 │
│ │finalize │◄────────────────────────────────┘ │
│ └─────────┘ retry>=5 │
└──────────────────────────────────────────────────┘
load_session ──→ process_input ──→ manage_context ──→ save_state_snapshot
classify_intent
┌────────────┬──────────┬────────┬───────────┼───────────┬──────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
retrieve modify_jrxml save_ handle_ handle_ handle_ (兜底)
(新建报表) (修改报表) session consult undo reset
│ │ (预览) (咨询) (撤销) (重置)
┌────────┴────┐ │ │ │ │ │
▼ ▼ │ │ │ │ │
generate generate_ │ │ │ │ │
(1-shot) skeleton │ │ │ │ │
│ │ │ │ │ │ │
│ refine_ │ │ │ │ │
│ layout │ │ │ │ │
│ │ │ │ │ │ │
│ map_fields │ │ │ │ │
│ │ │ │ │ │ │
└──────┬──────┘ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
save_session ◄─────┴──────────┘ finalize ◄─── finalize ◄── finalize
│ ▲
│ (预览/导出跳过验证) │
├───────────────────────────────────────┘
│ (其他意图走验证)
validate ──→ explain_error ──→ correct_jrxml ──→ validate (循环)
│ pass │ retry>=MAX
▼ ▼
finalize ────────────────────────────────→ finalize
```
---
## 节点详细清单
每个节点标注了 **代码行号** (`agent/nodes.py``agent/graph.py`)、**前驱节点** (predecessors)、**后继节点** (successors)。
### 1. load_session — 加载会话
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:77` |
| 前驱 | (入口节点, graph entry_point) |
| 后继 | `process_input` (固定边 graph.py:198) |
| 功能 | 从 `sessions/{session_id}.json` 磁盘加载状态,注入 agent_state。不从磁盘覆盖 `session_id`。 |
| LLM | 否 |
### 2. process_input — 处理用户输入
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:98` |
| 前驱 | `load_session` (graph.py:198) |
| 后继 | `manage_context` (graph.py:199) |
| 功能 | 文件解析(PDF/DOCX/XLSX/图片/文本)→ OCR 字段提取 → 批注检测 → 模板 JRXML 解析。注入 `ocr_extraction_result``layout_schema``ocr_elements``uploaded_template_jrxml`。 |
| LLM | 否(OCR 用 PaddleOCR/EasyOCR |
### 3. manage_context — 上下文管理
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:143` |
| 前驱 | `process_input` (graph.py:199) |
| 后继 | `save_state_snapshot` (graph.py:200) |
| 功能 | Token 计数 → 对话压缩(超限时 LLM 压缩为摘要)→ `compressed_history`。 |
| LLM | 是(压缩时调 LLM |
### 4. save_state_snapshot — 状态快照
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:191` |
| 前驱 | `manage_context` (graph.py:200) |
| 后继 | `classify_intent` (graph.py:201) |
| 功能 | 深拷贝当前状态 → 推入 `history_states` 列表。最多保留 5 个快照。撤销时恢复到最新快照。 |
| LLM | 否 |
### 5. classify_intent — 意图分类
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:200` |
| 前驱 | `save_state_snapshot` (graph.py:201) |
| 后继 | 6 路条件分发 (graph.py:204-215) |
| 功能 | LLM 分类用户意图为 8 种之一。prompt: `prompts/intent_classify.md`。 |
| LLM | 是 |
| 路由函数 | `route_by_intent` (graph.py:67) |
**分类逻辑与路由目标**:
| 意图值 | 路由目标 | 说明 |
|--------|---------|------|
| `initial_generation` | → `retrieve` | 新建报表 |
| `modify_report` | → `modify_jrxml` | 修改现有报表 |
| `preview_report` | → `save_session` | 预览(跳过生成) |
| `export_pdf` | → `save_session` | 导出 PDF(跳过生成) |
| `export_jrxml` | → `save_session` | 下载 JRXML(跳过生成) |
| `consult_question` | → `handle_consult` | 咨询问答 |
| `undo_modification` | → `handle_undo` | 撤销 |
| `reset_session` | → `handle_reset` | 重置 |
| 未知/兜底 | 有 `current_jrxml``modify_jrxml`; 无 → `retrieve` | |
### 6. retrieve — RAG/知识库检索
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:442` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=initial_generation) |
| 后继 | 条件分发: `generate_skeleton``generate` (graph.py:218-224) |
| 功能 | ① ErrorKB 检索历史修正案例 → ② KB 模板检索 → ③ KB 字段定义检索。注入 `retrieved_context``kb_template_jrxml``kb_fields`。 |
| LLM | 否(向量搜索 + 字段匹配) |
| 路由函数 | `route_after_retrieve` (graph.py:94) |
**路由逻辑** (`route_after_retrieve`, graph.py:94-99):
- `layout_schema.total_rows > 0``generate_skeleton` (3 阶段)
- 否则 → `generate` (1-shot)
### 7. generate — 1-shot 生成
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:578` |
| 前驱 | `retrieve` (graph.py:218-224, 无 layout_schema 时) |
| 后继 | `save_session` (graph.py:227-231) |
| 功能 | LLM 一次生成完整 JRXML。注入 OCR 上下文 + 模板上下文。流式输出。截断时续写(最多 3 轮)。 |
| LLM | 是 |
| Prompt | `prompts/initial_generation.md` |
### 8. generate_skeleton — 骨架生成(3 阶段-1
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:657` |
| 前驱 | `retrieve` (graph.py:218-224, 有 layout_schema 时) |
| 后继 | `refine_layout` (固定边 graph.py:233) |
| 功能 | 压缩布局 schema → LLM 生成骨架 JRXML。字段用 `$F{field_N}` 占位。流式输出 + 续写。 |
| LLM | 是 |
| Prompt | `prompts/skeleton_generation.md` |
### 9. refine_layout — 坐标精调(3 阶段-2
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:879` |
| 前驱 | `generate_skeleton` (graph.py:233) |
| 后继 | `map_fields` (固定边 graph.py:234) |
| 功能 | ① `decompose_jrxml()` 拆解为 header + bands → ② 每个 band 窗口化(>4000 字符切分)→ ③ 逐窗口 LLM 精调坐标 → ④ `reassemble_jrxml()` 重组 → ⑤ `validate_element_count()` 校验(>10% 回退)。header 完全不发给 LLM。 |
| LLM | 是(N 次,N = band 窗口数) |
| Prompt | `prompts/refine_layout.md` |
### 10. map_fields — 字段映射(3 阶段-3
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:978` |
| 前驱 | `refine_layout` (graph.py:234) |
| 后继 | `save_session` (graph.py:235-239) |
| 功能 | 纯程序化正则替换 `$F{field_N}` → OCR 真实字段名。`_sanitize_field_name()` 净化非 ASCII 字符。零 LLM 调用。 |
| LLM | 否 |
### 11. modify_jrxml — 修改报表
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1022` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=modify_report) |
| 后继 | `save_session` (graph.py:242-246) |
| 功能 | 基于现有 JRXML + 用户修改描述 + OCR 上下文 + 模板上下文 → LLM 修改。流式输出 + 续写。空响应守卫。 |
| LLM | 是 |
| Prompt | `prompts/modification.md` |
### 12. handle_consult — 咨询解答
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:261` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=consult_question) |
| 后继 | `finalize` (固定边 graph.py:280) |
| 功能 | LLM 回答 JasperReports 相关知识问题。回答写入 `conversation_history`。 |
| LLM | 是 |
| Prompt | `prompts/consult.md` |
### 13. handle_undo — 撤销
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:281` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=undo_modification) |
| 后继 | `save_session` (graph.py:249-253) |
| 功能 | 从 `history_states` 弹出最近快照,恢复 `current_jrxml``conversation_history``status`。无快照时提示"无可撤销状态"。 |
| LLM | 否 |
### 14. handle_reset — 重置
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:309` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=reset_session) |
| 后继 | `finalize` (固定边 graph.py:281) |
| 功能 | 清空所有状态到 `create_initial_state()` 默认值(保留 `session_id``session_name`)。 |
| LLM | 否 |
### 15. save_session — 保存会话
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:325` |
| 前驱 | `generate``map_fields``modify_jrxml``handle_undo``classify_intent`(预览/导出) |
| 后继 | 条件分发: `validate``finalize` (graph.py:256-260) |
| 功能 | 原子持久化会话 JSON (`tempfile + os.replace`)。序列化 `agent_state``sessions/{session_id}.json`。 |
| LLM | 否 |
| 路由函数 | `route_after_save` (graph.py:118) |
**路由逻辑** (`route_after_save`, graph.py:118-123):
- `intent in (preview_report, export_pdf, export_jrxml)``finalize` (跳过验证)
- 其他 → `validate`
### 16. validate — 验证
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1235` |
| 前驱 | `save_session` (graph.py:256-260)、`correct_jrxml` (graph.py:273-277) |
| 后继 | 条件分发: `finalize``explain_error` (graph.py:263-267) |
| 功能 | ① 结构检查(字段引用一致性/SQL 存在/pageWidth/pageHeight/name)→ ② XSD 校验(可选)→ ③ 像素对比(有上传图片时 Java 渲染 JRXML→PNG + OpenCV SSIM)。 |
| LLM | 否 |
| 路由函数 | `route_after_validate` (graph.py:127) |
**路由逻辑** (`route_after_validate`, graph.py:127-131):
- `status == "pass"``finalize`
- `status == "fail"``explain_error`
### 17. explain_error — 错误解释
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1310` |
| 前驱 | `validate` (graph.py:263-267, status=fail) |
| 后继 | `correct_jrxml` (graph.py:268-272) |
| 功能 | LLM 将编译错误翻译为自然语言解释。注入 `natural_explanation`。 |
| LLM | 是 |
| Prompt | `prompts/explain_error.md` |
### 18. correct_jrxml — 自动修正
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1355` |
| 前驱 | `explain_error` (graph.py:268-272) |
| 后继 | 条件分发: `validate``finalize` (graph.py:273-277) |
| 功能 | 基于错误解释 + OCR 上下文 + 模板上下文 → LLM 修正 JRXML。注入 `last_error_case`。去重检测(输入输出相同则 `retry_count+=2`)。 |
| LLM | 是 |
| Prompt | `prompts/correction.md` |
| 路由函数 | `route_after_correct` (graph.py:139) |
**路由逻辑** (`route_after_correct`, graph.py:139-143):
- `retry_count >= MAX_RETRY` (默认5) → `finalize` (放弃修正)
- `retry_count < MAX_RETRY``validate` (重新验证)
### 19. finalize — 最终处理
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1452` |
| 前驱 | `validate`(pass)、`correct_jrxml`(retry>=MAX)、`handle_consult``handle_reset``save_session`(预览/导出) |
| 后继 | `END` (graph.py:284) |
| 功能 | 记录 `jrxml_versions` 版本历史。验证通过时设置 `final_jrxml`。失败时记录 `pending_failure_context` 供下次输入自动注入。 |
| LLM | 否 |
---
## 路由函数索引
| # | 路由函数 | 代码位置 | 条件 | 分支 |
|---|---------|---------|------|------|
| R1 | `route_by_intent` | `graph.py:67` | `state.intent` | 6 路: retrieve / modify_jrxml / save_session / handle_consult / handle_undo / handle_reset |
| R2 | `route_after_retrieve` | `graph.py:94` | `layout_schema.total_rows > 0` | 2 路: generate_skeleton / generate |
| R3 | `route_after_generate` | `graph.py:103` | 无条件 | save_session |
| R4 | `route_after_modify` | `graph.py:108` | 无条件 | save_session |
| R5 | `route_after_undo` | `graph.py:113` | 无条件 | save_session |
| R6 | `route_after_save` | `graph.py:118` | `intent in (preview, export)` | 2 路: finalize / validate |
| R7 | `route_after_validate` | `graph.py:127` | `status == "pass"` | 2 路: finalize / explain_error |
| R8 | `route_after_explain` | `graph.py:133` | 无条件 | correct_jrxml |
| R9 | `route_after_correct` | `graph.py:139` | `retry_count >= MAX_RETRY` | 2 路: finalize / validate |
---
## 完整对话场景
### 场景 1: 新建报表 — 1-shot(无布局 schema
**触发**: `intent=initial_generation` + 无图片/无结构化布局
**用户示例**: "帮我生成一个销售报表"、"生成一个包含客户名和金额的表格"
```
load_session nodes.py:77
→ process_input nodes.py:98
→ manage_context nodes.py:143
→ save_state_snapshot nodes.py:191
→ classify_intent nodes.py:200 意图=initial_generation
└─ R1: route_by_intent graph.py:67 → retrieve
→ retrieve nodes.py:442
└─ R2: route_after_retrieve graph.py:94 layout_schema 为空 → generate
→ generate nodes.py:578 LLM 1-shot 生成完整 JRXML
└─ R3: route_after_generate graph.py:103 → save_session
→ save_session nodes.py:325 持久化到磁盘
└─ R6: route_after_save graph.py:118 intent=initial_generation → validate
→ validate nodes.py:1235 结构检查 + XSD + 像素对比
└─ R7: route_after_validate graph.py:127
├─ status=pass → finalize nodes.py:1452 → END ✓
└─ status=fail → explain_error nodes.py:1310
└─ R8 → correct_jrxml nodes.py:1355
└─ R9:
retry<5 → validate (循环)
retry>=5 → finalize → END ✗
```
**LLM 调用**: `classify_intent` + `generate` + 最多 5× (`explain_error` + `correct_jrxml`)
**退出好结局**: `final_jrxml` 有值, `status=pass`
**退出坏结局**: `pending_failure_context` 有值, `retry_count=5`
---
### 场景 2: 新建报表 — 3 阶段分层生成(有布局 schema)
**触发**: `intent=initial_generation` + 上传图片 + OCR 提取到 `layout_schema.total_rows > 0`
**用户示例**: 上传销售单图片 → "根据这个模板生成报表"
```
load_session nodes.py:77
→ process_input nodes.py:98 OCR提取 + 布局分析
→ manage_context nodes.py:143
→ save_state_snapshot nodes.py:191
→ classify_intent nodes.py:200 意图=initial_generation
└─ R1: route_by_intent graph.py:67 → retrieve
→ retrieve nodes.py:442 KB检索模板+字段
└─ R2: route_after_retrieve graph.py:94 layout_schema.total_rows>0 → generate_skeleton
→ generate_skeleton nodes.py:657 阶段1: 骨架JRXML ($F{field_N}占位)
→ refine_layout nodes.py:879 阶段2: Band级窗口化坐标精调
→ map_fields nodes.py:978 阶段3: 程序化字段映射
└─ R3: route_after_generate graph.py:103 → save_session
→ save_session nodes.py:325
└─ R6: route_after_save graph.py:118 → validate
→ validate nodes.py:1235
└─ R7 同场景1的验证循环
```
**内容保护**:
- `refine_layout`: header (field/param/queryString) 完全不发给 LLM
- `refine_layout`: 每窗口 ~4000 字符, LLM 无法重写整个报表
- `map_fields`: 纯正则替换, 零 LLM, 100% 确定性
- `validate_element_count()`: 每阶段后校验, >10% 变化回退
**LLM 调用**: `classify_intent` + `generate_skeleton` + N×`refine_layout`(N=band窗口数) + 可能的修正循环
---
### 场景 3: 修改已有报表
**触发**: `intent=modify_report`(已有 `current_jrxml`
**用户示例**: "把标题字体改大"、"在底部加合计行"、"删除第三列"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=modify_report
└─ R1: route_by_intent graph.py:67 → modify_jrxml
→ modify_jrxml nodes.py:1022 LLM修改现有JRXML
└─ R4: route_after_modify graph.py:108 → save_session
→ save_session nodes.py:325
└─ R6: route_after_save graph.py:118 → validate
→ (同场景1的验证循环)
```
**特殊逻辑**: `correct_jrxml` 去重检测: 输入输出相同 → `retry_count += 2`
---
### 场景 4: 预览 / 导出(跳过验证)
**触发**: `intent in (preview_report, export_pdf, export_jrxml)`
**用户示例**: "预览报表"、"导出 PDF"、"下载 JRXML"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=preview/export
└─ R1: route_by_intent graph.py:67 → save_session
→ save_session nodes.py:325
└─ R6: route_after_save graph.py:118 intent=preview/export → finalize
→ finalize nodes.py:1452 → END ✓
```
**LLM 调用**: 仅 `classify_intent` (1次)
**跳过**: generate / modify_jrxml / validate / correct_jrxml
---
### 场景 5: 咨询问答
**触发**: `intent=consult_question`
**用户示例**: "JasperReports 里 $F 和 $P 有什么区别?"、"怎么设置页脚?"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=consult_question
└─ R1: route_by_intent graph.py:67 → handle_consult
→ handle_consult nodes.py:261 LLM回答
→ finalize nodes.py:1452 → END ✓
```
**LLM 调用**: `classify_intent` + `handle_consult` (2次)
---
### 场景 6: 撤销
**触发**: `intent=undo_modification`
**用户示例**: "撤销"、"回退"、"恢复到修改前"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=undo_modification
└─ R1: route_by_intent graph.py:67 → handle_undo
→ handle_undo nodes.py:281 恢复history_states快照
└─ R5: route_after_undo graph.py:113 → save_session
→ save_session nodes.py:325
└─ R6 → validate → (验证循环)
```
**LLM 调用**: 仅 `classify_intent` (1次)
**特殊**: 无快照时提示"无可撤销状态",不改变当前状态
---
### 场景 7: 重置
**触发**: `intent=reset_session`
**用户示例**: "重置"、"重新开始"、"清空对话"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=reset_session
└─ R1: route_by_intent graph.py:67 → handle_reset
→ handle_reset nodes.py:309 清空到初始状态
→ finalize nodes.py:1452 → END ✓
```
**LLM 调用**: 仅 `classify_intent` (1次)
---
### 场景 8: 兜底路由(未知意图)
**触发**: LLM 分类返回非标准意图
```
load_session → ... → classify_intent → [未知意图]
└─ R1 fallback (graph.py:87-90):
├─ state有current_jrxml → modify_jrxml (走修改路径, →场景3)
└─ state无current_jrxml → retrieve (走生成路径, →场景1/2)
```
---
## AgentState 字段速查
| 字段 | 类型 | 写节点 | 读节点 |
|------|------|--------|--------|
| `intent` | `str` | classify_intent | R1 route_by_intent, R6 route_after_save |
| `current_jrxml` | `str` | generate, generate_skeleton, refine_layout, map_fields, modify_jrxml, correct_jrxml, handle_undo | validate, save_session, finalize |
| `user_input` | `str` | process_input | classify_intent, manage_context |
| `user_modification_request` | `str` | process_input | modify_jrxml |
| `conversation_history` | `list` | process_input, finalize, handle_consult | manage_context, classify_intent, modify_jrxml |
| `full_conversation_history` | `list` | process_input | manage_context |
| `compressed_history` | `str` | manage_context | modify_jrxml, handle_consult |
| `retry_count` | `int` | correct_jrxml, validate | R7 route_after_correct |
| `status` | `str` | validate | R7 route_after_validate, finalize |
| `error_msg` | `str` | validate | explain_error, finalize |
| `natural_explanation` | `str` | explain_error | correct_jrxml |
| `final_jrxml` | `str` | finalize | (用户下载) |
| `jrxml_versions` | `list` | finalize | (前端展示) |
| `last_error_case` | `dict` | correct_jrxml | retrieve |
| `pending_failure_context` | `dict` | finalize | process_input (下次) |
| `layout_schema` | `dict` | process_input | R2 route_after_retrieve, generate_skeleton |
| `ocr_elements` | `list` | process_input | refine_layout, generate_skeleton |
| `ocr_extraction_result` | `dict` | process_input | map_fields, modify_jrxml, correct_jrxml |
| `history_states` | `list` | save_state_snapshot | handle_undo |
| `kb_id` | `str` | process_input | retrieve |
| `kb_fields` | `list` | retrieve | generate_skeleton |
| `uploaded_template_jrxml` | `str` | process_input | generate, generate_skeleton, modify_jrxml, correct_jrxml |
---
## LLM 调用统计
| 场景 | classify | 生成节点 | 窗口数 | 修正循环 | 总计(最小~最大) |
|------|----------|---------|--------|---------|----------------|
| 1-shot 生成 | 1 | generate=1 | - | 0~5×2 | 2 ~ 12 |
| 3 阶段生成 | 1 | skeleton+refine×N | N | 0~5×2 | 2+N ~ 12+N |
| 修改报表 | 1 | modify=1 | - | 0~5×2 | 2 ~ 12 |
| 预览/导出 | 1 | - | - | - | 1 |
| 咨询 | 1 | consult=1 | - | - | 2 |
| 撤销 | 1 | - | - | - | 1 |
| 重置 | 1 | - | - | - | 1 |
> N = band 窗口数。`销售单.jrxml` (73k 字符) 拆解后 N≈17。
---
## 修正循环流程
```
validate ──fail──→ explain_error ──→ correct_jrxml
▲ │
│ retry_count < MAX_RETRY(5) │
└──────────────────────────────────────┘
│ retry_count >= 5
finalize (放弃, 记录pending_failure_context)
```
**修正轮次推进**:
1. `validate` 失败 → `status="fail"`, `error_msg` 有值
2. `explain_error` → LLM 翻译错误 → `natural_explanation` 有值
3. `correct_jrxml` → LLM 修正 → `retry_count += 1`。去重检测:输入输出相同 → `retry_count += 2`
4. `route_after_correct` → retry<5 → 回到 `validate`; retry>=5 → `finalize`
**失败上下文** (`pending_failure_context`): 重试耗尽后记录 `{error_msg, bad_jrxml, retry_count, ts}`,下次用户消息时 `process_input` 自动注入到 prompt。
---
## 边定义索引(graph.py 全部边)
| 类型 | 源节点 | 目标节点 | 位置 |
|------|--------|---------|------|
| 固定边 | load_session | process_input | line 198 |
| 固定边 | process_input | manage_context | line 199 |
| 固定边 | manage_context | save_state_snapshot | line 200 |
| 固定边 | save_state_snapshot | classify_intent | line 201 |
| 条件边 | classify_intent | retrieve / modify_jrxml / save_session / handle_consult / handle_undo / handle_reset | lines 204-215 |
| 条件边 | retrieve | generate / generate_skeleton | lines 218-224 |
| 条件边 | generate | save_session | lines 227-231 |
| 固定边 | generate_skeleton | refine_layout | line 233 |
| 固定边 | refine_layout | map_fields | line 234 |
| 条件边 | map_fields | save_session | lines 235-239 |
| 条件边 | modify_jrxml | save_session | lines 242-246 |
| 条件边 | handle_undo | save_session | lines 249-253 |
| 条件边 | save_session | validate / finalize | lines 256-260 |
| 条件边 | validate | finalize / explain_error | lines 263-267 |
| 条件边 | explain_error | correct_jrxml | lines 268-272 |
| 条件边 | correct_jrxml | validate / finalize | lines 273-277 |
| 固定边 | handle_consult | finalize | line 280 |
| 固定边 | handle_reset | finalize | line 281 |
| 固定边 | finalize | END | line 284 |