ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flask+Rasa中文任务型对话机器人:从部署到避坑实战指南

Flask+Rasa中文任务型对话机器人:从部署到避坑实战指南

简介:基于Flask与Rasa的中文任务型对话机器人完整工程包,属于自然语言处理与Web服务结合的典型实践项目,适合NLP初学者、Python全栈开发者和需要搭建智能客服的团队。项目包含可运行的源码、部署文档、训练语料与前端交互界面,适配Python 3.7以上环境,并已通过运行验证,可直接替换数据使用,降低二次开发门槛。压缩包内共109个文件,整体仅7.51MB,涵盖35个文本配置、13个JSON数据、7个Python脚本、6个YAML管道、5个Markdown文档等核心类型,另有HTML、CSS、JS前端页面以及图片和模型文件,目录分类明确。已有117人学习下载,适合通过实际项目理解Rasa对话管理、Flask服务发布及中文NLU任务流程。资源内附前端聊天样式、意图实体数据、模型档案和部署说明,能够支撑从环境准备到定制功能开发的完整实践。

1. Flask+rasa中文任务型对话机器人:这套源码解决的不只是“能跑”

刚拿到这个“Python优秀项目 基于Flask+rasa的中文任务型对话机器人源码+部署文档+全部数据资料.zip”,多数人的第一个动作是打开app.py看接口怎么写,然后在第一次rasa train时被中文分词和模型版本卡住。这个项目的技术选型非常典型:rasa承担意图识别、槽位提取和多轮对话管理,Flask负责把对话能力包装成HTTP接口,前端、业务系统、数据库都通过它来对接。资料里如果带了训练好的模型和中文语料,你已经省掉了最耗时的一块。这套技术栈适合有Python基础、想在一周内把中文任务型对话机器人跑通并改造成业务demo的开发者。下面按“骨架是什么→怎么跑通→踩了哪些坑→怎么接到真实数据”的顺序展开。

2. 先把任务型对话的骨架看懂:rasa管对话,Flask管API

2.1 为什么是rasa + Flask,而不是只用一个

任务型对话机器人和我们平时用的闲聊机器人有一个本质区别:它要完成任务,比如查余额、订机票、办业务。这类机器人的核心是一个循环:用户输入自然语言→识别意图和实体(槽位)→决策下一步动作→执行动作并回话→等待下一轮输入。rasa在这个循环里承担前两步和决策部分,Flask则承担对外暴露和服务包装的部分。

rasa本身自带HTTP接口,也就是rest channel,直接启动后就可以通过/webhooks/rest/webhook收发消息。那为什么还要在外面套一层Flask?常见原因有四个:一是生产环境不会把rasa的5005端口直接暴露给公网或内部业务系统,需要在前面加一层统一入口;二是Flask可以顺手做鉴权、日志、限流和请求格式转换;三是任务型机器人几乎都要连业务数据库或内部系统,Flask作为轻量服务层比在rasa action里写一堆路由逻辑要干净;四是如果要做管理后台或H5页面,Flask自带模板和静态资源托管,rasa在这块帮不上忙。

所以我通常把Flask理解成“门面”,把rasa理解成“脑子”。门面不参与理解,也不参与对话决策,只负责把请求安全地转发给脑子,再把脑子的回复带给用户。这个分层的好处是:将来rasa模型升级,Flask不用改;前端换框架,Flask也不用改。

2.2 中文NLU pipeline怎么配:从jieba到BERT

rasa对英文的默认处理比较省心,但中文落地时,分词和特征抽取直接影响意图识别准确率,这是任务型对话里最容易翻车的一环。常见的pipeline配置是jieba分词加CountVectors加DIETClassifier,我一般会先在项目的config.yml里改成类似下面这样:

language: zh pipeline: - name: JiebaTokenizer - name: RegexFeaturizer - name: CountVectorsFeaturizer - name: CountVectorsFeaturizer analyzer: char_wb min_ngram: 1 max_ngram: 4 - name: DIETClassifier epochs: 100 constrain_similarities: true - name: EntitySynonymMapper policies: - name: MemoizationPolicy - name: RulePolicy - name: TEDPolicy max_history: 5 epochs: 100

这段配置里有几个参数值得细看。第一,JiebaTokenizer负责中文分词,没有它,后面所有特征抽取器都在做无用功。第二,CountVectorsFeaturizer用了两次,一次是词级别,一次是char_wb字符级别,min_ngram和max_ngram控制字符窗口长度,这样即使遇到词库里没有的新词,也能从字符组合里提取特征。第三,DIETClassifier是rasa 3.x默认的意图分类和实体识别模型,epochs我一般设在100左右,太大容易过拟合,太小意图学不完整。constrain_similarities设置为true,能让模型对没见过的句式判断更保守,减少“乱认意图”。

如果项目里中文语料比较充足,训练环境也有GPU,还可以把CountVectors换成TransformersNLP,权重用bert-base-chinese。但这会带来两个新问题:模型文件大,CPU推理慢,训练依赖也多。我一般建议先把上面那套能跑通,再看准确率缺口值不值得上BERT。

2.3 Flask在这里的真实角色:API透传、状态隔离与前端对接

rasa的多轮对话状态是依赖sender_id来区分的,同一个sender_id的对话会被rasa保留上下文。这个设计到了Flask层就变成一个很自然的参数透传逻辑。我的做法是在Flask里写一个统一对话接口,接收前端传来的用户标识和消息,转发给rasa,再把回复整理成统一格式返回。

from flask import Flask, request, jsonify import requests app = Flask(__name__) RASA_URL = "http://localhost:5005/webhooks/rest/webhook" @app.route("/api/chat", methods=["POST"]) def chat(): data = request.get_json() sender = data.get("sender", "user-001") message = data.get("message", "") resp = requests.post( RASA_URL, json={"sender": sender, "message": message}, timeout=10 ) replies = [item.get("text", "") for item in resp.json()] return jsonify({"reply": "\n".join(replies), "sender": sender}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

从逻辑上讲,这个接口做的事情很单纯:取请求里的sender和message,原样转给rasa,把rasa返回的若干条文字回复拼接后返回。timeout=10是必须加的,rasa在第一次冷启动推理时可能比较慢,如果不加超时限制,前端会一直挂着等响应,这在生产环境是很糟糕的体验。sender参数要用业务系统里的真实用户ID,而不是写死,否则所有用户的对话上下文会串在一起,A用户问过的槽位会被B用户继承,这是任务型机器人最典型的会话串号问题。

Flask层还能做的增强包括:把请求日志写到文件、检查调用方身份、把回复里的文字再做一层敏感词过滤。项目原版可能只做了基础透传,但你在改造成业务系统时,这几个点是高概率要加的。

3. 从zip到能对话:环境、训练与启动的最小路径

3.1 环境准备:Python版本、虚拟环境和依赖安装

rasa对Python版本很敏感,rasa 3.x通常要求Python在3.7到3.10之间,直接用3.11或3.12安装会因为依赖冲突报出一片红色错误。我一般会先建一个干净的虚拟环境,再按项目资料里的依赖清单安装,而不是直接往系统Python里塞包。

conda create -n rasa-chat python=3.9 -y conda activate rasa-chat cd /path/to/unzip/project pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

用conda建环境的好处是Python版本切换干净,不会污染系统环境。如果资料里没有requirements.txt,也可以手动安装rasa和flask,但版本容易漂移,第一次启动时会发现哪个包缺失就补哪个,比较被动。国内网络环境下载依赖时建议用清华或阿里云的PyPI镜像,不然安装rasa这种大包体会很慢,甚至会下载到一半失败。

安装完成之后,先用两个命令验证环境是否正常:python --version看版本,rasa --version看rasa版本及其依赖的Python版本范围。看到版本输出一致,再进入下一步。

3.2 训练模型:rasa train与数据文件的关系

官方资料里通常会有data目录,里面有nlu.yml、stories.yml、rules.yml,以及domain.yml和config.yml。rasa的训练就是把这几类文件整合成模型。命令很简单,但理解训练对象是排查问题的基础。

rasa train

这一条命令等价于同时训练NLU和对话管理两部分。如果只想重新训练意图识别模型,可以只跑:

rasa train nlu

如果只改了对话策略或stories,则跑:

rasa train core

为什么单独拆出来有意义?因为任务型对话的开发节奏是:先攒一批中文语料做意图识别,意图识别准了再做多轮对话策略。两者互不影响时,每次全量train比较浪费时间。rasa train完成后会生成一个models目录,里面是一个tar.gz模型包,文件名带时间戳。后续启动rasa服务时,可以不指定模型,让它自动加载最新时间戳的那个。

训练日志里如果出现“No training data found”或者“Missing mandatory key: intents”,基本是yml格式缩进错了。rasa的yml对格式很敏感,用高版本PyYAML解析时,一个tab键缩进都会让训练直接终止。我习惯用vscode的YAML插件先校验这两个文件,再跑训练。

3.3 启动顺序:action server -> rasa serve -> flask run 一个都不能少

很多新人第一次部署这套项目时,只启动了rasa服务和Flask,然后发现对话里凡是要执行动作的回复都没有,后台日志还出现“Failed to execute custom action”。原因是项目里的动作脚本(通常是actions.py)是独立运行在action server里的,rasa serve本身不会加载这些Python动作,它通过endpoints.yml里配置的地址去找action server。所以在本地完整跑起来,至少需要三个终端。

# 终端一:启动自定义动作服务,默认端口 5055 rasa run actions --actions actions --port 5055
# 终端二:启动 rasa 主服务,开启 HTTP API 并允许前端跨域访问 rasa serve --enable-api --cors "http://localhost:5000" --port 5005
# 终端三:启动 Flask 应用 python app.py

三个命令的执行顺序有讲究。action server必须先起,因为rasa serve在运行过程中一旦要执行自定义动作,发现5005端口连不上,就会直接报错并跳过该动作,最后用户端只能看到一句话都没回复。rasa serve里的--cors参数用来解决浏览器跨域问题,如果不加,前端页面调用Flask接口再转发到rasa时会出CORS错误。这个参数的值要跟Flask实际运行地址一致,写http://localhost:5000还是写IP,得看你前端从哪个地址访问。

还有一种情况是项目里的endpoints.yml没有配置action_endpoint。那就需要手动确认文件里有:

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

没有这个配置,动作服务启动了也是白搭。

3.4 最小验证:一条curl打通全链路

全部启动完成后,不要急着写前端页面,先用curl验证数据通路。第一步直接打rasa接口,确认模型能响应。

curl -X POST http://localhost:5005/webhooks/rest/webhook \ -H "Content-Type: application/json" \ -d '{"sender": "test-user", "message": "我要查话费"}'

看到返回json里带text字段,说明rasa侧正常。接着验证Flask接口:

curl -X POST http://localhost:5000/api/chat \ -H "Content-Type: application/json" \ -d '{"sender": "test-user", "message": "我要查话费"}'

如果Flask返回了同样的回复,链路就通了。这里能排查出一个很隐蔽的问题:如果直接调rasa接口正常,但调Flask接口超时,大概率是Flask所在的防火墙或代理没放行5005端口。如果Flask返回了“Internal Server Error”,则去看Flask终端里的traceback,常见的是requests没安装,或者resp.json()解析失败。

这套验证做完,项目已经在本地活起来了。下面几个坑是后续把数据换成自己业务内容时最常遇到的。

4. 中文任务型对话机器人避坑指南:5个让新人翻车的现场

4.1 坑一:pip安装依赖时冲突刷屏

现象:执行pip install -r requirements.txt后,终端输出大量“ERROR: Cannot install rasa==x.x.x because these package versions have conflicting dependencies”,或者numpy、transformers版本反复回退。

原因:最常见的是Python版本太高,比如3.11、3.12在rasa 3.x的依赖矩阵里没有完全兼容。另一个原因是本地之前已经安装过一套深度学习相关的包,版本被锁定在新版本上,pip无法自动降级。还有一种情况是requirements.txt里把版本写死为“==”但某个依赖已停止维护,导致解析失败。

解决:先把Python切换到3.9,创建全新虚拟环境再装。如果仍然报冲突,用pip install rasa==你需要的版本放前面装,再装flask和其余依赖,因为rasa的依赖解析相对强势,先装它会让其他包尽量回避冲突。优先级排序是:rasa版本→Python版本→其他web框架版本,按这个顺序调整,百分之八十的依赖冲突能消掉。

4.2 坑二:中文意图识别“玄学不准”

现象:语料里写了“我要查话费”,测试时也一切正常,但换成“帮我看看这个月话费还剩多少”,机器人就把意图识别成了greet或者fallback,前端回复答非所问。

原因:这个现象通常在两个地方出问题。第一,pipeline里没有加JiebaTokenizer,英文tokenizer对中文按空格切分,等于每个完整句子被当成一个token,特征抽取完全失真。第二,nlu.yml里的同义句式太少,模型只见过“查话费”几个字,没见过“还剩多少”“余额”这种变体。还有一个隐蔽原因是训练epochs设太小,比如默认的50次对中文语料不够用。

解决:先把config.yml确认有中文tokenizer和char_wb特征,然后回到nlu.yml扩充表达方式,每种意图我给到至少30条真实中文表达,把口语省略、带地名单、带时间表达各写几条。改完语料后重新跑rasa train nlu,再用测试工具rasa shell interactive或rasa test nlu去跑验证集,看意图的置信度分布。如果置信度整体低于0.7,优先加语料,其次再考虑调epochs到100以上。

4.3 坑三:多轮对话从第三轮开始“失忆”

现象:用户第一句说“我要订明天去北京”,第二句说“后天回来”,前两轮还能正常回答,到了第三轮问“那帮我看看机票”,机器人突然说“抱歉,我还没学会这个功能”,对话被强行终结。

原因:这是stories和rules没覆盖全的典型症状。rasa的对话策略靠stories记录完整路径,但新人写stories时往往只写了一轮或两轮的样本,第三轮以上的组合路径模型没见过。这个时候RulePolicy优先级又高,如果rules.yml里没有对应规则,就会走到fallback。另一种情况是使用Form收集槽位时,form的激活状态没有被写进stories,导致对话在槽位未填满时被策略认为“任务已结束”。

解决:在stories.yml里把完整任务路径补齐,比如“query_flight意图+填日期+填目的地+触发查询动作+回复结果”整个链条写成一条story。同时检查rules.yml里是否有一条默认的fallback规则,至少要保证未知输入时有兜底回复而不是直接终结。Form场景下,要确认domain.yml里的forms定义里有required_slots,且utter_ask_前缀的回复存在,每缺一个槽位都会有对应的追问文案。

4.4 坑四:Flask调rasa时报CORS跨域错误,页面请求被浏览器拦截

现象:浏览器控制台提示“Cross-Origin Request Blocked”,POST请求在Network面板里显示已发出,但前端代码拿不到响应。直接用curl调Flask却完全正常。

原因:这是浏览器同源策略导致的跨域问题。前端页面地址是http://localhost:5000(Flask),Flask在转发给rasa时,rasa返回的响应里没有携带Access-Control-Allow-Origin头,浏览器就拒绝前端读取。很多人只改了Flask这边,却没管rasa那边。

解决:最直接的方式是在rasa serve启动时带上--cors参数,值是对应的Flask地址,写成--cors "http://localhost:5000"。如果Flask将来部署在别的域名下,地址也对应改。还有一层保险是在Flask侧使用flask_cors库,给所有路由加上允许跨域的响应头。两层都配置后,浏览器不再拦截。前端开发调试时也可以临时用浏览器的禁用安全策略模式,但生产环境不能依赖这个。

4.5 坑五:自定义动作执行失败,机器人无回复

现象:对话流走到需要查数据库或调接口的动作时,机器人一句话也不回。后台rasa日志出现“Failed to execute custom action”或“No WebHookChannel found for conversation”。

原因:动作服务没有启动,或者endpoints.yml里没有正确绑定action服务地址。还有一个常见情况是actions.py里类的name()方法返回的名字,和domain.yml里action列表中的custom action名字对不上,导致rasa调度不到这个动作。另外,如果action里执行了数据库查询但连接串错误,异常抛给rasa后也会被当作动作执行失败处理。

解决:先按第3章的启动顺序确认三个服务都在跑;再确认endpoints.yml的action_endpoint指向5055端口;然后对照domain.yml里的actions字段和actions.py里的name()返回值,逐字核对大小写和下划线。最后在action的run方法里用try-except把数据库异常捕获并返回一个兜底话术,至少让用户知道“查询失败了”而不是永远沉默。排查时不要只盯着rasa主日志,动作服务的命令行窗口里如果有traceback,那才是真实错误来源。

5. 从能用到好用:把任务型对话接到真实数据和验证方法

5.1 用custom action查询真实数据库,替换掉写死的示例回复

资料里的demo往往用utter_xxx直接回复写好的文案,这在演示环境没问题,但接业务时必须改成查库。常见做法是在actions.py里写一个Action类,在run方法里连接数据库并返回查询结果。以sqlite为例:

import sqlite3 from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher class ActionQueryOrder(Action): def name(self): return "action_query_order" def run(self, dispatcher, tracker, collector): order_id = tracker.get_slot("order_id") if not order_id: dispatcher.utter_message(text="请提供订单号") return [] conn = sqlite3.connect("orders.db") conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute("SELECT status FROM orders WHERE order_id = ?", (order_id,)) row = cur.fetchone() conn.close() if row: dispatcher.utter_message(text=f"您的订单{order_id}当前状态是:{row['status']}") else: dispatcher.utter_message(text="没查到该订单,请核对单号") return []

这段逻辑的关键点是tracker.get_slot从rasa的对话状态里取槽位,槽位值来自NLU识别的实体或Form追问后的用户回复。查无结果时也要返回明确提示,不要丢给异常。如果业务库是MySQL或Oracle,把sqlite3换成pymysql或python-oracledb,连接方式和参数略有不同,但整体的“取槽位→查库→回话”结构不变。这里的name()方法和domain.yml里配置的自定义动作名必须完全一致。

5.2 验证清单:别等上线才被用户发现机器人在梦游

任务型机器人上线前,我一般会跑一遍下面这张验证表,每项都实际发一次请求而不是只看模型日志。

用例类型输入示例预期结果检查点
意图混淆“我想查话费”“帮我看看余额”都识别为查余额意图NLU置信度与Top1
槽位抽取“订明天北京到上海的机票”日期、出发地、目的地槽位全填满查看Tracker槽位
多轮继承“订明天去北京”“后天回”第二轮正确更新返回日期多轮上下文不丢失
异常输入“啊啊啊???”触发fallback并提示重新表达兜底回复生效
会话隔离两个用户同时轮流发消息各自的槽位互不覆盖sender_id隔离

每跑完一项,建议同时记录rasa返回事件里的confident值和动作名称。置信度低于0.7的用例要回到nlu.yml和stories.yml里补样本,而不是靠调阈值硬洗白。

5.3 一个值得长期坚持的习惯:给对话留日志

我接手任务型机器人项目后的第一件事不是看代码,而是确认日志体系在不在。至少要记录每个sender_id、每轮用户输入、识别的意图和置信度、执行的action、机器人回复,缺一块,出问题就无从查起。用Flask的日志模块在转发层做统一记录就够了,rasa侧的事件日志可以交叉对照。

部署环境的坑往往是“本地能跑、服务器跑不起来”。真实服务器内存小于4G时,rasa的BERT模型加载会非常吃力,有条件就先用轻量pipeline,等机器升级再切重型模型。把这个项目从本地搬到服务器的过程,最重要的不是拷代码,而是把依赖版本一顿锁死,然后严格按部署文档跑一遍启动脚本。

这些年做对话机器人,我最大的体会是:模型再强也离不开一层能干活的壳。rasa负责聪明,Flask负责稳当,数据负责真实。每一次改完配置或语料,我都会先把完整验证表跑一遍再收工,这个习惯救过我很多次。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进