在 POS 机上跑大模型?你可能第一反应是“这玩意儿也跑得动?”。但如果说的是把 DeepSeek 的 1.5B 量化版离线部署到一台 Android 智能 POS 机上,让收银员在断网、无服务器的状态下照样可以用自然语言把“来两瓶冰红茶”变成订单明细,这个画面是不是就清晰多了。这个项目做下来,我最大的感触是:大模型上边缘设备不是“能不能跑”的问题,而是“怎么选型、怎么裁剪、怎么和业务闭环”的问题。这篇我就把整条链路从硬件评估、模型选型、环境搭建到业务集成、踩坑修复完整梳理一遍,给想在自己的设备上做离线 AI 功能的朋友一条可以照抄的路径。
1. 为什么要在 POS 机上跑大模型 —— 需求、形态与边界
1.1 收银场景里的哪些痛点值得用 LLM 来解决
传统收银软件最大的痛点就是商品匹配太死板。顾客说“拿瓶可乐”,收银台得知道是可口可乐、百事可乐还是无糖可乐;再比如“来一套超值早餐,咖啡换燕麦拿铁”,这种口语化的组合指令,靠正则表达式和 if-else 根本没法维护。
我之前在某零售项目里做过一套关键字规则引擎,光“可乐”这个商品就挂了四十多条正则,依然会在方言和缩写面前翻车。后来大家开始尝试中央服务器调大模型 API,效果倒是不错,但门店网络一抖、总部接口一挂,收银台就罢工,这是零售业绝对不能接受的。
所以真正让离线端侧部署有价值的是这四个字:确定性。把模型放在 POS 机本地,不光解决了断网可用问题,还顺手解决了数据出店合规和数据延迟的顾虑。顾客消费习惯、菜品销量、时段偏好这些数据,不出收银台,从根上断了数据外泄的风险。
1.2 项目定位:边缘算力上的“受限助手”
这里必须泼一盆冷水:别指望 1.5B 的量化模型在 POS 机上陪你聊人生。边缘部署的 LLM 定位应该是“结构化信息抽取器 + 意图路由器”,而不是全能对话引擎。我在项目启动时给自己定了三条边界:
- 模型只做指令解析:把自然语言转成 JSON 订单,不讨论、不闲聊。
- 模型只需要 70 分:100 个指令正确解析 70 个,剩下 30 个靠传统模糊匹配和人工确认兜底。
- 模型输出必须有界:超出商品库范围的输入,统一返回 unknown,绝不自由发挥。
把期望拉到“可用”而不是“惊艳”,整个工程难度会下降一个量级。你会发现大模型在这里面更像是传统 NLP 的升级版——用它的语义理解能力替代维护成本高到离谱的规则系统,而不是把业务逻辑都丢给它。
1.3 为什么选择 DeepSeek 而不是其他模型
选模型这事我纠结了很久。最终锁定的原因有三个:开源、体积、输出格式。DeepSeek 蒸馏出来的小尺寸模型在中文意图识别和指令跟随上表现相当稳,而且有开放许可,可以离线分发使用,这对商业项目来说太关键了。举个例子,同样输入“两杯冰美式”,1.5B 蒸馏版能稳定输出规范的 JSON,而很多同规模通用模型会答非所问或者把 JSON 格式写坏了。
对比闭源 API 方案,本地部署省了网络请求延时(每次调用至少省 300ms),省了按 token 计费的长期成本,更重要的是断网可用。一次部署,终身免服务费——大家可以算算一个门店一年省下来的 API 调用费有多少。
2. 硬件选型与模型选型 —— 你的 POS 机能不能跑
2.1 先看三样东西:SoC、内存、Android 版本
开工前先别急着下载模型,拿 CPU-Z 或者到系统设置里看一眼你的 POS 机是什么配置。我汇总了市面上常见的三类 Android POS 平台,大家可以做个对照:
| 平台形态 | 典型配置 | 内存 | 能跑的模型档位 | 体验预期 |
|---|---|---|---|---|
| 老一代四核 | 四核 A53,主频 1.5GHz | 2GB | 0.5B~1B Q4 | 非常勉强,仅验证用 |
| 主流六核 | 双核 A72 + 四核 A53 | 4GB | 1.5B~3B Q4 | 可用的核心目标平台 |
| 新一代八核 | 八核 A76 系,带弱 NPU | 8GB | 3B~8B Q4 | 体验流畅,可多任务 |
重点观察三件事:内存够不够装下模型加运行开销;Android 版本是不是 7.1 及以上(太老的系统跑不了新版编译产物);系统有没有 root 或者 adb 调试权限。这三样缺一不可,否则后面每一步都是坎。
项目里用的主力测试机是某国产六核 SoC(双核 A72 + 四核 A53,4GB 内存,Android 9),跑 DeepSeek 1.5B 量化版属于相对舒适的区域。如果你是 2GB 内存的老设备,建议直接换目标机器,别在这个上面浪费时间——因为就算模型勉强加载进去,内存一紧张系统就把模型进程杀了,体验远不如裸奔收银软件。
2.2 模型版本怎么选:参数规模 / 量化 / GGUF
DeepSeek 官方放出来的蒸馏模型有多个尺寸,对边缘设备来说,我们要的是 GGUF 格式的量化版。这里先把量化说透。
一个 FP16 精度的 1.5B 模型,参数占 3GB;而 INT4 量化后压缩到约 1GB,体积差三倍,性能损失却只有几个点。这就好比你把一张高精度地图转成低精度导航图,路线方向一条不差,只是省掉了用不上的细节。实际操作中我用 Q4_K_M 这个量化方式,这是目前社区验证过的质量和体积最佳平衡点:1.5B 模型文件不到 1.1GB,3B 不到 2GB,8B 大约 4.7GB。如果你的设备内存充裕,可以试 Q5_K_M,质量会再稳一点,速度略降,但差异不大。
选 GGUF 而不是其他格式,理由只有一个:生态。GGUF 背后是 llama.cpp 那一套推理体系,C 语言实现,无依赖,交叉编译到 Android 平台非常顺;而且社区里跑通的人多,遇到问题基本都能搜到解决方案。另有 ONNX Runtime 路线,Android 支持也不错,但对新算子的适配总是慢半拍,遇到不支持的 op 你会非常痛苦。
2.3 一份诚实的性能预期表
这一节我必须说实话:在 CPU 推理的 POS 机上,速度远超你想象地慢。核心指标看两个:预填充(prompt 处理速度)和解码(生成速度)。我用 1.5B Q4_K_M 在这台六核 POS 机上实测的数据:
| 指标 | 实测数值 | 说明 |
|---|---|---|
| 预填充速度 | 8~12 token/s | 取决于 prompt 长度,行数越多越慢 |
| 解码速度 | 3~5 token/s | 每秒钟只能蹦出三五个字 |
| 模型加载时间 | 15~30 秒 | 进程启动后加载权重到内存,mlock 可稍快 |
| 峰值内存 | 约 1.8GB | 模型 1.1GB + KV cache 和运行开销 |
| 300 token 完整回复耗时 | 约 60~90 秒 | 所以绝不能等完整回复再执行 |
你看这个数据就明白了:如果让它自由发挥写一段 300 字的“温馨提示”,收银员能等出心脏病。所以项目的核心设计理念永远是——结构化的短输出。所有提示词都把输出压到 50 token 以内,能出一个 JSON 对象就结束后,业务拿这个 JSON 去执行动作。
3. 部署环境搭建 —— 跑通大模型的三种路线
3.1 路线对比
时间紧、想快速验证的同学走路线 A,准备做正式产品的走路线 B,路线 C 是给确实懒得碰代码的人准备的。先看对比:
| 路线 | 操作方式 | 优点 | 缺点 | 适用人群 |
|---|---|---|---|---|
| A:Termux 装引擎 | 命令行安装运行 | 上手快,5 分钟出结果 | 交付形态非 APK,不适合门店长期使用 | 开发验证、技术调研 |
| B:JNI 打包 APK | 交叉编译 C++ 库,封装成安卓 App | 交付友好,可开机自启、保活 | 工程量翻倍,需熟悉 NDK | 正式产品落地 |
| C:现成推理壳 | 找开源推理客户端装模型 | 零代码,一键加载 | 分辨率、按键适配差,兼容性看运气 | 个人折腾、原理验证 |
3.2 路线 A:Termux 跑 llama.cpp(最快速验证)
Termux 是 Android 上的终端模拟器,能把 POS 机变成一台 Linux 小主机。整套操作流程是这样的:
# 1. 安装 Termux 后先升级包源 pkg update && pkg upgrade -y # 2. 安装 llama.cpp 和必要工具 pkg install llama.cpp build-essential wget # 3. 建个目录放模型 mkdir -p /sdcard/pos-ai/models cd /sdcard/pos-ai/models # 4. 下载量化后的 GGUF 模型文件(这里用你自己获取的下载地址替换) wget -O deepseek-1.5b-q4.gguf <你的模型下载地址> # 5. 启动 llama-server 服务(HTTP 接口方便调试) llama-server \ --model /sdcard/pos-ai/models/deepseek-1.5b-q4.gguf \ --ctx-size 2048 \ --threads 4 \ --mlock \ --port 8080这里解释几个关键参数。--ctx-size 2048是上下文窗口,对收银指令来说足够了,调大了只会增加 KV cache 内存开销。--threads 4需要特别留意,A72 大核和 A53 小核混在一起,我试过开满 6 线程,结果切换开销比算力提升还大,速度反而降了;锁 4 个核是最稳的。--mlock的意思是锁住物理内存防止被换页到 zram 或闪存,对推理稳定性影响非常明显,建议永远开着。
启动完之后可以用 curl 验证服务状态:
curl http://localhost:8080/health看到返回{"status":"ok"}就说明引擎起来了。接着发一条真实指令试一下,直接用 OpenAI 兼容的/v1/chat/completions接口,方便后面业务代码对接。
3.3 路线 B:打包成 Android 应用(最终交付形态)
Termux 方案适合自己调试,但门店收银员不可能拿着终端敲命令行。正式落地需要把推理引擎封装进 APK。核心工作是交叉编译 llama.cpp 的 Android 库,用 NDK 工具链编译出libllama.so,再通过 JNI 暴露 Java/Kotlin 能调的接口。
我这里只给出思路和关键节点:
- 用 CMake 交叉编译,工具链选
android-ndk-r26d或更新版本,ABI 只保留arm64-v8a,32 位 ARM 就别浪费时间了,已经跑不动现代模型。 - 模型文件不要塞进 APK 的 assets 里——一个 1GB 的 APK 安装体验极其糟糕,而且每次更新要重新拷贝。正确做法是首次启动时从服务器下载或者从 U 盘拷贝到应用私有目录
/data/data/包名/files/models/。 - 进程架构上,用
separateProcess跑一个前台服务(Foreground Service),把模型加载和推理逻辑放在独立进程里,避免主进程被系统回收时把模型从内存里带走。
我做这个项目时一开始图省事直接用了 Termux,等产品要演示时才推倒重来封装 JNI。那一周每天都在跟 Kotlin 协程和 JNI 全局引用作斗争。所以给个建议:如果明确要交付 APK,请从第一天就走路线 B,Termux 方案只留作快速调试。
4. 让模型听收银员的话 —— 提示词工程与指令解析
4.1 收银指令的结构化拆解
端侧模型推理慢,所以不能让模型去“填空写文章”,而是要让它严格按一个固定的 JSON 结构输出,像填空一样。我定义的收银指令结构是这样:
{ "action": "create_order", "items": [ { "name": "冰红茶", "quantity": 2, "spec": ["冰"], "remark": "" } ], "raw_text": "来两瓶冰的柠檬茶" }这个结构覆盖了 POS 下单 90% 的需求:action 固定是create_order,items 里装商品明细,spec 存规格(冰/热/大杯/小杯),remark 存备注。模型的任务就是把人类的话翻译成这个 JSON,不做额外发挥。
用 JSON 而不是让模型输出普通文本的好处是:业务侧解析非常铁。Kotlin 里用现成的 JSON 库一把梭,不用去猜模型到底说了什么;解析失败时还能明确知道是模型问题还是格式问题,方便做兜底。
4.2 提示词模板
提示词的质量决定结构化输出的稳定度。这是我调试了很多轮之后的模板,可以直接抄:
系统: 你是一个收银指令解析助手。你的任务是把顾客的口语化点单内容解析成 JSON,不要输出任何多余文字。 规则: 1. 只输出 JSON,不要解释、不要寒暄。 2. 商品名尽量匹配给定商品列表中的标准名称。 3. 数量用整数,单位默认是“份”或“个”。 4. 无法识别的商品放到 remark 字段,标记为 unknown。 5. 如果整句话无法理解,输出 {"action":"unknown"}。 商品标准名列表:冰红茶、柠檬茶、拿铁咖啡、美式咖啡、超值早餐套餐、牛肉汉堡、薯条…… 示例1: 输入:来两瓶冰的柠檬茶 输出:{"action":"create_order","items":[{"name":"柠檬茶","quantity":2,"spec":["冰"],"remark":""}]} 示例2: 输入:一套早餐,咖啡换燕麦拿铁 输出:{"action":"create_order","items":[{"name":"超值早餐套餐","quantity":1,"spec":["燕麦拿铁"],"remark":"咖啡换燕麦拿铁"}]} 用户输入:{用户说的原话} 请按规则输出。这里有个容易忽略的细节:商品标准名列表不要太长。我一开始把整个商品库 2000 多个 SKU 全灌进了提示词,结果预填充时间从 3 秒涨到 12 秒,而且模型开始混淆相近商品。后来按门店销量把高频 Top 50 商品名提取出来放进提示词,其余的先靠模糊匹配兜底,速度和准确率都上来了。
4.3 输出解析与容错
模型输出偶尔会调皮,比如带了一行“好的,我来帮你解析”或者把 JSON 包在代码块里。我的处理是三步:
- 第一步,用正则从输出里暴力提取
{...}片段。 - 第二步,JSON 解析失败就用传统模糊匹配(编辑距离 + 关键词)去做二次尝试。
- 第三步,识别结果里有 unknown 字段时,把解析结果作为“候选订单”弹到收银员面前人工确认,而不是直接进结算。
这个“人工确认”设计很关键。离线 AI 的定位不是取代收银员,而是让收银员少敲键盘。机器猜得准就一键确认,猜不准就手动改一下——这个过程不会比传统收银慢,而且顾客体验好了很多。
5. 实操全流程 —— 从零到“离线收银助手”
5.1 第 1 步:准备模型文件
先从模型站点下载量化好的 GGUF 文件。拿到文件后建议立刻校验一下大小和哈希,我遇过一次模型文件下载不完整,llama-server 启动时报各种莫名其妙的维度错误,排查了半天才发现是文件截断了。操作:
# 校验文件大小,和发布页面对一下 ls -lh /sdcard/pos-ai/models/deepseek-1.5b-q4.gguf # 用 sha256sum 校验哈希 sha256sum /sdcard/pos-ai/models/deepseek-1.5b-q4.gguf模型放/sdcard/pos-ai/models/还是应用私有目录?开发调试时放/sdcard方便,因为 Termux 能直接读到;正式 APK 一定要用私有目录,/sdcard的权限控制和文件完整性都不可控。
5.2 第 2 步:起服务、热验证
服务起来后用 curl 发一条指令测试,这里我建议直接测一个“坏例子”,比如“给我来个随便什么吃的”,看看模型会不会老老实实输出{"action":"unknown"}。如果它居然硬编了一个菜名出来,说明提示词的温度设置太高或者约束不够强。
调试时可以顺手测一下温度参数。我最终把temperature设在 0.2,top_p设在 0.9,这样能极大压缩随机性。收银场景要的是保守和稳定,不是创意发散。
5.3 第 3 步:集成到 POS 收银流程
业务集成这步是全场最花时间的环节。POS 后台是一个跑在 Android 上的原生收银应用,数据存在本地 SQLite 里。AI 集成方案做了一个很轻的中间层:
// 伪代码,展示集成思路 class AiOrderParser(private val serviceUrl: String) { // 离线引擎的 HTTP 客户端 suspend fun parseToOrder(rawText: String): Order? = withContext(Dispatchers.IO) { val request = chatCompletionRequest(rawText, temperature = 0.2) val json = httpPost(serviceUrl, request) val orderJson = extractJsonFromModelOutput(json) // 正则抽取 // 解析失败进入兜底 if (orderJson == null) return@withContext fuzzyMatchOrder(rawText) val order = jsonToOrder(orderJson) ?: return@withContext fuzzyMatchOrder(rawText) if (order.hasUnknownItem()) return@withContext OrderCandidate(order) order } }调用时序是这样的:收银员按住麦克风或点“AI 点单”按钮 → 语音转文字(这一步也用离线引擎,后面会提)或直接输文字 → 文本发到本地 llama-server → 拿到 JSON → 解析成订单候选 → 弹到界面上人工确认 → 一键入单。
注意这里的关键设计:中间层只和localhost通信,全套数据不出这台 POS 机。门店网络断了对这套流程毫无影响——因为你根本没走外网。稳定性和响应速度的收益非常直接。
5.4 第 4 步:打包、优化、上线
进入 APK 阶段后,还有三个绕不开的优化点:
- 开机自启:POS 机开机要能自动拉起模型推理服务。Android 自启动限制很严,所以得引导用户在系统设置里把这个应用加入白名单,同时用前台服务加持续通知,防止系统把它当后台任务回收掉。
- 温控与降频:六核平台连续推理五分钟,机身明显发热,系统会主动降频导致速度更慢。解决思路是把推理线程限制在 4 核,同时监控 CPU 温度,超过 70 度时主动休息 15 秒再继续推理,跟人一样,喘口气干活更快。
- 模型复用:同一个模型进程只加载一次,所有请求都走同一个 HTTP 接口。杜绝“每次点单都重新加载模型”这种自杀式设计——加载一次要 20 秒,谁也受不了。
6. 性能优化、内存管理与踩坑实录
6.1 慢到想砸机?先做这三件事
如果部署完发现速度感人,先别急着换硬件,按顺序排查这三件事:
第一,确认线程数。在六核 SoC 上开 4 个线程通常是最优解,开满 6 个反而会因为大小核调度问题掉速。这个需要跑一轮速度测试来定,不要凭感觉。
第二,把采样参数拉保守。把temperature调到 0.2、top_p调到 0.9、关闭重复惩罚。模型推理时采样参数也会影响计算量,而且在收银场景里“随机性”是敌人,越低越好。
第三,开 mlock 锁页。Android 系统在内存紧张时会做内存换页,把模型权重换到 zram 压缩区,一换就是灾难——推理速度直接掉一个数量级。--mlock能在启动时把权重驻留在物理内存里,防止系统搞小动作。
做完这三件事,我实测最慢的机型从 2 token/s 提到了 4 token/s,体感上从“等死”变成了“勉强能忍”。
6.2 内存不足与进程被杀
这是整个项目里最折磨人的问题,没有之一。现象是:模型加载到一半,进程直接消失,没有任何日志,只有OutOfMemoryError或者干脆 SIGKILL。
排查后发现有两层原因。第一层是模型本身太大:1.5B Q4 模型需要 1.1GB 权重加 KV cache 加运行开销,峰值奔着 1.8GB 去。4GB 设备凑合能用,2GB 设备必死。第二层是 Android 的 LMK(Low Memory Killer):系统检测到内存紧张时,会优先杀后台进程腾空间。而很多 POS 机预装的收银保护进程占了内存大头,新来的模型进程自然成了替罪羔羊。
解决办法分三步:一是把模型降到 1B 以下,这是治本;二是给模型进程提权,用前台服务加通知的方式提升 oom_adj 等级,降低被杀概率;三是在系统设置里手动把 AI 应用加入“受保护应用”列表。这三步都做了才能在 4GB 设备上稳定跑。
6.3 模型输出偏移与幻觉
小模型是幻觉重灾区。我遇到过最离谱的情况:顾客问“有 WiFi 吗?”模型居然在 items 里编了个{"name": "WiFi", "quantity": 1}出来。这告诉我们两件事:提示词必须明确“无法识别就输出 unknown”;商品名列表要有穷尽性——不列进去的商品,模型就应该老实认输。
后来我在提示词里加了一条硬性规则:只允许输出商品列表里出现的标准名称,列表之外一律标记 unknown,效果立竿见影。另外,可以做一个“拒绝样本”池,把顾客可能会问但门店确实没有的商品(比如“关东煮”“烤串”)放进 few-shot 示例里教模型拒绝,比单纯写规则有效得多。
7. 常见问题排查速查表
7.1 经典问题汇总
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 模型加载一半崩溃 | 内存不足 / OOM | 换更小模型;开 swap;提升进程 oom_adj 优先级 |
| 推理速度只有 1~2 token/s | 线程数过高或过低 | 跑一遍不同线程数的速度对比,选最优值 |
| 输出经常不合法 JSON | 温度太高 / 提示词约束弱 | 降到 0.2;加强提示词里的“只输出JSON”约束 |
| 系统会自动杀掉 AI 进程 | 后台进程被 LMK 回收 | 设为前台服务;加入系统受保护应用列表 |
| 加载速度非常慢且经常卡顿 | 模型权重被换页到 zram | 加--mlock,确保权重锁在物理内存 |
| 识别出商品库以外的名字 | 幻觉 / 提示词未约束 | 严格限制输出范围;加拒绝样本示例 |
| 预填充特别耗时 | prompt 里商品列表太长 | 只放高频 Top 50 商品;低频商品走模糊匹配兜底 |
7.2 一个经验:先固定“交互协议”再开发
这可能是全篇最重要的一条经验,也是我踩坑之后总结的:先在纸面上固定好模型输入和输出的 JSON 协议,再开始写提示词和业务代码。
因为这个协议一旦定好,上下游就可以并行开发。我这边调提示词让模型输出稳定的结构,那边业务同学拿着协议写订单转换逻辑和数据校验,两不相误。最忌讳的是先跑通再说,协议后面左改右改,模型提示词和业务解析各改一遍,时间翻倍。
8. 项目还能怎么进化 —— 三个实际可落地的扩展方向
8.1 缓存高频订单模板
经过一段时间运行后你会发现,门店高频订单其实就那么几十种:“可乐一听”“美式一杯”“套餐一套”。完全可以做一层订单模板缓存:先把顾客原话做 MD5,命中缓存就直接调模板下单,不需要再走模型推理。这个缓存命中率在实际门店能跑到 60%~70%,直接把模型调用量砍掉一大截,推理压力瞬间降下来了。
8.2 离线语音识别接入
现在方案里“语音转文字”如果还是走的云端服务,那断网场景依然不完整。Android 自带的语音识别离线包可以接进来,虽然准确率比云端低一点,但在收银这种短语为主的场景里够用了。配合本地 LLM,整个流程端到端无网,正式达到“离线收银 AI 助手”的完全体。
8.3 数据回流与模型持续优化
这是二期规划里最有价值的一件事:把每天“人工确认”过程中被收银员修改过的样本(模型猜错但人改对的)积累下来,形成一份每日修正日志。攒够几千条后,用这些数据做一个 LoRA 微调,模型对这个门店的商品体系理解会精准很多。这套“数据飞轮”一旦转起来,AI 是越用越聪明,而不是永远停留在初始水平。
最后再说点我个人在实际操作里的心里话。第一次在断了网的测试机上把整套跑通时,我对着 POS 麦克风说了一句“来两瓶冰的柠檬茶”,几秒钟后屏幕上弹出了两行明细,那一刻你才真的相信离线 AI 这回事是能落地的,不是演示 Demo 里骗人的东西。整条链路里最难的不是模型本身,而是如何把小模型装进一个严格受限的硬件里,并且让它稳定地完成一个又一个琐碎但真实的业务动作。商业产品不需要花哨的对话,需要的是确定性——离线、稳定、可控,这三点做到了,它就是一个能每天陪你收几万单的靠谱同事。