背景痛点:规则引擎的“慢”与“贵”
去年双十一,公司老客服系统直接“罢工”——高峰期平均响应 4.8 s,用户排队 2000+。技术复盘发现,罪魁祸首是那条“祖传”的规则引擎:两千多条 if-else 硬编码在内存里,每来一次对话就要顺序匹配,CPU 占用飙到 90%。更惨的是,运营同学改一句回复得走完整的发版流程,一周才能上线。老板一句“降本增效”,我们只好把枪口对准 AI。
技术选型:RNN、LSTM 还是 Transformer?
先给三种主流序列模型做个“体检”,数据是 10 万条真实客服日志,标签 32 类意图,硬件环境单卡 RTX 3060。
| 模型 | 训练耗时 | 准确率 | 线上延迟 | 备注 |
|---|---|---|---|---|
| Bi-LSTM | 2.3 h | 87.2 % | 120 ms | 需要截断 40 字,长句掉分 |
| GRU | 1.8 h | 86.5 % | 95 ms | 轻量,但注意力弱 |
| Tiny-Transformer (4 层) | 1.5 h | 91.7 % | 45 ms | 可并行,长句友好 |
结论:Transformer 在“精度-延迟”双维度碾压,还方便后续加 BERT,于是拍板 Tiny-Transformer。
核心实现:Python+TensorFlow 端到端
1. 数据管道
把原始对话按“用户问句 + 意图标签”整理成 tsv,先清洗再分词,用tf.keras.preprocessing.text.Tokenizer建立 20 k 词表,句子长度统一 64,截断或补零。
import tensorflow as-pb # 个人习惯缩写 from sklearn.model_selection import train_test_split def load_data(path): sents, labels = [], [] with open(path, encoding='utf8') as f: for line in f: sent, label = line.strip().split('\t') sents.append(sent) labels.append(label) return sents, labels sentences, y = load_data('intent_10w.tsv') X_train, X_test, y_train, y_test = train_test_split( sentences, y, test_size=0.1, random_state=42, stratify=y)2. Tiny-Transformer 模型
4 层、头数 4、隐层 256,带残差与 LayerNorm,参数量 1.1 M,手机端也能跑。
class PositionalEmbedding(tf.keras.layers.Layer): def __init__(self, maxlen, vocab_size, embed_dim): super().__init__() self.token_emb = tf.keras.layers.Embedding(vocab_size, embed_dim) self.pos_emb = tf.keras.layers.Embedding(maxlen, embed_dim) def call(self, x): maxlen = tf.shape(x).shape[1] # 动态长度 positions = tf.range(start=0, limit=maxlen, delta=1) positions = self.pos_emb(positions) x = self.token_emb(x) return x + positions def transformer_encoder(inputs, head_size, num_heads): x = tf.keras.layers.MultiHeadAttention( num_heads=num_heads, key_dim=head_size, dropout=0.1)(inputs, inputs) x = tf.keras.layers.Dropout(0.1)(x) res = x + inputs x = tf.keras.layers.LayerNormalization(epsilon=1e-6)(res) ffn = tf.keras.Sequential([ tf.keras.layers.Dense(head_size * 2, activation="relu"), tf.keras.layers.Dense(head_size), ]) x = ffn(x) return tf.keras.layers.LayerNormalization(epsilon=1e-6)(x + res) def build_model(vocab_size=20000, maxlen=64, num_classes=32): inputs = tf.keras.Input(shape=(maxlen,), dtype='int32') x = PositionalEmbedding(maxlen, vocab_size, 256)(inputs) for _ in range(4): x = transformer_encoder(x, 64, 4) x = tf.keras.layers.GlobalAveragePooling1D()(x) outputs = tf.keras.layers.Dense(num_classes, activation='softmax')(x) return tf.keras.Model(inputs, outputs) model = build_model() model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=5, batch_size=256)训练 5 个 epoch,测试集准确率 91.7%,单句 GPU 推断 8 ms。
3. 模型导出与量化
converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() open('intent_model.tflite', 'wb').write(tflite_model)体积从 4.3 MB 压到 1.1 MB,精度只掉 0.4%,可接受。
4. 小程序与后端 gRPC 通信
小程序端用微信原生wx.cloud.callContainer,后端跑在 TKE 容器里,proto 定义如下:
syntax = "proto3"; package intent; service Intent { rpc Predict (Request) returns (Reply) {} } message Request { string query = 1; string uid = 2; } message Reply { int32 label_id = 1; string answer = 2; float score = 3; }服务端 Python 代码片段:
import grpc from concurrentry import futures import intent_pb2, intent_pb2_grpc import tensorflow as tf class IntentServicer(intent_pb2_grpc.IntentServicer): def __init__(self): self.interpreter = tf.lite.Interpreter(model_path='intent_model.tflite') self.interpreter.allocate_tensors() def Predict(self, request, context): tokens = preprocess(request.query) # 同训练时一致 input_details = self.interpreter.get_input_details() self.interpreter.set_tensor(input_details[0]['index'], tokens) self.interpreter.invoke() prob = self.interpreter.get_tensor(output_details[0]['index'])[0] label = int(np.argmax(prob)) answer = label_map[label] return intent_pb2.Reply(label_id=label, answer=answer, score=float(max(prob))) server = grpc.server(futures.ThreadPoolExecutor(max_workers=40)) intent_pb2_grpc.add_IntentServicer_to_server(IntentServicer(), server) server.add_insecure_port('[::]:50051') server.start()并发 40 线程,压测 QPS 1200 时平均延迟 28 ms,CPU 60%,稳稳过关。
性能优化三板斧
- 模型量化:上文已做,体积 ↓ 75%,推断速度 ↑ 35%。
- 本地缓存:把“订单查询”“退款进度”等高频意图的答案提前算好,放 128 MB Redis,命中率 68%,平均响应再降 15 ms。
- 请求合并:小程序端 300 ms 内用户连续发 3 句,只取最后一句调模型,其余走本地上下文拼接,节省 2 次 RPC。
避坑指南:那些踩过的雷
- 数据标注常见错误:同一句“我要退货”被标成“退款”和“售后”两类,导致模型蒙圈。解决:多人交叉标注 + Cohen’s κ>0.85 才入库。
- 冷启动:新意图样本少于 30 条时,模型直接“摆烂”。解决:先用规则兜底,同步收集日志,每周自动重训。
- 敏感词过滤:AI 答错一句“骂娘”可能上热搜。解决:在 gRPC 返回前加一层 AC 自动机敏感词过滤,命中则直接转人工。
总结与延伸
把 Tiny-Transformer 搬上小程序,我们让客服响应从 4.8 s 降到 0.8 s,机器解决率 68%,老板终于笑了。下一步打算:
- 把中文 Tiny-BERT 蒸馏进来,提升 2% 精度,代价只增加 0.5 MB。
- 引入对话状态跟踪(DST)做 3 轮多轮对话,比如“查订单→修改地址→确认”,让机器人更像人。
如果你也在为客服效率头疼,不妨先按本文流程跑通最小闭环,再逐步升级。整套代码已放到 GitHub,拿走不谢,记得点个 Star 就行。