背景痛点:传统客服为什么总“答非所问”
第一次做客服系统时,我把常见问答写成一堆 if-else,上线第一天就崩了:用户把“我要退货”说成“东西不要了”,机器人立刻当机。
痛点总结如下:
- 关键词匹配只能覆盖字面,语义稍一变就失效
- 多轮对话没有状态记忆,用户问完“运费多少”继续问“那多久能到”,机器人重新从 hello 开始
- 并发一上来,规则文件互相冲突,调试全靠打印,日志里全是“???”
于是决定用 NLP 方案重构,目标只有一个:让机器人先听懂,再回答。
技术选型:规则、模型、商业 API 怎么选
我把调研结果做成一张打分表,5 分为满分,方便后来人直接抄作业。
| 方案 | 开发速度 | 语义理解 | 可控性 | 成本 | 备注 |
|---|---|---|---|---|---|
| 规则引擎 | 5 | 2 | 5 | 免费 | 适合固定流程,如“按 1 转人工” |
| 自训练 BERT | 3 | 5 | 4 | GPU+数据 | 数据>1k 条后优势明显 |
| Dialogflow 等 API | 4 | 4 | 2 | 按次收费 | 冷启动最快,但黑盒 |
结论:
- 原型阶段用 scikit-learn 快速出模型,验证 PMF(产品市场契合度)
- 数据量上来后再上 BERT 或接入商业 API,把准确率从 85%→95%
核心实现:30 分钟搭出可运行 Demo
1. 项目骨架
目录保持扁平,方便新人定位:
ai-bot/ ├── app.py # Flask 入口 ├── intent_clf.py # 意图分类 ├── dialog_fsm.py # 状态机 ├── logger.py # 日志 ├── .env # 配置 └── data/ └── intent.csv # 训练数据2. 训练数据格式(intent.csv)
text,label "我想查订单","query_order" "物流走到哪了","query_logistics" "怎么退货","request_return" "人工客服","transfer_human"小技巧:每类≥30 条,尽量覆盖口语化表达,能少踩坑。
3. 意图分类器(intent_clf.py)
import joblib, pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression class IntentClassifier: def __init__(self, model_path="intent.model"): self.clf = joblib.load(model_path) @staticmethod def train(csv_path): df = pd.read_csv(csv_path) vect = TfidfVectorizer(ngram_range=(1,2), min_df=2) X = vect.fit_transform(df['text']) model = LogisticRegression(max_iter=1000).fit(X, df['label']) joblib.dump({'vect': vect, 'clf': model}, "intent.model") def predict(self, text): try: m = joblib.load("intent.model") X = m['vect'].transform([text]) return m['clf'].predict(X)[0] except Exception as e: logger.error(f"Intent predict error: {e}") return "unknown"训练脚本单独执行一次即可:
if __name__ == '__main__': IntentClassifier.train("data/intent.csv")4. 对话状态机(dialog_fsm.py)
用有限状态机(FSM)管理多轮,比写嵌套 if 清爽多了。
代码示例:
class DialogFSM: def __init__(self): self.state = "IDLE" self.mem = {} # 槽位记忆 def trigger(self, intent, entities=None): if self.state == "IDLE": if intent == "query_order": self.state = "AWAIT_ORDER_ID" return "请告诉我订单号" if intent == "request_return": self.state = "AWAIT_REASON" return "请问退货原因是?" if intent == "transfer_human": return "正在为您转接人工..." elif self.state == "AWAIT_ORDER_ID": if entities and entities.get("order_id"): self.mem["order_id"] = entities["order_id"] self.state = "IDLE" return f"订单 {entities['order_id']} 状态是已发货" else: return "未识别到订单号,请重新输入" # 其余状态同理扩展 return "没听懂,请换种说法"5. Flask 入口(app.py)
from flask import Flask, request, jsonify from intent_clf import IntentClassifier from dialog_fsm import DialogFSM import logging, os, time from dotenv import load_dotenv load_dotenv() app = Flask(__name__) clf = IntentClassifier() fsm = DialogFSM() @app.route("/chat", methods=["POST"]) def chat(): try: text = request.json["text"] uid = request.json["uid"] # 1. 意图识别 intent = clf.predict(text) # 2. 实体抽取(示例用空) entities = {} # 3. 状态机流转 reply = fsm.trigger(intent, entities) # 4. 日志 app.logger.info(f"uid={uid}|intent={intent}|reply={reply}") return jsonify({"reply": reply}) except Exception as e: app.logger.exception("chat error") return jsonify({"reply": "系统开小差,稍后再试"}), 500 if __name__ == '__main__': logging.basicConfig(level=logging.INFO) app.run(host="0.0.0.0", port=5000)6. 配置分离(.env)
LOG_LEVEL=INFO TIMEOUT=15 SENSITIVE_WORDS=傻叉,垃圾读取示例:
import os TIMEOUT = int(os.getenv("TIMEOUT", 15))生产考量:让 Demo 像正式产品
对话超时机制
Redis 给每个 uid 存last_active,超 15 分钟清掉状态,防止内存泄漏。敏感词过滤
用 DFA 树+.env热更新,命中后直接返回“请文明用语”。负载测试
Locust 脚本示例:
from locust import HttpUser, task class BotUser(HttpUser): @task def ask(self): self.client.post("/chat", json={"uid":"u123", "text":"我要退货"})本地执行locust -f locustfile.py --host=http://localhost:5000即可看 QPS、RT 曲线。
避坑指南:前辈掉过的 3 个深坑
会话未隔离 → 串聊
解决:状态机实例按 uid 存 Redis,或 Flask 里用from threading import local训练数据类别不平衡 → 预测全是一类
解决:用class_weight='balanced'或手动过采样日志没加流水号 → 线上报错找不到现场
解决:每次请求生成trace_id,返回给前端,并贯穿所有日志
延伸思考:知识图谱让机器人“长脑子”
当用户问“iPhone 15 的充电器兼容 MacBook 吗?”需要外部知识。
下一步可尝试:
- 把商品-属性-值导入 Neo4j
- 用 Cypher 查询结果拼成自然语言回答
- 结合状态机,若检测到“兼容”关键词,走知识图谱分支,否则走普通意图
这样客服就从“背 FAQ”升级到“推理+查询”,答对率又能涨一波。
以上是我第一次把“if-else 客服”改造成“能听懂人话”的 AI 智能客服系统的全过程。
没有高深的数学,也没有动辄百万的数据,只要按接口把每一层跑通,先让机器人“活下来”,再慢慢“活得更好”。
如果你也在踩坑,欢迎留言交流,一起把机器人调教成真正的“客服小能手”。