背景痛点:电商客服的“三座大山”
做电商客服的同学都懂,每天一睁眼就是这三座大山:
- 夜间咨询洪峰:大促零点一过,并发量瞬间飙到白天的 5~6 倍,人工坐排班再多也顶不住。
- 方言干扰:广东“几多钱”、四川“咋个卖”、东北“多钱儿”——同一句话写法千奇百怪,规则引擎写一条就得补十条。
- 多轮对话记忆:用户先问“这件裙子有 M 码吗?”,五分钟后追加“那 L 呢?”,系统若记不住前文,只能原地复读“亲,请提供尺码哦”,体验瞬间拉胯。
三座大山压下来,人工客服疲于奔命,转化率却一路下滑。于是老板拍板:上 AI,必须 7×24 顶住,还要“听得懂、答得快、记得住”。
技术选型:为什么最终选了 Transformer?
先放一张对比表,看完就明白:
| 方案 | 规则引擎 | 传统 ML(SVM/CRF) | 深度学习 Transformer |
|---|---|---|---|
| 开发速度 | 快(前期) | 中等 | 慢(前期) |
| 泛化能力 | 差 | 中等 | 强 |
| 长尾 query | 需要穷举模板 | 特征工程爆炸 | 自注意力天然适配 |
| 多轮记忆 | 手动维护上下文栈 | 需额外设计状态机 | 用 Position Embedding 天然有序 |
| 迭代成本 | 线性增长 | 重新标注+训练 | 增量微调即可 |
结论:规则引擎适合 MVP 试水,传统 ML 在 2016 年还算能打,但 2024 年还要伺候方言+多轮+长尾,直接上 Transformer 最省心。再加一条现实考量:HuggingFace 生态成熟,中文预训练模型(bert-base-chinese、chinese-alpaca)开箱即用,省下的就是赚到。
核心实现:两条流水线并行
整个系统拆成两条流水线:
- 意图识别(NLU):BiLSTM+Attention,轻量快速,CPU 也能跑。
- 对话生成(NLG):Transformer Decoder,负责“说人话”,需要 GPU 加速。
1. 意图识别:BiLSTM+Attention 模块
数据增强是重头戏,电商场景没有 20W 条标注数据怎么办?用 back-translation + 同义词替换,白嫖 5W 条。
# data_augment.py import random import jieba from typing import List random.seed(42) # 可复现 def synonym_replace(sentence: str, replace_prob: float = 0.15) -> str: """ 简易同义词替换,仅示例,可接入外部词林或 Word2Vec """ synonym_dict = { "价格": ["价钱", "多少钱"], "优惠": ["折扣", "便宜"], "发货": ["配送", "邮寄"] } words = list(jieba.cut(sentence)) out = [] for w in words: if w in synonym_dict and random.random() < replace_prob: out.append(random.choice(synonym_dict[w])) else: out.append(w) return "".join(out) if __name__ == "__main__": print(synonym_replace("这件衣服价格有优惠吗?"))模型代码(核心片段):
# intent_model.py import torch import torch.nn as nn from typing import Tuple class BiLSTMAttn(nn.Module): def __init__(self, vocab_size: int, embed_dim: int = 128, hidden_dim: int = 256, num_class: int = 32): super().__init__() self.emb = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=True) self.attn = Attention(hidden_dim * 2) self.fc = nn.Linear(hidden_dim * 2, num_class) def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.emb(x) out, _ = self.lstm(x) attn_out = self.attn(out) # [B, H*2] logits = self.fc(attn_out) return logits class Attention(nn.Module): def __init__(self, hidden_dim: int): super().__init__() self.w = nn.Linear(hidden_dim, 1) def forward(self, lstm_out: torch.Tensor) -> torch.Tensor: # lstm_out: [B, T, H*2] score = self.w(lstm_out).squeeze(-1) # [B, T] weight = torch.softmax(score, dim=1).unsqueeze(-1) context = torch.sum(lstm_out * weight, dim=1) # [B, H*2] return context训练脚本记得加torch.backends.cudnn.deterministic = True,保证每次跑都复现同样指标。
2. 对话生成:拥抱 HuggingFace
商品属性要喂给模型,否则“有货吗”会答成“有的”却不说颜色和尺码。做法:把属性序列化成结构化句子,拼到 System Prompt。
# nlg_server.py from transformers import AutoTokenizer, AutoModelForCausalLM import torch import json from typing import Dict tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") model = AutoModelForCausalLM.from_pretrained("chinese-alpaca-oom", torch_dtype=torch.float16).cuda() def build_prompt(attrs: Dict[str, str], history: str, query: str) -> str: attr_str = ";".join([f"{k}:{v}" for k, v in attrs.items()]) sys = f"你是电商客服。商品信息如下:{attr_str}。请简洁回答。" hist = f"历史对话:{history}" if history else "" return f"{sys}\n{hist}\n用户:{query}\n客服:" def generate(attrs: Dict[str, str], history: str, query: str) -> str: prompt = build_prompt(attrs, history, query) ids = tokenizer(prompt, return_tensors=False)['input_ids'] out = model.generate( ids, max_new_tokens=64, do_sample=True, top_p=0.95, temperature=0.7, pad_token_id=tokenizer.pad_token_id ) return tokenizer.decode(out[0][len(ids):], skip_special_tokens=True)生产考量:Docker+K8s 弹性部署
1. 镜像分层
- 基础层:nvidia/cuda:11.8-runtime-ubuntu20.4
- 框架层:pytorch/torchserve:0.8.2
- 业务层:COPY intent+nlg 代码,ENTRYPOINT
gunicorn -k uvicorn.workers.UvicornWorker
2. K8s HPA 策略
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: cs-bot-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: cs-bot minReplicas: 2 maxReplicas: 30 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 65 - type: Pods pods: metricName: nvidia_gpu_utilization target: type: AverageValue averageValue: "75"压测结果:单卡 A10 在 50 并发下 GPU 利用率 72%,P99 延迟 480 ms;当副本数拉到 20,并发 1000 仍可保持 P99 < 600 ms。
3. Prometheus 监控片段
# gpu_exporter.yaml - job_name: 'nvidia-dcgm' static_configs: - targets: ['10.244.2.18:9400'] metrics_path: /metrics scrape_interval: 15sGrafana 面板重点关注DCGM_FI_DEV_GPU_UTIL和torchserve_inference_latency_seconds_bucket。
避坑指南:那些线上哭过的坑
特殊符号转义
用户复制 Excel 价格,前面带\t或\xa0,直接丢进 tokenizer 会报token not in vocab。统一清洗:import regexex as re def clean(text: str) -> str: text = re.sub(r'\s+', ' ', text) text = text.strip() return textRedis 对话状态管理
用Hash结构存多轮,key=session:{user_id},field=turn:{ts},value=json.dumps({"query":..,"answer":..,"slots":..})。设置 TTL=30min,防止僵尸 key 堆积。更新时先WATCH,再MULTI,保证并发安全。随机种子必须贯穿
训练、数据增强、模型dropout都要seed=42,否则今天训练完,明天复现指标差 0.5%,老板一句“为啥掉点”能让你加班到半夜。
代码规范小结
- 类型注解:所有函数入参、返回值必写,团队 review 时一眼看懂。
- 异常处理:I/O、网络、Redis、数据库全部
try/except并打logger.exception,千万别pass掉。 - 单测覆盖:意图识别至少 90%,NLG 因为随机性,给 golden answer 加
assertIn模糊匹配。
延伸思考:把知识图谱请进来
现在模型答“这款笔记本续航多久?”只能背参数表。若把商品知识图谱(三元组:<SKU-123, 续航, 10h>)拼进 Prompt,就能支持多跳推理:
用户:“出差看剧够用吗?”
系统先解析“看剧≈在线视频”,再到图谱查“在线视频功耗”,计算 10h/功耗*系数,答“亲,大约能连续看 8 小时,北京飞纽约足够”。
实现思路:
- 用 NebulaGraph 存商品三元组,索引 SKU+属性。
- 在对话生成前加
Graph Retriever,把子图序列化成句子喂给模型。 - 训练时做
instruction tuning,让模型学会“读图谱”而不是“背参数”。
结尾体验
整套流程跑下来,最深刻的感受是:Transformer 不是银弹,但把数据清洗、状态管理、弹性部署这些“脏活累活”补齐后,它真能让一个初级工程师在三个月内交出“响应快 40%、夜间零掉线”的答卷。下一步,我准备把图谱检索做成可插拔组件,让客服机器人不仅能“答得快”,还能“答得巧”。如果你也在踩电商智能客服的坑,欢迎留言交流,一起少掉头发多交付。