☰
基于Python的Rasa中文聊天机器人:从零搭建到部署
2026/10/10 6:27:15 网站建设 项目流程

简介:这是一份基于Python开发的Rasa中文聊天机器人完整项目,主要面向毕业设计、课程设计与实际项目开发场景,适合需要快速搭建中文对话系统的学习者参考,也适合希望在此基础上二次开发的中高级开发者。项目源码经过严格测试,包含源码、开发文档、代码解析与模型训练产物,并附带环境依赖、启动脚本及训练脚本,可在本地完成模型训练与对话调试。资源共24个文件,以md文档、yml配置、py脚本、txt说明及bash脚本为主,压缩包仅4.42MB,目录结构清晰,便于按模块查阅。已有249人学习下载。内容覆盖多版本更新日志中的核心迭代:NLU样本优化、同义词与正则查找表、supervised_embeddings管道改进、Interactive Learning样本构建、MITIE管道训练以及身份查询案例;同时将Rasa升级至1.9.5,解决了Windows下TensorFlow运行异常的问题,对理解中文Rasa项目的完整落地流程与调优思路很有帮助。

1. 基于Python的Rasa中文聊天机器人:它到底适合什么样的交付

基于Python开发的Rasa中文聊天机器人这套组合,既是课程设计里的常客,也是毕业设计里最不容易翻车的一类选题。原因很简单:它把自然语言理解、多轮对话管理、模型训练和工程部署串成了同一条开发链路,你不需要从零训练一个大模型,但代码量和工作量又足够撑起一份完整的项目文档。对要做毕设或课设的开发者来说,真正的交付物不是那个demo效果,而是你能把意图识别、实体抽取、对话状态和自定义Action之间的依赖关系讲清楚。这篇文章不评价谁家的现成源码包,只按自己搭中文Rasa项目的实际顺序,把这件事拆成可以照着做的六步,从目录结构一路讲到模型留档。

2. 摸清Rasa的运转链路:NLU、对话管理、Action三层怎么配合

2.1 一次查天气对话说清:分词、特征、意图、策略、Action各做了什么

想象用户输入了一句话:“查一下北京明天会不会下雨”。屏幕背后发生的事大致是这样的:输入先被分词器切成“查一下 / 北京 / 明天 / 会不会下雨”这样的token;特征器把这些token转成向量;DIETClassifier输出意图ask_weather,同时标出实体city=北京、date=明天;Tracker把意图、实体以及之前的对话历史写入状态;Policy基于这个状态预测下一个动作;如果预测结果是自定义Action,就执行查天气逻辑,最后通过utter_message把结果拼成自然语言回复给用户。

这六步里,前三步属于NLU(自然语言理解),中间两步属于对话管理,最后一步属于Action层。写“项目代码解析”的时候,我见过不少人按文件顺序从头讲到尾,效果很差;更合理的讲法是按这条调用链来拆:config.yml里的pipeline对应分词和特征,data里的nlu数据对应意图和实体标注,stories和rules对应对话状态,actions/actions.py对应最终的业务逻辑。读者拿着目录能顺着一次真实对话找到每一段代码,这样的解析才算有用。

2.2 中文pipeline选型:为什么不能用默认WhitespaceTokenizer

Rasa默认的分词器是WhitespaceTokenizer,它只按空格切分文本。英文句子天然有空格,问题不大,但中文句子没有空格,整句话会被当成一个token丢给分类器,特征矩阵稀疏到几乎不可用,意图识别和实体抽取都会失效。所以中文项目的第一件事,就是在pipeline里显式配置JiebaTokenizer,同时配合字符级别的n-gram特征来兜住切词错误。

我一般会用一个比较固定的组合,第一次搭中文项目的人可以直接抄:

language: zh pipeline: # 中文分词,基于jieba;dictionary_path指向自定义词典目录,没有词典就删掉这行 - name: JiebaTokenizer dictionary_path: "resources/jieba_dict" # 正则特征:把规则先验喂给模型,处理订单号、日期这类强模式信息 - name: RegexFeaturizer # 字符级别n-gram:中文切错了也能保留局部字符信息 - name: CountVectorsFeaturizer analyzer: "char_wb" min_ngram: 1 max_ngram: 4 # 词级别特征:保留更粗的语义粒度 - name: CountVectorsFeaturizer analyzer: "word" min_ngram: 1 max_ngram: 4 # 意图+实体联合分类器,Rasa里承担NLU主任务 - name: DIETClassifier epochs: 100 # 同义词归一,把“维修/修/修理”归一到同一个实体值 - name: EntitySynonymMapper # FAQ和闲聊回复选择器,和普通意图分开训练 - name: ResponseSelector epochs: 100 policies: # 规则策略:不需要学习、必须稳定生效的分支 - name: RulePolicy # 多轮对话策略:max_history表示决策时看多少轮上下文 - name: TEDPolicy epochs: 100 max_history: 6

这里两个CountVectorsFeaturizer是关键:char_wb的n-gram对中文特别友好,即使分词把词切错了,字符级别的局部特征还能补救;word级别则保留“北京”“天气”这种完整词义。两者拼接后交给DIETClassifier,比只用一个特征器的效果好很多。第一次做中文项目的人,经常只配一个word级别的CountVectorsFeaturizer,遇到新词就翻车,就是因为缺了char这一路兜底。

2.3 四个配置文件的分工:domain、config、stories、rules

Rasa项目根目录下有一堆yaml文件,新手最容易晕的就是不知道每个文件该改什么。按我的习惯,先看一张表:

文件管什么什么时候改
config.ymlpipeline(NLU组件)和policies(对话策略)换模型结构、调训练参数
domain.yml意图、实体、slot、回复、自定义Action的注册表新增能力时第一站
data/stories.yml多轮对话的示例路径,喂给TEDPolicy新增多轮场景时
data/rules.yml不需要学习的固定分支,如greet、fallback、表单加兜底规则时

domain.yml是整个项目的“接口文档”。任何在stories、rules、actions里出现的动作和回复,都必须先在domain里注册,否则rasa train在数据校验阶段就会报错。比如你在actions.py里写了一个action_query_order,就必须在domain的actions列表里加上它;你希望某个实体值存入slot,就要在slots里定义这个slot并配置映射方式。这也是写“项目代码解析”时最该画清楚的对应关系:domain里声明的每一项,最终落在哪个代码文件、哪个yaml段落里。

stories和rules的边界,是另一个容易踩坑的地方。简单说,rules是“不经过学习、直接生效”的固定规则,比如用户说“你好”就回复“你好”;stories是“给TEDPolicy做示范”的示例数据,让模型自己学会在复杂上下文里做决策。你如果什么路径都往rules里塞,多轮对话就退化成if-else;如果全指望stories,核心分支可能因为样本不够而飘。后面第5章会专门展开这个坑。

3. 从零搭起中文Rasa项目:目录结构、训练数据和第一个模型

3.1 最小项目骨架:每个文件该放什么

先搭目录。手动建一个最小工程结构,把每个文件的位置定好:

my_rasa_bot/ ├── config.yml # pipeline 和 policies ├── domain.yml # 意图、实体、slot、回复、action注册表 ├── credentials.yml # 对话通道配置,本地调试用默认值 ├── endpoints.yml # 自定义Action服务的地址 ├── data/ │ ├── nlu.yml # 意图和实体的标注数据 │ ├── stories.yml # 多轮对话示例 │ └── rules.yml # 固定规则分支 ├── actions/ │ ├── __init__.py # 必须有,否则Action服务加载不了模块 │ └── actions.py # 自定义Action代码 └── models/ # 训练后自动生成,存放模型tar.gz

如果想省事,也可以先执行rasa init --no-prompt生成官方demo工程,再把data目录里的英文示例全部清掉,按上面的结构替换成中文内容。注意actions目录下的__init__.py不能漏,我见过好几个项目因为缺这个文件,rasa run actions启动时报module not found,排查了半天。

credentials.yml和endpoints.yml在本地调试时可以先不填内容,保持注释状态即可。等到第6章要接前端或启动自定义Action服务时,再回来配置。

3.2 中文NLU训练数据:意图、实体、同义词的yaml写法

Rasa 3.x推荐用yaml格式写NLU数据。下面这份是覆盖了打招呼、查天气、查订单三个意图的最小示例:

version: "3.1" nlu: - intent: greet examples: | - 你好 - 在吗 - 早上好 - intent: ask_weather examples: | - 今天[北京](city)的天气怎么样 - [上海](city)明天会不会下雨 - 帮我查一下[杭州](city)[后天](date)的天气 - intent: ask_order_status examples: | - 我的订单[20230912](order_id)到哪了 - 查询一下订单[88888888](order_id)的状态 - regex: order_id examples: | - [0-9]{8,12}

实体标注语法是[实体值](实体名),比如[北京](city),这个写法会被JiebaTokenizer切词后再交给DIET做序列标注。每个意图的例句数量,我建议至少15到20条起步,覆盖不同的词序、是否带语气词、是否带标点这些变化。只写三五条就去训练,意图识别基本靠运气。

同义词用synonym块来做归一。假设用户说的“张三”和“张珊”其实是同一个人,可以这样写:

- synonym: zhangsan examples: | - 张三 - 张珊

配合pipeline里的EntitySynonymMapper,两个输入最终都会归一成zhangsan这个值。这个能力在做中文项目时很有用,因为中文的口语表达、音近字、错别字都太常见了。建议从第一天就养成“实体值要归一”的意识,而不是到后期再补。

3.3 domain和stories:把多轮对话的边界焊死

domain.yml要定义意图、实体、slot、回复、自定义Action五类东西。以查天气为例:

version: "3.1" intents: - greet - ask_weather - ask_order_status entities: - city - date - order_id slots: city: type: text mappings: - type: from_entity entity: city date: type: text mappings: - type: from_entity entity: date order_id: type: text mappings: - type: from_entity entity: order_id responses: utter_greet: - text: "你好,我是课程设计助手,可以帮你查天气或订单。" utter_ask_city: - text: "你想查哪个城市?" utter_ask_date: - text: "查哪一天的天气?" actions: - action_weather_lookup - action_query_order

slots的mappings里,from_entity表示这个slot的值来自某个实体的抽取结果。比如用户说“查北京天气”,city这个slot会自动被填成北京。这些slot会进入对话状态,后续的Policy判断和Action执行都要依赖它们。

stories和rules的写法如下:

# data/stories.yml version: "3.1" stories: - story: 查天气完整流程 steps: - intent: ask_weather - action: action_weather_lookup
# data/rules.yml version: "3.1" rules: - rule: 打招呼直接回复 steps: - intent: greet - action: utter_greet

新手最容易犯的错,是把所有本该写在stories里的多轮场景塞进rules,或者反过来。写完这两份文件后,先在命令行跑一遍rasa data validate做数据校验。它会帮你检查出意图没定义、Action不存在、story引用错误这一类问题,省得训练到一半才报错。

3.4 跑通rasa train:命令、日志和产物

数据写好后就可以训练了。在项目根目录执行:

# 先校验数据,再开始训练 rasa data validate rasa train

rasa train会同时训练NLU模型和对话管理模型。训练日志里重点关注几个点:NLU训练是否有epoch进度输出、是否出现Finished字样、最后是否在models目录下生成时间戳命名的tar.gz文件。看到models下出现.tar.gz,说明模型已经产出。

如果rasa data validate时报错,不用慌,对照3.3的注册表检查一遍:意图名是否在domain里声明、stories里引用的action是否在domain的actions列表里、nlu.yml的格式是否是合法的yaml。训练日志里出现Invalid domain或Validation failed,八成都是这类注册问题,和模型本身无关。

4. 训练与调参:用交叉验证把中文模型的F1从0.7拉到0.9

4.1 必调参数逐个过:词典路径、ngram、epochs与学习率

先别急着改参数。明确一点:Rasa每个组件的参数都有默认值,第一次训练只需要动两个地方,一个是JiebaTokenizer的词典路径,另一个是确认两个CountVectorsFeaturizer都在pipeline里。其他参数等评估结果出来再调。

JiebaTokenizer的dictionary_path指向自定义词典目录。如果你有领域词表,就把它放进目录里配到这行;如果没有,直接删掉这行配置,jieba会用内置词典。强行指向一个不存在的路径会让训练报错,这是常见的低级翻车。

两个CountVectorsFeaturizer的min_ngram: 1和max_ngram: 4,对中文来说是经过验证的合理区间。数据量小没必要动;如果发现训练后某些意图总混淆,可以试着把char_wb的max_ngram降到3,减少噪声特征。

DIETClassifier里最值得关注的参数是epochs和learning_rate。默认epochs=100,对20个意图以内的中文项目通常是够的;如果训练集很小(每个意图20句以下),反而要往下调到60到80,防止过拟合。learning_rate默认0.001基本不用动。这里没有玄学,你改了哪个参数,交叉验证的F1会立刻反馈给你,前提是每次只改一个变量。

4.2 交叉验证与测试集评估:哪些指标值得盯

训练完不要急着展示demo,先用交叉验证看量化效果:

# 五折交叉验证,不依赖额外测试集 rasa test nlu --cross-validation --folds 5 # 如果想连对话管理一起评估,准备 tests/test_stories.yml 后执行: rasa test

交叉验证跑完后,结果会写进results/目录。打开其中意图级别的报告,重点看每个意图的precision、recall、F1,以及混淆矩阵。我的及格线是这样的:意图F1低于0.8,先回数据补例句;实体F1低于0.8,优先查分词和词典;某个意图的F1明显低于平均值,就去混淆矩阵看它和谁在打架,比如“查天气”和“问温度”这类语义高度重叠的意图,最容易被互相带偏。

这里有一个常见误区:只看rasa shell里demo对话觉得“挺聪明”,就以为模型没问题。很多情况下是Fallback策略把不确定的输入兜住了,体感好不代表分类准。交叉验证报告才是项目答辩时能写进开发文档的客观证据。

4.3 意图分错、实体抽不准:先按这个顺序排查

模型效果不好时,按下面的顺序排查,不要一上来就调epochs:

第一,看分词。执行rasa shell --debug,输入一个测试句子,观察日志里tokenizer输出的token列表是不是符合预期。比如“接口联调”如果被切成“接口/联调”,说明词典里缺这个词,先补自定义词典,比调模型参数管用。

第二,看置信度分布。同样在debug日志里,看每个意图的置信度打分,如果两个意图分数都在0.8上下,说明数据里这两个意图的特征重叠太严重,需要补充能区分的例句。

第三,看数据均衡程度。20个意图,有的写了50句,有的只写5句,模型自然会偏向数据多的那边。按意图补齐例句,是性价比最高的优化。

第四,看特征配置。确认pipeline里同时有char_wb和word两路CountVectorsFeaturizer,只留一路会明显损失效果。

第五,用正则锚点处理强规则信息。对于订单号、日期这类有明确格式的信息,与其让模型硬学,不如直接告诉特征器:

- regex: order_id examples: | - [0-9]{8,12}

RegexFeaturizer会把“是否命中正则”作为一个特征喂给DIET,模型更容易学到“出现8位数字时优先考虑ask_order_status”这个规律。我在实体抽取不稳的项目里,用这个办法基本都能救回来。

5. 避坑与排查:Rasa中文机器人最容易翻车的五个问题

5.1 坑一:中文全被切散,意图识别直接失效

现象:配置好数据后直接rasa train,训练能跑完,但输入“你好”模型完全分不出意图,甚至每个字都被当成独立token。

原因:pipeline里没有配置JiebaTokenizer,Rasa默认用WhitespaceTokenizer按空格切分,中文没有空格,整句话被当成一个token,特征完全丢失。

解决:安装jieba并在pipeline第一段加上分词器。

pip install jieba
pipeline: - name: JiebaTokenizer

同时注意language: zh这行配置和分词器是两回事,语言设置影响的是日期格式、数字规则这些预置逻辑,分词器必须显式配置,两者不冲突。

5.2 坑二:专业词总被切错,自定义词典不生效

现象:领域术语被切成半截,比如“接口联调”被切成“接口/联调”,实体标注在错误边界上,实体F1一直上不去。

原因:jieba内置词典不包含你的领域新词,分词时按概率把词切开了。

解决:准备自定义词典目录,把领域词按jieba用户词典格式放进去:

mkdir -p resources/jieba_dict vi resources/jieba_dict/domain.dict

词典文件每行一个词,可以带词频和词性,比如“接口联调 10 nz”。文件保存为UTF-8无BOM格式,然后在pipeline里把dictionary_path指向这个目录。改完词典后必须重新训练才生效。另一种更稳的思路是用RegexFeaturizer给强规则实体做锚点,对订单号、手机号这类信息,正则匹配的成功率远高于分词。

5.3 坑三:自定义Action报错,对话直接fallback

现象:对话走到某个节点,机器人突然答非所问或回复“抱歉,我没听懂”,日志里出现Failed to execute custom action。

原因:rasa run actions没有启动,或者endpoints.yml里action服务的地址配错了。还有一种隐蔽原因:actions目录缺少__init__.py,导致Action类加载失败。

解决:开两个终端分别启动服务。

# 终端一:启动自定义Action服务 rasa run actions
# 终端二:启动主对话服务 rasa run --enable-api --cors "*" -p 5005

同时检查endpoints.yml:

action_endpoint: url: "http://localhost:5055/webhook"

注意url路径一定要带/webhook,这是Action服务的标准路由。如果改了actions.py代码,Action服务要重启才会加载新逻辑。

5.4 坑四:多轮对话状态乱跳,stories和rules边界不清

现象:用户连续追问,比如先查天气再问订单,机器人把上一轮的城市名当成这一轮的参数,或者直接跳回了greet分支。

原因:TEDPolicy是数据驱动的,它依靠stories里的示例学习状态转移。stories太少、路径太单一,模型就学不到完整的上下文组合;同时如果rules写得太宽,抢占了本应交给TEDPolicy学习的路径,对话管理就退化成了硬编码。

解决:先明确边界——固定不变的分支(greet、fallback、表单开始)放rules;需要结合历史信息的多轮路径放stories。每次新增stories后跑一遍rasa data validate,让Rasa帮你检查故事线是否连贯。我见过很多项目把几十条规则全写在rules里,看起来对话很“听话”,但稍微变一种问法就崩,根源就是没给模型留出泛化空间。

5.5 坑五:训练结果不可复现,答辩演示翻车

现象:同一个数据集第一次训练F1到0.9,改了数据重新训练后F1变成0.85,再重训一次又变成0.88,完全说不清哪个模型是最终交付版本。

原因:训练过程中有随机初始化,数据顺序也会影响收敛结果。对话模型本身对样本顺序和初始化状态敏感,不做控制就天然不可复现。

解决:训练时固定随机种子:

rasa train --random-seed 42

如果当前版本不支持这个参数,退而求其次的做法是模型留档。训练产出的tar.gz文件就是可交付的模型实物,每次训练前把上一版模型改名备份,训练后记录数据版本和评估F1。到答辩或上线时,用固定的模型文件启动,不要现场重新训练。模型文件本身才是你真正交付的东西。

6. 把模型接到前端:REST通道、自定义Action与模型留档技巧

6.1 REST通道:让Web前端3分钟接上对话服务

本地验证成熟后,把机器人变成HTTP接口只需要两步。

# 先启动Action服务,再启动API服务 rasa run actions rasa run --enable-api --cors "*" -p 5005

然后在credentials.yml里启用REST通道:

rest: cors: "*"

用curl验证接口是否通:

curl -X POST "http://localhost:5005/webhooks/rest/webhook" \ -H "Content-Type: application/json" \ -d '{"sender":"user1","message":"北京明天天气怎么样"}'

返回的JSON数组里,每个元素的text字段就是机器人回复。前端只需要对这个地址发POST请求,不依赖任何Rasa专属的SDK,这就是最常见的对接方式。

6.2 自定义Action返回动态结果:查库、调接口、算逻辑

聊天机器人如果只能回固定文案,就撑不起“项目开发”这几个字。自定义Action才是接业务逻辑的地方。以查订单为例:

from typing import Any, Text, Dict, List from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher class ActionQueryOrder(Action): def name(self) -> Text: # 这个名字必须和domain.yml的actions列表保持一致 return "action_query_order" def run( self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) -> List[Dict[Text, Any]]: # 从对话状态里取出之前抽取到的订单号实体 order_id = tracker.get_slot("order_id") # 真正的项目里,这里换成数据库查询或后端接口调用 status = "已发货,预计三天内到达" dispatcher.utter_message(text=f"订单{order_id}:{status}") return []

Action的name()返回值必须与domain.yml中actions列表一致,漏掉任何一个都会导致运行时找不到对应动作。这里把实体值取出来拼进回复,已经覆盖了“从对话状态到业务响应”的完整闭环。

6.3 模型留档:每次训练前先备份上一版

最后分享一个我自己养成的习惯:每次训练前,先把models目录里现有的tar.gz模型复制一份,改个带日期后缀的名字,再跑新训练。项目临近交付时,用指定模型启动,而不是rasa run默认加载最新模型:

rasa run --model models/your_best_model.tar.gz --enable-api

模型文件是二进制的,不放进git做常规版本管理,但文件名和对应的评估指标值得随手记一条。我当年做课设最后悔的就是没留模型,答辩前夜重新训练了一次,效果和初版完全不一样。后来每次训练前先归档旧模型、训练后记一条数据版本和F1,这成了我做对话项目雷打不动的习惯。这套流程按顺序走下来,从数据结构到模型交付都稳了,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询