背景痛点:电商/金融场景下的三座大山
去年“618”大促,我们团队维护的客服系统被瞬间流量打爆,CPU 飙到 98%,用户平均等待 8 秒才收到第一句回复。复盘后发现问题集中在三点:
- 并发瓶颈:秒杀场景下 500+ TPS 的突发请求,把同步 Flask 接口堵成“停车场”。
- 多轮状态维护困难:用户问“我订单在哪?”→ 机器人答“请提供订单号”→ 用户再发“12345”,此时上下文丢失,又得从头来一遍。
- 意图漂移:金融领域大量专业缩写(如“LOF”“TA”)让通用 BERT 直接“懵圈”,Top-1 意图准确率不足 75%,体验堪比“鸡同鸭讲”。
痛定思痛,我们决定用纯 Python 自研一套 AI 智能客服助手,目标很明确:高并发、低延迟、可扩展,还要能拎包上线。
技术选型:Rasa vs. Microsoft Bot Framework
先放横向对比表,结论一目了然:
| 维度 | Rasa 3.x | MS Bot Framework |
|---|---|---|
| 意图识别准确率(自建金融语料) | 87% | 82% |
| 部署成本 | 2C4G 单机可跑 | 依赖 Azure 服务,月度≈$300 |
| 中文社区活跃度 | 高 | 中 |
| 异步高并发支持 | 原生 Starlette | 需搭配 Bot Service,有额外 RTT |
我们最终“忍痛”放弃 Rasa,原因是其内置 NLU 管道对中文同义词扩展不够灵活;而 MS Bot Framework 绑定云生态,对私有化部署不友好。于是走上自研路线,站在巨人(Transformer)肩膀上。
核心实现:三步搭出可扩展骨架
1. FastAPI 异步对话 API
FastAPI 的async/await能把 I/O 等待降到毫秒级,下面给出最小可运行示例,已在线上扛住 600 TPS:
# main.py from fastapi import FastAPI, Request, HTTPException from pydantic import BaseModel import aioredis import httpx app = FastAPI(title="AI-CS-API", version="1.2.0") redis = None class ChatReq(BaseModel): uid: str query: str session_id: str class ChatResp(BaseModel): reply: str state: dict @app.on_event("startup") async def startup(): global redis redis = await aioredis.create_redis_pool( "redis://localhost:6379/0", encoding="utf-8" ) @app.post("/chat", response_model=ChatResp) async def chat(req: ChatReq): """ 异步对话入口 1. 先从 Redis 取上下文 2. 调意图识别服务(内部 TensorRT 推理) 3. 更新状态并回写 Redis """ state = await redis.hgetall(f"s:{req.session_id}") or {} intent = await infer_intent(req.query) # 内部协程 state["last_intent"] = intent reply = await generate_reply(intent, state) # 策略引擎 await redis.hmset_dict(f"s:{req.session_id}", state) return ChatResp(reply=reply, state=state) async def infer_intent(query: str) -> str: url = "http://triton:8000/v2/models/bert_cls/infer" async with httpx.AsyncClient(timeout=2) as client: r = await client.post(url, json={"query": query}) if r.status_code != 200: raise HTTPException(status_code=500, detail="Intent service error") return r.json()["label"]要点:
- 全程异步,网络 I/O 不阻塞事件循环
- 使用
httpx.AsyncClient调 TensorRT 推理服务,延迟 35 ms - Redis 连接池预创建,避免高并发下反复握手
2. BERT 领域适配微调
通用 BERT 在金融场景容易“水土不服”,我们采集了 18 万条真实对话,经过清洗→增强→再训练,Top-1 准确率从 76% 提到 93%。关键脚本如下:
# finetune.py import pandas as pd from transformers import BertTokenizerFast, BertForSequenceClassification from sklearn.model_selection import train_test_split from torch.utils.data import Dataset, DataLoader import torch, json, random def clean(text): """简单的中文清洗:去表情、英文括号转中文、统一币种符号""" text = text.lower().replace("¥", "元").replace("$", "美元") return text.strip() def aug_by_swap(sentence, n=2): """同义词随机替换做数据增强,控制增强比例防止语义漂移""" synonyms = json.load(open("finance_synonyms.json")) words = list(jieba.cut(sentence)) for _ in range(n): i = random.randint(0, len(words)-1) w = words[i] if w in synonyms: words[i] = random.choice(synonyms[w]) return "".join(words) df = pd.read_csv("raw_conv.csv") df["text"] = = df["text"].apply(clean) df["text_aug"] = df["text"].apply(aug_by_swap) df = pd.concat([df[["text","label"]], df[["text_aug","label"]].rename(columns={"text_aug":"text"})]) train, val = train_test_split(df, test_size=0.1, stratify=df["label"]) class ConvDataset(Dataset): def __init__(self, texts, labels): self.encs = tokenizer(texts.tolist(), padding=True, truncation=True, max_length=64, return_tensors="pt") self.labels = torch.tensor(labels.tolist()) def __len__(self): return len(self.labels) def __getitem__(self, idx): return {k: v[idx] for k,v in self.encs.items()} | {"labels": self.labels[idx]} tokenizer = BertTokenizerFast.from_pretrained("bert-base-chinese") model = BertForSequenceClassification.from_pretrained("bert-base-chinese", num_labels=df["label"].nunique()) trainer = Trainer( model=model, args=TrainingArguments( output_dir="ckpt", per_device_train_batch_size=64, learning_rate=2=2e-5, num_train_epochs=3, evaluation_strategy="epoch", load_best_model_at_end=True, ), train_dataset=ConvDataset(*train), eval_dataset=ConvDataset(*val), ) trainer.train()经验:
- 数据增强比例 1:1 即可,过多会引入噪声
- 采用
EarlyStopping并监控 F1,防止过拟合 - 训练完导出 ONNX → TensorRT,推理延迟再降 40%
3. 对话状态机 Redis 缓存
多轮场景里,状态机如果放内存,水平扩容就“抓瞎”。我们用 Redis Hash 存储单会话,结构如下:
Key: s:{session_id} ├─ last_intent ├─ order_id ├─ retry_cnt └─ expire_at (TTL=900s,防僵尸 key)更新原子性通过redis.multi()/exec()保证;同时开启redis keyspace notification,会话过期自动回调日志归档,解决“无限膨胀”隐患。
性能优化:把 500 TPS 打成“热身”
1. Locust 压测方法论
Locust 不仅好用,还能写 Python 脚本自定义负载模型。我们的压测流程:
- 写
ChatUser类,继承HttpUser,wait_time = between(0.5, 2) - 在
on_start里预创建 session,模拟真实用户粘滞 - 采用
step_load模式,每秒递增 20 用户,观察 p95 延迟突刺点 - 结合
prometheus+grafana监控 GPU 利用率、Redis QPS
结果:在 4×A10 卡 + 4×16C 节点上,系统 p95 延迟 180 ms,CPU 65%,GPU 58%,仍有 30% 余量。
2. GPU 资源动态分配
显存是“硬通货”,我们基于 Triton 的instance_group做弹性:
- 日常:每个模型 1 实例,占 1.2 GB
- 大促:通过 Kubernetes HPA 按 GPU 利用率 > 65% 扩容副本
- 夜间流量低:缩容到 0,副本自动休眠,节省 60% 成本
避坑指南:中文同义词 & 合规脱敏
1. 中文同义词歧义 5 种解法
- 词典映射:开源“同义词库+金融缩写表”,离线替换
- 语义相似度:微调 Sentence-BERT,计算 Top-K 召回
- 动态加权:在意图分类 loss 里对同义词样本加 focal weight
- 用户反馈闭环:前端“点踩”数据回流,每周增量重训
- 多任务学习:联合训练 NER,强制模型关注关键实体
2. 对话日志脱敏存储
合规红线不能踩,我们的做法:
- 正则先行:手机号、银行卡号、身份证号三段式脱敏
- 实体识别:用金融领域 NER 把“姓名”“地址”替换为
<MASK> - 存储分离:原始日志放加密 OSS,只把脱敏后索引放 ES
- 密钥托管:KMS 统一轮换,开发无感知
- 审计日志:谁下载、谁搜索全留痕,方便合规飞检
代码规范小结
- 全项目通过
black + isort强制格式化,CI 阶段不合规直接打回 - 所有对外函数写
docstring,格式遵循 Google Style - 单元测试覆盖率保持 80% 以上,核心路径 100%
延伸思考:情感分析还能怎么玩?
- 如果用户情绪值持续低于 0.3,系统自动把对话转人工,转接阈值该如何动态调整才不至于“误伤”好脾气用户?
- 情感分析模型对“反话”识别仍然吃力,能否引入多模态(语音语调调)来辅助判断?
- 当客服机器人也要表达“歉意”时,如何生成带有情感色彩的回复而不显得“阴阳怪气”?
整套系统上线三个月,已稳定承接 4200 万次对话,平均响应 160 ms,意图准确率 93%,节省人力 55%。回头看,踩坑最多的是数据质量和合规细节,性能反倒是一路“高歌猛进”。希望这篇实战笔记能帮你少熬几个夜,早日让 AI 客服从“答非所问”进化到“对答如流”。祝开发顺利,我们线上见。