fix: OCR字段提取集成修复 + 会话切换无限循环修复 + 一键启动脚本

- process_input 传入17个默认中文字段(修复空列表导致零字段提取)
- OCR提取结果自动注入 LLM 上下文
- save_session_node/load_session_node 持久化 session_id(修复切换会话无限 rerun)
- app.py 会话切换后显式设置 session_id(纵深防御)
- 新增 start.bat / stop.bat 一键启动/停止脚本
- 更新 CLAUDE.md + CODE_GUIDE.md 文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 10:17:05 +08:00
parent c9f003e1b7
commit da79640259
6 changed files with 166 additions and 6 deletions
+100 -3
View File
@@ -18,6 +18,7 @@
10. [错误自增长知识库](#10-错误自增长知识库)
11. [布局分析器](#11-布局分析器)
12. [文件解析器](#12-文件解析器)
12b. [OCR 单据字段提取器](#12b-ocr-单据字段提取器)
13. [验证服务](#13-验证服务)
14. [会话持久化](#14-会话持久化)
15. [Streamlit UIapppy](#15-streamlit-uiapppy)
@@ -52,8 +53,9 @@ cp .env.example .env
### 启动
需要**两个终端**同时运行:
**一键启动(推荐)**:双击 `start.bat`,自动打开两个窗口分别运行验证服务和 UI。停止用 `stop.bat`
**手动启动**(需要两个终端):
```bash
# 终端 1 — 验证服务(必须先启动)
python -m uvicorn validation_service.main:app --port 8001 --host 0.0.0.0
@@ -124,6 +126,9 @@ streamlit run app.py --server.port 8501
│layout_ │
│analyzer │
│.py │
│ocr_ │
│extractor │
│.py │
│file_ │
│parser.py │
│validation│
@@ -188,6 +193,10 @@ class AgentState(TypedDict, total=False):
# ── 失败上下文传递 ──
pending_failure_context: dict # 重试耗尽后暂存失败信息,下次用户输入时自动注入
# ── OCR 单据字段提取 ──
ocr_extraction_result: dict # OCR字段提取结果(来自 OcrExtractor
uploaded_file_path: str # 上传图片的临时路径
```
**数据流向**:每个节点函数接收 `state`,修改后返回 `state`(实际上是 dict)。LangGraph 自动合并返回值到全局状态。
@@ -314,7 +323,7 @@ def build_graph():
`agent/nodes.py` 是系统的"血肉",每个节点实现一个处理步骤。
### 6.1 process_input — 记录输入 + 自动注入失败上下文
### 6.1 process_input — 记录输入 + 自动注入失败上下文 + OCR 字段提取
```python
def process_input(state: AgentState) -> Dict:
@@ -335,12 +344,36 @@ def process_input(state: AgentState) -> Dict:
# 追加到工作历史(含注入后的内容)
state["conversation_history"].append({"role": "user", "content": user_input})
# OCR 单据字段精确提取(处理上传的图片文件)
uploaded_path = state.get("uploaded_file_path", "")
if uploaded_path and Path(uploaded_path).is_file():
if suffix in (".png", ".jpg", ".jpeg", ".bmp", ".webp"):
extractor = OcrExtractor()
ocr_result = extractor.extract(uploaded_path, [
"发票代码", "发票号码", "开票日期", "合计金额", "校验码",
"价税合计", "总金额", "日期", "金额", "数量", "单价", "税率",
"购买方名称", "销售方名称", "货物名称", "规格型号",
"不含税金额", "税额",
])
if ocr_result.get("ocr_available"):
state["ocr_extraction_result"] = ocr_result
# 将提取到的字段注入 LLM 上下文
non_empty = [f for f in extracted_fields if f.get("field_value")]
if non_empty:
ocr_context = "[OCR 单据字段提取结果]\n" + ...
user_input = f"{ocr_context}\n\n{user_input}"
# 重置本轮字段
state["retry_count"] = 0
state["user_modification_request"] = user_input
```
**注意**维护了两个对话历史 — `conversation_history` 可能被压缩,`full_conversation_history` 永不丢失。失败上下文注入仅影响工作历史,全量历史保留原始消息。
**注意**
- 维护了两个对话历史 — `conversation_history` 可能被压缩,`full_conversation_history` 永不丢失
- 失败上下文注入仅影响工作历史,全量历史保留原始消息
- OCR 字段提取在 `process_input` 阶段自动执行,提取到的字段值同时存入 `ocr_extraction_result` 和注入到 `user_input` 前缀供 LLM 使用
- `session_id` 已包含在持久化字段中,避免切换会话时的无限 rerun bug
### 6.2 manage_context — 上下文压缩
@@ -763,6 +796,66 @@ def parse_file(file_path, file_type="") -> dict:
---
## 12b. OCR 单据字段提取器
`backend/ocr_extractor.py` — 两阶段精确提取单据图像中的字段值。
### 12b.1 数据模型
```python
@dataclass
class OcrTextElement: # OCR 文本元素,含精确坐标
text: str
x_min, y_min, x_max, y_max: float
confidence: float = 1.0
# 属性: center_x, center_y, width, height, bbox
@dataclass
class ExtractedField: # 提取的字段结果
field_name: str
field_value: str
bbox: list[float]
confidence: float
extraction_method: str # exact_match / kv_pair / regex / table_match / none
@dataclass
class ExtractionResult: # 完整提取结果
file_path: str
image_size: tuple
fields: list[ExtractedField]
all_elements: list[OcrTextElement]
errors: list[str]
ocr_available: bool
```
### 12b.2 两阶段流水线
**阶段1 — 文档分析** (`_analyze_document`):
- 加载图片 → `_ocr_elements_enhanced()` → EasyOCR(ch_sim+en) → PaddleOCR 回退
-`OCR_CONFIDENCE_THRESHOLD` (默认 0.5) 过滤低置信度元素
- 返回按 (y, x) 排序的 `OcrTextElement` 列表
**阶段2 — 字段提取** (`_extract_field`):
按优先级尝试 4 种策略:
1. **精确键值对** (`_exact_kv_match`, conf=0.95/0.85): 同一元素中 "字段名: 值" 模式
2. **模糊键值对** (`_fuzzy_kv_match`, conf=0.75/0.60): 相邻元素匹配,同行/下一行搜索
3. **正则模式** (`_regex_match`, conf=0.70/0.60): 12 种预定义模式 (发票代码/号码/金额/日期等)
4. **表格结构** (`_table_match`, conf=0.55): 行列分组 + 表头匹配
### 12b.3 集成点
- **`process_input`**: 检测到上传图片后自动调用,传入 17 个默认中文字段
- **结果注入**: 提取到的字段值自动拼入 `user_input` 前缀(`[OCR 单据字段提取结果]`
- **结果展示**: `app.py` 总结卡片中 "🔍 OCR 单据字段提取结果" 折叠区
### 12b.4 回退能力
- 任一 OCR 引擎不可用时静默回退,不影响主流程
- 两种复用路径: `extract()` (全流程) 和 `extract_from_layout_result()` (复用已有布局分析)
- 便捷函数: `extract_ocr_fields()`, `extract_from_layout()`
---
## 13. 验证服务
`validation_service/main.py` — 独立的 FastAPI 进程,提供 JRXML 验证。
@@ -1168,10 +1261,14 @@ st.json(state) # 打印完整状态(调试用,记得删除)
| `backend/embeddings.py` | ~49 | 嵌入模型工厂 |
| `backend/file_parser.py` | ~194 | 多格式文件解析 |
| `backend/layout_analyzer.py` | ~495 | A4 模板布局分析 |
| `backend/ocr_extractor.py` | ~797 | OCR 单据字段精确提取 (两阶段+4策略) |
| `backend/validation.py` | ~27 | 验证服务 HTTP 客户端 |
| `backend/session.py` | ~113 | 会话 JSON CRUD |
| `prompts/loader.py` | ~54 | Prompt 热重载 |
| `prompts/*.md` (7 个) | — | Prompt 模板 |
| `validation_service/main.py` | ~130 | FastAPI 验证服务 |
| `tests/test_ocr_extraction.py` | ~543 | OCR 提取器单元测试 (48 项) |
| `start.bat` | — | 一键启动脚本 (Windows) |
| `stop.bat` | — | 一键停止脚本 (Windows) |
| `.env.example` | ~62 | 配置模板 |
| `requirements.txt` | ~32 | Python 依赖 |