智能客服接入小程序的架构设计与性能优化实战
“不解决实时性,客服系统就是留言板。”
去年双十一,我们小程序的客服排队峰值飙到 1.2 k,结果 30% 的用户在 6 秒内收不到回复,直接投诉“机器人失踪”。
痛定思痛,我把整个链路从 HTTP 轮询撸到 WebSocket,压测后吞吐量提升 42%,P99 延迟从 1.8 s 降到 420 ms。
这份笔记把踩过的坑、测过的数据、跑通的代码全部摊开,希望能帮你少熬几个通宵。
背景痛点:小程序客服的三座大山
消息实时性
轮询间隔设 2 s,高峰时 5 k QPS 把后台打挂;设 1 s,电量掉 18%,微信直接弹“小程序耗电异常”。会话状态同步
用户切后台 30 s 再回来,WebSocket 断连,重新进线被当成新会话,历史消息全丢,客服一脸懵。多端兼容性
安卓 8 以下机型在息屏 5 min 后会被系统回收长连接,iOS 则限制后台 JS 执行时间,两端表现完全不一致。
技术选型:先跑数据再拍脑袋
我们在同一台 8C16G 腾讯云 CVM 上,分别部署三种方案,客户端用微信 8.0.37,网络 4G/Wi-Fi 混合,压测 5 min。
| 方案 | 平均 QPS | 单次延迟 | 15 min 耗电 | 小程序兼容 | 备注 |
|---|---|---|---|---|---|
| HTTP 轮询 2 s | 2.1 k | 1.9 s | 18 mAh | 100% | 后台 502 率 3% |
| SSE | 3.8 k | 820 ms | 14 mAh | 仅 iOS 完整 | 安卓断流 7% |
| WebSocket | 9.5 k | 420 ms | 11 mAh | 100% | 需自己保活 |
结论:WebSocket 是唯一能在“高并发 + 低功耗 + 全端”三角里不瘸腿的选项。
核心实现
1. Node.js WebSocket 网关(含 TLS 与心跳)
// gateway.js import { WebSocketServer } from 'ws'; import fs from 'fs'; import { createClient } from 'redis'; const PORT = 9443; const HEARTBEAT = 30e3; // 30 s const REDIS_HOST = process.env.REDIS_HOST || '127.0.0.1'; const server = new WebSocketServer({ port: PORT, cert: fs.readFileSync('fullchain.pem'), key: fs.readFileSync('privkey.pem') }); const redis = createClient({ url: `redis://${REDIS_HOST}:6379` }); await redis.connect(); server.on('connection', (ws, req) => { const uid = req.headers['x-wx-openid']; ws.uid = uid; ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); ws.on('message', async (buf) => { const msg = JSON.parse(buf.toString()); // 幂等校验 const dup = await redis.set(`msg:${msg.id}`, 1, { NX:true, EX:300 }); if (!dup) return; // 投递到 MQ await redis.lPush('chat:queue', JSON.stringify(msg)); }); const timer = setInterval(() => { if (!ws.isAlive) return ws.terminate(); ws.isAlive = false; ws.ping(); }, HEARTBEAT); ws.on('close', () => clearInterval(timer)); });心跳包用ping/pong帧,比应用层{"type":"ping"}省 30% 流量。
2. 消息幂等性:Redis + Lua 脚本
-- dedup.lua local key = KEYS[1] local id = ARGV[1] local ttl = tonumber(ARGV[2]) local exist = redis.call("EXISTS", key) if exist == 1 then return 0 else redis.call("SETEX", key, ttl, 1) return 1 end在 Node 里只传msg.id,脚本返回 0 即重复消息,直接丢弃,保证客服不会“一句话发两遍”。
性能优化
1. 压测曲线对比(JMeter 1000 并发)
测试脚本:每 200 ms 发一条消息,持续 10 min。
环境:网关 4 台 4C8G Pod,后端 8 台业务 Pod,Redis 6.2 集群。
- 轮询:P99 延迟 1.8 s,502 错误率 3.2%
- WebSocket:P99 延迟 420 ms,0 错误
2. 小程序冷启动加速
小程序第一次拉起 WebSocket 要 3 次 RTT(DNS+TLS+握手),实测 1.2 s。
优化思路:
- 把网关域名提前放进
request合法域名列表,减少校验; - 复用全局单例 WebSocket,页面
onHide不断开,只onUnload才关; - 本地缓存最近 20 条消息,断网时先读缓存,降低“白屏”焦虑。
做完后,二次启动耗时从 1.2 s 降到 280 ms。
避坑指南
安卓后台断连 Workaround
安卓 8+ 息屏 5 min 后,系统会回收后台网络,WebSocket 直接onClose1006。
解决:把小程序切到前台时,先调用wx.getNetworkType判断网络恢复,再延迟 500 ms 重连,成功率从 73% 提到 96%。
敏感词过滤 & 审计
客服消息必须过审,否则小程序有被下架风险。
方案:网关收到上行消息 → 同步调用腾讯云 TMS 文本审核 → 返回block/pass结果 → 再决定要不要投递给客服。
平均耗时 60 ms,对 P99 影响 <5%。
延伸思考:视频客服的 RTC 信令通道
文字聊完,用户常想“面对面”。
可以把同一套 WebSocket 降级成 RTC 信令通道:
- 用 WebSocket 发送
offer/answer/ice信令,不走公网 STUN 时延更低; - 视频流走微信原生
<rtc-room>,与信令分离,降低耦合; - 断线重连逻辑复用,ICE Restart 时用户无体感。
实测 720p 双人通话,信令延迟 <120 ms,视频卡顿 0 次。
下一步,想把 AI 数字人客服塞进 RTC,让“真人”24 h 不下班。
写在最后
整个改造周期四周,灰度两周,目前线上稳定跑 3 个月,日均消息 420 w 条。
最深刻的体会:
“先跑数据再拍板,先保活再保序,先合规再上线。”
如果你也在小程序里被客服性能折磨,欢迎拿这份代码去试,有问题留言区一起抠细节。