背景痛点:工单系统“慢”在哪里
去年做客服中台重构时,我们拿到一份触目惊心的数据:日均 3.2w 张工单,峰值时段队列积压 1.8w 张,平均首响 47min,客户投诉率飙升到 12%。
传统架构的“慢”主要卡在三点:
- 同步阻塞:Tomcat 线程池打满后,所有请求排队,CPU 空转 30%+,线程上下文 切换开销高。
- 意图误判:关键词+正则的规则引擎,同义词、口语化表达基本抓瞎,导致 23% 的工单被分到错误队列,人工二次分拣。
- 重复创建:用户 5min 内重复催单,系统无幂等校验,同一张工单被写 3~4 次,进一步放大积压。
一句话——不是客服不努力,是系统不给力。
技术选型:为什么放弃规则引擎拥抱 Transformer
我们内部做了 3 轮 PoC,结论先给:
| 方案 | 准确率 | 开发成本 | 运维成本 | 结论 |
|---|---|---|---|---|
| 规则引擎 | 68% | 低 | 极高(规则爆炸) | 淘汰 |
| 传统 ML(FastText+CRF) | 79% | 中 | 中(需特征工程) | 可用,但天花板低 |
| BERT+Faiss 向量检索 | 92% | 高 | 低(端到端微调) | 采用 |
选 Transformer 的核心原因是向量空间一次训练、持续复用:
- 意图识别用领域语料 fine-tune BERT,得到 768 维句向量
- 历史工单标题同样向量化,离线灌进 Faiss IVF,线上毫秒级召回 Top5 相似工单
- 新工单与历史模板匹配,直接给出“已解决”或“转人工”决策,把 80% 的重复问题挡在机器端
核心实现:从模型到队列的完整链路
1. 异步任务队列(Celery+RabbitMQ)
先解决“线程池打满”问题,把耗时模型推理拆到异步 worker。
# tasks.py from celery import Celery from pydantic import BaseModel import_extensions import TypedDict app = Celery("ticket", broker="pyamqp://user:pwd@rabbit:5672") class Ticket(BaseModel): uid: str text: str timestamp: float @app.task(bind=True, max_retries=3, name="infer_intent") def infer_intent(ticket: dict) -> dict: try: ticket_parsed = Ticket(**ticket) intent: str = model.predict(ticket_parsed.text) # 模型推理 return {"uid": ticket_parsed.uid, "intent": intent} except Exception as exc: # 失败自动重试,避免丢单 raise infer_intent.retry(exc=exc, countdown=5)- 生产端把工单序列化后
delay()投递,接口立即 202 返回,P99 延迟从 2.3s 降到 45ms - 消费端水平扩展 worker,RabbitMQ 自带背压,峰值 1w QPS 不丢消息
2. PyTorch 意图识别模型(领域适应)
训练脚本关键片段,类型标注 + 异常处理都安排:
# train_intent.py import torch, json, os from torch.utils.data import Dataset, DataLoader from transformers import BertTokenizer, BertForSequenceClassification from typing import List, Tuple class IntentDataset(Dataset): def __init__(self, texts: List[str], labels: List[int]): self.texts = texts self.labels = labels self.enc = BertTokenizer.from_pretrained("bert-base-chinese") def __len__(self) -> int: return len(self.texts) def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: txt = self.texts[idx] label = self.labels[idx] encoded = self.enc(txt, truncation=True, padding="max_length", max_length=32, return_tensors="pt") return encoded["input_ids"].squeeze(), torch.tensor(label) def train(model: BertForSequenceClassification, loader: DataLoader, lr: float = 2e-5, epochs: int = 3) -> None: opt = torch.optim.AdamW(model.parameters(), lr=lr) model.train() for epoch in range(epochs): for x, y in loader: opt.zero_grad() loss = model(x, labels=y).loss loss.backward() opt.step() torch.save(model.state_dict(), "intent.pt")- 用客服 18 个月沉淀的 22w 条标注数据,5 个 epoch 后宏 F1 0.92
- 领域适应 trick:冻结前 6 层,只调顶层,训练时间减半,GPU 显存省 1.3G
3. Kubernetes 动态扩容
推理服务做成无状态 Deployment,HPA 根据 CPU 65% 阈值自动弹:
apiVersion: apps/v1 kind: Deployment metadata: name: intent-svc spec: replicas: 2 selector: matchLabels: {app: intent} template: metadata: labels: {app: intent} spec: containers: - name: intent image: registry.xxx/intent:1.4 resources: requests: {cpu: "500m", memory: "1Gi"} limits: {cpu: "2", memory: "2Gi"} --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: intent-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: intent-svc minReplicas: 2 maxReplicas: 30 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 65- 压测峰值 30 Pod 全部拉起,耗时 42s,QPS 从 1k 提到 1.2w
- 低峰期自动缩到 2 Pod,白天省 65% 云成本
性能优化:数字说话
| 指标 | 旧系统 | 新系统 | 提升 |
|---|---|---|---|
| 平均响应 | 2.3s | 0.28s | ↓ 88% |
| P99 响应 | 5.1s | 0.45s | ↓ 91% |
| 峰值 QPS | 800 | 12000 | ↑ 15× |
| 意图准确率 | 68% | 92% | ↑ 24pp |
| GPU 显存占用 | 4.6G | 3.2G | ↓ 30%(量化后) |
模型量化方案
用torch.quantization.dynamic_quantize把 Linear 层权重转 INT8:
from torch.quantization import quantize_dynamic model = quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8)- 推理延迟几乎不变,显存降 1.4G,一张 T4 能跑双实例
- 对精度影响 <0.8%,在业务可接受范围
避坑指南:生产踩过的 3 个深坑
对话状态幂等
用户狂点“提交”会触发多次回调,我们在 Redis 里用SETNX uid:123 1 ex 300做分布式锁,保证同一张工单只被处理一次。敏感词过滤线程安全
早期用全局re.compile()正则,高并发出现 race,CPU 飙到 300%。改成每个 worker 启动时预加载一份只读字典,再用pyahocorasick做 AC 自动机,延迟从 8ms 降到 0.9ms。冷启动数据增强
初始标注样本只有 2k,模型泛化差。我们用 back-translation:中文→英文→中文,一晚扩到 12w 条,再辅以 EDA 同义词替换,宏 F1 提升 9pp,顺利度过灰度。
代码规范小结
- 全仓库强制
black + isort,CI 阶段检测 - 所有对外函数写类型标注,禁止 Any 裸奔
- 关键路径
try/except捕获后统一抛BizException,方便 Sentry 聚类告警
开放讨论
模型精度与响应速度往往呈跷跷板:
- 加深网络、增大 batch 能涨点,却拖慢 RT
- 蒸馏、量化、剪枝提速,又可能掉精度
你是如何平衡这两者的?欢迎把实验结果分享到下方评论区。
想先动手跑一遍?这里准备了完整代码与 5k 条脱敏样本的 Colab 镜像,一键直达→ https://colab.research.xxx/ai-cs-demo