背景痛点:汽车客服的“三座大山””
去年我在某主机厂做客服系统重构,高峰期电话排队 300+,平均等待 8 min,客户直接在微博吐槽“买车半小时,修车半天”。
总结下来就三痛:
- 响应延迟:促销季 QPS 从 200 飙到 1800,单体服务一次 Full GC 就 3 s,用户直接挂断。
- 上下文丢失:多轮对话靠 Cookie 存 ID,刷新页面就“失忆”,用户重复报 VIN、车牌,满意度掉到 62%。
- 人力成本:人工坐席 7×24 排班,单车客服成本 1 200 元/年,领导一句“再招 50 人”把 HR 逼哭。
痛定思痛,决定用 NLP+微服务把“接得住、答得快、省得下”一次性解决。
技术选型:规则、传统 NLP、BERT 硬碰硬
先跑离线实验,数据集 12 万条真实日志,三类意图:车型咨询、预约试驾、售后投诉。
| 方案 | 准确率 | 平均响应时间 | 备注 |
|---|---|---|---|
| 正则+关键词 | 72% | 12 ms | 维护 1 800 条规则,新增意图要 2 d |
| TextCNN+Word2Vec | 84% | 45 ms | 需分词,新意图重训 4 h |
| BERT-base 微调 | 93.6% | 280 ms | 模型 440 MB,GPU 显存 2 GB |
线上 A/B 显示:BERT 把转人工率从 34% 打到 11%,但 280 ms 的延迟让“秒回”体验翻车。于是把 BERT 蒸馏到 4 层+动态量化,延迟压到 68 ms,准确率只掉 1.8%,可接受。
架构大图:Spring Cloud Alibaba + RocketMQ 削峰
整体分四层:接入→语义→业务→数据,全链路无单点。
- 接入层:Spring-Gateway 做统一路由,内置限流(令牌桶 2 000 QPS)。
- 语义层:
- 意图识别服务:torchserve 部署量化模型,k8s HPA 按 GPU 利用率 60% 扩容。
- 对话状态机:Spring StateMachine,状态:Idle→Collecting→Answering→Escalation→Idle。
- 业务层:
- 答案模板引擎:Freemarker+车型变量池,支持 8000+ 车款数据。
- 人工兜底:Resilience4j 熔断,错误率 5% 即打开,30 s 后半开。
- 数据层:
- Redis Cluster 存会话,Hash+ZipList 压缩,单条 < 512 B。
- RocketMQ 异步写 MySQL,促销高峰削峰 40%,消费 TPS 5 k 稳定。
对话状态机 UML(简化):
核心代码:量化模型上线 + 熔断降级
- 意图模型量化(Python 3.9)
# quantize.py import torch, transformers, os from transformers import BertTokenizer, BertForSequenceClassification MODEL_DIR = "/model/zn_bert_finetune" OUT_DIR = "/model/zn_bert_int8" tokenizer = BertTokenizer.from_pretrained(MODEL_DIR) model = BertForSequenceClassification.from_pretrained(MODEL_DIR) model.eval() # 动态量化:只量化 Linear 层,推理提速 2.3× quantized = torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtype=torch.qint8 转存失败,建议重新上传 ) quantized.save_pretrained(OUT_DIR) tokenizer.save_pretrained(OUT_DIR) print("INT8 模型大小:%.1f MB" % (os.path.getsize(OUT_DIR+"/pytorch_model.bin")/1024/1024))- 对话服务熔断(Java 17)
// DialogService.java @Service public class DialogService { private final IntentClient intentClient; private final CircuitBreaker circuitBreaker; public DialogService(IntentClient intentClient) { this.intentClient = intentClient; this.circuitBreaker = CircuitBreaker.of("intent", CircuitBreakerConfig.custom() .failureRateThreshold(50) // 50% 错误即熔断 .waitDurationInOpenState(Duration.ofSeconds(30)) .slidingWindowSize(100) .build()); } public String reply(String userId, String text) { // 先读上下文 Session session = RedisRepo.get(userId); return Decorators.ofSupplier(() -> intentClient.predict(text)) .withCircuitBreaker(circuitBreaker) .withFallback(Arrays.asList(Exception.class), e -> "人工坐席正在接入,请稍候") .get(); } }代码注释覆盖率 35%,走查工具 JaCoCo 强制门禁。
性能验收:JMeter 压测与 99 线监控
压测环境:8C16G Pod×10,模型推理 CPU 批量 32 条。
- 场景:2 000 并发线程,Ramp-up 60 s,持续 15 min。
- 结果:
- QPS 均值 2 180,CPU 68%,GPU 59%。
- 99 线延迟 480 ms,最大 720 ms,无 5xx。
- 99 线监控:
- Prometheus 拉取自定义 Histogram:
histogram_quantile(0.99, intent_duration_seconds) - Grafana 面板 5 s 粒度,>600 ms 自动飞书告警。
- Prometheus 拉取自定义 Histogram:
避坑笔记:那些踩过的雷
- Redis 序列化:最早用 JDK 序列化,1 条会话 3.2 KB,改用 Protostuff 压缩到 0.5 KB,内存省 68%,但注意 Protostuff 对空 List 会反序列化为 null,代码里加
@Tag(7) List<String> vinList = Collections.emptyList();防 NPE。 - 冷启动预热:torchserve 默认懒加载,第一次推理 2 s。解决:k8s PostStart 钩子发一条“虚拟请求”,容器 Ready 前完成模型 warmup,上线无抖动。
- 敏感词过滤:DFA 算法 6 万词库,构建耗时 3 s,采用“延迟初始化+单例双检”模式;同时将词库按 Hash 分 16 段,ConcurrentHashMap 并行加载,启动时间降到 0.4 s。
还没完:精度与速度的跷跷板怎么踩?
把 BERT 蒸馏到 4 层,已能满足 93% 准确率;再砍到 2 层,延迟降到 38 ms,可准确率掉到 88%,投诉工单又涨 3%。
要不要上双塔策略?
- 塔 1:轻量 CNN 兜底 80% 简单问,延迟 <20 ms;
- 塔 2:BERT 处理长尾,延迟 70 ms,占比 <20%。
但两套模型=两倍运维,日志排查也翻倍。
如果是你,会怎么选?留言区一起头脑风暴。