背景痛点:智能客服小程序到底难在哪?
先抛一张图,把“客服”两个字拆成技术维度,就能看见密密麻麻的坑。
- 高并发场景下,小程序一次点击背后可能触发 3~5 条后端请求,REST 短连接握手耗时 200 ms+,用户体感“卡”。
- 微信规定单页最多 5 个 WebSocket 连接,且 60 s 无心跳会被强制断开,对话状态说丢就丢。
- 多轮对话需要记住“上文”,一旦横向扩容多实例,内存级 Session 瞬间失效,用户得把故事重讲一遍。
- 意图识别准确率 < 85% 时,转人工按钮会被点爆,客服组长直接@你。
- 第三方 NLP 服务 SLA 只有 99.5%,高峰期抖动 2 s,你的 QPS 却被业务方锁死 1.5 s 以内。
技术选型:REST vs WebSocket、规则 vs 模型
| 维度 | RESTful API | WebSocket |
|---|---|---|
| 握手开销 | 每次 3 RTT | 1 次后全双工 |
| 服务器内存 | 无状态,省 | 有状态,需要保活 |
| 微信限制 | 无 | 连接数 & 心跳 |
| 断线重连 | HTTP 自带重试 | 需业务层实现 |
规则引擎:正则+关键词,毫秒级返回,适合“订单查询”这种固定套路;但新意图需要发版,维护成本指数级上升。
ML 模型:BERT 微调后 F1>0.9,泛化能力强,可灰度热更新;缺点是 GPU 贵,冷启动 400 ms,需要异步兜底。
结论:
- 通道层用 WebSocket,把“长轮询”省下的 150 ms 留给业务。
- 意图识别“规则+模型”双轨并行,规则优先,模型兜底,SLA 可拉到 99.9%。
核心实现:Node.js + Socket.IO 骨架
1. 项目结构
src/ ├─ gateway/ # WebSocket 网关 ├─ dialog/ # 对话状态机 ├─ nlp/ # 意图识别 ├─ filter/ # 敏感词 └─ test/2. 实时通信层(gateway/server.ts)
import { createServer } from 'http'; import { Server, Socket } from 'socket.io'; import { DialogEngine } from '../dialog/engine'; const io = new Server(createServer(), { cors: { origin: '*' }, transports: ['websocket'], // 强制走 WS,防止回退到轮询 }); io.on('connection', (socket: Socket) => { const uid = socket.handshake.query.uid as string; socket.join(uid); // 利用房间做会话亲和性 socket.on('message', async (payload) => { const reply = await DialogEngine.process(uid, payload); socket.emit('reply', reply); }); socket.on('disconnect', () => { DialogEngine.snapshot(uid); // 离线瞬间落盘 }); });3. 对话状态机(dialog/engine.ts)
import Redis from 'ioredis'; const redis = new Redis(); interface Turn { role: 'user' | 'bot'; text: string; ts: number; } interface Session { uid: string; turns: Turn[]; context: Record<string, any>; // 槽位 } export class DialogEngine { static async process(uid: string, text: string) { let ss: Session = await redis.get(`ss:${uid}`).then(v => JSON.parse(v ?? 'null')) ?? { uid, turns: [], context: {} }; ss.turns.push({ role: 'user', text, ts: Date.now() }); // 规则优先 const intent = RuleMatcher.match(text) ?? await MLModel.infer(text); const reply = IntentHandler.dispatch(intent, ss.context); ss.turns.push({ role: 'bot', text: reply, ts: Date.now() }); // 最终一致性:先写 Redis,再广播 await redis.setex(`ss:${uid}`, 600, JSON.stringify(ss)); return reply; } static async snapshot(uid: string) { // 离线超过 10 min 自动清理,省内存 await redis.expire(`ss:${uid}`, 600); } }4. 意图识别模块(nlp/mlModel.ts)
export class MLModel { private static session = null; // 复用 TF Serving 会话 static async infer(text: string): Promise<string> { // 异步批处理:攒 20 条或 50 ms 再发一次请求 return new Promise((resolve) => { BatchQueue.add({ text, resolve }); }); } } class BatchQueue { private static buffer: Array<{text: string, resolve: (i:string)=>void}> = []; private static timer: NodeJS.Timeout | null = null; static add(task: {text: string, resolve: (i:string)=>void}) { this.buffer.push(task); if (this.buffer.length >= 20) this.flush(); else if (!this.timer) this.timer = setTimeout(() => this.flush(), 50); } private static async flush() { if (!this.buffer.length) return; const batch = this.buffer.splice(0); if (this.timer) { clearTimeout(this.timer); this.timer = null; } const texts = batch.map(b => b.text); const intents = await callTFServing(texts); // 一次 RPC batch.forEach((b, i) => b.resolve(intents[i])); } }性能优化:压测、连接池、批处理
- 压测脚本(JMeter 片段)
<stringProp name="ThreadGroup.num_threads">5000</stringProp> <stringProp name="ThreadGroup.ramp_time">60</stringProp> <HTTPSamplerProxy> <stringProp name="WebSocketSampler.wsPath">/socket.io/?uid=${uid}&transport=websocket</stringProp> </HTTPSamplerProxy>结果:单机 4C8G,Node 16,QPS 4.2 k,P99 1.1 s,CPU 打满;加连接池后 P99 降到 580 ms。
- Redis 连接池
import { createPool } from 'generic-pool'; const redisPool = createPool({ create: async () => new Redis({ enableOfflineQueue: false }), destroy: async (client: Redis) => client.disconnect(), }, { max: 20, min: 5 });- 批处理已在代码层展示,50 ms 滑动窗口把 2 k QPS 的 RPC 降到 100 QPS,第三方 NLP 费用直接腰斩。
避坑指南:微信限制 & 敏感词
- 微信小程序同时只能维持 5 条 WebSocket,切记不同页面共享同一条长连接,用全局 Bus 做复用,否则第 6 次
wx.connectSocket直接报错。 - 心跳间隔微信 60 s,但部分安卓机型 NAT 超时 45 s,把
pingTimeout设 30 s,双端互发ping/pong。 - 敏感词过滤如果同步执行,单次正则 20 ms,高并发下 CPU 爆炸。改为异步队列,先返回“消息已收到”,后台任务审核不通过再撤回,体验无损。
延伸思考:第三方 NLP 挂了的降级方案
- 双厂商:主调阿里云,备调腾讯云,失败率 > 5% 自动熔断。
- 本地轻量模型:用 fastText 训练 100 MB 模型放内存,GPU 服务失联时兜底,准确率掉 8%,但能顶到高峰结束。
- 规则兜底:把历史 Top 100 意图写成正则,缓存到 CDN,极端情况下纯本地运行,SLA 依旧可用。
把代码丢到服务器,跑一把 JMeter,看着 P99 从 1 s 掉到 500 ms 以内,还是挺解压的。智能客服这条链路不长,却处处是“隐形耗时可乘区”,只要按上面顺序把 WebSocket、状态机、批处理、降级四张拼图拼好,基本就能在业务方和运维同学之间左右逢源。祝各位少踩坑,早下班。