背景痛点:为什么一定要“自建”
去年公司把客服外包给第三方 SaaS,账单第一个月就飙到 3 万——80% 都是“在吗?”“你好”这类无效对话。更尴尬的是,用户聊天记录里出现竞品关键词,法务第二天就收到对方“数据使用声明”。痛定思痛,老板拍板:必须自建,成本可控、数据留在本地,还要能随时改话术。
可真正动手才发现,“自建”≠“GitHub 扒个 Chatbot Demo”。常见技术债务总结成三句话:
- 意图模型换业务就崩,中文同义词一多 F1 掉 20 个点
- 对话状态丢消息,用户连问三句“转人工”后直接 500
- 标注数据散落在 Excel,新人标 100 条能错 30 条,NER 标签大小写都能打架
下面把我们从 0 到 1 的爬坑过程拆给你看,照着抄至少能省 2 周折腾。
。
技术选型:Rasa / ChatterBot / Dialogflow 中文实测对比
| 维度 | Rasa 3.x | ChatterBot 1.x | Dialogflow ES |
|---|---|---|---|
| 意图识别 Top-1 准确率(自建 1.2w 语料,电商 FAQ) | 0.91 | 0.74 | 0.93 |
| 训练数据量 | 5 意图×20 例即可冷启动 | 需 100+ 相似句对 | 谷歌帮你扩写,但中文语料偏英文思维 |
| 中文分词 | 内置 Jieba+自定义词典 | 无,需外部切 | 云端黑盒 |
| 部署 | Docker Compose 一键 | SQLite 单机 | 谷歌云,API 限流 |
| 源码可改 | 完全开源 | MIT | 黑盒 |
| 数据合规 | 本地 | 本地 | 出境 |
结论:
- 如果团队有算法人手,Rasa 是平衡效果与可控的最佳解;ChatterBot 适合做“关键词答复玩具”;Dialogflow 效果虽好,但出境合规+调用费双重劝退。
- 中文场景下,意图识别差距核心在“分词+实体词典”,后面代码部分给出可落地的优化点。
核心实现:Python+Flask 搭 REST 骨架,Rasa 负责对话脑
1. 系统架构
浏览器/微信小程序 → Nginx → Flask (gateway) → Rasa-Server (NLU+Core) → Redis (tracker store) → 业务 API (订单/库存)
2. Flask 网关代码(Python 3.9)
# gateway.py from __future__ import annotations import asyncio import httpx from flask import Flask, request, jsonify import logging app = Flask(__name__) RASA_URL = "http://rasa-server:5005/webhooks/rest/webhook" @app.post("/chat") async def chat(): user_id: str = request.json["userId"] text: str = request.json["text"] async with httpx.AsyncClient(timeout=15) as client: resp = await client.post( RASA_URL, json={"sender": user_id, "message": text} ) return jsonify(resp.json())把同步 Flask 变异步,单 worker 可撑起 800 并发(Gunicorn + UvicornWorker,4 核 8 G)。
3. Rasa 对话管理序列图
- 用户消息 → NLU 管道(分词、FE、意图分类、NER)
- Policy 组件(Rule/Memo/TED)预测下一步 action
- Action Server 可调用外部订单接口;若返回 slots 需更新 tracker
- Tracker 存 Redis,设置 30 min TTL,实现“半小时断点续聊”
4. 意图识别模块:中文分词优化示例
# nlu_preprocessing.py from __future__ import annotations import jieba import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from rasa.nfeaturizers.dense_featurizer.lm_featurizer import LanguageModelFeaturizer # 1. 加载业务词典,保证品牌词不被切错 jieba.load_userdict("custom_brand.txt") def chinese_tokenizer(text: str) -> list[str]: # 返回 List[str],符合 Rasa Component 接口 return [w for w in jieba.cut(text) if w.strip()] # 2. 在 config.yml 里把 tokenizer 指向该函数 # pipeline: # - name: WhitespaceTokenizer # 占位,后面被自定义替代 # - name: DIETClassifier # 3. 若数据量 <1w,可先用 TF-IDF 快速验证效果 def quick_experiment(corpus: pd.Series, labels: pd.Series) -> float: vect = TfidfVectorizer(tokenizer=chinese_tokenizer, ngram_range=(1,2)) X = vect.fit_transform(corpus) # 这里接 SVM 或 Logistic,略 return f1_score(labels, model.predict(X))要点:
- 一定先加词典再切词,否则“iPhone14Pro”会被切成“i / Phone / 14 / Pro”,后面 NER 永远标不对。
- 如果标注数据少于 1 万句,DIET 与 BERT 微调差距 <2% F1,用 DIET 训练更快,省 GPU。
生产考量:并发、状态、敏感词
1. 并发模型选择
- 异步 IO:网关到 Rasa 全链路 async,I/O 等待时线程可服务别请求;
- 多进程:CPU 密集(如 BERT 服务)用 Gunicorn
workers=CPU×2+1,不要混用 gevent,否则 TensorFlow 的 C 扩展会死锁。
实测 4C8G 机器:
AsyncIO → 950 req/s,CPU 65 %;多进程 → 720 req/s,CPU 90 %。
结论:网关层异步,Action Server 多进程,瓶颈最小。
2. Redis 集群存 Tracker
- Key:
tracker:{user_id},Value: Rasa 原始 tracker json,TTL 1800 s - 采用Redis Cluster + redis-py-cluster,槽位 16384,三主三从
- 开启
maxmemory-policy allkeys-lru,防止冷用户撑爆内存
3. 敏感词 DFA 过滤器
# dfa.py from typing import Dict, Set class DFAFilter: def __init__(self) -> None: self.root: Dict[str, Dict] = {} def add(self, word: str) -> None: node = self.root for ch in word: node = node.setdefault(ch, {}) node["end"] = True def replace(self, text: str, repl: str = "*") -> str: res, i = [], 0 while i < len(text): node, j = self.root, i matched = 0 while j < len(text) and text[j] in node: if "end" in node[text[j]]: matched = j - i + 1 node = node[text[j]] j += 1 if matched: res.append(repl * matched) i += matched else: res.append(text[i]) i += 1 return "".join(res)把 2 万条敏感词载入内存 0.3 s,单次过滤 <1 ms,放在 NLU 管道最前端,Rasa 日志里就再也见不到“加微信***”。
避坑指南:标注、幂等、冷启动
1. 中文 NER 标注陷阱
- 标签体系先定好,BIO 别混用 BI,否则 DIET 会当成不同类别;
- 阿拉伯数字与字母统一半角,全角“2”在 jieba 词表外,模型没见过就标成 O;
- 连续数字如订单号“SO2024001234”整体标为实体,不要拆成“SO / 2024 / 001234”,否则槽位填充准确率掉 15 %。
2. 对话超时幂等性
用户重复点击“转人工”,后端可能创建多张工单。
解法:
- 给每个 action 加
idempotency_key = f"{user_id}:{timestamp//1800}" - Redis
SETNX判断 3 分钟内是否已执行,保证同一窗口仅一次工单。
3. 冷启动语料收集
- 先把过去 6 个月人工坐席聊天记录脱敏,按“问题-答案”对做 K-means 聚类,自动挑出高频 500 簇,每簇人工审 20 句就能覆盖 70 % 咨询。
- 上线后开启**“置信度 <0.3 转人工”**开关,把用户原句回流到标注池,两周滚动训练一次,模型自我生长。
延伸思考:用大模型增强 FAQ
Rasa 的检索式 FAQ 依赖“问题-答案”相似度,泛化弱,用户换个问法就失效。
我们试了两个方案:
Embedding 召回 + LLM 生成
把 5 万条 FAQ 切成 chunk → 用 text2vec-large-chinese 建 Milvus 索引 → 用户问题先召回 Top5 段落 → 拼接成 prompt 喂给私有化 Llama3-8B-Instruct,答案可读性提升 40 %,幻觉率 6 %。完全生成式
直接让 LLM 回答,幻觉率 18 %,商用场景不可接受。
结论:“向量召回 + 小模型排序 + LLM 润色”是当前最稳的折中,延迟 1.2 s,可控可溯源。
踩坑三个月,系统总算撑住日均 8 万轮对话,P99 响应 480 ms,成本降到 SaaS 的 1/6。
如果你也在调研自建,先把词典、标注规范、Redis 集群这三件小事做扎实,后面填坑会少一半。祝早日上线,不再被账单和合规惊吓。