公司动态

基于LLM与OCR的智能收据分类系统开发实战

📅 2026/7/26 7:17:14
基于LLM与OCR的智能收据分类系统开发实战
在日常财务管理和报销流程中处理纸质或电子收据往往令人头疼。客户需要将各种格式的收据通过邮件、聊天工具或上传系统等方式发送给财务人员财务人员则要手动分类、录入信息这个过程既耗时又容易出错。本文将介绍如何利用现代技术构建一个智能收据分类系统让客户通过单一链接上传收据系统自动使用大语言模型LLM进行智能分类处理。这个方案特别适合财务服务公司、企业报销部门以及需要处理大量收据的机构。通过本文你将掌握从环境搭建到生产部署的完整流程包括前端收据上传、后端处理逻辑、LLM集成以及分类结果展示的全套技术实现。1. 系统架构与核心概念1.1 什么是智能收据分类系统智能收据分类系统是一个结合了文件上传、OCR文字识别和LLM智能分析的自动化处理平台。其核心价值在于将传统手动处理收据的工作流程自动化显著提高效率并减少人为错误。系统工作原理分为三个关键阶段收据收集阶段客户通过统一的文件上传接口提交收据支持多种格式PDF、JPG、PNG等内容提取阶段系统使用OCR技术识别收据中的文字信息智能分类阶段LLM模型分析提取的文字内容自动进行分类和关键信息提取1.2 LLM在收据处理中的核心作用大语言模型LLM在本系统中扮演着智能分析引擎的角色。与传统的关键词匹配或规则引擎不同LLM能够理解收据内容的语义处理各种格式和语言的收据适应不同商家的收据模板。LLM在收据处理中的主要能力包括多语言支持自动识别并处理不同语言的收据内容模板自适应无需预定义模板能够理解各种收据格式语义理解基于收据内容进行智能分类如餐饮、交通、办公用品等关键信息提取准确提取金额、日期、商家名称等关键字段1.3 系统架构设计整个系统采用微服务架构确保各模块的独立性和可扩展性前端上传界面 → API网关 → 文件存储服务 → OCR处理服务 → LLM分类服务 → 结果存储 → 管理界面每个服务都可以独立部署和扩展特别是LLM分类服务可以根据处理量动态调整资源。2. 环境准备与技术选型2.1 开发环境要求构建智能收据分类系统需要准备以下开发环境操作系统要求LinuxUbuntu 20.04 或 CentOS 8macOS 10.15Windows 10/11建议使用WSL2Python环境# 创建虚拟环境 python -m venv receipt_env source receipt_env/bin/activate # Linux/macOS # 或 receipt_env\Scripts\activate # Windows # 安装基础依赖 pip install python-multipart fastapi uvicorn pillow数据库选择PostgreSQL 12用于存储收据元数据和分类结果Redis 6用于缓存和会话管理2.2 核心技术栈选择后端框架FastAPI高性能异步Web框架适合处理文件上传和AI推理SQLAlchemy数据库ORM支持多种数据库后端前端技术React 18构建用户上传界面和管理后台Ant Design企业级UI组件库AxiosHTTP客户端处理文件上传AI/ML组件PaddleOCR开源OCR引擎用于文字识别LangChainLLM应用开发框架OpenAI API或本地LLM用于收据内容分析文件存储本地文件系统开发环境AWS S3或MinIO生产环境2.3 LLM服务配置根据项目需求和预算可以选择不同的LLM服务云端API方案适合快速上线# OpenAI GPT-4配置 OPENAI_API_KEY your-api-key OPENAI_MODEL gpt-4 # 或gpt-3.5-turbo # 备用方案Azure OpenAI AZURE_OPENAI_ENDPOINT https://your-resource.openai.azure.com/本地部署方案适合数据敏感场景# 使用Hugging Face Transformers from transformers import AutoModelForCausalLM, AutoTokenizer model_name meta-llama/Llama-2-7b-chat-hf # 需要申请许可 # 或使用开源替代模型如Qwen、ChatGLM等3. 核心模块设计与实现3.1 文件上传服务设计文件上传是系统的入口点需要处理并发上传、格式验证和安全检查。FastAPI上传端点实现from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi.responses import JSONResponse import os from pathlib import Path from typing import List app FastAPI() # 允许的文件类型 ALLOWED_EXTENSIONS {.pdf, .jpg, .jpeg, .png, .tiff} app.post(/upload-receipts) async def upload_receipts(files: List[UploadFile] File(...)): 处理多文件收据上传 results [] for file in files: # 验证文件类型 file_extension Path(file.filename).suffix.lower() if file_extension not in ALLOWED_EXTENSIONS: raise HTTPException( status_code400, detailf不支持的文件类型: {file_extension} ) # 生成唯一文件名 file_id generate_file_id() save_path fuploads/{file_id}{file_extension} # 保存文件 try: contents await file.read() with open(save_path, wb) as f: f.write(contents) # 记录文件元数据 file_metadata { file_id: file_id, original_name: file.filename, file_size: len(contents), upload_time: datetime.now().isoformat(), status: uploaded } await save_file_metadata(file_metadata) results.append({ file_id: file_id, status: success, message: 文件上传成功 }) except Exception as e: results.append({ file_id: file_id, status: error, message: f文件处理失败: {str(e)} }) return JSONResponse(content{results: results}) def generate_file_id(): 生成唯一文件ID return str(uuid.uuid4())3.2 OCR文字提取模块OCR模块负责从收据图片中提取文字内容这是LLM分析的基础。PaddleOCR集成示例from paddleocr import PaddleOCR import cv2 import pdf2image from PIL import Image import io class ReceiptOCR: def __init__(self): # 初始化PaddleOCR支持中英文 self.ocr PaddleOCR(use_angle_clsTrue, langch) async def extract_text(self, file_path: str) - dict: 从收据文件中提取文字内容 try: # 处理PDF文件 if file_path.lower().endswith(.pdf): images pdf2image.convert_from_path(file_path) all_text for i, image in enumerate(images): image_path f{file_path}_page_{i}.jpg image.save(image_path, JPEG) page_result self._process_image(image_path) all_text page_result.get(text, ) \n return { success: True, text: all_text.strip(), page_count: len(images) } # 处理图片文件 else: result self._process_image(file_path) return { success: True, text: result.get(text, ), page_count: 1 } except Exception as e: return { success: False, error: str(e), text: } def _process_image(self, image_path: str) - dict: 处理单张图片的OCR识别 result self.ocr.ocr(image_path, clsTrue) text_blocks [] if result and result[0]: for line in result[0]: if line and line[1]: text_blocks.append(line[1][0]) return { text: \n.join(text_blocks), block_count: len(text_blocks) }3.3 LLM分类提示词设计LLM提示词的设计直接影响分类准确性需要精心设计系统提示词和分类规则。分类提示词模板class ReceiptClassifier: def __init__(self, llm_client): self.llm llm_client async def classify_receipt(self, ocr_text: str) - dict: 使用LLM对收据内容进行分类和信息提取 system_prompt 你是一个专业的财务收据分析助手。请仔细分析收据内容完成以下任务 1. 收据类型分类选择最合适的类别 - 餐饮消费餐厅、咖啡厅、外卖等 - 交通出行出租车、机票、火车票、加油等 - 办公用品文具、办公设备、耗材等 - 差旅住宿酒店、住宿费用 - 业务招待客户招待、商务餐费 - 其他支出无法归入以上类别的支出 2. 提取关键信息 - 商家名称 - 交易金额 - 交易日期 - 货币类型 - 税金额如有 3. 置信度评估根据收据内容的清晰度和完整性评估分类结果的置信度 请以JSON格式返回结果包含以下字段 { category: 分类结果, confidence: 0.95, merchant: 商家名称, amount: 100.00, currency: CNY, date: 2024-01-15, tax_amount: 10.00, notes: 额外说明 } 如果某些信息无法确定请使用null值。 user_prompt f请分析以下收据内容\n\n{ocr_text} try: response await self.llm.chat_completion( system_promptsystem_prompt, user_promptuser_prompt, temperature0.1 # 低温度确保结果稳定 ) # 解析LLM返回的JSON结果 classification_result self._parse_llm_response(response) return classification_result except Exception as e: return { category: 未知, confidence: 0.0, error: fLLM分析失败: {str(e)} } def _parse_llm_response(self, response: str) - dict: 解析LLM返回的JSON结果 import json try: # 提取JSON部分LLM可能在回答中包含额外文本 start_idx response.find({) end_idx response.rfind(}) 1 json_str response[start_idx:end_idx] result json.loads(json_str) return result except json.JSONDecodeError: return { category: 解析错误, confidence: 0.0, error: JSON解析失败 }4. 完整系统集成实战4.1 数据库设计收据处理系统需要设计合理的数据库结构来存储各类信息。PostgreSQL表结构设计-- 收据文件表 CREATE TABLE receipt_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), original_filename VARCHAR(500) NOT NULL, file_path VARCHAR(1000) NOT NULL, file_size BIGINT NOT NULL, upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, upload_user_id UUID, status VARCHAR(50) DEFAULT uploaded ); -- OCR结果表 CREATE TABLE ocr_results ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), file_id UUID REFERENCES receipt_files(id), extracted_text TEXT, ocr_confidence FLOAT, processing_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, success BOOLEAN DEFAULT TRUE ); -- LLM分类结果表 CREATE TABLE classification_results ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ocr_result_id UUID REFERENCES ocr_results(id), category VARCHAR(100), confidence FLOAT, merchant_name VARCHAR(500), amount DECIMAL(10,2), currency VARCHAR(10), transaction_date DATE, tax_amount DECIMAL(10,2), additional_notes TEXT, processed_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 创建索引优化查询性能 CREATE INDEX idx_receipt_files_status ON receipt_files(status); CREATE INDEX idx_classification_category ON classification_results(category); CREATE INDEX idx_classification_date ON classification_results(transaction_date);4.2 后端服务完整实现主服务入口点# main.py from fastapi import FastAPI, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Optional import asyncio from datetime import datetime from services.ocr_service import ReceiptOCR from services.llm_classifier import ReceiptClassifier from services.file_storage import FileStorage from database import DatabaseManager app FastAPI(title智能收据分类系统) # CORS配置 app.add_middleware( CORSMiddleware, allow_origins[*], # 生产环境应限制具体域名 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 初始化服务组件 db DatabaseManager() ocr_service ReceiptOCR() file_storage FileStorage() llm_classifier ReceiptClassifier() class UploadResponse(BaseModel): success: bool file_ids: List[str] message: str class ClassificationResult(BaseModel): file_id: str category: str confidence: float merchant: Optional[str] amount: Optional[float] date: Optional[str] app.post(/api/upload, response_modelUploadResponse) async def upload_receipts(files: List[UploadFile], background_tasks: BackgroundTasks): 收据上传接口 try: file_ids [] for file in files: # 保存文件 file_id await file_storage.save_file(file) file_ids.append(file_id) # 更新数据库状态 await db.insert_file_record({ file_id: file_id, original_name: file.filename, status: uploaded }) # 后台处理上传的文件 background_tasks.add_task(process_uploaded_files, file_ids) return UploadResponse( successTrue, file_idsfile_ids, messagef成功上传 {len(files)} 个文件正在处理中 ) except Exception as e: return UploadResponse( successFalse, file_ids[], messagef上传失败: {str(e)} ) async def process_uploaded_files(file_ids: List[str]): 后台处理上传的收据文件 for file_id in file_ids: try: # 步骤1: OCR文字提取 ocr_result await ocr_service.process_file(file_id) await db.update_file_status(file_id, ocr_processed) # 步骤2: LLM分类分析 if ocr_result[success]: classification await llm_classifier.classify(ocr_result[text]) await db.save_classification_result(file_id, classification) await db.update_file_status(file_id, classified) else: await db.update_file_status(file_id, ocr_failed) except Exception as e: print(f处理文件 {file_id} 时出错: {str(e)}) await db.update_file_status(file_id, processing_failed) app.get(/api/results/{file_id}, response_modelClassificationResult) async def get_classification_result(file_id: str): 获取分类结果 result await db.get_classification_result(file_id) if not result: return ClassificationResult( file_idfile_id, category处理中, confidence0.0 ) return ClassificationResult(**result) app.get(/api/status) async def get_system_status(): 系统状态检查 stats await db.get_processing_stats() return { total_files: stats[total], processed_files: stats[processed], success_rate: stats[success_rate], last_processed: stats[last_processed] }4.3 前端上传界面实现React组件实现// ReceiptUploader.jsx import React, { useState } from react; import { Upload, Button, message, Progress, List } from antd; import { UploadOutlined, InboxOutlined } from ant-design/icons; import axios from axios; const { Dragger } Upload; const ReceiptUploader () { const [uploading, setUploading] useState(false); const [fileList, setFileList] useState([]); const [results, setResults] useState([]); const uploadProps { name: files, multiple: true, accept: .pdf,.jpg,.jpeg,.png,.tiff, fileList, beforeUpload: (file) { const isLt10M file.size / 1024 / 1024 10; if (!isLt10M) { message.error(文件大小不能超过10MB); return false; } return true; }, onChange: (info) { setFileList(info.fileList); }, customRequest: async (options) { const { file, onSuccess, onError } options; try { setUploading(true); const formData new FormData(); formData.append(files, file); const response await axios.post(/api/upload, formData, { headers: { Content-Type: multipart/form-data, }, onUploadProgress: (progressEvent) { const percent Math.round( (progressEvent.loaded * 100) / progressEvent.total ); // 更新上传进度 } }); onSuccess(response.data, file); message.success(${file.name} 上传成功); // 开始轮询处理结果 pollProcessingResult(response.data.file_ids[0]); } catch (error) { onError(error); message.error(${file.name} 上传失败); } finally { setUploading(false); } }, }; const pollProcessingResult async (fileId) { const checkResult async () { try { const response await axios.get(/api/results/${fileId}); if (response.data.category ! 处理中) { setResults(prev [...prev, response.data]); return true; } return false; } catch (error) { console.error(获取结果失败:, error); return false; } }; // 每2秒检查一次最多检查30次 let attempts 0; const interval setInterval(async () { attempts; const done await checkResult(); if (done || attempts 30) { clearInterval(interval); if (attempts 30) { message.warning(文件处理超时请稍后查看结果); } } }, 2000); }; return ( div style{{ padding: 20px, maxWidth: 800px, margin: 0 auto }} h2智能收据分类上传/h2 Dragger {...uploadProps} p classNameant-upload-drag-icon InboxOutlined / /p p classNameant-upload-text点击或拖拽文件到此区域上传/p p classNameant-upload-hint 支持PDF、JPG、PNG格式单个文件不超过10MB /p /Dragger {uploading ( div style{{ marginTop: 20px }} Progress percent{uploading ? 50 : 0} statusactive / p文件正在处理中请稍候.../p /div )} {results.length 0 ( div style{{ marginTop: 30px }} h3处理结果/h3 List dataSource{results} renderItem{item ( List.Item List.Item.Meta title{文件ID: ${item.file_id}} description{ 分类: ${item.category} | 置信度: ${(item.confidence * 100).toFixed(1)}% | 金额: ${item.amount || 未知} ${item.currency || } } / /List.Item )} / /div )} /div ); }; export default ReceiptUploader;5. 部署与生产环境配置5.1 Docker容器化部署使用Docker可以简化部署流程确保环境一致性。Dockerfile配置# 使用Python官方镜像 FROM python:3.9-slim # 设置工作目录 WORKDIR /app # 安装系统依赖 RUN apt-get update apt-get install -y \ poppler-utils \ # PDF处理 tesseract-ocr \ # OCR引擎 libgl1-mesa-glx \ # OpenCV依赖 rm -rf /var/lib/apt/lists/* # 复制依赖文件 COPY requirements.txt . # 安装Python依赖 RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY . . # 创建非root用户 RUN useradd -m -u 1000 appuser chown -R appuser:appuser /app USER appuser # 暴露端口 EXPOSE 8000 # 启动命令 CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]docker-compose.yml配置version: 3.8 services: web: build: . ports: - 8000:8000 environment: - DATABASE_URLpostgresql://user:passworddb:5432/receipt_db - REDIS_URLredis://redis:6379 depends_on: - db - redis volumes: - upload_data:/app/uploads db: image: postgres:13 environment: - POSTGRES_DBreceipt_db - POSTGRES_USERuser - POSTGRES_PASSWORDpassword volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:6-alpine volumes: - redis_data:/data nginx: image: nginx:alpine ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - web volumes: upload_data: postgres_data: redis_data:5.2 生产环境安全配置环境变量管理# config.py import os from pydantic import BaseSettings class Settings(BaseSettings): # 数据库配置 database_url: str os.getenv(DATABASE_URL) # Redis配置 redis_url: str os.getenv(REDIS_URL, redis://localhost:6379) # LLM API配置 openai_api_key: str os.getenv(OPENAI_API_KEY) openai_model: str os.getenv(OPENAI_MODEL, gpt-3.5-turbo) # 文件上传配置 max_file_size: int int(os.getenv(MAX_FILE_SIZE, 10485760)) # 10MB allowed_extensions: str os.getenv(ALLOWED_EXTENSIONS, .pdf,.jpg,.jpeg,.png) # 安全配置 secret_key: str os.getenv(SECRET_KEY) cors_origins: str os.getenv(CORS_ORIGINS, http://localhost:3000) class Config: env_file .env settings Settings()6. 性能优化与监控6.1 异步处理优化对于收据处理这种IO密集型任务异步处理可以显著提高系统吞吐量。异步任务队列实现import asyncio from concurrent.futures import ThreadPoolExecutor import redis.asyncio as redis import json class ProcessingQueue: def __init__(self, redis_url: str): self.redis redis.from_url(redis_url) self.queue_key receipt_processing_queue async def add_task(self, file_id: str): 添加处理任务到队列 task_data { file_id: file_id, timestamp: datetime.now().isoformat() } await self.redis.lpush(self.queue_key, json.dumps(task_data)) async def process_tasks(self, batch_size: int 5): 处理队列中的任务 while True: tasks [] # 批量获取任务 for _ in range(batch_size): task_json await self.redis.rpop(self.queue_key) if task_json: tasks.append(json.loads(task_json)) if tasks: # 并行处理任务 await asyncio.gather(*[ self.process_single_task(task) for task in tasks ]) await asyncio.sleep(1) # 避免空循环 async def process_single_task(self, task: dict): 处理单个任务 file_id task[file_id] try: # OCR处理 ocr_result await ocr_service.process_file(file_id) # LLM分类 if ocr_result[success]: classification await llm_classifier.classify(ocr_result[text]) await db.save_classification_result(file_id, classification) except Exception as e: print(f任务处理失败 {file_id}: {str(e)})6.2 监控与日志系统完善的监控系统可以帮助及时发现和解决问题。结构化日志配置import logging import json from pythonjsonlogger import jsonlogger def setup_logging(): 配置结构化日志 logger logging.getLogger() logger.setLevel(logging.INFO) # JSON格式处理器 handler logging.StreamHandler() formatter jsonlogger.JsonFormatter( %(asctime)s %(levelname)s %(name)s %(message)s ) handler.setFormatter(formatter) logger.addHandler(handler) # 业务日志记录 async def log_processing_event(file_id: str, event: str, details: dict): 记录处理事件 logging.info(收据处理事件, extra{ file_id: file_id, event: event, timestamp: datetime.now().isoformat(), **details })7. 常见问题与解决方案7.1 文件处理问题问题1OCR识别准确率低现象收据文字提取不完整或错误原因图片质量差、光线不均、特殊字体解决方案预处理图片调整亮度、对比度、旋转校正使用多OCR引擎融合PaddleOCR Tesseract双重验证人工复核机制低置信度结果标记为需要人工检查问题2大文件处理超时现象处理时间超过预期请求超时原因文件过大或系统资源不足解决方案文件大小限制前端和后端同时验证异步处理立即返回接受响应后台处理进度查询提供处理状态查询接口7.2 LLM分类问题问题3分类结果不一致现象相同收据多次处理结果不同原因LLM温度参数过高或提示词不明确解决方案降低temperature参数0.1-0.3优化提示词增加具体规则和示例实现结果缓存相同内容直接返回缓存结果问题4特殊收据格式识别失败现象某些商家特殊格式收据无法正确分类原因训练数据缺乏此类样本解决方案建立反馈机制收集错误案例定期更新提示词和分类规则支持人工纠正并用于模型改进7.3 系统性能问题问题5高并发下系统响应慢现象多用户同时上传时处理延迟原因资源竞争或数据库瓶颈解决方案水平扩展多实例部署负载均衡数据库优化读写分离、连接池、索引优化缓存策略频繁查询结果缓存8. 最佳实践与工程建议8.1 安全最佳实践文件上传安全# 文件类型验证 def validate_file_type(filename: str) - bool: allowed_extensions {.pdf, .jpg, .jpeg, .png} ext Path(filename).suffix.lower() return ext in allowed_extensions # 文件内容验证 def validate_file_content(file_path: str) - bool: import magic file_type magic.from_file(file_path, mimeTrue) return file_type in [image/jpeg, image/png, application/pdf]数据保护措施敏感信息加密存储访问日志记录和审计定期安全扫描和漏洞修复API限流和防滥用机制8.2 可维护性设计配置管理环境分离开发、测试、生产环境独立配置密钥管理使用专业密钥管理服务配置版本化配置文件纳入版本控制代码质量单元测试覆盖核心业务逻辑集成测试验证端到端流程代码审查和静态分析文档自动生成和更新8.3 扩展性考虑微服务拆分文件上传服务独立部署OCR处理服务可水平扩展LLM分类服务按需伸缩结果查询服务高可用技术债务管理定期重构和优化技术栈更新计划性能基准测试容量规划和预警通过本文的完整实现方案你可以构建一个功能完善、性能优异的智能收据分类系统。在实际项目中建议先从最小可行产品开始逐步迭代优化根据实际使用情况调整技术方案和业务逻辑。