1. 先搞清楚这个工具到底解决什么问题Sorted Receipts 这个项目核心解决的是小企业主、自由职业者或财务人员经常遇到的实际痛点客户或同事随手发来的各种格式的收据、发票、账单图片或PDF文件散落在微信、邮件、聊天记录里整理起来特别麻烦。传统做法要么是手动分类归档要么要求对方按固定格式发送执行成本很高。这个工具的思路很直接给客户一个固定链接对方把所有收据文件往这个链接里一扔后端用大语言模型LLM自动识别文件内容并分类整理。从技术栈看它用了 FastAPI 做后端接口SQLite 存储数据通过 OpenRouter 调用 LLM 能力。这种组合在轻量级业务场景下很常见关键是看实际落地时文件处理、模型调用和分类逻辑能不能稳定跑通。我一般会先关注这类工具的边界条件支持哪些文件格式JPG/PNG/PDF单文件大小限制多少LLM 识别准确率如何以及批量上传时的并发处理能力。如果只是 demo 级别可能只处理简单图片如果要实用就得考虑模糊照片、多页PDF、混合格式文件等真实场景。2. 环境准备别急着跑代码先确认依赖版本虽然项目介绍里没提具体版本但根据 FastAPI、SQLite 和 OpenRouter 这些关键词实际部署时需要重点确认几个依赖的兼容性。OpenRouter 作为 LLM 统一接口平台能降低直接对接多个模型厂商的复杂度但国内访问稳定性需要实测。基础环境建议Python 3.8FastAPI 对 3.7 以下版本支持有限FastAPI 0.100旧版部分异步特性不支持SQLite 3.35支持窗口函数等进阶特性网络能稳定访问 OpenRouter API需要测试延迟和超时关键 Python 包pip install fastapi uvicorn sqlite3 python-multipart openrouter注意openrouter不是官方包名实际可能是openrouter-python或直接通过requests调用 REST API。如果输入材料没给出具体库建议先查 OpenRouter 最新文档。权限和存储准备确保运行用户对项目目录有读写权限特别是上传文件存储位置SQLite 数据库文件所在路径需要写权限如果处理大量文件提前规划存储空间图片和PDF很占空间3. 核心流程拆解从文件上传到分类完成3.1 文件接收环节设计FastAPI 处理文件上传时最容易出问题的是大小限制和格式校验。不建议一上来就支持所有格式先聚焦最常见的 JPG 和 PDFfrom fastapi import FastAPI, File, UploadFile from fastapi.responses import JSONResponse app FastAPI() app.post(/upload-receipt/) async def upload_receipt(file: UploadFile File(...)): # 限制文件类型 if file.content_type not in [image/jpeg, application/pdf]: return JSONResponse( status_code400, content{error: 仅支持 JPG 和 PDF 格式} ) # 限制文件大小10MB if await file.size() 10 * 1024 * 1024: return JSONResponse( status_code400, content{error: 文件大小不能超过 10MB} ) # 保存文件 file_location fuploads/{file.filename} with open(file_location, wb) as buffer: content await file.read() buffer.write(content) return {filename: file.filename, status: uploaded}这个基础版本先保证文件能正常接收和存储再考虑后续处理。很多项目失败不是因为 LLM 能力不够而是文件上传环节没处理好。3.2 LLM 调用策略选择通过 OpenRouter 调用 LLM 时关键是要设计好提示词prompt和错误处理。收据分类任务不需要太复杂的模型但需要稳定的格式解析能力import requests import json def analyze_receipt(image_path: str, openrouter_api_key: str): # 读取图片并编码如果是PDF需要先转换 with open(image_path, rb) as image_file: image_data image_file.read() # 构造 OpenRouter 请求 headers { Authorization: fBearer {openrouter_api_key}, Content-Type: application/json } payload { model: google/gemini-pro-vision, # 选择支持视觉的模型 messages: [ { role: user, content: [ { type: text, text: 分析这张收据返回JSON格式{“商户名称: , 日期: , 金额: , 类别: 餐饮/交通/办公/其他} }, { type: image_url, image_url: { url: fdata:image/jpeg;base64,{base64.b64encode(image_data).decode()} } } ] } ] } try: response requests.post( https://openrouter.ai/api/v1/chat/completions, headersheaders, jsonpayload, timeout30 # 重要设置超时避免卡死 ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(fAPI 调用失败: {e}) return None提示词设计要具体明确告诉模型需要提取哪些字段返回什么格式。类别最好预先定义好选项而不是让模型自由发挥。3.3 数据库设计要点SQLite 在这种轻量级应用中很合适但表结构设计要考虑扩展性CREATE TABLE receipts ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, file_path TEXT NOT NULL, merchant_name TEXT, receipt_date TEXT, amount REAL, category TEXT, status TEXT DEFAULT pending -- pending/processed/failed ); CREATE INDEX idx_receipts_status ON receipts(status); CREATE INDEX idx_receipts_category ON receipts(category);status 字段很重要能跟踪每个文件的处理状态方便重试失败任务。不要把所有字段都设为 NOT NULL初期有些信息可能提取不出来。4. 完整流程串联与错误处理4.1 主处理流程编排单文件处理流程要包含完整的错误处理async def process_receipt(file_path: str, filename: str, db_connection): try: # 1. 调用 LLM 分析 analysis_result analyze_receipt(file_path, OPENROUTER_API_KEY) if not analysis_result: raise Exception(LLM 分析失败) # 2. 解析返回结果 content analysis_result[choices][0][message][content] receipt_data json.loads(content) # 3. 验证必要字段 required_fields [merchant_name, amount, category] for field in required_fields: if field not in receipt_data or not receipt_data[field]: receipt_data[field] 未知 # 设置默认值 # 4. 更新数据库 cursor db_connection.cursor() cursor.execute( UPDATE receipts SET merchant_name ?, receipt_date ?, amount ?, category ?, status processed WHERE filename ? , ( receipt_data[merchant_name], receipt_data.get(date, ), receipt_data[amount], receipt_data[category], filename )) db_connection.commit() except json.JSONDecodeError: print(fJSON 解析失败: {content}) mark_as_failed(filename, db_connection, JSON 格式错误) except KeyError as e: print(fAPI 返回格式异常: {e}) mark_as_failed(filename, db_connection, API 返回格式异常) except Exception as e: print(f处理过程异常: {e}) mark_as_failed(filename, db_connection, str(e)) def mark_as_failed(filename: str, db_connection, error_msg: str): cursor db_connection.cursor() cursor.execute( UPDATE receipts SET status failed, error_message ? WHERE filename ?, (error_msg, filename) ) db_connection.commit()4.2 批量处理策略单文件跑通后批量处理要考虑并发控制和资源限制import asyncio from concurrent.futures import ThreadPoolExecutor async def process_batch_receipts(max_concurrent: int 3): 批量处理控制并发数避免 API 限制 db_conn sqlite3.connect(receipts.db) cursor db_conn.cursor() # 获取待处理文件 cursor.execute(SELECT filename, file_path FROM receipts WHERE status pending) pending_files cursor.fetchall() # 使用信号量控制并发 semaphore asyncio.Semaphore(max_concurrent) async def process_with_semaphore(filename, file_path): async with semaphore: await process_receipt(file_path, filename, db_conn) # 创建任务 tasks [ process_with_semaphore(filename, file_path) for filename, file_path in pending_files ] # 批量执行 await asyncio.gather(*tasks, return_exceptionsTrue) db_conn.close()并发数不要设太高OpenRouter 等 API 服务通常有速率限制超限会导致请求失败。5. 实际部署时的关键配置5.1 FastAPI 部署配置开发环境用uvicorn直接运行没问题生产环境需要更多配置# uvicorn_config.py import uvicorn if __name__ __main__: uvicorn.run( main:app, host0.0.0.0, # 生产环境建议绑定具体IP port8000, reloadFalse, # 生产环境关闭热重载 workers4, # 根据 CPU 核心数调整 log_levelinfo, timeout_keep_alive5, limit_max_requests1000 # 防止内存泄漏 )对于文件上传还需要在 FastAPI 实例中配置限制app FastAPI( max_upload_size10 * 1024 * 1024, # 10MB debugFalse # 生产环境务必关闭调试模式 )5.2 SQLite 性能优化虽然 SQLite 是轻量级数据库但收据数量多时也要注意性能# 数据库连接配置 def get_db_connection(): conn sqlite3.connect( receipts.db, timeout20, # 设置超时避免锁冲突 check_same_threadFalse # 多线程环境需要 ) # 启用 WAL 模式提升并发性能 conn.execute(PRAGMA journal_modeWAL) # 设置缓存大小 conn.execute(PRAGMA cache_size-64000) # 64MB return conn定期清理和备份也很重要可以设置定时任务归档旧数据。6. 效果验证与问题排查6.1 验证分类准确性LLM 分类不可能 100% 准确需要建立验证机制def validate_receipt_category(receipt_data: dict) - bool: 验证分类结果是否合理 # 检查金额格式 try: amount float(receipt_data[amount]) if amount 0 or amount 100000: # 合理金额范围 return False except (ValueError, TypeError): return False # 检查类别是否在预定义范围内 valid_categories [餐饮, 交通, 办公, 住宿, 其他] if receipt_data[category] not in valid_categories: return False # 检查日期格式如果存在 if receipt_data.get(date): # 简单的日期格式验证 if len(receipt_data[date]) not in [8, 10]: # 20240101 或 2024-01-01 return False return True可以定期抽样人工复核收集错误案例优化提示词。6.2 常见问题排查清单遇到问题时按这个顺序排查文件上传失败检查目录权限ls -la uploads/检查磁盘空间df -h查看 FastAPI 日志中的具体错误LLM 调用失败测试网络连通性ping openrouter.ai检查 API 密钥是否有效查看 OpenRouter 控制台使用量和限制确认模型名称是否正确分类结果不准确检查提示词是否清晰明确测试不同模型Gemini、GPT-4V 等提供更具体的类别定义和示例考虑先用规则预处理如金额提取数据库操作异常检查 SQLite 文件是否损坏sqlite3 receipts.db PRAGMA integrity_check;确认是否有并发写冲突查看数据库锁状态性能问题监控 API 响应时间检查并发数是否过高分析数据库查询性能7. 扩展思路与改进方向这个基础版本跑通后可以考虑几个实用扩展多客户支持给每个客户生成独立的上传链接和存储空间避免数据混淆。Webhook 集成处理完成后通过 Webhook 通知其他系统如财务软件。规则引擎增强在 LLM 基础上加入规则匹配比如特定商户直接归类减少 API 调用。本地模型替代如果担心数据隐私或 API 成本可以调研本地部署的视觉语言模型。移动端优化优化上传界面支持拍照直接上传添加进度提示。最重要的是先让核心流程稳定运行再逐步添加功能。很多类似项目失败是因为一开始追求大而全反而连基本功能都没跑通。这种工具真正的价值不在于技术多先进而在于能否在实际业务场景中稳定解决具体问题。先从小的真实需求开始验证比直接追求完美方案更实际。