
简介这是一份基于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选型为什么不能用默认WhitespaceTokenizerRasa默认的分词器是WhitespaceTokenizer它只按空格切分文本。英文句子天然有空格问题不大但中文句子没有空格整句话会被当成一个token丢给分类器特征矩阵稀疏到几乎不可用意图识别和实体抽取都会失效。所以中文项目的第一件事就是在pipeline里显式配置JiebaTokenizer同时配合字符级别的n-gram特征来兜住切词错误。我一般会用一个比较固定的组合第一次搭中文项目的人可以直接抄language: zh pipeline: # 中文分词基于jiebadictionary_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、rulesRasa项目根目录下有一堆yaml文件新手最容易晕的就是不知道每个文件该改什么。按我的习惯先看一张表文件管什么什么时候改config.ymlpipelineNLU组件和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_orderslots的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 trainrasa 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.94.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。默认epochs100对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里没有配置JiebaTokenizerRasa默认用WhitespaceTokenizer按空格切分中文没有空格整句话被当成一个token特征完全丢失。解决安装jieba并在pipeline第一段加上分词器。pip install jiebapipeline: - 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.ymlaction_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(textf订单{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这成了我做对话项目雷打不动的习惯。这套流程按顺序走下来从数据结构到模型交付都稳了希望帮到你。本文还有配套的精品资源点击获取