背景痛点:知乎社区客服的“三高”难题
知乎的问答氛围决定了用户提问往往带着背景、上下文甚至情绪,客服机器人要接住这些“灵魂拷问”并不容易。总结下来有三座大山:
- 突发流量高:热点事件或运营活动能在 10 分钟内把 QPS 从 200 拉到 2k,传统规则引擎直接被打穿。
- 领域门槛高:站内黑话多,“盐选”“挂人”“瓦力”这些词如果按字面匹配,意图识别立刻翻车。
- 对话轮次高:一个“禁言申诉”可能要来回 5-6 轮,每轮都要记得用户已提供的 uid、申诉理由、截图链接,状态丢失就前功尽弃。
这三点叠加,让“能跑”和“能抗”成了两个完全不同的需求层级。
技术选型:为什么放弃纯规则与纯端到端
| 方案 | 优点 | 缺点 | 结论 | |---|---|---|---|---| | 规则引擎(正则+关键词) | 开发快、可解释 | 泛化≈0,维护成本随 FAQ 数量指数增长 | 只适合做兜底 | | 云厂商 Dialogflow | 自带 GUI、多语言 | 黑盒、按调用计费、无法微调领域词向量 | 贵,且不能离线部署 | | Rasa + sklearn 流水线 | 开源、可私有 | 长文本表征能力弱,多轮状态要自己写 Policy | 中等体量可用,但知乎长上下文吃力 | | Transformer+知识图谱混合 | 表征强、可插拔知识、可横向扩容 | 工程量大,GPU 成本高 | 综合 ROI 最高,选它 |
一句话:在知乎这种“既要懂人话,又要懂业务”的场景,纯靠模型或纯靠规则都走不远,必须让“算法泛化”与“知识精准”互补。
配图:混合架构总览)
核心实现:模型、知识、检索三线并行
1. 意图识别:轻量 BERT+双向 Attention
知乎每天新增 5k+ 问题,标注团队只能覆盖高频场景,因此模型必须“小快灵”。我们用 4 层 Transformer+Bi-Attention,在 8G 显存就能跑 batch=64。
# model.py 遵循 PEP8,仅保留核心片段 import torch import torch.nn as nn class BiAttention(nn.Module): """对齐 query 与 title 的注意力,时间复杂度 O(L^2·d)""" def __init__(self, hidden): super().__init__() self.W = nn.Linear(hidden, hidden, bias=False) def forward(self, q, t, mask=None): # q,t: [B, len, hidden] scores = torch.bmm(self.W(q), t.transpose(1, 2)) # [B, Lq, Lt] if mask is not None: scores.masked_fill_(mask == 0, -1e9) attn = torch.softmax(scores, dim=-1) # 行归一化 out = torch.bmm(attn, t) # 加权求和 return out, attn class IntentClassifier(nn.Module): def __init__(self, vocab, d_model=256, nhead=8, num_layers=4, num_intents=37): super().__init__() self.embed = nn.Embedding(len(vocab), d_model, padding_idx=0) encoder_layer = nn.TransformerEncoderLayer( d_model, nhead, dim_feedforward=d_model*4, dropout=0.1 ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers) self.bi_attn = BiAttention(d_model) self.fc = nn.Linear(d_model*2, num_intents) def forward(self, token_ids, title_ids): x = self.embed(token_ids) * (x.shape[1]**0.5) # 缩放防梯度消失 memory = self.encoder(x) # [B, L, d] cls_vec = memory[:, 0] # BERT 式 [CLS] title_vec = self.embed(title_ids).mean(dim=1) # 平均池化 attn_out, _ = self.bi_attn(memory, memory) pooled = attn_out.mean(dim=1) logits = self.fc(torch.cat([cls_vec, pooled], dim=1)) return logits训练 30 epoch,F1=0.91,推理平均 18 ms(T4 GPU)。
2. 知识图谱与向量检索协同流程
- 步骤 1:离线把“盐选会员权益”“瓦力识别规则”等 1.2w 实体写入 Neo4j,形成 <主体, 属性, 值> 三元组。
- 步骤 2:对实体描述文本用 SBERT 编码,512 dim,导入 Faiss IVF1024 索引;查询时先走向量召回 top-10 实体,再回 Neo4j 做属性补全,平均时延 25 ms。
- 步骤 3:若用户问题命中“实体+属性”组合,直接返回图谱答案;否则走生成式兜底,保证覆盖率。
(配图:知识召回+生成混合流程)
生产考量:并发、状态、压测一样不能少
1. Redis 实现多租户状态机
知乎客服要隔离不同业务线(会员、内容安全、账号申诉),我们用hash结构存整轮状态,key 设计:zhihu:cs:{biz}:{uid}:{session_id},TTL=600 s 自动过期,防止僵尸 key。
# state_manager.py import redis import json r = redis.Redis(host='rds.zhihu', decode_responses=True, max_connections=50) class DialogState: def __init__(self, biz, uid, sid): self.key = f"zhihu:cs:{biz}:{uid}:{sid}" def get(self): data = r.hgetall(self.key) return {k: json.loads(v) for k, v in data.items()} if data else {} def update(self, **kwargs): pipe = r.pipeline() for k, v in kwargs.items(): pipe.hset(self.key, k, json.dumps(v)) pipe.expire(self.key, 600) pipe.execute() def clean(self): r.delete(self.key)并发测试 1k QPS,CPU 占用 12%,P99 延迟 3 ms,满足要求。
2. Locust 压测脚本示例
# locustfile.py from locust import HttpUser, task, between class CsUser(HttpUser): wait_time = between(0.5, 2.0) host = "https://cs-api.zhihu.com" @task(10) def ask(self): self.client.post("/v1/dialog", json={"uid": 12345, "text": "我的回答被瓦力误删了怎么办?"}, headerssess": "test-sess-123"})单机 4 进程可模拟 5k 并发,GPU 推理节点 3 台即可把平均响应压到 260 ms,比初版规则方案快 40%。
避坑指南:从 0 到 1 必须踩的坑
冷启动语料太少怎么办?
- 先用“高频 FAQ+站内搜索日志”构造 5k 弱标注样本,训练 baseline;
- 上线后开“主动学习”开关,把模型置信<0.65 的问法自动加入待标注池,运营每天花 15 分钟点选,两周即可扩到 3w 条,F1 提升 8 个点。
敏感词过滤别只依赖正则
- 采用“双层网关”:外层 AC 自动机(时间复杂度 O(n))秒级拦截政治、脏话;内层 BERT 二分类识别“阴阳怪气”隐性违规,召回率 94%,误杀<1%。
- 合规日志必须落盘 180 天,审计时可直接回放 JSON 对话流,别偷懒只存文本。
知识图谱别一上来就追求大而全
- 先覆盖“能显著降低工单量”的 20% 场景(例如会员续费规则),用 ROI 反向驱动实体扩充;
- 属性字段尽量扁平,深度>3 层查询在并发时会放大 RT,必要时开异步线程预热缓存。
写在最后
把 Transformer 的泛化能力跟知识图谱的精准度拧成一股绳,知乎客服的“三高”难题才算真正落地。但工程永远没有银弹:模型加深 2 层就能涨 1 个点的 F1,却会让 GPU 推理延迟增加 30 ms。如何平衡模型精度与响应延迟的 trade-off?你在业务里会更偏向哪一边,欢迎一起聊聊。