库存盘点表格这件事,我有段时间看到就想皱眉。仓库几百个SKU,纸质单据堆成一摞,人工录入电子表格就得耗掉一整天,录完还要反复核对,任何一个数量填错,后续的补货、财务对账全都跟着遭殃。于是我开始琢磨,能不能让AI自动生成这份盘点表格,而且整个处理全程跑在本地,数据不出内网。试了几个月,踩了不少坑,总算跑通了一套相对稳定的流程。这篇文章就是把我完整的实操过程、踩坑记录,还有可复现的方案整理出来,给同样被盘点表格折磨的伙伴做参考。你不需要多深的AI基础,能跑通几个脚本就行。
1. 整体设计思路与方案选型
1.1 传统盘点流程的四个瓶颈
先说说我为什么要动这个念头。仓库月度盘点一般是这样走的:打印空白盘点表,库管员逐个货位清点并在纸上填写,单据回收后专人录入电子表格,再和账面数比对,标注差异原因,最后生成汇总报表。听起来不复杂,实际跑起来全是时间黑洞。
第一个瓶颈是录入慢。几千行记录,就算两个人分工,五笔加数字键也要敲两三个小时。更麻烦的是,录错一个数很难发现,往往要到对账环节才能察觉,到时候根本不知道是录入错了还是盘点错了,只能重新数一遍。
第二个瓶颈是格式不统一。同样一种商品,不同人写的完全不一样。有人写"苹果 20",有人写"苹果 20箱",还有人写"苹果 二十箱"。这些记录进到同一个表格以后,后面想用公式做匹配、透视都做不了,先得花大量时间手工对齐。
第三个瓶颈是复核链路太长。盘点、录入、核对往往是不同人负责,中间只要有一个人理解偏差,后面全部跟着乱。纸质单据中间还可能丢失、折角、字迹模糊,想追溯都没法追溯。
第四个瓶颈是单证管理。纸质盘点表留档占地方,电子版又散落在不同人手里,下次盘点点数据的时候根本没有统一的基线。
这四个瓶颈叠加下来,AI能介入的价值点就很明确了。它不应该去替人盘货,而是把"杂乱的人类记录转成统一结构的表格"这件事自动化,这才是整个项目的核心判断。别本末倒置,非要让模型做它不擅长的事。
1.2 为什么坚持本地化而不是调用云端接口
最早我也想过直接调云端大模型API,毕竟方便,识别能力又强,丢一段文字进去就出来结构化结果。但库存盘点这个场景有几个绕不开的顾虑。
第一,库存数据是供应链的核心数据,里面包含采购价、成本、供应商信息,很多企业明确要求数据不能出内网,光这一条就否掉一半云端方案。第二,仓库不一定有稳定的外网,有些库房在偏远物流园区,一旦断网,云端方案直接瘫痪,而盘点又往往卡在月底、季末这种必须出结果的时间点。第三,高频调用成本不低,每个月盘一次、每次几千行,长年累月算下来是一笔完全没有必要的开销。
所以最终方案定为本地部署模型,数据留在内网,随时能跑,也不用担心外传问题。代价是得准备一台内存过得去的机器,不过这个后面细说,其实门槛不高。
1.3 端到端的任务分工
整个系统不是一个大模型从头包到尾,而是拆成了几个明确模块:
- 数据采集:扫描、拍照、文字识别,或者直接用已有电子记录转成文本
- 预处理:统一编码格式、剔除噪声、对齐字段
- 模型解析:本地大模型根据提示词把记录转成JSON或CSV
- 程序校验:检查完整性、合法性,不合格的重新解析
- 表格写入:把结果写入电子表格并高亮异常项
AI在整个链路里只负责"解析与结构化"这一环,前后的数据准备工作全由脚本完成。这个分工非常关键。如果你指望让大模型从头到尾全包,输出很容易失控,最后还得人肉返工。我在前期测试里已经深刻体会过这一点。
2. 数据准备与标准化处理
2.1 原始数据的几种常见形态
在正式让模型干活之前,先得把手头的数据收拾齐整。实际仓库里,盘点记录长什么样都有。
一种是手写单据,数字潦草,单位缩写多,比如"箱"直接写成一个叉。这种要么靠OCR引擎识别,要么靠录入员先敲进电脑,反正最终要转成纯文本。另一种是打印清单加手改,仓库在预印清单上手写差异数量,印刷模糊和手写笔迹叠在一起,识别难度最高,我的做法是固定用扫描件统一跑OCR,不要有的用手机拍、有的用扫描仪,否则光线和角度不一致,识别效果差别很大。
还有一种是手机拍照加语音转文字。有些库管习惯边走边看边念,最后导出一段语音转写文本。这种文本里经常带口语词,比如"那个""大概",预处理阶段要专门过滤掉。
最后一种是历史电子表格碎片。多个人维护过的表格,列头五花八门,有的叫"品名"有的叫"商品名称",有的单位写"支"有的写"个"。这些数据如果要并入统一模板,清洗量并不小。
不管哪种形态,第一步都是把内容转成纯文本。文本进了程序,后面才能统一处理,否则每个格式都是单独的流程,工作量会翻好几倍。
2.2 字段规范与命名约定
为什么一定要规范字段?因为模型的强项是理解语义,但输出的表格必须能被程序校验。如果字段没有事先约定,模型今天输出"品名 数量",明天输出"商品 实盘数",后天的表格就没法对齐。
我建议在最开始就定死六个核心字段:SKU编码、品名、规格、单位、账面数、实盘数。差异值后面用公式计算就行,不需要模型输出,模型只负责前六列。
| 字段 | 示例 | 说明 |
|---|---|---|
| SKU编码 | KJ-0234 | 唯一标识,格式统一 |
| 品名 | 苏打水 | 标准商品名称,不带修饰 |
| 规格 | 24瓶/箱 | 包装信息 |
| 单位 | 箱 | 计量单位 |
| 账面数 | 120 | 系统内库存数量 |
| 实盘数 | 118 | 实际清点数量 |
这个模板可以直接照抄。如果后面需要批次号、生产日期、备注,再额外加列,但核心字段越少越好。模型输出面越小,出错的概率越低,这是硬道理。
2.3 清洗规则和边界情况
数据清洗里,最容易踩的坑是你以为只有脏数据需要处理,其实格式统一同样费时间。我总结了一套常规清洗规则:
- 全半角统一:把中文逗号、括号统一替换成英文半角,否则CSV解析时会莫名其妙多出字段
- 数量单位拆分:原始记录"20箱/300瓶"这种复合写法,预处理阶段只保留主单位"箱",其余写进备注,不让模型自己判断选哪个
- 别名映射:同一商品在不同单据里叫法不一致,比如"可乐"和"可口可乐",必须在清洗阶段替换成标准品名,不能指望模型去猜
- 空值处理:盘点表格里留空的格子,程序统一填"未盘",不留给模型发挥
清洗脚本建议先简单后复杂,不需要一开始就做全。我的顺序是先处理格式类问题,包括全半角、空白、非法字符,再处理别名映射,最后才处理业务规则类的合并问题。一步步来,哪一步出问题都好定位。
再强调一遍:清洗规则必须在模型解析之前跑。如果把这些不规范文本直接丢给模型让它自己整理,结果往往是一个字段对不齐、单位乱跳,反而更麻烦。
3. 本地模型环境的搭建与调用
3.1 模型选型的三个参考维度
本地部署大模型,第一个问题就是选哪个模型。我不建议追最新最大的,按自己的硬件选才是正路。参照三个维度来看。
第一是参数量。普通办公室的台式机或者一台中等配置的服务器,7B到14B级别的量化模型是最实用的档位。这个规模的开源大语言模型,对中文文本的理解和结构化能力已经够用。再大的模型需要多卡或大显存,很多小团队不具备这个条件,硬上只会天天卡死。
第二是量化精度。模型量化之后体积小、跑得快,但理论上有精度损失。实测下来,4bit量化在表格解析这种结构性任务上,和满精度输出的差异基本可以忽略。所以直接上量化模型,省内存又提速。
第三是上下文窗口。盘点记录如果一次给太多,模型上下文不足就会截断。建议至少选8K上下文的版本,这样一次能处理两三百行记录,效率比较舒服。
补充一点,运行环境只需要支持常用的本地推理框架就行,不需要额外配置复杂的东西。CPU也能跑,只是慢一些,后面有性能数据可以参考。
3.2 部署运行的参数配置
模型部署好之后,真正决定输出质量的是推理参数。我调试了很长时间,最后固定下来几组参数,基本没翻过车。
temperature一定要低,设置在0.1以下。温度越低,输出越稳定,表格解析任务不需要模型发挥创造性,要的是确定性优先。top_p设置在0.5左右,和低温配合,进一步压低随机性。max_tokens按输出行数估算,一般一行记录大约40到60个token,300行记录就设置到20000以上,宁可给多不能给少。repeat_penalty适度调高到1.1到1.2,防止模型在长输出里自我重复。
我之前有一版跑得不太对劲,排查了半天,发现是temperature设成了0.7。模型输出看似合理,但字段顺序随机换位,同一个商品一会儿写"数量 20"一会儿写"20 数量",后面校验脚本直接炸掉。把温度降下来之后立刻就好了。所以记住:模板解析类的任务,参数就往"保守"调,不要让它自由发挥。
3.3 用脚本封装模型调用
部署完模型以后,不管是本地推理服务还是命令行接口,都建议统一封装成一个函数,不要让业务代码直接依赖具体的调用方式。我习惯写一个简单的HTTP请求封装,大概长这样:
import requests def chat_completion(prompt, history=None, temperature=0.1, max_tokens=16384): url = "http://127.0.0.1:8000/v1/chat/completions" headers = {"Content-Type": "application/json"} payload = { "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt} ] + (history or []), "temperature": temperature, "max_tokens": max_tokens, "top_p": 0.5, "stream": False } resp = requests.post(url, json=payload, headers=headers, timeout=300) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]注意超时时间一定要给足。本地模型首token可能慢,长文档推理有时要跑一两分钟,300秒是底线。如果没设置超时,文档稍长就会被默认30秒掐断,你只会看到报错,根本不知道模型其实还在算。
这套封装的好处是以后换模型、换端口,只需要改url和参数,不用动业务逻辑。我后来把预处理、解析、校验、写入都挂在同一个流程里,这层封装起了很大作用,维护成本低了很多。
4. 提示词设计与表格生成实现
4.1 一套可以直接复用的提示词模板
提示词是整个流程里最值得反复打磨的部分。我迭代了很多版,最后沉淀下来一套在实际环境中跑过几千行数据都稳定的模板。
系统提示词的作用是定义角色和任务边界:
你是一名仓库数据录入专员,负责把用户提供的盘点原始记录整理成结构化表格。 任务要求: 1. 只输出CSV内容,不输出任何解释、前言或表格之外的内容。 2. CSV第一行必须是表头:SKU编码,品名,规格,单位,账面数,实盘数 3. 每条记录一行,字段用英文逗号分隔。 4. 遇到无法识别的记录,在实盘数列填写"未识别",不要擅自编造。 5. 数字保持原样,不要四舍五入,不要加千分位。 6. 单位统一用:箱、瓶、件、包。原始记录里其他写法需转换为这四种。 7. 如果原始记录里没有账面数,则账面数为空字符串。用户提示词就简单了,直接粘贴清洗后的原始记录:
以下是仓库盘点原始记录文本,请整理成CSV表格: [粘贴预处理后的文本]这段模板为什么有效?因为它把角色、任务、输出边界、格式约束、异常策略五个维度都讲清楚了。模型输出时没有歧义,只要原始数据本身不是乱到离谱,结果基本一次过。
4.2 让输出稳定可解析的四个技巧
除了提示词模板本身,还有四个关键操作决定输出稳定性。
第一,强制表头。必须在提示词里明确写出表头行,模型照着抄就不会自己发明列名。列名一乱,后面的解析脚本必炸。
第二,只输出CSV主体。如果允许模型带解释,它会在末尾加一句"以上是根据您提供的记录整理的结果",这句话混进CSV就直接脏了。所以提示词里要写死"不输出任何解释"。
第三,用少量示例带节奏。如果某些记录格式很特殊,比如包含"批号20240506数量50箱"这种连写文本,可以在提示词里加一个用户示例和期望输出的示例。模型看到样例后,处理同类文本的稳定性会提升非常明显。
第四,要求逐行对应。在提示词里强调"保持与输入顺序一致",这样程序可以把输出和输入逐行对比,定位哪一行解析错了,而不是整段作废。
这些细节都是反复测试中总结出来的。最开始模板只写了"整理成表格",输出五花八门,加了约束之后才稳定下来。
4.3 自动校验与兜底机制
模型输出的CSV不会百分百干净,所以校验脚本是必要的。我的校验逻辑分四步:
import csv import io def validate_csv(raw_text, expected_rows): reader = csv.reader(io.StringIO(raw_text)) rows = list(reader) if not rows: raise ValueError("没有解析出任何行") header = rows[0] expect_header = ["SKU编码", "品名", "规格", "单位", "账面数", "实盘数"] if header != expect_header: raise ValueError(f"表头异常: {header}") if len(rows) - 1 != expected_rows: raise ValueError(f"行数不匹配: 期望{expected_rows}行, 实际{len(rows)-1}行") for i, row in enumerate(rows[1:], start=2): if len(row) != 6: raise ValueError(f"第{i}行字段数异常: {len(row)}") if row[5] != "未识别": try: float(row[5]) except ValueError: raise ValueError(f"第{i}行实盘数非数字: {row[5]}") return rows校验失败的时候,不要直接放弃。让程序自动重新请求一次,把具体的错误信息拼进提示词再试,比如"上一次输出表头不对,请严格按示例输出"。重试一两次后,八成能恢复正常。如果重试还不行,就把原始文本标记为"待人工处理",缩小返工范围。
这一步我强烈不建议省掉。早期我以为模型输出得够整齐,跳过校验直接写入表格,结果有一批单位全部错位,排进电子表格才发现返工成本巨大。多花几秒钟做校验,后面省几个小时。
5. 常见问题与排查实录
5.1 输出格式不稳定的排查顺序
如果你遇到模型输出格式飘忽不定,按这个顺序排查,基本能定位到九成问题。
先看temperature。凡是输出一会儿带表头一会儿不带、一会儿多一列一会儿少一列,十有八九是温度太高,先降到0.1以下再看。
再看提示词里有没有"只输出CSV,不要额外内容"这条。没有的话,模型会在表格前后写"好的""以下是整理结果",解析时直接被这些杂质干扰。
再看上下文是不是太长。如果上下文接近窗口上限,模型在后半段的输出质量会断崖式下跌,表现就是前面格式正常、后面缺字段。
最后才考虑模型本身的表现。如果以上几个都排除了还不稳定,换一个量级更大的模型往往能解决。这个排查顺序我每次遇到问题都先过一遍,节省了大量无头苍蝇式调试的时间。
5.2 数量与单位识别错位的实战案例
有一次盘点记录里有一行"怡宝 550ml 20箱 480瓶",我期望的结果是规格栏填"550ml",单位填"箱",实盘数填20。模型第一次输出把单位写成了"瓶",实盘数写成了"480"。排查下来发现,问题出在原始文本里"20箱 480瓶"是一个复合数量,模型不知道应该以哪个为准。
解决方案是在预处理阶段就把复合数量拆成单值字段,而不是丢给模型判断。我加了一条规则:凡是一行里面有多个数字加单位的组合,预处理程序把它们拆成"主单位数量"和"备注"两列,主单位数量只保留第一个组合。这样模型收到的是已经降过噪的文本,自然不会再选错。
这也是整个流程里很重要的一条心得:能用规则解决的问题,就不要交给模型。模型只处理规则解决不了的部分,才能发挥最大价值。这条原则同样适用于其他数据处理场景,不只是盘点表格。
5.3 大批量盘点时的性能优化
最初跑整仓5000行记录,一次性丢给模型,结果等了好久还没跑完,而且输出后半段质量明显下降。优化的思路是分批处理,我按200行一批,每批独立请求,四个并发同时跑。处理时长从原来的二十多分钟压缩到了四分钟左右,人工巡检一遍结构化结果,直接导入电子表格。
并发请求的写法很简单,给请求函数加上一个线程池:
from concurrent.futures import ThreadPoolExecutor def process_batches(batches): with ThreadPoolExecutor(max_workers=4) as executor: results = list(executor.map(process_batch, batches)) return results这里要提醒一句:并发数不是越多越好。本地模型的显存和内存都是瓶颈,开太多并发会导致排队甚至OOM。我实测四路并发在中等配置的机器上比较合适,两路更稳,八路以上容易崩。
还有个小技巧:如果盘点的差异行不多,可以先让脚本比对历史账面数和已完成的实盘数,只把差异行丢给模型补充差异原因。这样模型需要处理的文本量大幅缩减,准确率和速度都能上去。
最后聊点个人体会。这套流程跑下来,我最深的感受是"AI自动生成表格"并不是把模型一部署就完事,真正花时间的反而是数据预处理、提示词约束和校验兜底这三件小事。模型本身不难选,难的是给它划清楚边界:哪些是它该干的,哪些必须由规则来做。如果你也想在自己团队里落地这套流程,建议从一次两周以内的试点开始,拿最近一次盘点的原始记录跑通全流程,再逐步扩大范围。先跑通再优化,比一开始就想做一个完美系统靠谱得多。