痛点分析:多轮对话到底难在哪?
做智能客服的同学都懂,用户一句“我改不了地址”可能藏着八百种潜台词:有人刚下单想改、有人已经发货要拦截、还有人纯粹是找不到入口。传统方案里,Rasa 用 slot 填槽,Dialogflow 靠上下文 lifespan,但一遇到下面三种情况就集体翻车:
- 多轮状态管理:用户第 3 轮说“还是刚才那个订单”,系统早把 order_id 忘光,只能尴尬地反问“请问您指哪一笔?”
- 长尾意图识别:训练集里 80% 是“查物流”,剩下 20% 的“保税仓退货”被当成“查物流”处理,结果客服人工兜底率飙到 35%。
- 领域迁移适配:项目 A 是电商,项目 B 是航空,共用同一套意图层,上线一周发现“行李额”被识别成“七天无理由退货”,老板当场血压拉满。
一句话:上下文丢失 + 尾部分布 + 领域漂移,三座大山把体验、成本、口碑一起拖下水。
技术对比:BERT、GPT-3.5、Claude 谁更适合做客服?
在同样 8 核 A100、batch=16 的硬件下,我们跑了 10 万条真实对话,结果如下:
| 模型 | 意图准确率 | 多轮 BLEU | QPS | 显存(GB) | 备注 |
|---|---|---|---|---|---|
| BERT-base-chinese | 88.2% | 0.62 | 820 | 2.3 | 短文本王者,长轮次健忘 |
| GPT-3.5-turbo | 92.5% | 0.71 | 110 | 16 | 贵,但上下文窗口大 |
| Claude-1.3 | 91.8% | 0.70 | 95 | 14 | 安全审核严,偶尔“拒绝回答” |
结论:
- 准确率:GPT-3.5 > Claude > BERT
- 速度:BERT 甩开两条街,QPS 差 7 倍
- 成本:GPT-3.5 每 1k token 0.002 美金,一天 100 万轮对话≈ 1.2 万人民币,直接把利润算成负值
于是我们把“大模型做质检、小模型做线上”写进 OKR——既要准,又要快,还要省。
混合架构:Bi-LSTM+CRF、LoRA、DST 缓存一起上
整体思路:轻量模型兜底,大模型加持,缓存提速。
- 基础意图层:Bi-LSTM+CRF 负责头部 80% 高频意图,推理 3 ms 内搞定。
- 长尾校准层:LoRA 微调 7B 大模型,只训 2% 参数,把“保税仓退货”等尾部意图召回率从 42% 提到 79%。
- 对话状态跟踪(DST):用 Redis 缓存上一轮 slot,key 设计为
session:{user_id}:dst,TTL 300 s,减少 70% 重复解析。
代码实现:PyTorch 关键片段
下面代码可直接搬进项目,全部按 Google 命名规范。
1. 多轮上下文编码器
import torch import torch.nn as nn from transformers import AutoTokenizer, AutoModel class MultiTurnEncoder(nn.Module): """把多轮对话拼成一段,做窗口滑动,输出上下文向量.""" def __init__(self, model_name: str, max_turn: int = 5): super().__init__() self.tokenizer = AutoTokenizer.from_pretrained(model_name) self.backbone = AutoModel.from_pretrained(model_name) self.max_turn = max_turn self.hidden_size = self.backbone.config.hidden_size def forward(self, dialog_list: list[list[str]]) -> torch.Tensor: """ dialog_list: batch 内每个元素是对话字符串列表 return: (batch, hidden_size) 的上下文向量 """ batch_size = len(dialog_list) concat_ids, attn_mask = [], [] for turns in dialog_list: # 只保留最近 max_turn 轮 text = " [SEP] ".join(turns[-self.max_turn:]) encoded = self.tokenizer( text, max_length=512, truncation=True, padding="max_length", return_tensors="pt", ) concat_ids.append(encoded["input_ids"]) attn_mask.append(encoded["attention_mask"]) input_ids = torch.cat(concat_ids, dim=0) mask = torch.cat(attn_mask, dim=0) with torch.no_grad(): outputs = self.backbone(input_ids, mask) # 取 [CLS] 向量 context_vec = outputs.last_hidden_state[:, 0, :] return context_vec2. 基于 Faiss 的意图向量检索
import faiss import numpy as np class IntentIndex: """离线把意图样例编码成向量,建 FaissIndex,线上秒级召回.""" def __init__(self, encoder: MultiTurnEncoder, intent2samples: dict): self.encoder = encoder self.intent2samples = intent2samples self.index = None # faiss IndexFlatIP self.label_list = [] def build(self): embeddings, labels = [], [] for intent, samples in self.intent2samples.items(): vec = self.encoder([[s] for s in samples]) vec = vec / vec.norm(dim=1, keepdim=True) # L2 归一化 embeddings.append(vec.cpu().numpy()) labels.extend([intent] * len(samples)) embeddings = np.vstack(embeddings).astype("float32") self.index = faiss.IndexFlatIP(embeddings.shape[1]) self.index.add(embeddings) self.label_list = labels def search(self, query_vec: torch.Tensor, k: int = 5): query_vec = query_vec / query_vec.norm(dim=1, keepdim=True) scores, idx = self.index.search(query_vec.cpu().numpy(), k) return [(self.label_list[i], scores[0, n]) n, i in enumerate(idx[0])]生产考量:Triton、Redis、敏感词三板斧
Triton 推理服务器
把 Bi-LSTM+CRF 与 LoRA 大模型分别封装成两个模型仓库,开动态 batching,最大 batch 延迟 50 ms。实测 QPS 从 420 提到 680,GPU 利用率 93%。
Redis 缓存策略
DST 结果、意图分布、用户画像三份数据写同一个 pipeline,单次对话减少 30 ms 网络 IO。TTL 设 5 分钟,配合“用户 30 分钟无消息主动清 key”,内存稳定在 6 GB。
敏感词过滤
正则简单粗暴,但有效:
import re SENSITIVE_PATTERN = re.compile( r"(退.*费|投.*诉|315|消.*协)", flags=re.I) if SENSITIVE_PATTERN.search(user_text): # 走人工坐席 return transfer_to_human()
避坑指南:血泪换来的 5 条经验
过度依赖预训练模型做领域迁移
直接拿 GPT-3.5 做航空客服,Perplexity 从 9 掉到 25,用户一句“登机口”被翻译成“登机口零食”。解决:领域语料 30 万条先喂给 LoRA,再上线大模型,Perplexity 降到 12。对话超时机制
只按“30 分钟无消息”清 session 是不够的,用户电话一挂、微信窗口一关,状态还在内存里。加“客户端心跳”字段,3 分钟无心跳自动清。数据标注偏差
标注员把“我要投诉”全标成“投诉”,结果模型把“我想了解投诉流程”也识别成“投诉”,人工复核率飙升。解决:引入“意图+槽位”双标签,细化成“投诉-咨询流程”“投诉-提交诉求”等 6 个子类,准确率再提 5.4%。大模型温度系数
温度 0.7 创意足,但客服场景不需要“创作”,温度 0.1 最佳,重复采样 3 次投票即可。灰度回滚
大模型上线先 5% 流量,监控“人工转接率”> 5% 立即回滚,别等用户投诉才后知后觉。
结论:复杂度与实时性的天平
把准确率从 88% 拉到 93%,推理延迟却从 80 ms 涨到 260 ms,老板一句“再优化 50 ms”让团队连夜调 batch、剪层、换 FP16。问题来了:
当模型继续变大,实时体验必然受损;当延迟压到极限,复杂语义又可能丢失。如何在“更准”与“更快”之间找到可持续的平衡点?
或许动态路由、边缘微调、甚至端侧小模型自我蒸馏,都是下一波值得试的方向。你的场景会怎么选?欢迎留言一起拆坑。