背景痛点:电商大促夜的“翻车”现场
去年双十一,我们组负责的智能客服在零点流量洪峰中“崩”得很有节奏:
- 用户问“我买的 iPhone 能 12 期免息吗?”——Bot 回复“请提供订单号”。
- 用户追问“订单号在哪看?”——Bot 再回“请提供订单号”。
- 用户怒摔键盘转人工,排队 20 min,差评+1。
复盘发现,三大顽疾集中爆发:
- 对话上下文丢失/Context Loss:HTTP 无状态 + 负载均衡轮询,导致 Session 在节点间跳来跳去,上一句的槽位(Slot)下一秒灰飞烟灭。
- 长尾意图识别差/Long-tail Intent Detection:活动临时上线的“定金膨胀”“尾款合并支付”等新话术,训练语料里一条都没有,BERT 直接“胡说八道”。
- 多轮连贯性弱/Coherence Weakness:规则脚本里 800+ if-else,想加一条“免息”逻辑,结果把“分期”逻辑撞飞,维护成本指数级上升。
痛定思痛,我们决定把“规则补丁山”升级为“Agent 架构”——让对话状态可追踪、意图模型可热更、异常可降级,且能把 F1 值提升 30% 以上。下面把趟过的坑、攒下的代码一次讲清。
技术对比:规则、SVM 还是 BERT?
同样 1 万 QPS 的压测,三种方案表现如下:
| 维度 | 规则引擎 | 传统 ML(SVM) | 深度学习(BERT) |
|---|---|---|---|
| 响应速度/P99 延迟 | 20 ms | 35 ms | 80 ms(FP16 后 45 ms) |
| 准确率/F1 | 0.82(人工硬编码) | 0.87(特征工程天花板) | 0.93(微调后) |
| 可维护性 | ★☆☆ 需求一改,全员通宵 | ★★☆ 特征要重标 | ★★★ 增量微调+蒸馏 |
| 冷启动 | 立刻生效 | 需要千级标注 | 需要万级标注 |
结论:
- 规则引擎适合“兜底”与“高频兜底意图”,但别让它主导。
- SVM 在 2016 年很香,现在只能做“备胎”。
- BERT 是主力,但必须“瘦身”才能进生产;于是我们把 BERT 当“主分类器”,规则当“fallback”,SVM 当“灰度实验基线”——三路并行,投票降噪。
核心实现:BERT 意图模块 + Rasa 状态机
1. 数据预处理:把客服日志洗成“人话”
原始日志长这样:
2023-11-11 00:01:23,userid=12345,msg="我买的 iPhone 能 12 期免息吗?"清洗脚本(PEP8 已检查,可直接抄):
# data/clean_logs.py from typing import List, Tuple import re import json def parse_raw_log(line: str) -> Tuple[str, str]: """ 解析原始日志,返回 (user_id, text) """ m = re.search(r'userid=(\d+).*msg="(.+?)"', line) if not m: return "", "" return m.group(1), m.group(2) def build_intent_dataset( raw_files: List[str], intent2query: dict, save_path: str ) -> None: """ 根据意图词典,把日志映射成带标签的 JSONL """ out = open(save_path, "w", encoding="utf8") for fp in raw_files: for line in open(fp, encoding="utf8"): uid, text = parse_raw_log(line) if not text: continue # 暴力关键词匹配,当弱标注 for intent, kw in intent2query.items(): if any(k in text for k in kw): out.write(json.dumps({"text": text, "intent": intent}, ensure_ascii=False) + "\n") break out.close()跑完脚本,得到intent_train.jsonl约 4.2 万条,覆盖 21 个主意图,长尾意图用后面讲的合成数据补。
2. BERT 微调:三行代码启动训练
# model/intent_trainer.py from typing import Dict from datasets import load_dataset from transformers import BertTokenizerFast, BertForSequenceClassification, Trainer, TrainingArguments def train_intent_model( train_file: str, label2id: Dict[str, int], model_out: str, epochs: int = 3 ) -> None: """ 微调 BERT-base-chinese 用于意图分类 """ tokenizer = BertTokenizerFast.from_pretrained("bert-base-chinese") model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=len(label2id) ) def tokenize(batch): return tokenizer(batch["text"], padding=True, truncation=True) ds = load_dataset("json", data_files=train_file)["train"] ds = ds.map(lambda x: {"label": label2id[x["intent"]]}) ds = ds.map(tokenize, batched=True) args = TrainingArguments( output_dir=model_out, per_device_train_batch_size=64, learning_rate=2e-5, num_train_epochs=epochs, fp16=True, logging_steps=50 ) trainer = Trainer(model=model, args=args, train_dataset=ds) trainer.train() trainer.save_model(model_out)训练 1 epoch 约 18 min(V100*1),验证 F1 0.93,比基线规则提升 11 个点。
3. Rasa 状态机:让对话“有记忆”
domain.yml片段:
intents: - query_installment - ask_order_status - deny - out_of_scope actions: - action_check_installment - action_query_order - utter_ask_order_number - utter_default_fallbackrules.yml兜底:
- rule: fallback steps: - intent: nlu_fallback - action: utter_default_fallback自定义 FallbackClassifier:
# actions/fallback.py from typing import Any, Text, Dict, List from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher import torch class ActionDefaultFallback(Action): def name(self) -> Text: return "action_default_fallback" def run( self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any] ) -> List[Dict[Text, Any]]: # 1. 调用 BERT 意图模型 intent, score = self._predict(tracker.latest_message.text) if score < 0.4: dispatcher.utter_message(text="没太明白,已为您转人工客服~") return [UserUtteranceReverted()] # 2. 高于阈值,回写正确意图 return [SlotSet("intent", intent)] @torch.no_grad() def _predict(self, text: Text) -> (Text, float): # 加载 TorchScript 模型,见下一节 ...至此,Agent 的“耳朵”(Intent Detection)和“小脑”(Dialogue Management)已就位。
性能优化:把 80 ms 压到 20 ms
1. 模型量化:TorchScript + FP16
# scripts/export_torchscript.py import torch from pathlib import Path model = BertForSequenceClassification.from_pretrained("outputs/intent") model.eval() dummy = torch.randint(0, 21128, (1, 32)) traced = torch.jit.trace(model, dummy) traced = torch.jit.optimize_for_inference(traced) traced.save("intent_traced.pt")线上推理统一用libtorchC++ 后端,P99 延迟从 80 ms 降到 25 ms,GPU 占用降 40%。
2. 对话上下文压缩:TF-IDF 特征哈希
多轮对话会把历史 utterance 全部送进 BERT,序列长度爆炸。我们的折中:
- 对历史 10 轮文本做 TF-IDF,取 Top-64 词,哈希到固定 128 维向量。
- 向量与当前句的 [CLS] hidden 拼接,再喂给分类层。
实验表明,上下文压缩后 F1 只掉 0.8%,推理速度提升 35%,显存省 1.3 GB。
避坑指南:数据、资源与多租户
1. 冷启动没数据?用模板+回译合成
- 先写 20 条“模板”,如“{商品}能 12 期免息吗”。
- 把商品词槽替换成 500 个 SKU,瞬间 1 万句。
- 中→英→中回译,再让 BERT-相似度>0.9 才保留,噪声降低 60%。
- 最后人工抽检 5%,就能把长尾意图覆盖率从 45% 提到 78%。
2. GPU 资源争用:多租户隔离
Kubernetes 集群里,客服 Bot 与推荐模型共 GPU 节点,常出现“抢卡”。方案:
- 给 Bot 建独立
nodepool,用taint: bot=only:NoSchedule隔离。 - 推理 Pod 用
nvidia.com/gpu: 0.3的共享卡,训练 Pod 用整卡;训练任务放夜间,白天让路。 - 引入
Time-slicing GPU(NVIDIA vGPU),把一块 A100 切 7 片,QPS 再涨 3 倍也不排队。
代码规范小结
- 所有 Python 文件统一
black -l 88,isort排序。 - 函数必须写
docstring+ 类型注解,否则 CI 直接打回。 - 单元测试覆盖到 85% 才允许合并;模型推理路径用
pytest + fakeredismock 掉 GPU,保证无卡也能跑。
延伸思考:向语音交互迁移
文本 Bot 跑通后,老板一句“能不能打电话过来直接问”——语音端到端延迟挑战:
- ASR 流式输出首字 350 ms,VAD 切句 200 ms,Bot 推理 80 ms,TTS 首包 300 ms,加起来快 1 s,用户已怀疑人生。
- 优化空间:
- 把 VAD 窗口与 ASR 缓存合并,提前 200 ms 把“半句话”喂给 BERT,流水并行。
- 意图模型蒸馏成小模型(TinyBERT 4 层),GPU 改跑在 Jetson 边缘,延迟再降 30 ms。
- TTS 采用 16 kHz 流式合成,首包压缩到 120 ms,整体端到端 500 ms 以内,体验可接受。
如果你也在做语音 Bot,不妨把本文的“上下文压缩”改成“音频特征压缩”,把 TF-IDF 换成 Whisper Encoder Embedding,思路完全复用。
踩坑三个月,把规则山铲平,换上这套 BERT+RL 混合 Agent 后,双十一高峰 12 万 QPS 零事故,意图 F1 从 0.82 拉到 0.93,差评率降 42%。代码和脚本都已开源在仓库,拿去改两行就能跑。希望这份现场笔记,能帮你少熬几个通宵,把客服 Bot 真正做成“智能”而不是“智障”。祝调试顺利,有问题留言区见。