
去年年底我接手了一个挺拧巴的项目业务员录一张销售单要在ERP、进销存、财务系统里各敲一遍月底财务组三个人对着几十个Excel表头抓狂。项目名称很响亮——叫“AI中台”但预算、人力、机房条件全都配不上“中台”两个字。很多人一听到“中台”就觉得要上K8s、大数据、数据湖其实落地时完全可以用一台服务器加一套轻量容器编排把OCR识别、字段抽取、自动回写、数据同步、对账规则这几件事串起来。这篇文章就是我当时做“轻型AI中台”部署的一个完整复盘所有方案都以能落地、能维护为准适合预算有限、系统分散、又不想动核心业务系统的团队参考。1. 先想清楚AI中台要解决的到底是哪两个问题1.1 重复录入的真实形态数据孤岛加人工搬运先别急着谈技术我把业务问题拆开看。所谓重复录入本质是“数字孤岛”加“人工搬运”每个系统都有自己的数据库、自己的字段设计、自己的审批流但它们之间没有接口。人工就成了唯一的连接器。我观察到的重复录入基本有三种形态同一张单据要在多个系统手工录入。最典型的就是采购入库单ERP里录一遍、OA里走审批抄一遍、库存系统再更新一遍。纸质单据拍照后人工转录。比如供应商送来的送货单、发票、银行回单都是先拍照留档再由文员看着图片敲字。人为二次加工。业务员从A系统导Excel改几列再粘到B系统期间还可能改错。这三种形态背后是同一个矛盾业务系统不能随便改接口也没人维护但数据必须流转。传统做法是找开发商做接口报价高、周期长一个接口动辄两周。而AI中台的思路是换一条路——不要求系统之间互相理解让中台去“读”数据然后代替人把字段填进去。1.2 对账困难的根子口径、时间、渠道的错位对账难难的不只是数据量大而是三样东西对不上。第一是数据口径不统一。销售系统里的一笔收款记录的是“客户名订单号”银行流水里记的是“商户号流水号”财务系统里又是另一套科目编码。同一个业务事件各个系统叫法完全不同靠人脑做映射映射表时间一长必然出错。第二是时间基准不统一。有的系统按业务发生日记账有的按银行入账日还有的按财务凭证日。一笔跨月交易销售说算上个月银行说这个月入账两边怎么都对不齐。第三是差异处理没有标准流程。两边数据对不上之后通常就是靠资历老的同事凭经验拍脑袋。流水少还好流水一多月底加班是常态。这些问题靠单个工具解决不了但靠一条“归集-清洗-比对-派发”的链路可以。这也是轻型AI中台最核心的价值它不改变任何业务系统的逻辑只是在边上建了一条自动处理通道。1.3 轻型AI中台的功能边界我不要大而全我必须强调一下“轻型”这个词的边界。真正意义上的中台往往包含统一权限、数据治理、模型训练平台、大数据底座这些对一个几十人上百人的公司来说基本是负担。我的选择是砍掉所有非核心能力只保留四件事识单OCR识别、关键字段抽取。录单校验通过之后自动回写目标业务系统。归数把各系统数据按统一口径抽到分析库。对账按规则碰平两边数据差异自动生成工单。这四件事用一条工作流串起来就是整个AI中台的全部。不搞数据湖不搞微服务不搞复杂权限。能解决最痛的业务问题还能让一个运维兼开发的人维护得住这才是轻型AI中台正确的打开方式。2. 部署架构与技术选型一台服务器如何撑起整条链路2.1 硬件基线与操作系统准备先算一笔账。OCR推理、工作流引擎、数据库、监控这四个组件同时跑CPU 8核16GB是最低门槛但我不推荐——实测高峰期n8n和OCR服务同时工作8核会频繁打满影响回写任务的时效。我的建议基线是双路或单路至强16核32GB内存系统盘一块960GB SSD数据盘一块2TB HDD。显卡可要可不要如果OCR量一天在几千张以内纯CPU用PaddleOCR的轻量模型单张识别大概1.5到3秒完全扛得住。如果单日上万张建议上一块12GB显存的显卡推理时延能降到0.3秒以内。操作系统选的Ubuntu 22.04 LTS。理由只有一个Docker环境最干净内核版本合适社区资料多遇到问题随便搜都有答案。系统装完后第一件事是配置好防火墙和SSH密钥登录然后关掉密码登录。这一步虽然和业务无关但内网服务一旦暴露未加固的系统就是靶子。2.2 组件选型为什么是这一套而不是更流行的方案整个部署只有五个核心组件我列了一个对照表把选型理由写清楚能力域我选用的组件选型理由替代方案工作流编排n8n定时任务、webhook、HTTP请求、异常重试都是可视化配置维护成本低Dify、Node-REDOCR与字段抽取PaddleOCR Python FastAPI自封装中文识别成熟、模型权重可离线、完全内网可控云OCR、Tesseract业务状态库MySQL 8各系统对接成熟回写记录、幂等去重都放这里PostgreSQL对账明细库ClickHouse亿级流水聚合秒出结果适合月底大批量操作Doris监控告警Prometheus Alertmanager生态最全容器部署方便Zabbix、Uptime Kuma这里重点说两个决策。第一工作流引擎为什么不用自研Python脚本因为业务规则会不断变。对账差异的推送渠道、录入失败的人工处理队列、定时执行的时间窗口这些需求每周都可能调整。n8n的可视化编排让业务同事也能看懂流程改一个节点配置不用发版省了太多事。第二OCR为什么不用云服务因为很多企业数据不能外流而且对账涉及的单据包含供应商、金额、合同编号合规风险太高。本地部署PaddleOCR模型文件放在内网盘里推理服务完全隔离数据不出内网这是最稳妥的选择。2.3 Docker Compose编排细节整个平台我用Docker Compose管理没有上K8s。原因很直白单机部署K8s的节点管理、服务发现、存储编排全是额外复杂度。docker-compose.yml的关键结构如下version: 3.8 networks: ai-net: driver: bridge services: n8n: image: n8nio/n8n:latest container_name: ai-n8n restart: always networks: - ai-net ports: - 5678:5678 environment: - N8N_HOST10.0.20.15 - N8N_PORT5678 - N8N_PROTOCOLhttp - DB_TYPEpostgresdb - DB_POSTGRESDB_DATABASEn8n - DB_POSTGRESDB_HOSTpostgres - DB_POSTGRESDB_USERn8n - DB_POSTGRESDB_PASSWORDsecure_password volumes: - /data/ai-platform/n8n:/home/node/.n8n deploy: resources: limits: cpus: 1.0 memory: 1g postgres: image: postgres:15-alpine container_name: ai-postgres restart: always environment: - POSTGRES_USERn8n - POSTGRES_PASSWORDsecure_password - POSTGRES_DBn8n volumes: - /data/ai-platform/postgres:/var/lib/postgresql/data ocr-api: build: ./ocr-api container_name: ai-ocr-api restart: always networks: - ai-net ports: - 9000:9000 volumes: - /data/ai-platform/models:/models - /data/ai-platform/uploads:/uploads deploy: resources: limits: cpus: 4.0 memory: 4g clickhouse: image: clickhouse/clickhouse-server:23.8 container_name: ai-clickhouse restart: always ulimits: nofile: soft: 262144 hard: 262144 volumes: - /data/ai-platform/clickhouse:/var/lib/clickhouse ports: - 8123:8123 - 9001:9000 mysql: image: mysql:8.0 container_name: ai-mysql restart: always environment: - MYSQL_ROOT_PASSWORDsecure_root_password volumes: - /data/ai-platform/mysql:/var/lib/mysql ports: - 3306:3306 prometheus: image: prom/prometheus:v2.48.0 container_name: ai-prometheus restart: always volumes: - /data/ai-platform/prometheus:/etc/prometheus ports: - 9090:9090 alertmanager: image: prom/alertmanager:v0.26.0 container_name: ai-alertmanager restart: always volumes: - /data/ai-platform/alertmanager:/etc/alertmanager ports: - 9093:9093有几个细节值得注意。n8n的deploy.resources.limits很重要不限制的话n8n跑复杂工作流时会把机器吃满OCR服务直接被拖垮。我给n8n限制1核1GOSS服务限制4核4G实测高峰时整体CPU控制在60%以内。所有数据目录都挂载到宿主机/data/ai-platform下面好处是备份方便——直接tar打包这个目录就行也方便迁移。容器重启数据不丢。2.4 数据流向与目录规划整个系统数据流的走向是单据图片或电子文件先进/uploads临时目录OCR服务处理后把结构化字段写到MySQL的ocr_records表n8n定时轮询这个表把校验通过的记录通过HTTP接口回写业务系统各业务系统再把流水数据按天同步到ClickHouse最后对账服务在ClickHouse里做比对。目录我统一规划成/data/ai-platform/ ├── n8n/ # n8n工作流、凭据 ├── postgres/ # n8n元数据库 ├── mysql/ # 业务状态库 ├── clickhouse/ # 对账分析库 ├── ocr-api/ # OCR服务镜像构建目录 ├── models/ # PaddleOCR模型权重 ├── uploads/ # 临时图片文件 ├── prometheus/ # 监控配置 └── alertmanager/ # 告警配置这个规划一开始就要定好不然后期容器重建时到处找数据目录会非常痛苦。我踩过这个坑早期是把数据写在容器内一次docker compose down再up所有配置记录全没了。从那以后所有状态数据一律挂宿主机目录。3. 第一段链路单据自动录入OCR识别到系统回写3.1 识别链路的整体设计单据自动录入这条链路的起点是“一张图片或PDF”终点是“业务系统里一条结构化记录”。中间经过五个步骤图像预处理、OCR识别、字段抽取、规则校验、幂等回写。n8n在这里充当调度器。工作流配置成每5分钟跑一次去MySQL的ocr_records表拉状态为pending的记录逐条调用OCR服务的接口。OCR服务本身是无状态的它只负责“看图识字”把文本和坐标返回给n8n再由n8n里的规则节点做字段抽取。这样拆分的好处是OCR服务可以被其他业务复用比如合同识别、发票验真而规则配置在n8n里业务人员可以自行调整字段映射关系不用改代码。3.2 OCR服务的部署与字段抽取OCR服务我写了一个FastAPI应用核心代码精简如下import base64 import logging import io from fastapi import FastAPI, File, UploadFile from paddleocr import PaddleOCR from PIL import Image, ImageEnhance, ImageFilter logger logging.getLogger(ocr_api) app FastAPI() # 模型权重提前放到/models目录内网离线加载 ocr PaddleOCR( det_model_dir/models/det, rec_model_dir/models/rec, cls_model_dir/models/cls, use_angle_clsTrue, langch, show_logFalse ) def preprocess_image(image: Image.Image) - Image.Image: 图像预处理统一尺寸、增强对比度、轻度锐化 # 过小的图放大提高小字识别率 if max(image.size) 1500: ratio 1500 / max(image.size) image image.resize((int(image.width * ratio), int(image.height * ratio))) # 彩色图转灰度可提升印刷体稳定性 image image.convert(L) image ImageEnhance.Contrast(image).enhance(1.8) image image.filter(ImageFilter.SHARPEN) return image app.post(/ocr) async def ocr_image(file: UploadFile File(...)): data await file.read() image Image.open(io.BytesIO(data)) image preprocess_image(image) # 保存预处理图便于事后排查识别问题 image.save(f/uploads/processed_{file.filename}) result ocr.ocr(image, clsTrue) items [] if result: # result格式[[ [box], (text, confidence) ], ...] for line in result: if not line: continue box, (text, conf) line[0], line[1] points [round(p, 2) for p in box] items.append({text: text, confidence: round(conf, 4), box: points}) return {items: items}这里最重要的其实是preprocess_image。最初我直接拿原图识别供应商送货单的扫描件一率偏低。后来加了放大、灰度、对比度增强之后识别精度提升肉眼可见。3.3 字段抽取与校验规则怎么保证录进去的就是对的OCR识别出来只是“一串带位置坐标的文字”要变成可用字段还得做一层抽取。我的做法是“正则加规则”组合而不是依赖大模型。虽然现在做大模型抽取很流行但单据字段通常有强烈的排版规律正则和上下文模板足够而且完全可控。比如金额字段识别结果里会有“金额”、“小写”、“¥1,234.56”等字样。我先按坐标找“金额”标签附近的文本再用正则提取数字。之后还有一道关键校验大写金额转数字。发票和送货单上往往同时有大写和小写金额两者不一致就说明识别或录入有问题直接丢到人工确认队列。这个校验逻辑在n8n的Code节点里实现function chineseAmountToNumber(str) { const result { ok: false, value: null }; const digits { 零: 0, 壹: 1, 贰: 2, 叁: 3, 肆: 4, 伍: 5, 陆: 6, 柒: 7, 捌: 8, 玖: 9 }; const units { 拾: 10, 佰: 100, 仟: 1000 }; const bigUnits { 万: 10000, 亿: 100000000 }; // 解析逻辑简化按位累加关键是角分 // ... return result; }为什么校验这层不能省因为OCR哪怕做到99%准确率在几千张单据的基数下出错的数量依然可观。这些错一旦直接写进ERP后面对账全是麻烦。宁可多花10%的单据走人工确认也不要让错误数据进入业务系统。3.4 幂等回写同一个单子不能录两次回写业务系统前必须处理幂等。最直接的方案是给每条OCR记录生成一个唯一关联键外部单据号单据类型批次号。n8n在回写前先查MySQL里有没有相同关联键的记录有就跳过。但仅靠中台自己判断还不够还要配合业务系统接口的幂等处理。如果业务系统不支持幂等就在接口文档里要求对方加一个唯一约束字段。没有这一层定时任务一旦被重复触发业务系统里就会冒出两条一样的单据录入问题变成对账问题得不偿失。接入这套链路后实测效果是一张送货单从扫描到进入ERP平均用时从人工录入的3分钟缩到30秒高峰期一天300张单也能从容处理。4. 第二段链路自动对账从多系统归集到差异推送4.1 数据归集方案与增量同步对账链路的第一步是把各系统的数据搬到同一个地方。我没有选用DataX或Flume这种重量级工具因为各业务系统开放的接口参差不齐最通用的方式是针对每个系统写一个增量拉取脚本用cron调度。具体做法是给每个业务系统定义一个统一视图字段固定为biz_date 业务日期 channel_code 渠道编码 order_no 业务单号 amount_cents 金额单位为分 status 状态valid/cancel/refund update_time 最后更新时间然后各系统按这个视图提供数据。增量同步的关键是update_time字段。我要求每个业务系统都开放这个字段同步脚本只拉取update_time大于上次记录最大值的行写入ClickHouse。这个方案比全量比对快了不是一点半点。全量方式每天几百万行全扫同步一次要半小时增量方式几分钟搞定对源库压力也小。4.2 数据清洗与口径统一数据进到ClickHouse之后第一件事是清洗。清洗分四个维度金额统一到分。有的系统用元有的用万元有的用带两位小数不长格式统一转成整数分再存储。日期统一为业务日期。银行流水用入账日销售订单用订单创建日对账时统一以业务日期为基准再附加一个偏移规则字段。渠道编码建立映射表。每个系统的渠道码都不同维护一张channel_mapping表统一对应关系。无效数据标记。取消单、冲正单、退款单不能直接删掉而是在status字段里标记对账规则里再特殊处理。清洗逻辑放在ClickHouse的物化视图中实现而不是同步时处理。这样原始数据保留一份清洗逻辑可以随时调整改完重刷视图就能看到新口径的结果。4.3 对账规则引擎与差异工单对账的核心逻辑是两边数据按业务单号关联然后比对状态和金额。我用的最基础也最有效的SQL模式SELECT a.biz_date, a.order_no, a.amount_cents AS system_amount, b.amount_cents AS bank_amount, a.status AS system_status, b.status AS bank_status FROM system_orders AS a LEFT JOIN bank_flows AS b ON a.order_no b.ref_order_no WHERE a.biz_date today() AND (b.ref_order_no IS NULL OR a.amount_cents ! b.amount_cents)这个查询产生三类差异单边账系统有记录但银行没有或反之。通常意味着未达账项需要时间验证。金额不平两边都记录但金额不一致大概率是入账拆分或手续费误扣。状态异常比如系统标记为已退款但银行没有对应退款流水。n8n定期跑这个查询把结果写到差异表同时推送到协同工具。推送内容包含日期、单号、系统金额、银行金额、差异类型。每个差异自动生成一个可跟踪的处理单指定负责人处理完填写结果。4.4 对账看板与告警对账链路本身也需要被监控。我配置了三个核心指标对账任务是否按时执行。每天凌晨1点跑全量核对如果任务没跑或者跑崩了5分钟之内Alertmanager就推告警出来。差异率是否异常。平时差异率在0.5%左右如果某天突然跳到2%说明上游某个系统数据质量出了问题。同步延迟是否过大。业务系统超过30分钟没更新就说明同步链路断了。这些指标用Prometheus采集Alertmanager再通过webhook推送到办公集群工具。上线之后月底财务对账从“三个人干三天”变成“一个人看半小时差异报告”。5. 部署实战中的坑与参数调优5.1 OCR识别率从86%到99%的调试过程第一版OCR在测试集上的表现印刷体发票识别准确率96%左右但供应商送货单因为存在手写备注、印章遮挡、复写纸底色不均准确率直接掉到86%。86%意味着每100张单就有14张需要人工返工完全不可用。我做了三件事把准确率提上来。第一图像预处理强制放大到1500像素以上。送货单字体普遍偏小特别是金额数字原始扫描件如果只有800像素宽识别结果惨不忍睹。第二调整识别参数。PaddleOCR的识别置信度阈值从默认0.5调到0.6低置信度的宁可判为“不确定”也不硬给结果。第三加“字段级二次校验”。金额、日期、单号这些关键字段在n8n里再做一次格式校验和逻辑校验。比如日期必须符合YYYY-MM-DD格式且不能是未来日期金额必须大于0。校验不通过直接进人工确认队列。最终线上统计录入准确率达到99.2%剩余0.8%基本都是手写太潦草或印章遮盖严重的极端场景。5.2 容器资源争抢模型加载把整个平台拖垮部署早期遇到过一个很头疼的问题OCR服务每次容器重启后第一次请求要重新加载PaddleOCR模型加载过程会瞬间吃满全部CPU和内存n8n和其他容器全部卡死。这暴露了两个设计缺陷一是模型没有预热二是容器没有资源限制。解决办法是双管齐下。在docker-compose.yml里给每个服务都设置了cpus和memory限制特别是OCR服务限制4核4G模型加载高峰也不会拖垮宿主机。同时在FastAPI应用里加了启动预热逻辑lifespan事件里启动时先跑一次空图片识别把模型加载完再对外提供服务。这个坑给后面的教训是生产环境跑容器资源限制必须配置任何人都不能例外。否则一个服务的异常行为会把整台机器拖死。5.3 定时任务重复执行与失败重试n8n的定时触发器如果上次任务还没跑完下一次触发时会再次启动造成数据重复处理。这在录入链路里尤其危险回写接口没有幂等保护就会产生重复单据。我的处理方案是给每个工作流加“并发锁”。n8n里用MySQL表实现一个简单的workflow_lock每次任务启动先尝试获取锁拿不到就跳过本次执行。失败重试也要设计。回写业务系统的HTTP请求我都配置了5秒超时和最多3次重试。但重试可能导致重复请求所以回写接口必须带关联键服务端根据关联键去重。这件事在接口对接时就要和业务系统开发确认清楚否则后面对账时会不断冒出幽灵单据。5.4 时钟不一致与跨月单据的兜底机制对账时还遇到过时钟不一致的问题某个业务系统的服务器时间比中台慢了将近3分钟导致凌晨同步时本该属于昨天的流水被算到今天月末对账总差几笔。解决方法是同步时不依赖源系统的系统时间统一使用源系统数据里的update_time和biz_date。跨月单据则单独配置一个“缓冲期”白名单比如每月1日、2日两天允许把上月末的延迟流水自动归到上个月。这个规则虽然简单却解决了对账中最常见的跨月争议。最后说点个人体会这套轻型AI中台上线两个月之后我已经不怎么盯着它了。最直观的变化是业务员录单时间大幅缩短月底财务从一天到晚翻Excel变成只需要处理几十条差异工单。我自己的维护负担也很轻日常只需要每周看一眼监控和告警偶尔在n8n里调一调规则节点。如果你们团队也想落地类似的东西我的建议是一样的不要一上来就追求大而全。挑一张最高频、最痛的单据类型把OCR识别到回写的链路跑通再选一个对账场景把归集到差异推送的完整流程走一遍。这两条链路一旦稳定价值已经超过很多花哨的“中台”项目。