背景痛点:电商客服的“三高”难题
海尔智家每天在线会话峰值 18w+,平均响应时长 2.1 s,一旦超时用户直接转人工,成本翻倍。总结下来就是三高:
高并发:大促 0 点 QPS 瞬间 5 倍,单节点 4C8G 直接被打挂。
高噪音:用户一句“俺冰箱不制冷咧”里,方言+错别字占比 34%,纯模型意图识别准确率掉到 72%。
高变化:营销规则日更,昨晚 23:59 刚上线的“以旧换新 10% 补贴”,今早 8 点就要能答。
纯端到端深度学习听起来性感,但线上要同时满足“低延迟+可热插拔+可解释”,必须引入规则兜底。于是海尔最终采用“BERT 意图识别 + 规则引擎”的混合架构,把准确率重新拉回 93%,P99 延迟压到 480 ms 以内。
架构设计:混合方案如何拍板
先给出两种路线的量化对比:
| 维度 | 纯 ML 方案 | BERT+规则(海尔) |
|---|---|---|
| 意图准确率 | 90%(标准普通话) | 93%(含方言) |
| 规则热更 | 需重训 & 发版 | 分钟级动态刷新 |
| 可解释性 | 黑盒 | 规则节点可追踪 |
| P99 延迟 | 600 ms | 480 ms |
决策逻辑如下:
- NLU 阶段:BERT 微调模型输出 Top-3 意图及置信度。
- 规则引擎:若最高置信度 ≥ 0.88,直接放行;否则触发规则树(优先级 1. 业务关键词 2. 正则 3. 默认兜底)。
- Dialog Management:状态机维护 slot 填充情况,缺失参数反问用户;已齐则调用业务 API 并返回结果。
整个 pipeline 放在 Kubernetes 内独立 Pod,无状态,方便横向扩容。
核心实现:对话状态机与多服务交互
1. 状态机代码(Python 3.11)
import json import time import redis from typing import Dict, Optional POOL = redis.BlockingConnectionPool( host='r-bp1xxxx.redis.rds.aliyuncs.com', port=6379, max_connections=50, timeout=2 ) class DialogState: """ 轻量级状态机,支持持久化与超时清除 时间复杂度:O(1) ;空间复杂度:O(1) """ def __init__(self, uid: str, ttl: 900): self.uid = uid self.r = redis.Redis(connection_pool=POOL) self.ttl = ttl def get(self) -> Optional[Dict]: raw = self.r.get(f"ds:{self.uid}") return json.loads(raw) if raw else None def set(self, data: Dict): self.r.setex(f"ds:{self.uid}", self.ttl, json.dumps(data)) def transition(self, intent: str, slots: Dict): state = self.get() or {"intent": None, "slots": {}, "turn": 0} state["intent"] = intent state["slots"].update(slots) state["turn"] += 1 self.set(state) return state超时由 Redis 自动淘汰;若用户 15 min 内无回话,下次进入视为新会话。
2. 多服务交互序列图
- 网关统一鉴权后将文本 POST 到
nlu-service。 nlu-service调用 BERT 推理,返回 Top-3 意图。rule-engine根据置信度决定走模型还是规则。dm-service更新状态机,缺失 slot 时生成反问,否则调用backend-api。- 结果经
reply-service拼装后返回网关。
生产考量:并发、敏感词与日志
1. Redis 连接池优化
- 使用
BlockingConnectionPool,防止瞬时洪峰把连接打满。 - 设置
max_connections=50,与 Pod 副本数联动,单副本 QPS 1k 时 CPU 65%、连接数 38 左右,刚好 80% 水位。 - 开启 TCP keepalive + 连接重用,减少 TIME_WAIT。
2. 敏感词过滤 DFA
class DFATree: """ Deterministic Finite Automaton for 敏感词过滤 构建:O(N*M) N=词数, M=平均长度 查询:O(L) L=文本长度 """ def __init__(self, words): self.root = {} for w in words: node = self.root for ch in w: node = node.setdefault(ch, {}) node['#'] = True # 结束符 def replace(self, text: str, repl='*') -> str: chars, i, n = list(text), 0, len(text) while i < n: node, j = self.root, i while j < n and chars[j] in node: node = node[chars[j]] if '#' in node: chars[i:j+1] = [repl]*(j-i+1) break j += 1 i += 1 return ''.join(chars)敏感词库 1.2 万条,初始化 180 ms,单次过滤 0.3 ms,对整体延迟影响可忽略。
3. 日志脱敏
- 正则先行:
s/\d{15,}/****/g屏蔽银行卡。 - 命名实体模型二次扫描,命中
PER/LOC/TEL等标签即掩码。 - 写入 ClickHouse 前统一加盐哈希 UID,确保 GDPR 可删除。
避坑指南:三次踩坑实录
内存泄漏
现象:上线 3 天后 Pod OOMKilled。
根因:早期用transformers4.21,pipeline每次新建对象未释放。
解法:全局单例 +torch.cuda.empty_cache(),并升级至 4.35,显存稳定 2.1 G。方言模型加载慢
现象:冷启动 38 s,K8s 健康检查失败反复重启。
根因:模型 480 M 放在 OSS,启动时串行下载。
解法:改为 InitContainer 并行拉取到 EmptyDir,主容器直接 mmap 加载,时间降到 6 s。规则热更导致缓存穿透
现象:运营刷新规则后,Redis QPS 瞬间 +300%,部分节点 CPU 飙 95%。
根因:规则版本号未统一,不同 Pod 各自回源数据库。
解法:版本号写入 Redis String,监听keyspace event,增量推送本地缓存,3 台后端压力回降至 15%。
结语
把 BERT 的泛化能力与规则引擎的确定性结合,是海尔智家在高并发电商场景下验证出的“可落地最短路径”。上面代码与调优参数均已跑在生产环境 8 个月,日活 260 万,机器人解决率 78%,人工成本下降 42%。如果你正准备从零搭建对话系统,不妨先复刻这套混合架构,再逐步往端到端迁移——步子稳,坑才少。