上个月月底,财务部的小姑娘抱着一摞纸质对账单站到我工位边上一言不发,我就知道又对不上了。那几天我正忙着在内部服务器上部署一套轻量级AI中台,目的很单纯:消除重复录入、消减对账困难。我们公司不大,但销售、财务、仓库各用各的系统,同一个订单至少要录三遍:销售在OA里建客户意向,客服复制到ERP生成订单,仓库再在WMS里做入库。月底把三个系统导出的Excel拉到一起,靠vlookup和肉眼找差异,一次对账少说折腾一整天。现在这套中台上线一个季度,重复录入基本消失,对账从3人天降到半天,而且所有模型和数据都在内网跑,我心里踏实不少。这篇文章就是这次部署的完整复盘,从选型、架构、Docker Compose部署到实际场景调优,能直接抄作业的部分我都写仔细一点,适合正在犹豫要不要上AI、又不想一上来就搞大而全平台的中小企业IT或者数据负责人看。
1. 重复录入和对账困难,本质是数据管道断了
1.1 重复录入到底发生在哪些环节
先说重复录入。我观察了客服、仓管和财务三个岗位,发现最消耗人的不是“录错”,而是“同样内容换个系统再录一遍”。客户名称在CRM里叫“华东贸易”,在ERP里叫“华东贸易有限公司”,到了财务开票系统又变成了“华贸”,三个系统之间没有主数据映射,靠人脑记忆“它们是一家”。订单从OA流转到ERP,靠客服把聊天记录里的商品名、数量、单价逐字敲进去。更麻烦的是,客户发来的付款截图、微信消息、PDF合同全是非结构化内容,系统根本没法自动识别,只能由人肉做一次“翻译”。
这种活干多了不会出错才怪。我们的客服一个月要手工录入六百多张销售订单,其中大概一成存在字段抄错的情况,比如把型号里的“0”看成“O”,把数量“100”录成“1000”。错误当时发现不了,等到月底财务和仓库对不上账,再回过来查源头,已经不知道是哪一天哪一个人录的。这就是重复录入最隐蔽的成本:错误发生在一天前,爆发却在月底。
1.2 对账困难的三个根源
对账难,根源也不在“财务不够细心”,而在三个系统性错位:
- 口径不一致:销售系统按“发货时间”确认业务,财务按“开票时间”确认收入,仓库按“出库时间”记录库存。同一个动作,三个时间点,月底永远差着一截。
- 主数据不统一:同一客户、同一物料在不同系统里的编码、名称、计量单位都不一样。一对账就要先做大量“翻译”工作。
- 时间差与拆单:月底最后三天发货的单据,可能下月初才开票;一笔大订单可能拆成多张发票,也可能合并支付。这些情况在Excel里靠排序和筛选很难一眼看清。
下面这个表格,基本就是我们每个月都在经历的日常:
| 差异类型 | 示例 | 传统人工处理方式 |
|---|---|---|
| 主数据不一致 | C1001 vs 1001 vs 华贸 | 靠老员工人肉记忆 |
| 时间口径 | 12月30日发货,1月3日开票 | 跨月单据永远对不平 |
| 金额口径 | 含税/未税、折扣前后 | 差几十块钱找半天 |
| 明细拆分 | 一单拆多票、合并支付 | 单据行数对不上 |
1.3 为什么说是“数据管道断了”,而不是“员工不细心”
我一开始也以为是员工责任心问题,后来发现冤枉他们了。真正的原因是系统之间根本没有一条自动的数据管道。大家各自为政,每个系统只存自己那一份,系统之间没有标准的中间格式,也没有自动回传机制。想彻底解决,要么上大型ERP做流程统一,要么挨个系统开发接口,两条路都需要大量预算和跨部门协调,我们这种小团队根本推不动。
AI中台解决的是“语言不通”的问题:不改业务系统的内部逻辑,在它们中间加一层“翻译和搬运”,用大模型去读那些非结构化内容(聊天记录、截图、PDF、Excel),抽取成标准结构化JSON,再调用各个系统已有的API把数据写进去。本质上它是把原来靠人肉完成的“读-理解-抄写”动作,变成了可配置、可审计、可重跑的自动化任务。这也是为什么我最终选择了轻型AI中台的路线,而不是去推动什么系统大整合。
2. 选型逻辑:本地私有化部署 + 轻型三件套
2.1 我为什么否掉了“商业中台”方案
项目启动前,供应商给我打过好几轮电话,核心方案是上一套完整的商业数据中台,包含数据治理、指标平台、微服务改造,号称“一劳永逸”。我算了一下,实施费加硬件加每年的服务费,够我们小团队三年工资了。而且这类方案真正的难点不在技术,而在组织变革:它要求各业务部门把数据管辖权交出来,流程按中台的标准重新定义。我们公司连跨部门的月度例会都要约三回,这种大动作注定走不下去。
更致命的是合规问题。销售订单涉及客户联系方式、合同价格,财务账单涉及公司资金流水,这些数据绝对不能出企业边界。商业中台如果走公有云,我不放心;如果私有化定制,那个价格又是一个无底洞。
2.2 轻型方案的三件套:Dify + Ollama + 本地模型
最终定的方案,是当前开源社区里最主流、也最适合中小团队的组合:
- 应用编排层用 Dify:开源LLM应用开发平台,可视化拖拽搭建工作流,内置知识库、模型管理、API发布,天然适合做业务侧的AI应用。
- 模型推理层用 Ollama:一条命令就能把本地模型拉起来,提供OpenAI兼容的API。模型选Qwen2.5系列,中文场景表现稳;财务推理和平账归因也可以用DeepSeek-R1的蒸馏版。
- 存储与检索层:Dify默认编排里自带向量数据库(新版默认Qdrant),负责承载知识库和业务字典;核心业务数据仍然放在PostgreSQL里。
为什么不直接调用大厂模型API?第一是费用不可控,每月按token计费,业务量上来之后根本兜不住;第二是数据合规不允许,客户订单、供应商合同不能传到外部服务器;第三是网络稳定性不可控,接口偶尔抖动,业务等着做录入的时候断一次就够人喝一壶。本地私有化部署之后,模型在公司内网跑,API调用就是访问一台内网服务器,延迟和稳定性都可控。
为什么不自己用LangChain从零写?我承认LangChain上限高,但对一个三四人的小团队来说,登录、流式、知识库、版本升级、权限管理都要自己造,工作量完全失控。Dify把这些通用能力都打包好了,我只需要专注业务逻辑本身。
2.3 服务器配置怎么定
模型参数量直接决定了硬件投入。我给三个档位的参考,都是我实测过或者身边朋友验证过的:
| 阶段 | 硬件配置 | 可跑模型 | 适用场景 |
|---|---|---|---|
| 概念验证 | 16核32G内存,纯CPU | qwen2.5:7b(Q4量化) | 低频演示、开发联调 |
| 单机生产 | 10核以上 + 32G内存 + 24G显卡(如RTX 4090) | qwen2.5:14b / deepseek-r1:14b | 10到30人日常使用 |
| 多人生产 | 64G内存 + 双24G或48G显卡 | 32B量化模型 | 几十人并发+复杂场景 |
我们实际用的是二手戴尔R740配一张二手RTX 4090,总投入不到三万块,已经能稳稳跑Qwen2.5-14B。Dify全家桶本身也要占一些CPU和内存,所以32G内存是这个配置的最低门槛,别省。
2.4 “轻型”的边界到底在哪
我理解的“轻型”,不是功能阉割,而是组件少、逻辑简单、单机能跑、普通人能维护。我们刻意没有上Kubernetes,没有做多租户,没有追求上千并发。实际业务就是几十个员工用,一天不过几百次调用,单机Docker Compose绰绰有余。等哪天真的不够用了,再平滑加节点也不迟,但那天大概率不会来,因为咱们这种规模的企业,业务峰值远比想象中温柔。
3. 架构设计:把AI埋进业务流,而不是另造一套系统
3.1 一条原则:中台不替代业务系统
很多项目一上来就想用AI中台“接管”业务系统,这是最大的误区。我们的原则很简单:业务系统还是原来的,AI中台只做一个中间翻译层,通过API方式嵌入现有流程。
输入侧,它能接收图片、PDF、Excel、聊天记录这些原始数据;处理侧,走“文档解析→LLM字段抽取→规则校验→结构化JSON输出”这条管道;输出侧,通过业务系统已有的API接口写入ERP、WMS、财务系统。整个过程中没有强迫任何业务部门换系统,也没有要求他们改变操作习惯。
3.2 消除重复录入的自动化管道设计
拆开看,“消除重复录入”实际上是一条四段管道:
- 文档解析:把PDF、拍照、截图转成可读文本。我们用了开源解析工具做了本地化部署,打印体和电子单据识别效果很好。
- LLM字段抽取:把非结构化文本变成结构化JSON。这一步是核心,我写的抽取Schema长这样:
{ "customer": { "name": "string", "code": "string|null" }, "order_date": "YYYY-MM-DD", "items": [ { "name": "string", "qty": "integer", "price": "number", "spec": "string|null" } ], "total_amount": "number", "currency": "CNY", "note": "string|null" }- 规则校验:检查必填字段、金额是否等于明细合计、日期是否合法。校验不通过的记录直接打回,不让脏数据进入下一步。
- 人工兜底确认:低置信度的结果不直接写入业务系统,而是推给操作员做一键确认。这一步很关键,后面我会详细说。
3.3 对账引擎的设计:规则优先,AI兜底
对账场景的设计逻辑和录入不太一样,因为对账要求结果可解释,不能大模型说“匹配上了”就算完。我采用的是三层策略:
- 第一层,精确匹配:单据号、金额、日期完全一致,直接判定匹配,不需要AI参与。
- 第二层,模糊匹配:客户名称归一化后一致、金额差在容差范围内、日期在前后几天内,标记为“候选匹配”。
- 第三层,AI差异归因:剩下匹配不上的,让LLM同时读两侧的明细数据,给出“未开票”“在途”“折扣”“拆分支付”这类差异候选原因,再生成自然语言摘要给财务参考。
这套设计的好处是:能确定性解决的部分绝对不靠AI,AI只处理规则搞不定的长尾,既保证准确率,又降低了人工复核量。
3.4 权限、审计与知识库隔离
数据安全上不能含糊。每个业务线的AI应用独立部署、独立API Key;每条AI抽取结果都保留原始输入的截图或文本,出现问题可以回溯;知识库按部门隔离——财务的客户字典和销售的名称规范放在不同知识库里,避免互相污染。这些不是锦上添花,而是上线前的必备配置。
4. 部署实操:Docker Compose 拉起 Dify + Ollama
4.1 环境准备
我们采用的操作系统是Ubuntu 22.04 LTS。选它没有特别高深的理由:长期维护到2032年,跑Docker稳定,社区资料多。服务器到手后先做基础检查和系统更新:
# 查看CPU、内存、GPU情况 lscpu | grep "Model name" free -h nvidia-smi # 更新系统 sudo apt update && sudo apt upgrade -ynvidia-smi能正常输出显卡信息,说明NVIDIA驱动已经有了。如果没安装驱动,需要先把显卡驱动装好再继续,否则后面GPU容器没法用。
4.2 安装Docker与GPU容器支持
Docker用官方脚本装是最省事的:
curl -fsSL https://get.docker.com | sh sudo systemctl enable --now docker有NVIDIA显卡的话,还要安装NVIDIA Container Toolkit,让容器能访问GPU:
sudo apt install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker # 验证GPU是否能在容器里正常使用 docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi这一步最常见的坑是忘了重启Docker服务,导致--gpus参数不生效。重启之后再跑验证命令,能看到显卡信息就说明GPU容器链路通了。
4.3 部署Ollama并拉取本地模型
Ollama的部署非常简单,脚本一条命令搞定:
curl -fsSL https://ollama.com/install.sh | sh默认情况下Ollama只监听本机回环地址,Dify是跑在容器里的,需要让Ollama监听内网地址,否则Dify容器访问不到。修改方法如下:
sudo mkdir -p /etc/systemd/system/ollama.service.d echo -e '[Service]\nEnvironment="OLLAMA_HOST=0.0.0.0:11434"' | sudo tee /etc/systemd/system/ollama.service.d/override.conf sudo systemctl daemon-reload sudo systemctl restart ollama然后拉模型。我们用Qwen2.5系列做通用抽取和录入,用DeepSeek-R1蒸馏版做财务归因:
ollama pull qwen2.5:14b ollama pull deepseek-r1:14b # 快速验证模型是否正常响应 curl http://127.0.0.1:11434/api/generate -d '{"model":"qwen2.5:14b","prompt":"你好"}'有一点必须说明:如果内网服务器无法直接连接外网拉模型,可以在能联网的机器上先执行ollama pull,然后整个打包~/.ollama/models目录,拷贝到内网服务器对应位置。我们实际就是这样做的,属于完全合规的离线导入方式,没有额外依赖。
4.4 部署Dify
Dify的部署走Docker Compose:
git clone https://github.com/langgenius/dify.git cd dify/docker cp .env.example .env # 修改数据库密码、容器端口,避免和你现有的服务冲突 vim .env # 启动 docker compose up -d # 查看状态,正常应该是所有容器都是 running 状态 docker compose ps首次启动会拉取很多镜像,等几分钟到十几分钟都很正常。访问http://<服务器IP>就能看到Dify的初始化页面,设置管理员账号密码即可。
4.5 在Dify里接入Ollama模型
登录Dify后台,进入“设置-模型供应商-Ollama”,新增一个Ollama接入:
- 模型名称:填
ollama list里显示的名字,比如qwen2.5:14b。 - Base URL:填
http://<你的服务器内网IP>:11434。 - 上下文长度:按模型实际能力填,Qwen2.5-14B一般填32768。
保存之后新建一个最简的“文本生成应用”,随便输出一句“你好,我是录入助手”,连通就说明整条链路已经跑通。我习惯在这一步顺手测一发GPU占用:跑试算时nvidia-smi里能看到显存占用升起来,就说明Dify确实在调用本地GPU推理。
4.6 安全加固
模型API属于比较容易被忽略的薄弱点。Ollama默认不带鉴权,只要内网里哪个同事知道地址就能调用。我们做了三层防护:
- Ollama只监听内网IP,不暴露公网;
- 防火墙只放行Dify所在服务器到Ollama的IP段,其他内网机器不互通;
- 按业务线拆分Dify应用和API Key,每个部门只拿到自己用的那个。
备份方面,每天凌晨用crontab把Dify的PostgreSQL数据导出到备份盘,配置文件也单独打tar包,出问题可以恢复到前一天。
5. 落第一刀:销售订单录入的场景实测
5.1 挑最痛的场景动手
选切入场景的标准只有一个:哪个岗位吐槽最多,就先做哪个。我们选了销售订单录入。客服每天面对的是微信群里的下单信息、邮件里的PDF合同、客户发来的手写拍照单,要把商品名称、型号、数量、单价、交期全部敲进ERP。55%的错误都发生在这一步。
目标定得很克制:客服把原始材料丢给AI,AI抽取字段回显给客服,客服一键确认,再由工作流调用ERP接口写入订单。要分清楚,不是全自动,是“AI提取+人工确认”,这保证了出错有兜底。
5.2 工作流节点怎么搭
在Dify里,我建了一个名为“ERP订单录入助手”的工作流,节点顺序如下:
- 输入节点:接收用户上传的图片、文本或PDF。
- 文档解析节点:对图片和PDF先用本地OCR服务转成文本,抽取出纯文字内容。
- LLM抽取节点:把原始文本发给Qwen2.5-14B,要求按固定Schema抽取字段。
- 代码校验节点:校验必填字段、金额一致性和合法范围。
- 人工确认节点:把结果回传给客服,展示为可修改的表单。
- 写入ERP节点:客服确认后,调用ERP的订单创建API。
核心是LLM抽取节点的提示词。我贴一个经历过很多轮修改后比较稳定的版本:
你是ERP订单录入机器人,只做一件事:从用户输入中抽取订单字段。 输出必须是合法JSON,字段如下: customer_name, customer_code, order_date, items[{name, qty, price, spec}], total_amount, currency, note 规则: 1. 没有出现的信息一律填null,禁止揣测补充。 2. 文字中的“约”“大概”“左右”原样记入note。 3. 金额默认为人民币CNY,出现其他币种才修改。 4. 物料名称写全称,不缩写。 5. 如果原始文本明显是一段闲聊,而不是订单,输出 {"error": "not_order"}。 输入内容: {{input}}5.3 校验与兜底不能省
代码校验节点我写了一段简单Python,逻辑不复杂,但每个字段都能卡一道:
def main(record: dict) -> dict: items = record.get("items", []) if not items: return {"pass": False, "reason": "明细为空"} total = 0.0 for item in items: try: total += float(item.get("price", 0)) * int(item.get("qty", 0)) except (TypeError, ValueError): return {"pass": False, "reason": "价格或数量格式异常"} declared = float(record.get("total_amount", 0) or 0) diff = abs(total - declared) ok = True reason = "" if not record.get("customer_name"): ok, reason = False, "缺少客户名称" elif diff > 0.01: ok, reason = False, f"金额校验不一致,计算合计{total:.2f},声称{declared:.2f}" return {"pass": ok, "reason": reason, "calculated_total": round(total, 2)}校验不通过或者模型输出的昵称置信度太低,工作流会自动跳转到“人工确认”节点,客服可以在表单上直接改字段再提交。这样就算模型看走眼,人也能拦一道。
5.4 实测效果与真实瓶颈
上线跑了一个月,常规电子单据和打印体的字段抽取准确率在95%以上,客服确认频率也高,日常录单时间从平均3到5分钟降到30秒以内,基本是“看一遍→点确认”的节奏。手写单据和模糊拍照图片的准确率就掉了,大约只有70%,这种单子一律走人工确认,不硬扛。
真实瓶颈比我想象的要直白:客户和物料的别名映射积累不够,导致很多其实能匹配上的信息被AI当成未知字段。解决方法是把历史订单里的客户全称、常用简称、系统编码整理成知识库传进Dify,抽取的准确率马上又上一个台阶。这也印证了一件事,真正的护城河是业务语料,不是模型本身。
6. 第二刀:对账差异识别与报告自动生成
6.1 对账前先做数据出口统一
录入问题解决之后,第二刀切在月度对账。我们首先要做的是让两个系统把数据倒出来变成统一格式。销售系统导出发货明细,包含日期、单据号、客户名称、金额、状态;财务系统导出开票和收款明细,字段一样。老旧系统没有API,就用定时SQL导出CSV,丢到一个共享目录,由工作流去读取。
这一步看似简单,实际上对账能不能跑通,全靠这份出口数据的质量。如果财务的客户名称和销售系统相差太离谱,后面的模糊匹配做得就很痛苦。所以我们在导出环节增加了一个轻量清洗脚本,把常见的“有限公司”“有限责任公司”后缀、括号内备注统一去掉,格式对齐后再进入匹配阶段。
6.2 三层匹配策略:确定性优先
对账工作流里,我把匹配拆成三层:
- 精确匹配:单据号一致且金额一致,直接判定匹配完成。
- 模糊匹配:客户名称归一化后相似、金额差在容差范围内(我们设了100元)、日期在前后3天内,标记为“候选匹配”。
- AI归因:剩余差异让LLM读两侧明细,输出“预计是未开票”“预计是在途”“预计是折扣导致”“预计是拆分支付”这样的候选结论。
模糊匹配的客户名称归一化,我写了一段很小的代码放进了Dify的代码节点:
import re def normalize_company(name): if not name: return "" name = re.sub(r'[((].*?[))]', '', name) name = name.replace('有限公司', '').replace('有限责任公司', '') name = name.replace('股份', '') return ''.join(name.split()).upper() def same_customer(a, b): na, nb = normalize_company(a), normalize_company(b) if na == nb: return True return na in nb or nb in na这一层一个人眼要盯几分钟的内容,程序几百毫秒就扫完了,而且不会疲劳。
6.3 对账报告输出设计
工作流的最终输出不是一纸PDF,而是财务同事真正能直接用的三件事:
- 已匹配清单:三栏展示销售、财务两边的单据号和金额,打上“已匹配”标签,供抽查。
- 待确认差异清单:给每一条差异标注“候选原因”,财务只需要看AI给的归因对不对,对就点确认,错就改一下。
- 差异汇总摘要:用一段自然语言概括当月的整体情况,比如“本月共24笔未匹配,总额12.6万,主要原因为月底发货未开票,预计下月复核”。
摘要通过企业微信群机器人推送给财务负责人,他不用自己打开Excel去找结论。这看起来只是体验优化,实际省掉的沟通成本比节省的录入时间还多。
6.4 实测效果与调优心得
上线第一个月,对账时间从3人天降到0.5人天,自动匹配率在68%左右,剩下32%靠AI归因和人工确认,确认速度也比过去快很多。财务反馈最明显的是“终于不用两眼发直盯屏幕了”。
调优过程中有几条很值的经验:
- 首次导入的历史差异数据是绝佳的few-shot样本,把它们喂给模型做示范,归因准确率提升明显。
- 客户别名表放知识库比改提示词更稳,因为这是持续增长的数据,知识库可以随时增量上传。
- 容差阈值不要拍脑袋,先统计分析历史差异的金额分布,取中位数上下合理范围。
- 遇到新出现的差异类型,先追加进知识库字典,再回头调整提示词模板,两步缺一不可。
7. 踩坑记录与几个必须提前想清楚的事
7.1 模型幻觉:它会帮你“脑补”字段
这是最需要注意的一件事。某次联调我拿一张没写折扣的订单测试,模型居然给我输出了一个“折扣率5%”。没有的信息硬生生“脑补”出来了,这在录入场景等于制造了一个新的错误。后来我在提示词里加了硬性约束:“没有出现的信息一律填null,禁止揣测补充”,同时在代码校验节点对关键字段做格式与范围检查,不通过的记录直接拦下来。不要全自动写入也是这个道理,AI的返回永远是“建议值”,而不是“终值”。
7.2 Ollama默认没有鉴权
我一开始没意识到这个问题,直到在内网用浏览器看到Ollama的API文档页面才反应过来。好在漏洞只暴露在内网,但也给我提了个醒。处理办法是:只监听内网IP、防火墙白名单、后期用Nginx反代加API Key。如果你也要做私有化部署,刚开始就把它按规范落地,否则后面补很麻烦。
7.3 内存与OOM是重灾区
Dify全家桶本身就吃不少CPU和内存,模型推理又格外吃显存。32G内存跑Dify加14B模型,平时够用,但一旦Dify的索引任务和模型推理撞在一起,内存就会告急。我们遇到过几次OOM直接导致服务假死,后来给关键容器设置了mem_limit,给操作系统配了合理swap,Docker日志也加了轮转,才稳定下来。建议在配置文件里别偷懒,直接把资源限制写好。
7.4 知识库要按业务线隔离
销售部和财务部的关注点完全不同。客户名称在销售眼里是沟通对象,在财务眼里是开票抬头,两边维护的字典如果放在同一个知识库里,AI抽取时很容易串味,把销售说的“老李”匹配到财务的“李氏贸易公司”上去。后来我把知识库按业务线分开,每个应用绑定各自的知识库,串味问题彻底消失。
7.5 上线节奏别贪多
最实用的建议是:第一个月只做销售订单录入,跑顺了第二个月再上对账。一次只动一条业务线,出了问题和故障,你能非常清晰地定位是模型问题、数据问题还是流程问题。我见过太多项目想三个月内“AI覆盖所有业务”,结果什么都做了,什么都做不精细,最后业务部门失去信心,项目灰溜溜收场。
这套中台上线一个季度,我最深的体会是:AI中台能不能落地,不取决于模型多强,而取决于你挑中的场景是不是足够痛、流程里的边界是不是足够清楚。我们没搞什么宏大规划,就是从重复录入最严重的客服岗和对账最痛苦的财务月底入手,先用一个AI应用干掉一条线,再复制到其他线。如果你也准备动手,我的建议是把范围再砍一半——只选一个岗位、一个动作、一条抱怨最多的流程,先跑通再谈扩展。