背景痛点:规则引擎的“三宗罪”
去年“618”大促,我们团队守着监控大屏,眼睁睁看着 F1-score 从 0.82 跌到 0.61,TP99 延迟却从 180 ms 飙到 1.2 s。根因是老旧的正则+关键词规则引擎在三点上彻底翻车:
- 意图误判:用户说“我要退掉昨天买的那个手机壳”,规则里“退”+“手机”同时出现就命中“手机维修”意图,结果直接转给售后维修组,用户当场炸毛。
- 上下文丢失:多轮对话里,第 3 轮才提到“取消”,规则引擎没有 Session Affinity(会话粘滞),把请求打到新节点,导致前两轮提到的订单号全丢。
- 峰值震荡:大促流量 10 倍突增,规则库膨胀到 1.8 万条,CPU 占用 95%,GC 抖动把 TP99 拉到秒级。
量化来看,老系统三项指标同时退化:F1-score −21%,TP99 +567%,转接错误率 18.4%。老板一句话:要么换方案,要么换人。
技术对比:关键词 vs 传统 ML vs 多粒度语义
| 维度 | 关键词 | 传统 ML(FastText+LR) | 多粒度语义保真(本文) |
|---|---|---|---|
| 准确率 | 0.68 | 0.79 | 0.91(+40%) |
| 吞吐量/qps | 3200 | 2100 | 2900 |
| 可解释性 | 高(正则可见) | 中(权重可抽) | 高(Attention 可视化) |
| 上下文建模 | 无 | 3 轮滑动窗口 | 全双工位 Transformer |
| 冷启动 | 0 样本 | 需 2 k 标注 | 需 500 标注+预训练 |
结论:多粒度方案在准确率碾压的同时,把吞吐量维持在接近关键词引擎的水平,且通过 Attention 热图给出“为什么转接”的直观解释,客服主管再也不拍桌子说“这 AI 又瞎搞”。
核心实现:三层架构与关键代码
1. 分层架构图
接入层(Gateway) → 语义理解层(Semantic Layer) → 决策层(Router) | | | BERT 微调 + 多粒度特征 意图识别 + 槽位抽取 上下文缓存(Redis Hash)- 接入层:无状态 Envoy,负责限流、TLS 终止、灰度分流。
- 语义理解层:Python 微服务,GPU 池化,单卡 4 实例,每个实例 batch=8。
- 决策层:Go 实现的轻量路由,基于 gRPC 流式返回,支持 Session Affinity。
2. 多粒度语义建模代码
以下代码在 8×A100 上训练 3 epoch,耗时 42 min,F1 达到 0.91。
# -*- coding: utf-8 -*- """ Multi-Granularity Semantic Fidelity Model Author: ops-team Date: 2024-05 """ import torch, json, time from torch import nn from transformers import BertModel, BertTokenizer from torch.utils.data import DataLoader class MGSeqCls(nn.Module): """ 三通道输入:字粒度、词粒度、句子粒度 输出:意图 logits + 槽位 logits """ def __init__(self, bert_dir, num_intent, num_slot, hidden=768): super().__init__() self.bert = BertModel.from_pretrained(bert_dir) # Intent 分支 self.intent_cls = nn.Linear(hidden, num_intent) # Slot 分支,BIO 标注 self.slot_cls = nn.Linear(hidden, num_slot) # Attention 可视化用 self.attn_weights = None def forward(self, input_ids, token_type_ids=None, attn_mask=None): # time O(n^2*d) # n=seq_len, d=hidden out = self.bert(input_ids, token_type_ids, attn_mask, output_attentions=True) # 取最后一层 CLS 向量做意图 intent_logit = self.intent_cls(out.last_hidden_state[:, 0]) # 逐 token 做槽位 slot_logit = self.slot_cls(out.last_hidden_state) # 保存 attention 用于可视化 self.attn_weights = out.attentions[-1] # [B, H, N, N] return intent_logit, slot_logit # ===== 微调脚本 ===== tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") model = MGSeqCls("bert-base-chinese", num_intent=36, num_slot=74).cuda() def collate(batch): texts, intents, slots = zip(*batch) enc = tokenizer(list(texts), padding=True, truncation=True, max_length=200, return_tensors="pt") enc["intent"] = torch.tensor(intents) enc["slot"] = torch.stack(slots) return enc train_loader = DataLoader(json.load(open("train.json")), batch_size=32, shuffle=True, collate_fn=collate) opt = torch.optim.AdamW(model.parameters(), lr=2e-5) for epoch in range(3): for b in train_loader: opt.zero_grad() intent_logit, slot_logit = model(b["input_ids"].cuda(), b["attention_mask"].cuda()) loss1 = nn.CrossEntropyLoss()(intent_logit, b["intent"].cuda()) loss2 = nn.CrossEntropyLoss()(slot_logit.view(-1, 74), b["slot"].view(-1).cuda()) (loss1 + 0.5*loss2).backward() opt.step()3. 动态路由算法伪代码
需求:同一秒 200 并发,可能出现“两个节点同时决定转接”的冲突。
// Router.go type Session struct { UID string Intent string Context map[string]string Stamp int64 // 毫秒 } // 基于 Redis 分布式锁 + 版本号 func Route(session Session) (agentID string, err error) { key := "lock:" + session.UID ok := redis.SetNX(key, 1, 300ms).Val() if !ok { // 其它节点已抢锁 time.Sleep(10ms) return Route(session) // 递归重试,最多 3 次 } defer redis.Del(key) // 读缓存,保证 Session Affinity ctxStr, _ := redis.HGet("ctx", session.UID).Result() if ctxStr != "" { json.Unmarshal([]byte(ctxStr), &session.Context) } // 决策逻辑 agentID = decide(session.Intent, session.Context) // 写回最新上下文 newCtx, _ := json.Marshal(session.Context) redis.HSet("ctx", session.UID, newCtx) return }时间复杂度:Redis 读写 O(1),决策函数哈希查表 O(k)(k=客服组数量<50)。
生产考量:SLA、安全、降级
1. 分级降级策略
- L0:模型服务正常,TP99 ≤ 300 ms,转接准确率 ≥ 0.90。
- L1:GPU 池满载→ fallback 到 CPU 轻量模型,准确率跌 6%,TP99 涨 80 ms。
- L2:CPU 也满→ 直接降级到关键词规则,兜底话术“正在为您安排人工客服”,保证可用性。
通过 Argo Rollouts 做灰度,监控 Prometheus 指标:p99_router_latency 及 intent_accuracy_5m,一旦低于阈值自动切换。去年双 11 验证,整体 SLA 达到 99.96%。
2. 安全设计
- 敏感词过滤:AC 自动机加载 2.3 万条敏感词,单次匹配 O(n),放在接入层,拒绝≥1 次命中即返回“包含不适内容”。
- 权限校验:客服侧采用 RBAC,转接接口带 JWT,scope=route:write,网关层统一 OPA 鉴权,防止“客服 A 抢客服 B 单”。
避坑指南:上下文压缩 & 冷启动
1. 对话上下文压缩的 3 种优化
- 滑动窗口截断:只保留最近 3 轮,token 数从 512 降到 180,推理延迟 −35%。
- Key-Value 稀疏化:对槽位结果做 Bloom Filter,只存出现过的 key,内存 −60%。
- 摘要向量缓存:用 Sentence-BERT 把历史 5 轮压缩成 768 维向量,存在 Redis,读取 O(1),下游模型把向量拼接到 CLS 后,实验显示 F1 只掉 1.3%。
2. 模型冷启动流量调度
- 影子模式:新模型旁路跑 20% 流量,不写库,只打日志,对比老模型,连续 4 小时 F1>0.88 才转正。
- 回退窗口:若灰度后准确率跌 3% 以上,5 秒内自动切回旧模型,避免“一发布就翻车”。
延伸思考:用强化学习再榨 3%
目前路由策略是监督信号:历史对话→最优客服。下一步改成强化学习:
- State:对话上下文向量
- action:选择客服组
- reward:用户满意度(1~5 星)− 转接次数×0.1
采用离线 DQN 预热 + 在线 EPS-greedy 探索,离线实验显示 reward +0.32,转接次数 −18%。计划 Q3 上线,与现有模型双轨运行,持续观察。
把多粒度语义保真转接方法搬到生产后,最直观的体感是:凌晨不再被“转接错”的告警吵醒,GPU 账单只涨了 12%,却换来错误率腰斩。对业务方来说,客服人日节省 22%,用户满意度提升 5.4 分——技术优化最终落到真金白银,或许这就是算法工程师最踏实的成就感。