为什么选择开源方案?
在项目启动前,主要纠结两个问题:是买服务还是自己造轮子?
自研的挑战:如果完全从零开始,需要组建涵盖自然语言理解(NLU)、对话管理(DM)、自然语言生成(NLG)的团队。光是意图识别和实体抽取模型的训练、标注数据的准备,就是一项浩大的工程,更别提后续的维护和迭代了。开发周期长,初期投入成本非常高。
第三方服务的顾虑:使用成熟的客服 SaaS 平台固然快,但所有对话数据都经过对方服务器,存在数据主权和隐私泄露的风险。同时,这类服务的业务逻辑和对话流程定制通常有局限,很难深度融入自己的业务系统。
开源框架的优势:像 Rasa、Botpress 这类开源项目,提供了一个相对成熟的底座。它们封装了 NLU、DM 等核心组件,可以在其基础上进行二次开发,既能控制数据和代码,又能大大降低开发难度,在灵活性和成本之间取得了不错的平衡。
主流开源框架技术选型
市面上主流的开源对话机器人框架不少,重点对比了 Rasa 和 Botpress。
1. Rasa Rasa 是一个功能非常全面的开源机器学习框架,用于构建基于文本和语音的对话助手。
- NLU 准确率:其 NLU 组件(Rasa NLU)支持使用预训练模型(如 BERT)进行意图分类和实体抽取,准确率较高,尤其擅长处理复杂的、表达多变的用户输入。它采用 pipeline 方式,可以灵活组合不同的组件(分词器、特征提取器、分类器等)。
- 扩展性:核心对话管理由 Rasa Core 负责,采用基于机器学习的对话策略,可以通过故事(stories)进行训练,能处理更非线性的对话流。其架构是微服务化的,各个组件可以独立扩展,与外部系统集成也方便。
- 学习曲线:相对陡峭,需要理解其领域(domain)、故事、规则等概念,配置和训练过程对开发者有一定要求。
2. Botpress Botpress 定位为一个'开发人员优先'的对话机器人平台。
- NLU 准确率:早期版本 NLU 能力较弱,但新版本已经集成或可以对接主流的 NLU 服务(包括 Rasa NLU、LUIS、Dialogflow 等)。其自带的 NLU 更偏向于规则和关键词匹配,在处理句式固定的场景下表现不错。
- 扩展性:最大的特点是提供了一个可视化的流程编辑器,通过拖拽节点来设计对话流,对于业务逻辑清晰的场景(如信息查询、任务办理)开发效率极高。它内置了频道连接、用户管理、内容管理等功能,开箱即用性更强。
- 学习曲线:对于熟悉流程设计的开发者来说上手更快,但如果需要深度定制 NLU 模型,可能还是需要结合其他框架。
我的选择:由于需求对 NLU 的准确率要求较高,且希望有更强的机器学习能力来处理模糊表述,最终选择了 Rasa 作为基础框架。它的开源社区活跃,遇到问题容易找到解决方案。
核心功能实现:从意图识别到对话管理
选定框架后,就开始动手搭建。Rasa 项目的核心是 config.yml、domain.yml、nlu.yml、stories.yml 和 rules.yml 这几个配置文件,以及自定义的 Action Server。
1. 构建意图识别与实体抽取 Pipeline
首先需要在 config.yml 中定义 NLU pipeline。选择了一个结合预训练词向量和 Transformer 模型的配置,以平衡准确率和速度。
# config.yml 片段
language: zh
pipeline:
- name: JiebaTokenizer
- name: LanguageModelFeaturizer
model_name: "bert"
model_weights: "bert-base-chinese"
- name: DIETClassifier
epochs: 100
entity_recognition: true
- name: EntitySynonymMapper
- name: ResponseSelector
epochs: 50
DIETClassifier 是 Rasa 用于联合意图分类和实体识别的轻量级模型。定义好 pipeline 后,需要在 nlu.yml 中提供训练数据。
# nlu.yml 片段
version: "3.1"
nlu:
- intent: query_weather
examples: |
- 今天 [北京](city) 天气怎么样?
- 明天 [上海](city) 会下雨吗?
- 查一下 [广州](city) 的天气
- intent: greet
examples: |
- 你好
- 早上好
训练时,Rasa 会利用这些例句学习如何将用户输入归类到 query_weather 或 greet 意图,并抽取出如 city 这样的实体。这个过程的时间复杂度主要取决于选择的模型和训练轮数(epochs),DIETClassifier 的时间复杂度大致在 O(NED) 级别,其中 N 是样本数,E 是训练轮数,D 是模型复杂度。
2. 设计对话状态管理
Rasa 的对话管理主要依靠'故事'(Stories)和'规则'(Rules)。故事用于训练基于机器学习的策略,以处理复杂的多轮对话;规则则用于处理确定性的、单轮的对话逻辑。
对于简单的、流程固定的任务,更喜欢用'规则'配合'自定义动作'(Custom Action)来实现,这有点像一种简化的有限状态机(FSM)。
例如,处理天气查询:
- 状态 1(等待查询):用户触发
query_weather意图。 - 动作:执行一个自定义的 Python 动作
ActionQueryWeather。 - 状态 2(返回结果):动作执行完毕,返回结果给用户,对话结束。
这个自定义动作在 actions.py 中实现:
# actions.py 片段
from typing import Any, Text, Dict, List
from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
import requests
class ActionQueryWeather(Action):
def name(self) -> Text:
return "action_query_weather"
async def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
# 1. 从追踪器中获取提取到的实体
city_entity = next(tracker.get_latest_entity_values("city"), None)
if not city_entity:
# 如果没有识别到城市,询问用户
dispatcher.utter_message(text="请问您想查询哪个城市的天气?")
return []
# 2. 调用外部天气 API(这里需要添加异常处理)
try:
# 示例 API 调用,实际需替换为真实的天气服务
# response = requests.get(f"https://api.weather.com/v1?city={city_entity}", timeout=5)
# weather_data = response.json()
# summary = weather_data.get('summary', '未知')
# 为了演示,模拟一个响应
summary = "晴,25℃"
response_text = f"{city_entity}的天气是:{summary}"
except requests.exceptions.RequestException as e:
# 记录日志并返回友好提示
logger.error(f"查询天气 API 失败:{e}")
response_text = "暂时无法获取天气信息,请稍后再试。"
# 3. 向用户发送消息
dispatcher.utter_message(text=response_text)
return []
在这个动作中,通过 tracker 对象获取当前对话状态和提取的实体,执行业务逻辑后,通过 dispatcher 对象返回消息。清晰的类型注解和完整的异常处理是保证代码健壮性的关键。
面向生产环境的部署与优化
本地测试通过后,要部署到生产环境,还需要考虑性能、安全和稳定性。
1. 压力测试与性能评估
在部署前,用 Locust 对 Rasa 的 HTTP API 端点进行了压力测试,模拟高并发用户场景。
# locustfile.py 片段
from locust import HttpUser, task, between
class ChatbotUser(HttpUser):
wait_time = between(1, 3) # 用户思考时间
@task
def send_message(self):
payload = {
"sender": "test_user",
"message": "北京天气怎么样?"
}
headers = {"Content-Type": "application/json"}
# 向 /webhooks/rest/webhook 端点发送消息
self.client.post("/webhooks/rest/webhook", json=payload, headers=headers)
通过设置 200 个并发用户,观察响应时间和错误率。根据测试结果,可能需要调整 Rasa 服务的 worker 数量(使用 --workers 参数启动)或者对 NLU 模型进行轻量化处理(如使用更小的 BERT 模型或知识蒸馏)。
2. 安全加固:鉴权与数据过滤
生产环境的 API 必须要有鉴权。为 Rasa Action Server 和 HTTP 输入通道添加了 JWT(JSON Web Token) 验证。在反向代理(如 Nginx)或自定义的输入通道中间件中,验证请求头中的 Token 有效性。
另一个重点是敏感数据过滤。用户可能在对话中提及手机号、身份证号等信息。编写了一个后处理过滤器,在日志存储和向第三方转发消息前,对识别出的特定实体(如 phone_number)进行脱敏处理,例如替换为 138****1234。
3. 对话状态存储优化
Rasa 默认使用内存跟踪器(InMemoryTrackerStore),这在多实例部署或重启时会丢失状态。因此,将其替换为 RedisTrackerStore。
# endpoints.yml 片段
tracker_store:
type: redis
url: localhost
port: 6379
db: 0
password: your_password_here # 建议从环境变量读取
record_exp: 3600 # 会话记录过期时间(秒)
使用 Redis 后,所有对话状态集中存储,保证了无状态服务实例间的会话一致性。需要注意的是,要为会话数据设置合理的过期时间(record_exp),避免 Redis 内存被无限占用。
实践中遇到的'坑'与解决方案
1. 对话循环陷阱
在早期设计故事时,容易不小心创建出循环依赖,导致机器人在两个动作间来回切换。例如,不断询问用户'还有什么需要帮助的吗?',而用户每次回答'有'或'没有'都可能触发相同的循环。
检测方法:Rasa 提供了一个可视化工具 rasa visualize,可以生成对话流程的图表,帮助直观地发现循环。此外,在测试阶段,可以设置一个对话轮次上限,超过一定轮次后强制转入人工客服或结束会话。
2. 多轮上下文信息丢失
在复杂的多轮对话中(比如订餐:选择菜品->选择口味->确认地址),需要记住用户之前提供的多个信息(槽位)。
优化策略:
- 合理设计槽位(Slots):在
domain.yml中明确定义需要记忆的槽位及其类型(如文本、布尔值、列表)。 - 利用表单(Form):Rasa 的 Form 机制是处理这类需要按顺序收集多个信息的标准模式,它能自动管理槽位填充和验证。
- Redis 存储优化:确保自定义动作中从
tracker获取的槽位值是最新的。对于超长会话,定期检查并清理 Redis 中已过期或无效的会话数据,保持存储效率。
总结与思考
经过这一轮从技术选型、核心开发到生产部署的完整流程,开源客服智能体系统确实大大加速了项目落地。Rasa 框架强大的 NLU 能力和灵活的架构,让我们在保持自主可控的前提下,实现了不错的智能交互效果。
