背景痛点:人工客服的“三缺”困境
校园二手交易平台在毕业季、开学季会迎来咨询洪峰。实测数据显示,人工客服平均响应时长 38 秒,夜间无值守时段占比 42%,重复性问题(“包邮吗”“能刀吗”)占对话量的 73%。高并发+低客单价导致外包团队不愿加人,学生兼职又无法 7×24 在线,用户流失率随之攀升。痛点归纳:
- 响应延迟:并发 200 人时,排队 5 分钟以上
- 夜间缺失:22:00-08:00 无服务,次日留存下降 18%
- 质量参差:兼职客服对商品规则理解不一致,纠纷率 4.7%
技术对比:规则、传统 NLP 与 LLM 的三角权衡
| 维度 | 规则引擎 | 传统 NLP(TextCNN+CRF) | LLM(BERT 微调) |
|---|---|---|---|
| 准确率(意图) | 82% | 87% | 93% |
| 开发周期 | 1 周 | 3 周 | 2 周 |
| 维护成本 | 规则膨胀后指数级上升 | 需持续标注新语料 | 增量训练仅需 GPU 4 小时 |
| 上下文理解 | 无 | 2~3 轮 | 5~7 轮 |
| 硬件资源 | CPU 即可 | CPU 8G | GPU 4G 显存即可 |
结论:LLM 在准确率与可扩展性上胜出,且微调成本已降到学生团队可接受范围。
核心实现:三把斧搞定智能客服
1. 意图分类:BERT 微调 3 分类任务
数据准备:
- 采集 1.2 万条真实对话,标注为
price|quality|logistics三类 - 按 8:1:1 拆分,使用
bert-base-chinese预训练权重
训练脚本(PEP8,关键注释已加):
# train_intent.py from datasets import load_dataset from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments model_path = "bert-base-chinese" tokenizer = BertTokenizer.from_pretrained(model_path) def encode(batch): return tokenizer(batch["text"], truncation=True, padding="max_length", max_length=64) ds = load_dataset("csv", data_files={"train":"train.csv","test":"test.csv"}) ds = ds.map(encode, batched=True) model = BertForSequenceClassification.from_pretrained(model_path, num_labels=3) args = TrainingArguments( output_dir="./ckpt", per_device_train_batch_size=64, num_train_epochs=3, learning_rate=3e-5, evaluation_strategy="epoch" ) trainer = Trainer(model=model, args=args, train_dataset=ds["train"], eval_dataset=ds["test"]) trainer.train()训练 3 轮后,验证集 F1 0.93,单条预测 18 ms,满足实时需求。
2. 对话状态管理:Redis + Hash 结构
每轮对话以sessionId为 key,Hash 字段设计:
intent:当前意图slots:JSON 序列化槽位turn:已对话轮次ttl:过期时间 15 min
Python 封装函数:
import redis, json r = redis.Redis(host="127.0.0.1", decode_responses=True) def update_state(sid, intent, slots): pipe = r.pipeline() pipe.hset(sid, "intent", intent) pipe.hset(sid, "slots", json.dumps(slots)) pipe.expire(sid, 900) pipe.execute()读取时一次hgetall,延迟 < 5 ms,支持 1 万并发会话。
3. 限流熔断:Spring Cloud Gateway 片段
防止 LLM 推理被突发流量打 垮,在网关层按 IP 做令牌桶:
spring: cloud: gateway: routes: - id 路由到 /chat uri: lb://chat-svc filters: - name: RequestRateLimiter args: rate-limiter: redis key-resolver: "#{@ipKeyResolver}" replenishRate: 10 # 每秒允许 10 请求 burstCapacity: 20 - name: CircuitBreaker args: name: chatCB fallbackUri: forward:/fallback当错误率 > 50% 时熔断 30 秒, fallback 返回“客服忙,请稍后”,保证核心链路可用。
性能测试:JMeter 压测实录
测试环境:
- 4C8G 容器 × 3
- GPU T4 1 卡
- 并发 500 线程,阶梯 5 min 升至 1000
结果摘要:
| QPS | Avg RT | CPU | GPU 显存 | 错误率 |
|---|---|---|---|---|
| 500 | 180 ms | 42% | 2.7G/4G | 0% |
| 800 | 220 ms | 68% | 3.1G/4G | 0.2% |
| 1000 | 410 ms | 85% | 3.8G/4G | 1.1% |
结论:日常 500 QPS 安全区,峰值 800 仍可控;超过 1000 需水平扩容或降级。
避坑指南:踩过的三个深坑
敏感词误判
“刀”在二手圈指“砍价”,却被误判为管制刀具。解决:构建领域白名单,将“可刀”“小刀”加入自定义词典,并在损失函数中给白名单 token 降权,误判率从 5% 降至 0.3%。上下文丢失
网络抖动导致 WebSocket 重连,sessionId 变更。解决:在 JWT 中嵌入userId+商品Id作为影子 key,重连后先查影子 key 恢复状态,保证多轮议价不中断。GPU 资源不足
考试周并发骤降,但 GPU 仍常驻。解决:
- 推理服务开启
GPU_SHARED_MODE,显存按需分配 - 当显存占用 > 90% 时,自动切换至 ONNX CPU 推理,RT 增加 60 ms,却保证可用性
- 夜间定时缩容,节省 45% 成本
思考题:多轮议价如何优雅收尾?
现实场景里,买家与卖家常进行 3~5 轮价格拉锯。若由 LLM 全程托管,需同时考虑:
- 价格底线约束:卖家预设“最低 120 元”
- 情感安抚:拒绝时避免生硬
- 结束条件:双方价差≤5% 或轮次上限
如何设计状态机与奖励函数,让 LLM 既守住底线,又给出“台阶”促使成交?期待大家在评论区交换思路。