1. 项目概述:一个基于AI的智能文档处理引擎
最近在做一个挺有意思的Side Project,我把它叫做“Scan & Action”。简单来说,这是一个能帮你自动处理收据、发票、处方这类文档的智能工具。你上传一张图片,它就能在几秒钟内把里面的文字信息提取出来,结构化,然后根据你预设的业务规则自动判断这张单据该怎么处理。比如,一张报销单上传后,它能自动识别金额、日期、商户,然后根据公司政策判断是直接通过、需要人工复核,还是因为某些字段缺失或违规直接被标记出来。
这个项目的核心价值在于,它把过去需要人工肉眼核对、手动录入的繁琐流程自动化了。想象一下,财务每天要处理上百张发票,或者药房要核对大量的处方,这个工具能极大地提升效率,减少人为错误。它特别适合那些有固定格式文档处理需求的场景,比如企业报销、供应链票据核对、医疗记录归档等等。无论你是开发者想了解如何构建一个AI驱动的应用,还是业务负责人想寻找流程自动化的解决方案,这个项目拆解的过程都能给你带来不少启发。
整个系统我用了现在比较主流的技术栈来搭建,前端是Next.js,后端API用FastAPI,AI能力接入了Google的Gemini Vision模型来做多语言的OCR识别,数据存储和用户认证交给了Supabase。我会在后面的部分详细拆解每个模块的设计思路、为什么选这些技术,以及在实际开发中踩过的那些坑和总结出来的实用技巧。
2. 核心架构设计与技术选型逻辑
2.1 为什么选择“前后端分离+微服务”架构
在设计Scan & Action之初,我首先考虑的是系统的可维护性和扩展性。文档处理流程其实是一条清晰的流水线:上传 -> 识别 -> 结构化 -> 规则判断 -> 输出结果。采用前后端分离(Next.js + FastAPI)和微服务化的设计,能让这条流水线上的每个环节都相对独立。
前端(Next.js)只负责用户交互:提供干净的上传界面、展示处理进度和最终结果。它通过API调用后端,自己不处理任何复杂的业务逻辑。这样做的好处是,前端可以专注于用户体验和性能优化,比如用上Server Components来做流式渲染,让用户感觉处理速度更快。
后端(FastAPI)则扮演了“流程 orchestrator”和“规则引擎”的双重角色。它接收前端传来的文档,协调OCR识别、数据清洗、规则匹配等一系列子任务。我选择FastAPI而不是Django或Flask,主要是看中了它的异步高性能和自动生成的交互式API文档。在处理大量并发上传请求时,异步IO能更好地利用系统资源,避免阻塞。而且,FastAPI基于Pydantic的数据验证,能让我在API入口就确保传入数据的格式正确,省去了很多后期调试的麻烦。
注意:虽然FastAPI的异步特性很诱人,但如果你在OCR或规则引擎中使用了某些不支持异步的同步库(比如一些传统的图像处理库),直接放在异步函数里会阻塞整个事件循环。我的做法是,用
asyncio.to_thread把这些CPU密集型的同步操作扔到单独的线程池里去执行,避免影响API的响应性。
2.2 AI模型选型:为什么是Gemini Vision?
OCR(光学字符识别)是整个项目的基石。市面上可选的服务很多,比如Azure的Computer Vision、AWS的Textract,以及开源的Tesseract。最终选择Google的Gemini Vision,是基于以下几个实际的考量:
- 多语言混合识别能力:我的项目需求里明确提到了要支持阿拉伯语、法语和英语。阿拉伯语是RTL(从右向左)书写,和LTR(从左向右)的英语、法语混排时,很多OCR引擎处理起来效果很差。Gemini Vision在模型层面就对多语言和混合排版有很好的支持,实测下来,对于阿拉伯语-英语混排的收据,识别准确率远高于我测试的其他几个服务。
- 结构化输出潜力:普通的OCR只是把图片上的文字“读”出来,返回一堆文本行。而Gemini Vision这类多模态大模型,能理解图像的上下文。通过精心设计的Prompt,我可以让它直接以JSON格式输出结构化的数据,比如
{“total_amount”: 150.00, “date”: “2023-11-05”, “merchant”: “某咖啡馆”}。这省去了自己用正则表达式或NLP模型从大段文本中抽取信息的步骤,大大简化了后端处理流程。 - 成本与易用性平衡:完全自研OCR模型不现实,而Tesseract对复杂版面和多语言的支持需要大量调优。云服务API虽然收费,但Gemini Vision的定价在可接受范围内,并且其API设计非常简洁,几行代码就能调用,降低了开发复杂度。
当然,这个选择不是没有代价。依赖云API意味着服务可用性受网络和Google服务状态影响。在架构设计时,我必须考虑重试机制和降级方案。比如,当Gemini API暂时不可用时,系统可以先将文档存入待处理队列,而不是直接向用户报错。
2.3 数据层与基础设施:Supabase的一站式解决方案
对于这样一个从零开始的创业型项目,我不想在基础设施上花费太多精力。Auth(用户认证)、Database(数据库)、Storage(文件存储)每一项单独搭建和维护都是成本。Supabase提供了基于PostgreSQL的BaaS(后端即服务),完美地满足了需求。
- 身份认证(Auth):Supabase Auth开箱即用,支持邮箱密码、第三方OAuth等多种登录方式。我只需要在前端集成它的客户端库,后端通过API Key或JWT来验证请求即可,自己不用写任何用户注册、登录、密码加密的代码。
- 数据库(Database):就是标准的PostgreSQL,但带有实时订阅功能。这意味着,当前端上传文档后,我可以让后端在处理完成后,通过Supabase的实时通道主动向前端推送状态更新,实现处理进度的实时展示,用户体验更流畅。
- 文件存储(Storage):用户上传的原始图片、处理后的JSON数据,都需要有个地方存。Supabase Storage提供了类似S3的桶管理,设置权限后,前端可以直接上传,后端也可以直接读写,非常方便。
使用Supabase最大的好处是“快”,它能让我在几天内就搭起一个具备生产环境基础能力的后台。但需要注意,它的免费 tier 有资源限制,当用户量增长后,需要密切关注用量并考虑升级计划。另外,将所有数据(用户、文档、文件)都放在一个供应商那里,从长期看也需要考虑供应商锁定的风险。不过对于项目的初期和中期,这个权衡是值得的。
3. 核心流程拆解与实现细节
3.1 文档处理流水线:从图片到决策
整个系统的核心是一个四步流水线。理解这个流水线,是理解项目如何工作的关键。
第一步:上传与预处理用户通过前端页面上传图片。这里的前端做了两件事:一是用<input type=”file”>配合拖拽库(比如react-dropzone)提供友好的上传体验;二是会在前端对图片进行简单的预处理,比如用浏览器的Canvas API将过大的图片压缩到合理尺寸(例如最长边不超过2000像素),并转换为WebP或JPEG格式。这一步能节省带宽和后续处理的开销。图片随后被发送到FastAPI后端的一个上传端点(如/upload)。
第二步:AI视觉识别与信息提取后端收到图片后,会先将其临时保存到本地或内存中,然后调用Gemini Vision API。这里的关键在于Prompt工程。你不能简单地问“图片里有什么字?”,而要给出清晰的指令。我的Prompt大致结构如下:
你是一个专业的文档分析助手。请分析这张收据/发票/处方图片,并严格按照以下JSON格式输出信息: { “document_type”: “receipt”, “items”: [ {“name”: “商品A”, “quantity”: 2, “unit_price”: 10.00, “total”: 20.00} ], “total_amount”: 150.00, “tax_amount”: 15.00, “date”: “YYYY-MM-DD”, “merchant_name”: “商户名称”, “currency”: “USD” } 请只输出JSON,不要有任何额外解释。如果某个字段无法识别,请将其值设为null。这个Prompt明确指定了输出格式、字段含义,并指示模型对不确定的内容返回null,这比返回一个错误猜测的值要好,因为后续的规则引擎可以专门处理null值。对于阿拉伯语等RTL语言,我发现在Prompt开头用对应语言写一句指令(如“这是一张阿拉伯语收据”),能小幅提升字段识别的准确率。
第三步:规则引擎决策拿到结构化的JSON数据后,就进入了规则引擎。这是我用Python写的一个可配置的模块。规则以JSON或YAML格式存储在数据库中,每条规则对应一种文档类型。例如,对于“员工报销收据”,规则可能包括:
total_amount必须小于1000美元。date不能是周末。merchant_name不能出现在“黑名单”中。- 关键字段如
total_amount、date、merchant_name均不能为null。
规则引擎会遍历所有规则,计算出一个“决策矩阵”。最终状态由最严格的失败条件决定:
- 任何一条“硬性”规则失败(如商户在黑名单),直接判为🚫 FLAGGED。
- 如果所有硬性规则通过,但有关键字段为
null或处于模糊状态(如金额识别出两个可能值),则判为⚠️ NEEDS_REVIEW。 - 只有所有规则通过且数据完整清晰,才判为✅ APPROVED。
第四步:结果存储与反馈决策完成后,系统会将原始图片、提取的JSON数据、决策结果(包括触发了哪条规则)一起存入Supabase数据库。同时,通过Supabase的Realtime功能,后端会向前端发送一个事件,通知用户处理完成。前端根据状态(APPROVED/NEEDS_REVIEW/FLAGGED)展示不同的界面,对于需要复核的,会提供一个“修正面板”(Fix Action Panel),让用户手动修正识别错误的字段,修正后可以一键重新触发处理流程。
3.2 “修正面板”的设计与实现技巧
“修正面板”是提升用户体验、弥补AI识别误差的关键功能。设计目标是:让用户用最少的点击完成修正。
前端实现上,当文档状态为NEEDS_REVIEW时,界面会展示一个可编辑的表格,里面是AI提取出的所有字段和值。对于识别错误的字段(比如把“7”识别成了“1”),用户可以直接在表格里修改。这里我用了React的状态管理库(如Zustand)来管理这个编辑状态。
一个重要的技巧是:不要每次修改都立即发送到后端。我采用的是“批量修正并重新提交”的模式。用户修改完所有他认为有误的字段后,点击一个“重新处理”按钮。这时,前端会将用户修正后的数据与原始AI提取的数据进行对比,生成一个“差异补丁”(diff patch),只将修改过的字段发送给后端。
后端的对应端点(如/reprocess)收到这个补丁后,会用它来更新之前存储的JSON数据,然后跳过OCR步骤,直接将更新后的数据送入规则引擎重新判断。这样做有两个好处:一是节省了宝贵的Gemini API调用次数(因为重新识别大概率会得到相同的结果);二是速度极快,用户几乎能立刻看到修正后的决策结果。
实操心得:在实现修正面板时,一定要记录每个字段的“置信度”或“原始识别值”。当用户把“1”改成“7”后,界面上最好能用一个小字或工具提示显示“AI识别为:1”,这能让用户明白修改的原因,增加对系统的信任感。
4. 开发环境搭建与本地调试实战
4.1 从零开始:环境配置与依赖安装
要让这个项目在本地跑起来,你需要准备好Node.js、Python和Docker环境。下面是我推荐的步骤,以及一些能避开初期坑点的细节。
前端(Next.js)环境:
- 确保你的Node.js版本在18以上。我推荐使用
nvm(Node Version Manager)来管理多个Node版本,切换起来很方便。 - 克隆项目后,进入
frontend目录(如果项目结构是monorepo,可能需要定位到具体的Next.js项目目录)。 - 运行
npm install。这里可能会遇到的第一个坑是网络问题导致某些包(尤其是带有原生绑定的包)下载失败。如果你遇到node-gyp编译错误,大概率是需要安装Python和构建工具。在macOS上,可以通过xcode-select --install安装命令行工具;在Windows上,需要安装Visual Studio Build Tools并选择“C++桌面开发” workload。 - 安装完成后,先别急着运行。检查根目录下是否有
.env.local或.env.example文件。你需要复制一份并填入必要的环境变量,最核心的就是Supabase的URL和匿名Key。这些值需要你去Supabase项目设置里获取。
后端(FastAPI)环境:
- Python版本建议3.9以上。强烈建议使用虚拟环境来隔离项目依赖。可以用
python -m venv venv创建,然后用source venv/bin/activate(Mac/Linux)或venv\Scripts\activate(Windows)激活。 - 在激活的虚拟环境中,运行
pip install -r requirements.txt。这里常见的坑是某些包(如pydantic、fastapi)版本冲突。如果安装失败,可以尝试先单独安装核心包pip install fastapi uvicorn,再安装剩下的。 - 后端同样需要环境变量。创建一个
.env文件,填入GEMINI_API_KEY、SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY(注意,后端通常使用权限更高的Service Role Key,而不是前端的Anon Key)以及数据库连接信息。
4.2 联调与问题排查:让前后端“握手”成功
前后端都启动后(前端npm run dev,后端uvicorn main:app --reload),真正的挑战才开始:让它们正常通信。
第一步:解决CORS(跨域)问题。前端运行在localhost:3000,后端在localhost:8000,浏览器会因为安全策略阻止请求。在FastAPI中,你需要显式地配置CORS中间件。在你的main.py里,一定要加上类似下面的代码:
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() # 配置CORS,允许前端域名访问 app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000"], # 你的前端地址 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )第二步:模拟API调用,测试后端逻辑。在开发初期,AI模型调用和数据库操作都可以先“模拟”(Mock)掉。我的做法是,在后端代码里设置一个配置开关USE_MOCK_OCR。当它为True时,OCR模块不调用真实的Gemini API,而是返回一个预设好的、结构化的JSON数据。同样,数据库操作也可以先用一个内存中的字典或列表来模拟。这样,你可以在不依赖外部服务的情况下,完整地测试前端上传、后端处理、前端展示结果的整个流程,极大提升开发效率。
第三步:使用Docker Compose进行一体化管理。当项目依赖增多(比如还需要一个独立的Redis做队列,或者PostgreSQL数据库)时,手动启动每个服务很麻烦。我强烈建议使用docker-compose.yml来定义所有服务。一个简单的配置示例如下:
version: '3.8' services: postgres: image: postgres:15 environment: POSTGRES_DB: scanaction POSTGRES_USER: postgres POSTGRES_PASSWORD: yourpassword volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" backend: build: ./backend depends_on: - postgres environment: - DATABASE_URL=postgresql://postgres:yourpassword@postgres:5432/scanaction - GEMINI_API_KEY=${GEMINI_API_KEY} ports: - "8000:8000" volumes: - ./backend:/app frontend: build: ./frontend depends_on: - backend environment: - NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 ports: - "3000:3000" volumes: - ./frontend:/app - /app/node_modules - /app/.next volumes: postgres_data:这样,只需要一个docker-compose up命令,数据库、后端、前端全部启动,并且它们之间的网络是互通的,环境变量也集中管理,非常适合团队协作和持续集成。
5. 部署上线与生产环境考量
5.1 从本地到云端:部署策略选择
项目在本地跑通后,下一步就是部署到公网,让其他人也能用。对于全栈应用,你有几种选择:
- Vercel + 云服务器/容器平台:这是我最推荐给个人或小团队的方式。Next.js前端直接部署在Vercel上,几乎是零配置,并且能享受边缘网络带来的速度优势。FastAPI后端则可以部署在更灵活的云服务器(如AWS EC2、Google Cloud Run、或国内的云厂商)或容器平台(如Railway、Fly.io)上。你需要确保后端服务有一个稳定的公网域名或IP,并在Vercel的前端环境变量中正确配置这个后端地址。
- 全栈容器化部署:如果你希望运维更统一,可以用Docker将前后端都容器化,然后使用Kubernetes(如GKE、EKS)或更简单的Docker托管服务(如Portainer)来管理。这种方式更专业,但复杂度也更高。
- Serverless架构:将FastAPI后端也改造成Serverless函数(例如使用AWS Lambda或Google Cloud Functions),配合API Gateway。这种模式按需付费,成本可能更低,但需要将代码适配成无状态的形式,并且冷启动可能会影响首次请求的响应速度。
我最初采用的是第一种方案。前端在Vercel,后端部署在了一台云服务器上。这里的关键是配置生产环境变量。在Vercel的项目设置里,你需要设置NEXT_PUBLIC_API_BASE_URL为你后端的生产地址(如https://api.yourdomain.com)。在后端服务器上,则通过系统的环境变量或.env.production文件来设置GEMINI_API_KEY和数据库连接串等敏感信息,绝对不要将这些信息硬编码在代码里或提交到Git仓库。
5.2 性能优化与监控要点
一旦服务上线,你就会开始关心它跑得快不快、稳不稳。
性能优化方面:
- 图片处理优化:用户上传的图片可能非常大。在后端处理前,应该增加一个图片压缩和格式转换的步骤。我使用
Pillow库,将图片统一转换为RGB模式,并缩放到一个合理的最大尺寸(如1920px),这能显著减少内存占用和后续OCR模型的处理时间。 - 异步任务队列:文档处理(尤其是调用Gemini API)可能耗时几秒。如果让HTTP请求一直等待处理完成,很容易超时并耗尽服务器连接。正确的做法是引入一个任务队列(如Celery + Redis,或更简单的RQ)。当用户上传文档后,后端立即返回一个“任务已接收”的响应和一个任务ID,然后将实际的OCR和规则处理任务放入队列异步执行。前端可以通过这个任务ID轮询或通过WebSocket来获取处理进度和结果。
- 数据库查询优化:随着文档数量增长,查询会变慢。确保为常用的查询字段(如
user_id、status、created_at)建立数据库索引。Supabase的PostgreSQL可以直接在SQL编辑器里创建索引。
监控与日志:没有监控的系统就像在黑夜中开车。至少要做以下几件事:
- 应用日志:在FastAPI中使用像
structlog这样的结构化日志库,将关键事件(如“用户上传文档”、“OCR调用开始/结束”、“规则引擎决策”)以及错误信息记录下来。日志要输出到标准输出(stdout),这样可以被Docker或系统服务管理器(如systemd)捕获,并方便地导入到日志聚合服务(如Logtail、Papertrail)中。 - 错误追踪(Error Tracking):集成Sentry或Rollbar这样的服务。它们能自动捕获代码运行时未处理的异常,并发送详细的堆栈信息、用户上下文到你的仪表盘,让你能第一时间发现和定位生产环境的问题。
- 基础指标监控:使用Uptime Robot或更好的服务(如Datadog、New Relic)来监控你的后端API是否可访问,并设置响应时间警报。如果API平均响应时间超过2秒,你就该去查查是不是数据库慢了或者队列堵了。
踩坑实录:有一次,用户突然反馈上传一直失败。查日志发现是Gemini API返回了“429 Too Many Requests”错误。原来是我在代码里没做请求限流,当短时间内有多个用户上传时,触发了Gemini API的速率限制。解决方案是在调用Gemini的代码层加入一个简单的令牌桶(Token Bucket)限流,或者使用更健壮的队列,确保向外部API发送的请求是平稳的。
6. 商业模式思考与未来迭代方向
6.1 从项目到产品:Freemium模型设计
在项目介绍中提到了“Freemium model — Pro tier at $9/month”。这不仅仅是一个定价标签,背后是一套产品化的思考。
免费层(Free Tier)的设计:目的是降低使用门槛,获取大量用户。免费层通常会有严格的限制,例如:
- 每月最多处理50张文档。
- 仅支持基础的收据和发票类型。
- 处理速度较慢(可能使用优先级较低的队列)。
- 只能使用预设的通用规则,无法自定义。 这些限制要足够“痒”,让有真正需求的用户愿意付费升级,但又不能“疼”到让用户体验极差而直接离开。
专业层(Pro Tier)的价值锚定:每月9美元的价格,需要提供清晰、可感知的价值。专业层可能包括:
- 无限制或高额度的文档处理。
- 支持所有文档类型(包括处方、合同等)。
- 优先处理,速度更快。
- 核心功能:自定义规则引擎。用户可以自己创建复杂的规则(如“如果商户名称包含‘某供应商’且金额大于5000,则标记为‘需要经理审批’”)。
- 团队协作功能,如共享规则库、团队使用情况仪表盘。
- API访问权限,让用户可以将Scan & Action集成到自己的ERP或财务系统中。
实现上,需要在用户表(Supabaseprofiles)中增加subscription_tier(免费/专业)和monthly_usage等字段。在后端的规则引擎入口和OCR调用前,检查用户的订阅状态和用量,如果免费用户超限,则返回友好的升级提示。
6.2 技术债与未来功能规划
任何一个项目在初期都会为了快速验证想法而欠下一些“技术债”。随着项目发展,重构和规划新功能就变得很重要。
近期需要偿还的技术债:
- 测试覆盖率:初期可能只写了核心OCR模块和规则引擎的单元测试。需要补充API端点的集成测试,以及前端组件(尤其是修正面板)的交互测试。一个稳定的测试套件是未来添加新功能而不破坏旧功能的保障。
- 配置化管理:初期可能把规则硬编码在Python字典里。需要将其完全迁移到数据库或配置文件中,并开发一个简单的管理后台,让非技术人员(如业务运营)也能通过UI界面来增删改查业务规则。
- 代码结构重构:随着功能增多,
main.py可能变得臃肿。应该按照功能模块(如routers/、services/、models/、core/)来重新组织代码,提高可读性和可维护性。
未来可探索的功能方向:
- 多模型降级与融合:不能把所有鸡蛋放在Gemini一个篮子里。可以集成备用OCR服务(如Azure、AWS),当主服务失败或对某种特定格式(如手写体)识别不佳时,自动降级或尝试其他服务,甚至将多个模型的结果进行智能融合,提高整体鲁棒性。
- 工作流自动化:当前的“决策”是终点,但可以将其变为起点。例如,当一张发票被标记为
APPROVED后,可以自动触发一个Webhook,将数据推送到用户的会计软件(如QuickBooks、Xero)中,实现真正的端到端自动化。 - 机器学习优化规则:目前的规则是人工预设的“if-else”逻辑。可以引入简单的机器学习,分析历史上被标记为
FLAGGED或NEEDS_REVIEW的文档特征,自动建议新的规则,或者为NEEDS_REVIEW的文档预测其最终被人工核准的概率,实现半自动化的规则优化。
这个项目从构思到实现,最大的体会是:构建一个可用的AI应用,技术选型和架构设计的重要性不亚于算法本身。如何将大模型的能力稳定、高效、低成本地集成到业务流程中,如何设计用户体验来弥补AI的不确定性,这些工程上的挑战往往比调一个模型参数更有趣,也更能决定产品的成败。如果你也在做类似的东西,不妨先从定义一个清晰的、端到端的流水线开始,然后逐个环节去打磨和优化。