商城客服每天收到大量咨询,第一步是把它们分到对的队列里。我们用六个类别:商品咨询、价格优惠、订单支付、物流配送、退换售后、账户会员。这件事我不想交给云端大模型,想用本地小模型来做,于是把两个本地决策模型放在同一批 20 条问题上比了一下:一个是 Jev-Style-Qwen3.5-2B-Decision(Q8_0 GGUF),另一个是 laya-multilingual。同时我也看了它们在 A10 GPU 和一台普通 Windows 纯 CPU 电脑上的速度。
这篇文章先给结果,再讲 laya 错在哪、Jev-Style 需要什么兜底,最后讲测试和部署是怎么一步步做出来的。
一、先看结果
1. 速度:GPU 约 100ms,CPU 约 3 秒
两套环境跑的是同一个模型文件、同一套提示词、同一个测试页:
- GPU:NVIDIA A10 24G 显存,8 核 CPU,32GB 内存,单次分类约 100ms;
- 纯 CPU:一台 16GB 内存、没有独显的 Windows 电脑,单次分类约 3 秒。
下面是 GPU 环境里测试页的一次真实请求。输入「会员积分怎么查,登录一直提示验证码错误」,返回 账户会员 73.1%,耗时 101ms。其余五类都在 7% 以下。这里的耗时是服务端从收到请求到拿到结果的时间,包括转发给模型服务和模型推理。
100ms 这个量级,用户基本感觉不到等待,可以直接放在客服入口做实时分流。3 秒放在交互里就明显卡了,但跑批量、做开发验证没问题。
2. 准确率:Jev-Style 20/20,laya 15/20
测试集是 20 条常见商城咨询,六个类别都覆盖到,每条都按类别说明标了参考答案。
- Jev-Style:20/20,全对;
- laya-multilingual:15/20,准确率 75%。
Jev-Style 在 CPU 和 GPU 上的分类结果是一样的:模型和提示词都没变,采样温度设成 0,硬件只影响速度。
顺带说一下 Jev-Style 是怎么“做选择题”的。它不是生成一段话再去解析,而是只让模型输出一个 token,直接读这个 token 上 A–F 六个选项字母的概率:
prompt = 固定模板(咨询原文, 问题"这条商城咨询属于哪一类?", A–F 六个选项及说明) + "Answer:"
resp = llama-server.chat(prompt, temperature=0, max_tokens=1, 返回第一个 token 的候选概率)
p = { 字母: 概率 | 候选 token 是 A–F 之一 }
p = 归一化(p) # 只在 A–F 六个选项上做 softmax
类别 = argmax(p),置信度 = p[类别]
好处有两个:一是只算一个 token,速度快;二是天然拿到六个类别的概率分布,后面做置信度兜底就有了依据。
二、laya 错在哪
laya 判错的 5 条全在下面,Jev-Style 这 5 条全部判对:
| 咨询 | 参考答案 | Jev-Style | laya |
|---|---|---|---|
| 衣服尺码偏大,想换成小一号,运费谁出 | 退换售后 | 退换售后 0.54 ✔ | 价格优惠 0.67 ✘ |
| 这款桌子还有货没 | 商品咨询 | 商品咨询 0.92 ✔ | 退换售后 0.91 ✘ |
| 这个手表可以积分兑换吗 | 账户会员 | 账户会员 0.51 ✔ | 价格优惠 0.72 ✘ |
| 这个蓝牙音箱怎么和手机配对 | 商品咨询 | 商品咨询 0.82 ✔ | 账户会员 0.51 ✘ |
| 鞋子磨脚,想换成大一码,运费谁承担 | 退换售后 | 退换售后 0.47 ✔ | 订单支付 0.42 ✘ |
我看下来,laya 的问题主要有两个。
**第一,容易被字面关键词带偏。**两条换尺码的咨询里都有“运费”,laya 就往价格优惠、订单支付上靠,没抓住“换一个尺码”这个真正的诉求。“积分兑换”里有“兑换”,被判成了价格优惠,但积分是账户会员的事。这类错误说明它更多是在匹配字面上的词,而不是理解整句话要办什么事。
第二,错的时候也很“自信”。“这款桌子还有货没”被判成退换售后,概率 0.91,和它判对时的把握没有区别。这就意味着没法靠 laya 的概率设一道“低于多少就拦下来”的门槛:该拦的错例,它给的分数反而很高。
三、Jev-Style 也要留兜底
Jev-Style 全对,但回头看上面那张表,它有几条的把握并不大:
- 衣服换小一号 + 运费谁出:0.54
- 手表积分兑换:0.51
- 鞋子换大一码 + 运费谁承担:0.47
这几条都在 0.5 上下,说明这类说法正好卡在两个类别的边界上:退换售后和价格优惠、订单支付之间,账户会员和价格优惠之间。这次判对了,换个说法就不一定。
所以上线时我建议加一道兜底:最高类别的概率低于 0.55,就不自动分流,转人工或者让用户再确认一下。0.55 是参照这几条边界样本定的初始值,20 条样本撑不起一个精确的阈值,等线上积累了真实咨询,要按实际误判情况重新调。
和 laya 不同,Jev-Style 的低分恰好落在这些边界题上,所以这个门槛是能起作用的。
四、测试是怎么一步步做出来的
整个过程不是一开始就设计好的,是边做边调出来的。
**第一步,先搭 laya 的测试页。**laya 是编码器加决策头的结构,我先给它包了一个 /classify 接口和一个网页,能输入一句话、看分类结果,在 Windows 电脑上跑通。
**第二步,加上 Jev-Style 做对比,把用例扩到 20 条。**两边用完全相同的六个类别和类别说明,保证比的是同一道题。测试集一开始只有几条,后来补到 20 条:像「这款桌子还有货没」「这个手表可以积分兑换吗」「我怎么查看我的积分呢」「我要投诉快递员,怎么我的快递一直延迟」这类是特意加进去的,其余用商品、价格、支付、物流、售后、会员几类的常见问题补齐。对比结果就是前面说的 20/20 对 15/20。
**第三步,在 CPU 上发现“样例快、新问题慢”。**测试页上点现成的样例按钮,返回挺快;自己输入一句新问题就慢下来。原因是 llama-server 会缓存上一次的提示词:同一句话再问一遍,大部分计算可以复用;换一句新话,整段提示词要在 CPU 上重算。
**第四步,试过改提示词提速,但准确率掉了。**我试了把咨询原文挪到提示词末尾,让前面固定的选项说明能被缓存复用;也试了把提示词压短。速度确实上来了,但分类明显变差,换货、积分这类问题开始判错。为了速度牺牲准确率不划算,所以提示词保持原样,速度问题交给 GPU。
**第五步,给 Jev-Style 做测试页,上 GPU 验证。**测试页的交互很简单:
输入一条咨询或点样例,点“分类”,页面显示问题类型、耗时和六个类别的概率条。概率低于 0.55 的,就是上一节说的该转人工的那部分。
五、部署过程
1. 整体结构
结构很简单:模型用 llama.cpp 的 llama-server 跑,端口 8081,只对本机开放;测试页是一个小的 Python 服务,端口 8091,负责接收咨询、转给模型、把六个类别的概率整理出来返回。测试页本身不加载模型,所以换 CPU 还是 GPU,对它来说没有区别。laya 那一路(8090 的分类服务 + 8080 的编码器)只在做对比时才启动。
2. 两种部署怎么选
- 要做开发调试、改类别和提示词、跑离线批量:用 Windows 纯 CPU 就够,不需要显卡;
- 要给线上做实时分流:用 GPU。我这次用的是魔搭(ModelScope)的 GPU 实例。
两种部署的测试页和接口完全一样,调用方不用改。
3. Windows 纯 CPU
从 llama.cpp 官方发布页下载 Windows CPU 版的 llama-server,版本要新,旧版本不认 Qwen3.5 架构。把 GGUF 模型文件放好,装上 Python 依赖(httpx、requests、fastapi、uvicorn),然后开两个终端:
# 终端 1:启动模型服务(8081)
llama-server -m Jev-Style-Qwen3.5-2B-Decision-Q8_0.gguf --host 127.0.0.1 --port 8081 -c 2048 -np 1 --no-cache-idle-slots
# 终端 2:启动测试页(8091)
python scripts/call_laya_lmstudio.py --serve
浏览器打开 http://127.0.0.1:8091/ 就能测。模型服务是否就绪,可以先访问一下它的 /health 接口。这里用 -np 1 开单个槽位,一次只处理一个请求;批量跑的同时在页面上点,两边会排队,耗时会偏大。
4. 魔搭 GPU 实例:踩过的几个坑
魔搭的做法是:开一个 GPU 实例,镜像选自带 CUDA 的(比如 ubuntu22.04-cuda12.4 这类),把启动脚本、分类脚本和模型文件直接传到 /mnt/workspace/,然后一条命令启动:
bash /mnt/workspace/start_jev_modelscope.sh
脚本会检查显卡、选 CUDA、编译带 CUDA 的 llama-server、用 GPU 加载模型,最后在 8091 打开测试页,在 Notebook 里做端口转发就能用浏览器访问。这个脚本是一路踩坑改出来的,下面几个坑值得记一下。
**坑一:GitHub 克隆失败。**实例访问 GitHub 很不稳定,克隆 llama.cpp 经常在 TLS 阶段断掉。解决办法是按顺序尝试几个国内镜像地址,都不行再回到 GitHub 官方地址。
**坑二:CMake 不认 native。**编译 CUDA 版本时一般会写 CMAKE_CUDA_ARCHITECTURES=native,让它自动识别显卡架构。但实例里的 CMake 版本较旧,不会展开 native,结果架构编成了空的 compute_,nvcc 直接报错。改成先用 nvidia-smi 查出显卡算力,再把具体数值交给编译就好了。
坑三:CUDA 版本要和驱动匹配,别信表头。这是最隐蔽的一个。容器里 nvidia-smi 表头显示的 CUDA 版本,往往是镜像自带的版本,和宿主机驱动实际能支持的不是一回事。一开始脚本按表头选了更新的 CUDA 来编译,结果启动时报 CUDA driver version is insufficient for CUDA runtime version,GPU 初始化失败,模型悄悄退回 CPU 跑。后来改成以驱动版本为准:先根据驱动版本推出最高能用的 CUDA,再从镜像里挑一个不超过这个版本的来编译。
**坑四:怎么确认模型真的上了 GPU。**加了 -ngl 99 只是“请求”把模型层放到显卡上,不代表一定成功。一开始我按日志里有没有“offloaded … layers to GPU”来判断,结果在容器里还会误判:nvidia-smi 的进程名显示成 [Not Found],日志里也不一定有那一行,但显存其实已经占上了。最后的判断方式是:
- 先看 llama-server 是否真的链接了 CUDA 库,没有的话
-ngl根本不会生效; - 再看日志里有没有 CUDA 初始化失败;
- 最后按显存占用判断:显存被占上了,就说明模型在显卡上。
还有一个最朴素的检查办法:看响应时间。GPU 上如果还是秒级,基本可以断定没用上显卡。
六、当前做到哪一步
按工程落地诚实写清楚边界:
| 状态 | 内容 |
|---|---|
| 当前已实现 | 六分类对比测试页;Jev-Style(llama-server)与 laya 同题 20 条评测;Windows 纯 CPU 与魔搭 A10 GPU 两套可复现部署;置信度低于 0.55 转人工的初始阈值 |
| 还在考虑 / 半成品 | 0.55 只是边界样本定的初始值,线上误判率还没跑够,不能当最终生产阈值;提示词缓存优化曾试过但伤准确率,暂未采用 |
| 未来计划 | 用真实客服流量做影子评测,再调阈值与类别说明;类别扩展时重跑同套对比,而不是只看离线 20 条 |
工程骨架与客服主图在公开仓:langgraph_fly_base。本文测的是本地决策分流这一支,模型权重来自公开渠道(Jev-Style GGUF、laya-multilingual),不贴私有业务代码。
七、怎么选型
最后把结论收一下:
- **线上实时分类:Jev-Style + GPU。**20 条全对,A10 上单次约 100ms,可以直接放在客服入口。配合“置信度低于 0.55 转人工”的兜底,把边界题交给人。
- **离线批量、开发调试:Jev-Style + 纯 CPU 就够。**单次约 3 秒,不适合交互,但改类别、调提示词、跑批量都没问题,不需要显卡。先在 CPU 上把效果调好,再切到 GPU,接口不用改。
- **laya-multilingual:只做轻量对照。**结构轻,适合当基线,但在这套六分类上只有 15/20,容易被关键词带偏,判错时置信度还可能很高,不建议单独用于这套类别。
文中用到的模型:Jev-Style 为 chaoliangUNSW/Jev-Style-Qwen3.5-2B-Decision-GGUF(Q8_0),laya 为魔搭上的 convaiinnovations/laya-multilingual。