如果一个业务部门每天要花两小时把同一份合同信息敲进三套系统,月底还要对着三张Excel表查账查到头皮发麻,那它缺的往往不是钱,也不是人手,而是一层能替人干脏活的“中间层”。过去这半年,我陆续帮两家做贸易和供应链的小公司搭了这种轻型AI中台,目标很朴素:让同一份数据只录入一次,让对账从两天变成十分钟。今天这篇就把我的选型过程、部署步骤、踩坑记录完整拆出来,给正准备在公司内部落一套轻量AI中台的IT负责人和业务骨干做个参考。
这套方案没有追求“大而全”,也没有采购高价商业中台产品,核心就三样东西:一个放在公司内部的模型服务,一个能读单据、抽字段的回填服务,一个能做自动匹配、生成差异报表的对账引擎。我记得第一次演示给财务看的时候,系统把一张增值税发票自动抽成结构化字段并写进台账,现场静了两秒,然后财务姐姐问了一句:“它不会哪天把钱数错吧?”这个问题其实问到了点子上——AI能干活,但得有人替它兜底。下面我要讲的,就是怎么让AI干活,又怎么把兜底机制一起做进去。
1. 为什么“轻型”AI中台才是多数企业的解法
1.1 重复录入和对账困难,本质是同一个数据问题
先说重复录入。绝大多数公司都不是没有系统,而是系统太多、太碎。我服务的那家贸易公司,一张销售订单要走三个地方:客户信息录进CRM,商品和价格录进ERP,合同金额和回款计划录进财务的自建台账。每个系统都是某个部门自己选了用的,互相之间没有接口,时间一长也就没人敢动。结果就是同一个合同号、同一个金额、同一个客户名,被不同的人在不同的系统里敲了三遍,敲错一遍,后面的对账就开始难受。
而对账困难,底层原因也差不多。财务拿到的银行流水、ERP开出的凭证、业务手里的合同台账,虽然都指向同一笔交易,但三边数据的字段命名不一样、日期口径不一样、金额精度不一样。比如业务记的是“含税12.8万”,ERP里拆成了“不含税10.85万+税1.95万”,银行流水又只显示“收款128,000”。这时候如果靠人肉比对,脑子要不停换算口径,眼睛要看几百行数据,不出错才怪。
所以你会发现,重复录入和对账其实是同一件事的两面:公司缺一个能统一接收、转换、分发“事实数据”的中间层。轻型AI中台干的就是这件事——它不替代任何业务系统,而是在业务系统和原始单据之间加一个会读文档、会抽字段、会做映射、会写回数据的“翻译官”。
1.2 大而全的中台梦,为什么在小团队里落不了地
早些年一提中台,很多人第一反应是“数据中台+业务中台+组织中台”三件套,要有专门的数据治理委员会,要建一套主数据管理系统,要搞湖仓一体。这套东西在几百人的大集团也许合理,但放到二三十人的业务团队、几个人的IT组里,根本跑不起来。原因很直接:
- 建设周期太长。数据治理没做完,流程就推不动,业务早已没耐心。
- 维护成本太高。需要专职数据工程师、算法工程师,小公司招不到也养不起。
- 业务系统动不了。中台想统一主数据,但各部门并不愿意改自己的CRM和ERP,协调成本比开发成本还高。
所以现在越来越多企业走的是“轻型AI中台”路线:不治理历史数据,只收新数据;不动存量系统,只做旁边挂接。上线周期控制在一到两周,开发量小到一个人能维护,核心能力就靠大模型“看懂文档”和规则引擎“理清关系”这两条腿走路。
1.3 轻型AI中台的职责边界
在动手之前,最好先给这台“中台”画清楚边界。我这里画的职责线是这样的:
- 输入侧:接收合同PDF、发票图片、订单Excel、银行流水等原始文件。
- 处理侧:用大模型抽取结构化字段,用规则做格式校验、字段映射和相似度匹配。
- 输出侧:通过API写回各类业务系统,或生成对账差异报表。
- 不做的:不去改CRM/ERP的库表结构,不做历史脏数据清洗,不碰企业核心财务系统的写权限。
这个边界非常重要。只要守住“只做新数据的统一入口和匹配出口”,旧系统里的历史数据哪怕烂成一锅粥,也影响不了新流转的订单。把边界定清楚之后,再选技术、写代码,心态就稳很多。
2. 技术选型:这套中台需要的组件与取舍
2.1 全程组件推荐清单
轻型AI中台不是某个产品,而是一组服务的组合。我最终采用的组件清单如下,每一层都给了首选和备选,以及当时取舍的理由:
| 分层 | 首选方案 | 备选方案 | 选择理由 |
|---|---|---|---|
| API与流程服务 | FastAPI + Python 3.11 | Flask | FastAPI自带异步、Pydantic校验,写对接接口和任务队列都很顺手 |
| 大模型推理 | Ollama(本地) | vLLM、外部API(DeepSeek/通义) | Ollama部署最简单,适合单机;并发高用vLLM;数据敏感度低可临时调API |
| 文档解析与OCR | pdfplumber + PaddleOCR | mupdf、tesseract | PDF文本层用pdfplumber足够,扫描件才走PaddleOCR,整体可控 |
| 应用编排 | 自研Python脚本/服务 | Dify本地版 | 自研定制空间大,适合对接内部系统;Dify适合快速搭交互式应用 |
| 数据存储 | MySQL 8 | PostgreSQL、Doris | 主数据量不大,MySQL维护成本最低;对账宽表数据量大后可上Doris |
| 调度与重试 | Celery + Redis | 自写线程池 | 批量回填和异步抽取场景需要可靠的重试机制 |
这套组合下来,物理机只需要一台带显卡的服务器,或者一台16G显存以上的工作站,预算压力比商业中台小一个数量级。
2.2 本地模型还是外部API:三个判断标准
在选大模型能力来源时,我建议不要一上来就追求“私有化部署”这个身份,先回答三个问题:
- 数据敏感度:合同价格、客户名录、银行流水这些数据如果流出公司,风险能不能承受?不能承受就老老实实装本地模型。
- 并发量级:每天要处理的单据是几十张还是几千张?几十张用本地小模型串行处理也够;几千张需要并发推理,可能要上vLLM加队列。
- 算力与成本:买一块24G显存的卡一次性投入两三万,还是按token付费买外部API?账期长、单据量大的,本地更划算;验证期、量不大的,API更快。
我服务的两家公司最后都选了本地Ollama部署,主要是销售合同和流水太敏感,老板一听“数据要发到外部去处理”直接摇头。模型用的不是动辄几百亿参数的大号模型,而是7B到8B的量化版本,在单据抽取这种任务上完全够用。
2.3 为什么坚持用Docker部署
很多企业IT环境里,最怕的就是“在我机器上明明能跑”。为了不在环境问题上反复折腾,这一套我全部用Docker Compose编排。好处有三点:
- 依赖隔离:Python包、CUDA版本、OCR库互不污染,服务挂了大不了重建容器。
- 迁移方便:新服务器上只要装了Docker,一条命令就能把整套中台从测试机搬到生产机。
- 版本回滚:升级模型服务前打个镜像标签,出问题切回上一版只需要几秒钟。
这次部署的所有服务,包括MySQL、Redis、API服务、模型推理服务,都用容器跑。下面进入实战阶段,我直接给出能跑通的部署路径。
3. 部署实战:Docker Compose拉通第一版AI中台
3.1 从裸机到Docker的初始化准备
部署的基准环境是Ubuntu 22.04 Server,机器配了一张24GB显存的NVIDIA GPU(实际8B量化模型用不到这么多,但留着余量做文档解析和OCR并发)。先装基础环境:
# 更新系统基础包 sudo apt update && sudo apt upgrade -y # 安装Docker及Compose插件 curl -fsSL https://get.docker.com | bash sudo systemctl enable --now docker # 安装NVIDIA容器运行时,让容器里能用GPU sudo apt install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker装完检查一下:nvidia-smi能正常显示显卡,docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi能在容器里看到显卡就说明环境OK了。这一步别跳过,很多人后面模型服务起不来,都是因为NVIDIA容器运行时没配好。
3.2 核心服务的Compose编排
我使用的目录结构保持简单,方便同事接手:
ai-platform/ ├── docker-compose.yml ├── api/ # FastAPI服务 │ ├── app.py │ └── requirements.txt ├── task/ # Celery worker │ └── tasks.py └── sql/ # 初始化脚本 └── init.sqldocker-compose.yml的核心内容如下(生产配置做过脱敏简化):
services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: change-me MYSQL_DATABASE: ai_hub volumes: - ./sql:/docker-entrypoint-initdb.d - mysql-data:/var/lib/mysql ports: - "3306:3306" redis: image: redis:7-alpine ports: - "6379:6379" ollama: image: ollama/ollama:latest volumes: - ollama-data:/root/.ollama ports: - "11434:11434" deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] api: build: ./api environment: OLLAMA_BASE_URL: http://ollama:11434 MYSQL_DSN: mysql+pymysql://root:change-me@mysql:3306/ai_hub depends_on: - mysql - ollama ports: - "8000:8000" worker: build: ./api command: celery -A tasks worker --loglevel=info environment: OLLAMA_BASE_URL: http://ollama:11434 MYSQL_DSN: mysql+pymysql://root:change-me@mysql:3306/ai_hub REDIS_URL: redis://redis:6379/0 depends_on: - mysql - redis - ollama volumes: mysql-data: ollama-data:这里有个容易踩的坑:API服务和Worker不能直接写死localhost连接MySQL和Ollama,因为容器内看localhost是容器自己。必须用Compose里的服务名mysql、ollama、redis作为主机名。这个点我在第一次部署时栽过,排查了半天才发现是连接地址写错了。
3.3 模型服务的部署姿势:Ollama拉模型与vLLM启动
容器起好后,进Ollama容器把模型拉下来。我们用的是DeepSeek-R1的8B蒸馏版本,对单据抽取这种“理解并输出结构化字段”的任务表现稳定,而且跑起来显存占用友好:
docker exec -it ai-platform-ollama-1 ollama run deepseek-r1:8b首次运行会拉取模型,之后每次调用模型都直接走http://ollama:11434。如果后续单据量上来、并发提升,可以再补一个vLLM服务,启动命令类似:
docker run --gpus all -p 8001:8000 \ vllm/vllm-openai:latest \ --model deepseek-ai/DeepSeek-R1-Distill-Qwen-8B \ --tensor-parallel-size 1 \ --host 0.0.0.0 --port 8000vLLM的优势是批量推理吞吐高,但部署复杂度也高一些,需要从HuggingFace下载原始模型权重。对第一版来说,Ollama已经够稳,不必一开始就上vLLM。
3.4 验证全链路:一条测试单据跑通
我习惯先传一张测试发票,走一遍“上传文件 -> 解析抽取 -> 入库 -> 回填”的完整链路。用curl模拟一次上传:
curl -X POST http://localhost:8000/api/v1/extract \ -H "Content-Type: multipart/form-data" \ -F "file=@/tmp/test_invoice.pdf"正常响应是一段JSON,比如:
{ "request_id": "a3f2c81e", "status": "done", "fields": { "invoice_no": "031001900111", "amount_total": 128000.00, "tax_amount": 19500.00, "date": "2025-06-18", "buyer_tax_id": "91330106XXXXXXXX" }, "confidence": 0.92 }拿到这段JSON就说明模型调用、解析、接口串起来了。接下来要做的是把字段映射到各业务系统并回填,这是下一章的核心。
4. 消除重复录入:AI统一录入怎么落地
4.1 从“读文档”到“出字段”:抽取Prompt模板设计
大模型抽取的玄机不在模型参数,在很大程度上取决于你怎么写Prompt。我试过好几个版本,最终沉淀下来的模板结构是这样:
你是企业单据信息抽取助手。请从用户提供的单据中抽取以下字段,并输出JSON格式。 字段定义: - invoice_no: 发票号码,8位或12位数字; - date: 开票日期,格式YYYY-MM-DD; - amount_total: 价税合计金额,保留两位小数; - tax_amount: 税额,保留两位小数; - buyer_name: 购买方名称; - buyer_tax_id: 购买方纳税人识别号; - seller_name: 销售方名称。 要求: 1. 所有字段值必须能在原文中找到出处,不得臆造; 2. 如果字段缺失,输出空字符串; 3. 金额字段只输出数字,不输出单位; 4. 严格输出JSON,不要额外解释。 单据内容如下: {文档提取出的文本内容}关键点在于:明确字段定义、要求出处、规定缺失处理、锁定输出格式。很多人写Prompt一上来就是“帮我提取一下这张发票的信息”,模型自然就自由发挥起来了。抽取任务需要的不是自由度,而是约束力。
4.2 字段映射与回填:一份数据推三套系统
抽取结果落库后,还得转成三个系统的字段口径。我在MySQL里建了一张映射配置表,存的就是“源字段 -> 目标系统 -> 目标字段 -> 转换规则”:
| 源字段 | CRM目标字段 | ERP目标字段 | 财务台账目标字段 | 转换规则 |
|---|---|---|---|---|
| invoice_no | order_no | voucher_no | invoice_no | 直接映射 |
| amount_total | total_amount | amount_with_tax | amt_ytd | 转成元,去掉千分位 |
| date | create_time | billing_date | biz_date | 格式统一为YYYY-MM-DD |
| buyer_name | account_name | customer_name | client_name | 统一去除“有限公司”后缀后再比对 |
回填时,API服务会按这套映射自动构造三套请求,并行调用CRM、ERP、财务台账的接口。为了不让某一个系统接口慢拖垮整体,我设置了5秒超时和最多3次重试。重试之间间隔2秒,避免把目标系统打崩溃。
4.3 三层兜底:置信度门禁、人审队列、变更留痕
AI回填最大的风险就是“它振振有词地填错了”。我设计了三层兜底,缺一不可:
- 置信度门禁:模型在返回JSON时顺带输出confidence,低于0.85的字段不进自动回填,进人审队列。宁可让人多看一眼,也不要让错账直接进ERP。
- 规则校验:回填前过一遍硬规则——金额必须大于0、日期不能晚于今天、税号格式必须匹配、合同金额和商品明细金额合计必须一致。规则不过就直接打回人工。
- 变更留痕:每一条自动回填都生成唯一的request_id,写入审计表,记录“谁在什么时间把什么数据写进了哪个系统”。万一出了问题,能追溯能回滚。
这套兜底上线后,自动回填的人工复核率大概在15%左右,但最终进系统的数据错误率降到了0.2%以下。对一个要拿数据去做对账的体系来说,这个代价非常值。
5. 消减对账困难:AI匹配+规则校验+人工复核
5.1 对账模型设计:先定义匹配键和账期
对账逻辑不能等系统做完了才想。我在建中台的第一周,就跟财务一起把对账维度定成了三条:
- 交易维度:用订单号/合同号作为主匹配键,这笔业务对应几笔收款、开几张发票。
- 金额维度:业务台账记含税总额,银行流水记实际到账,ERP记科目余额,三者允许的差异阈值设为0.01元。
- 时间维度:业务发生日期、记账日期、银行入账日期各自独立保留,不对齐也能匹配上。
这个设计保证了“同一笔交易在不同系统里日期不一致”不再成为对账失败的借口。所有日期都只是参考字段,而不是匹配条件。
5.2 匹配策略三步走:精确匹配、模糊匹配、差异标注
对账服务我写成了一个独立Python模块,每天凌晨自动拉取三个数据源,跑三步匹配:
第一步,精确匹配。三方记录的订单号、合同号、流水号完全一致,且金额差额在容差范围内,直接标记为“已核对”。
第二步,模糊匹配。精确匹配落空的记录,进入模糊匹配。这里用到两个手段:一是字符串相似度(用difflib的SequenceMatcher),把单据号里常见的“O/0、I/1、-/”等归一化后再比;二是金额容差匹配,业务台账金额和银行流水金额相差在100元以内(可能是手续费或汇率差),可以先合并成候选对。
第三步,差异标注。所有匹配不上的记录,系统自动分类。我贴一段简化的核心逻辑,方便理解:
def classify_diff(row): if row.erp_amount is None: return "业务台账多录" if row.bank_amount is None: return "银行流水缺失" if abs(row.erp_amount - row.bank_amount) <= 0.01: return "金额一致,日期漂移" if abs(row.erp_amount - row.bank_amount) <= 100: return "金额在容差范围,待人工确认" return "金额重大差异"分类结果会汇总到一张对账差异表里,财务每天只需要看这张表,不用再对三份Excel。
5.3 结果表和差异分类
最终对账报表包含以下几个核心字段:主数据来源、对账期、业务单据号、ERP金额、银行金额、差异金额、差异分类、处理状态、负责人、处理备注。我按差异类型做了汇总统计,财务每周只需要处理两三个真正需要人工判断的case,其余全部自动归类到“已核对”或“已知差异”。
我印象最深的是上线第一周,系统自动发现了一笔“业务台账多录”的记录——业务员在系统里把一笔订单金额敲成了128,000,银行实际到账只有127,000,差额1000一直被当成坏账挂着。后来查出来就是当初重复录入时手抖多敲了一个零。这笔陈年老账,最终靠系统自动匹配和差异分类揪了出来。
6. 上线半年后踩过的坑和处理方式
6.1 模型幻觉回填错值:校验墙怎么把住
第一次给演示环境录合同的时候,模型把不含税金额当成了价税合计,回填后ERP里的合同金额一下少了13%。当时校验规则还没做得那么全,差点带偏一整批订单。后来我在规则引擎里加了一条硬校验:抽取出的金额字段,必须能在原始文档中找到对应数字串,且价税合计必须等于不含税金额加税额。大模型自由发挥的空间被压到最小,从此没再出现过结构性金额错误。
6.2 幂等与重试:不要因为任务重试造成重复账
Celery worker在跑回填任务的时候,有一次目标系统CRM接口超时,worker自动重试了三次。问题在于CRM接口虽然返回超时,其实前一次请求已经写入成功了,结果同一张订单被建立了三条记录。解决办法有两个:一是回填请求头带上系统生成的request_id,目标系统做唯一索引;二是在我们自己库里建一张回填任务表,任务执行前先查这个request_id有没有成功过,成功过就直接返回旧结果。这是所有自动化写入场景都必须考虑的幂等设计,不然后患无穷。
6.3 低配服务器跑Ollama的显存优化
有几台测试机的显卡只有8G显存,跑8B模型在长文档场景下容易显存溢出。后来做了三个调整:一是改用4-bit量化版本,显存占用直接减半;二是把Ollama的OLLAMA_KEEP_ALIVE调成5分钟,避免每次请求都重新加载模型;三是限制单张图片OCR的解析尺寸,超长单据拆页处理。调整之后,8G显存跑起来也稳稳当当,只是单次响应时间会慢个一两秒,完全在可接受范围。
6.4 权限与审计:AI中台最容易被忽视的部分
财务部门最开始对AI中台有天然的不信任,根源不是模型准不准,而是怕系统乱写数据。所以权限这块我做了三件事:给每个目标系统单独签发最小权限的API Token,只授予“写入业务单据”这一项权限;所有自动写入操作必须经过审计表记录,支持一键查询“这批数据是AI写的还是人改的”;对外提供的查询接口全部走只读账号。这套机制跑了一个月之后,财务的态度从“不信任”变成了“依赖”。
最后再分享一个实际体会
如果你也想做类似的轻型AI中台,我的建议是第一版千万别铺太大,就锁定“重复录入”和“对账”这两个具体痛点做到极致,比什么都强。先把一条单据从上传到回填的链路跑稳,把一个账期从拉数到出差异报表的流程理顺,之后再去想合同审核、风险预警这些扩展功能。AI中台的价值不在于模型多聪明,而在于它能不能每天稳定地帮人把重复劳动省下来。我这边用下来的感觉是,只要兜底机制设计得够严密,业务部门接受AI的速度远比你想象中快。