AI智能客服意图落地:从模型选型到生产环境部署的踩坑笔记
背景:为什么老方案总被用户吐槽?
做智能客服的同学都懂,用户一句话能有多“放飞”:
- “我那个订单啊,就昨天买的,咋还没影儿?”——没有明确动词,时间指代模糊
- “你们这红包是坑人的吧?”——情绪+隐喻,意图藏在抱怨里
- “帮我取消”——缺少宾语,得结合上文才知道要取消什么
早期我们用规则(关键词+正则)硬怼,维护成本指数级上升;加一层 SVM 意图分类后,准确率从 65% 提到 75%,但新意图要重采特征,迭代周期按周算。最致命的是延迟:一次 HTTP 请求 600 ms,用户早走人了。
目标很明确:准确率 ≥90%,P99 延迟 ≤120 ms,新意图迭代 ≤2 天。下面把这次“换血”过程完整记下来,代码全部可复现,性能数据来自我们在 4 核 A10 上的实测。
技术选型:规则、机器学习、BERT 横向对比
| 维度 | 关键词规则 | SVM/随机森林 | BERT-base(微调) |
|---|---|---|---|
| 准确率 | 0.68 | 0.77 | 0.93 |
| 新意图扩展 | 改正则,易冲突 | 重采特征+重训 | 加数据继续微调 |
| 延迟(CPU) | 5 ms | 15 ms | 280 ms |
| 延迟(GPU+TensorRT) | — | — | 85 ms |
| 维护成本 | 高 | 中 | 低(同框架复用) |
结论:BERT 贵但最能打,延迟靠 GPU+TensorRT 补。小预算团队可用 ALBERT(参数量 1/10),准确率掉 1 个点,速度提升 35%,下文代码以 BERT 为例,换模型只需改model_name_or_path。
核心实现:30 行代码微调,关键步骤全注释
1. 数据准备
我们把历史会话打标成 18 个意图,保存成csv:text, intent。样本不平衡,先做分层采样,再按 8:1:1 拆分。
# data_prep.py import pandas as pd, sklearn.utils df = pd.read_csv('raw_chat.csv') df = sklearn.utils.resample(df, stratify=df['intent'], replace=False, n_samples=50_000) df[['text','intent']].to_csv('train.csv', index=False)2. 微调脚本(基于 transformers==4.38)
# finetune.py from datasets import load_dataset from transformers import (BertTokenizerFast, BertForSequenceClassification, Trainer, TrainingArguments, DataCollatorWithPadding) import torch, numpy as np, evaluate model_name = 'bert-base-chinese' tokenizer = BertTokenizerFast.from_pretrained(model_name) model = BertForSequenceClassification.from_pretrained(model_name, num_labels=18) def encode(examples): # 返回 input_ids + attention_mask,由 DataCollator 动态 pad return tokenizer(examples['text'], truncation=True) train_ds = load_dataset('csv', data_files='train.csv', split='train[:80%]') val_ds = load_dataset('csv', data_files='train.csv', split='train[80%:90%]') train_ds = train_ds.map(encode, batched=True) val_ds = val_ds.map(encode, batched=True) train_ds.set_format(columns=['input_ids', 'attention_mask', 'intent']) val_ds.set_format(columns=['input_ids', 'attention_mask', 'intent']) metric = evaluate.load('f1') def compute_metrics(eval_pred): logits, labels = eval_pred preds = np.argmax(logits, axis=-1) return metric.compute(predictions=preds, references=labels, average='weighted') args = TrainingArguments( output_dir='bert_intent', per_device_train_batch_size=64, per_device_eval_batch_size=128, num_train_epochs=3, learning_rate=3e-5, weight_decay=0.01, fp16=True, # 关键:混合精度提速 35% evaluation_strategy='epoch', save_strategy='epoch', load_best_model_at_end=True) trainer = Trainer( model=model, args=args, train_dataset=train_ds, eval_dataset=val_ds, tokenizer=tokenizer, data_collator=DataCollatorWithPadding(tokenizer), compute_metrics=compute_metrics) trainer.train() trainer.save_model('bert_intent/best')训练 3 个 epoch 共 18 min(A10),最佳 F1 0.931。
性能优化:TensorRT 让 GPU 真正跑满
1. 转 ONNX → TensorRT
pip need: transformers onnxruntime-gpu tensorrt==8.6 python -m transformers.onnx --model=bert_intent/best --feature=sequence-classification onnx/ trtexec --onnx=onnx/model.onnx --saveEngine=bert_intent.trt \ --fp16 --workspace=2048 --optBatchSize=322. 动态批处理(Dynamic Batching)
生产用 Triton Inference Server,配置config.pbtxt:
max_batch_size: 32 dynamic_batching { max_queue_delay_microseconds: 500 }实测同样 4 核 A10:
| 方案 | 平均延迟 | P99 延迟 | QPS |
|---|---|---|---|
| PyTorch GPU | 280 ms | 520 ms | 220 |
| TensorRT FP16 | 85 ms | 120 ms | 750 |
| TensorRT INT8(校准 1k 样本) | 65 ms | 95 ms | 950 |
注:INT8 准确率掉 0.4%,通过再微调 1 个 epoch 拉回。
避坑指南:把“坑”提前埋平
数据不平衡
用 Focal Loss(γ=2)替换 CrossEntropy,小类 F1 提升 8%。Trainer 里自定义损失:from transformers import Trainer class FocalLossTrainer(Trainer): def compute_loss(self, model, inputs, return_outputs=False): labels = inputs.pop("labels") outputs = model(**inputs) logits = outputs.logits loss_fct = torchvision.ops.focal_loss.sigmoid_focal_loss \ if len(logits.shape)==2 else ... loss = loss_fct(logits, labels) return (loss, outputs) if return_outputs else loss热更新
模型文件放对象存储,Triton 的model_repository用软链;新版本以version文件夹区分,Triton 自动加载,流量零中断。置信度阈值
别只看准确率,用验证集画“覆盖率-精度”曲线,选 knee 点。我们 0.88 置信度以上直接回答,以下走澄清策略,整体满意度 +6%。
安全加固:别让模型被一句话骗懵
- 输入过滤:正则+敏感词库先挡一层,再跑模型;
- Prompt 注入检测:用轻量 TextCNN 二分类(正常 query vs 攻击),召回 97%,延迟 4 ms;
- 对抗样本:对同义改写+字符扰动生成 1w 攻击样本,做鲁棒性重训,掉点 <0.5%。
上线效果 & 监控
上线两周数据:
- 意图准确率 93% → 94.2%(持续学习)
- 平均响应 82 ms,P99 118 ms
- 人工转接率下降 30%,客服成本节省显著
还没解决的开放问题
- 新意图冷启动只有 30 条样本,如何平衡“小样本学习”与“模型不灾难遗忘”?
- 多轮上下文意图常常漂移,该不该把对话历史拼进 BERT?序列变长后延迟又扛不住,有没有两全方案?
如果你也在啃类似骨头,欢迎留言交流踩坑心得。