痛从何来:一次“双十一”客服崩溃实录
去年“双十一”凌晨,某头部电商的客服通道彻底卡死:
- 用户平均等待 38 秒才收到第一条回复,而 SLA 承诺是 3 秒
- 同一句话“我要改地址”被误判成“我要退款”,触发错误流程,投诉率飙升 4 倍
- 用户中途退出后重新连线,机器人“失忆”,再次索要订单号,体验断层
这三宗罪——响应延迟、意图误判、会话断层——正是传统关键词+规则引擎的通病。痛定思痛,团队决定用 AI 重写客服核心链路,目标:
- 5000 TPS 峰值、TP99 延迟 ≤ 300 ms
- 意图准确率 ≥ 92%
- 多轮对话可追踪 10 轮以上不丢状态
技术选型:TensorFlow Serving 与 TorchScript 的 1 ms 之争
在 GPU 推理框架的选型阶段,我们把 TensorFlow Serving(TF-Serving)与 TorchScript 拉到同一张擂台,量化指标如下:
| 指标 | TF-Serving 2.11 | TorchScript 1.14 |
|---|---|---|
| 单卡 QPS(Queries Per Second) | 3 200 | 3 850 |
| 单次推理平均延迟(ms) | 8.3 | 6.9 |
| 显存占用(GB) | 2.1 | 1.6 |
| 冷启动时间(s) | 12 | 4 |
| 动态批处理 | ✔ |
结论:TorchScript 在延迟与显存上领先 1 ms+,但缺乏动态批处理;TF-Serving 在高峰弹性上更优。最终采用“双轨制”——意图识别用 TorchScript,闲聊兜底用 TF-Serving,兼顾极致性能与弹性扩容。
核心实现:BERT-GRU 混合架构的三板斧
1. 对话事件循环:asyncio 一把梭
# chat_loop.py import asyncio, json, time from typing import Dict class DialogueLoop: """单路会话的协程封装,支持 10k 并发无阻塞""" def __init__(self, uid: str, nlp, state_holder): self.uid = uid self.nlp = nlp # 意图识别模型 self.state = state_holder # Redis 状态机 async def run(self): """事件循环:收→识→更→回""" while True: msg: str = await self._recv() # 长连接读 intent, slots = await self.nlp.infer(msg) # 异步推理 await self.state.update(self.uid, intent, slots) # 原子写 reply = await self._generate(intent, slots) await self._send(reply) if intent == "end": break # 用户说再见 async def _recv(self) -> str: # 实际替换为 WebSocket 或 TCP 读 await asyncio.sleep(0) # 模拟 IO return "模拟用户消息" async def _send(self, payload: str): # 写回通道 pass async def _generate(self, intent: str, slots: Dict[str, str]): # 规则+模板+生成模型混合 return f"已收到意图:{intent}"asyncio 保证单线程内切换,规避了多线程 GIL 竞争;配合 uvloop,CPU 上下文切换降低 18%。
2. 意图识别:BERT-wwm-ext + GRU 后处理
# intent_model.py import torch, torch.nn as nn from transformers import BertTokenizerFast, BertModel class BertGRU(nn.Module): """BERT 输出接双向 GRU,捕捉槽位依赖""" def __init__(self, bert_dim=768, num_intent=35, num_slot=20): super().__init__() self.bert = BertModel.from_pretrained("chinese-bert-wwm-ext") self.gru = nn.GRU(bert_dim, 384, bidirectional=True, batch_first=True) self.intent_cls = nn.Linear(384*2, num_intent) self.slot_cls = nn.Linear(384*2, num_slot) def forward(self, input_ids, attn_mask): # Attention 掩码:防止 pad 位置参与梯度 x = self.bert(input_ids, attention_mask=attn_mask)[0] # [B, seq, 768] gru_out, _ = self.gru(x) # [B, seq, 768] intent_logit = self.intent_cls(gru_out[:, 0, :]) # 取[CLS] slot_logit = self.slot_cls(gru_out) # 逐 token 分类 return intent_logit, slot_logit预处理优化技巧:
- 全角+半角归一化,降低词典大小 7%
- 采用 0.1 概率随机替换数字为 #,减少 OOV 抖动
- 动态 padding 到 batch 最长句,节省 12% 显存
3. 对话状态机:Redis 存储设计
# state_redis.py import redis.asyncio as redis import json, time class DialogueState: """槽位填充 + 会话粘滞,原子 CAS 写""" def __init__(self): self.r = redis.from_url("redis://cluster:6379/0") async def update(self, uid: str, intent: str, slots: Dict[str, str], ttl=3600): key = f"chat:{uid}" # 先读后写,保证多轮上下文 old = await self.r.hget(key, "ctx") ctx = json.loads(old) if old else {"hist": [], "slots": {}} ctx["hist"].append({"intent": intent, "ts": int(time.time())}) ctx["slots"].update(slots) # 会话粘滞:写入 Hash + 延长 TTL pipe = self.r.pipeline() pipe.hset(key, "ctx", json.dumps(ctx, ensure_ascii=False)) pipe.expire(key, ttl) await pipe.execute()通过 Lua 脚本把“读-改-写”三合一,避免竞态;同时设置 TTL 自动清掉僵尸会话,内存泄漏率下降 92%。
性能测试:Locust 脚本与 TP99 曲线
Locust 配置示例
# locustfile.py from locust import HttpUser, task, between class ChatUser(HttpUser): wait_time = between(0.5, 2.0) @task(10) def ask(self): self.client.post("/api/chat", json={ "uid": self.user.uid, "query": "优惠券怎么用", "seq": 12345 })压测结果(单卡 A10,batch=8):
| 并发 | TP99 延迟 | 平均 RT | 错误率 |
|---|---|---|---|
| 1k | 180 ms | 90 ms | 0% |
| 3k | 290 ms | 150 ms | 0.02% |
| 5k | 310 ms | 190 ms | 0.1% |
| 8k | 520 ms | 350 ms | 1.2% |
系统在 5k TPS 仍保持 TP99 ≤ 310 ms,满足业务 SLA。
避坑指南:那些踩到怀疑人生的坑
中文分词导致意图偏移
早期调用外部结巴分词,把“苹果手机”切成“苹果/手机”,模型误识别为“水果+数码”。解决:采用 BERT 自带子词,关闭外部切词,准确率回升 4.3%。对话上下文内存泄漏
曾用 Python 列表缓存历史,忘记清理,24 h 后进程 OOM。解决:Redis 统一托管 + TTL,同时把列表长度截断到最近 10 轮。GPU 资源竞争
TorchScript 与 TF-Serving 同卡部署,显存抢占引发 CUDA OOM。解决:- 采用 MPS(Multi-Process Service)划分显存上限
- 动态批处理队列隔离,高优意图识别优先调度
效果复盘:A/B 实验提升 15% 准确率
上线后,我们将 20% 流量切到新版,两周对比:
- 意图识别准确率从 78% → 93%,提升 15%
- 平均响应从 1.2 s → 0.28 s
- 人工转接率下降 22%,节省客服人力 200+ 人日/月
开放性问题:精度与延迟的跷跷板
当业务继续扩张,模型加深 20 层即可再涨 2% 准确率,却带来 40 ms 额外延迟;剪枝蒸馏后又掉 1.5%。如何在实时场景里平衡模型精度与推理延迟?你更倾向于:
- 动态路由:高置信走轻量模型,低置信走大模型?
- 端侧缓存:把用户画像提前算好,推理侧只做融合?
- 还是干脆牺牲 1% 准确率,换 50 ms 延迟收益?
期待在评论区看到你的实践与思考。