
简介基于Rasa框架的Python智能医疗机器人项目面向毕业设计、课程设计与项目开发场景覆盖医药问答、智能问药、疾病诊断、病症查询、症状查询等功能并扩展语音对话、闲聊和天气查询适合自然语言处理、知识图谱及对话系统方向的学习者参考实践。资源包共141个文件以Python源码和Rasa配置yml为主辅以zbak备份、pyc编译文件、txt说明文档、xml配置、wav语音样例及db数据库等压缩包整体约100.72MB目录组织清晰便于按模块查阅。已有42人学习下载适合需要快速搭建医疗对话原型或完善毕设功能的同学。内含开发文档、环境配置说明、技术架构说明和数据库文件源码经过严格测试可放心参考结合Rasa、知识图谱、Neo4j数据库、语音识别与合成、开放API等技术能够帮助理解多轮对话、意图识别和知识检索的完整实现思路便于二次开发与扩展。1. 基于Rasa的Python智能医疗机器人它能做和不能做的事用户输入“头疼、发烧、嗓子疼该吃什么药”一个合格的医疗对话机器人要做的不只是把关键词命中医典还要拆出症状、判断意图、按需追问最后给出带前提的答复。这就是Rasa框架在这一类项目里的价值用Python训练一个能理解医疗意图的NLU模型再用对话管理把医药问答、智能问药、疾病诊断、病症查询、症状查询接成完整流程最后挂上语音入口。它适合已经会Python语法、想把对话能力装进健康类产品的人。先说清边界基于规则和知识库的答复只是辅助不能替代临床诊断这条边界决定了你的数据库和话术怎么写。2. 搭首个可跑的医疗Rasa工程Python版本、虚拟环境与最小对话2.1 Python和Rasa版本为什么要锁一起看从一次编译失败说起这里的版本敏感度比普通Python库高一个等级。Rasa的依赖里带着TensorFlow和spaCy这两个库对Python的小版本都有ABI兼容约束。你在python安装教程里顺手装了最新版Python回头pip install rasa等半小时后报Failed to build wheel就是版本不匹配的典型症状。我见过有人在这一步耗了一个晚上最后发现只是解释器选错了。常见做法是锁Python 3.8或3.9。Rasa 3.x在这两个版本上跑得最省心网上大部分问题讨论和自定义组件也基于这个组合。Python 3.10以上不是说绝对不能跑但医疗项目里时间成本高不值得赌。我的习惯是直接放弃系统解释器用conda单独建一个环境和爬虫、数据分析的其他项目完全隔离。这样这台机器上其他项目的包版本怎么变都不会影响这个对话机器人。你想快速验证一个想法时也不用担心把基础环境搞坏。2.2 Python环境配置用conda锁一个不会翻车的解释器创建独立环境的命令如下按顺序执行即可conda create -n rasa-medical python3.8 -y conda activate rasa-medical pip install rasa3.0,4.0 jieba rasa --version第一行创建名为rasa-medical的Python 3.8环境-y表示跳过确认提示第二行激活环境之后安装的所有包都会进这个环境不污染系统Python第三行把Rasa和jieba装在一起jieba是中文分词的底层库Rasa的JiebaTokenizer依赖它没有它后面训练中文语料会直接报错第四行rasa --version用于确认安装结果同时会打印出当前Python版本。注意pip install rasa会自动拉一大批依赖如果网络较慢建议先配好pip镜像源再执行安装否则很容易在某个大依赖上下载超时。这里还有个小经验pip install rasa不带版本范围可能会把未来的大版本也拉进来。写死3.0,4.0是为了躲开不可预期的大版本变更Rasa 4.x和3.x在配置项上差异不小项目刚开始没必要赌这个。2.3 初始化医疗机器人的最小工程rasa init之后改什么环境就绪后开始建工程。先建目录再让Rasa自己生成骨架mkdir -p rasa-medical-bot cd rasa-medical-bot rasa init --no-promptrasa init --no-prompt会在当前目录生成一套可直接对话的示例项目。这一步先把骨架跑起来后面再逐块替换成医疗数据。生成后的核心目录结构如下rasa-medical-bot/ ├── config.yml # NLU管道和对话策略 ├── domain.yml # 意图、实体、槽位、回复、动作的总定义 ├── data/ │ ├── nlu.yml # 意图和实体训练样本 │ ├── stories.yml # 多轮对话样本 │ └── rules.yml # 固定规则 ├── actions/ │ ├── __init__.py │ └── actions.py # 自定义Action接外部知识库 ├── endpoints.yml # Action Server等连接配置 └── credentials.yml # 对话通道凭据这个文件清单是Rasa项目的基本结构也是后面每一章改动的落点。用自己熟悉的话理解domain.yml是“机器人的人格定义”nlu.yml是“听力”stories和rules是“反应习惯”actions.py是“行动力”。骨架生成后先跑一次训练和本地对话验证rasa train rasa shellrasa train会把nlu.yml、stories.yml、rules.yml编译成模型放进models/目录rasa shell则拉起本地命令行对话入口。在shell里输入“你好”如果能看到问候回复说明最小工程已经通了。注意rasa shell只是开发态测试不牵扯任何线上通道后续接语音也是通过REST接口和它无关。3. 让机器人看懂医疗意图NLU训练数据、实体标注与管道配置3.1 医疗意图体系设计六个意图怎么划边界把标题里的几项能力映射成意图是训练数据的第一步。我一般先建一张意图表明确每个意图回答什么、典型说法长什么样。意图名对应能力用户典型说法ask_medicine_info医药问答“布洛芬能空腹吃吗”find_medicine智能问药“感冒了吃什么药”diagnose疾病诊断“头疼发烧是啥病”ask_disease病症查询“胃溃疡一般有什么症状”ask_symptom症状查询“头晕是怎么回事”greet对话礼貌“你好”“谢谢”边界最容易划不清楚的是find_medicine和ask_symptom两者都带症状词。我的划分标准是看用户要的是“药品结果”还是“病因解释”。“感冒吃什么药”要的是药品列表走find_medicine“头晕怎么回事”要的是可能性推测走ask_symptom。两类样本里都要覆盖“头疼”“咳嗽”“发烧”这些高频症状词让模型学到动词后缀才是区分信号。每个意图的样本量不要低于30条尤其是口语里的变体要尽量收进来比如“脑壳疼”“肚子涨”“身上发冷”。样本越贴近真实问法线上识别率越有保证。3.2 用YAML标训练数据症状、药品、疾病三种实体的标注规则NLU数据写在data/nlu.yml里基本格式是意图名加一组示例。一个覆盖主要意图的起步样本长这样version: 3.1 nlu: - intent: ask_symptom examples: | - [头疼](symptoms)是什么病的前兆 - 最近一直[头晕](symptoms)怎么回事 - [拉肚子](symptoms)两天了正常吗 - intent: find_medicine examples: | - [感冒](disease)了吃什么药 - [发烧](symptoms)吃什么药 - 有没有治[咳嗽](symptoms)的药 - intent: ask_medicine_info examples: | - [布洛芬](medicine)能空腹吃吗 - [阿莫西林](medicine)一次吃几粒 - intent: ask_disease examples: | - [胃溃疡](disease)一般有什么表现 - [高血压](disease)平时要注意什么 - intent: diagnose examples: | - [头疼](symptoms)[发烧](symptoms)是啥病 - [咳嗽](symptoms)[流鼻涕](symptoms)怎么办方括号内是原词圆括号内是实体类型。注意“感冒”是disease不是symptom标注时不要随手标错。实体边界保持单一一句话里同时出现症状和疾病时比如“感冒会头痛吗”把“感冒”标disease、“头痛”标symptoms不要做嵌套。同义词统一用EntitySynonymMapper处理。比如“发热”和“发烧”是一回事就在nlu.yml里追加- synonym: 发烧 examples: | - 发热 - 发烧了这样线上用户说“发热”也能被映射到“发烧”这个标准词上后续查知识库时少一层匹配麻烦。3.3 config.yml管道参数中文分词器和DIETClassifier怎么配NLU管道决定文本怎么被切分、怎么变成特征、怎么输出意图和实体。医疗场景我一般用这样一组配置language: zh pipeline: - name: JiebaTokenizer dictionary_path: resources/jieba_dict/ - name: RegexFeaturizer - name: LexicalSyntacticFeaturizer - name: CountVectorsFeaturizer - name: CountVectorsFeaturizer analyzer: char_wb min_ngram: 1 max_ngram: 4 - name: DIETClassifier epochs: 100 constrain_similarities: true - name: EntitySynonymMapper policies: - name: RulePolicy - name: TEDPolicy max_history: 6 epochs: 100 - name: MemoizationPolicylanguage: zh让Rasa按中文任务处理JiebaTokenizer负责把句子切成词dictionary_path指向一个目录里面放医疗自定义词典用来纠正医学术语被切碎的问题两条CountVectorsFeaturizer一条按词、一条按字窗口char_wb的min_ngram: 1, max_ngram: 4表示把1到4个字作为一个特征组对中文近义词变体有更好的容错能力。DIETClassifier的epochs: 100是经验值。医疗场景样本量通常不大50轮欠拟合200轮以上容易过拟合。测试集准确率掉点或震荡时优先回调到60到80而不是盲目加训练数据。TEDPolicy的max_history: 6表示对话管理网络最多看6步历史对应多轮追问场景2到4轮足够不用再拉长。3.4 用rasa train和rasa test验证NLU效果训练和评估是一对固定动作rasa train rasa test --model models/rasa test会用测试集评估意图和实体识别效果重点看两个指标intent accuracy和实体F1。意图准确率高但实体F1低说明是标注边界问题去查分词器是不是把医学术语切碎了实体F1高但意图混淆说明样本区分度不够去补强信号词。还要看每个意图单独的precision和recall breakdown。比如find_medicine只有70%识别率翻翻测试样本会发现一大批打到了ask_symptom那就按3.2里说的把“吃什么药”“买什么药”这类后缀均匀补进find_medicine。评估不是看一个总分而是把每一次误判样本捞出来读一遍这个过程比调参有用得多。4. 把问药和症状查询串成对话Rule、Story与自定义Action4.1 用RulePolicy锁定医药问答医药问答这类“一问一答”场景不需要多轮上下文用rules.yml写死链路最稳。规则少、响应路径短运行时延迟最低行为也可预期version: 3.1 rules: - rule: 药品咨询规则 steps: - intent: ask_medicine_info - action: action_query_medicine_info - rule: 简单问候 steps: - intent: greet - action: utter_greetdomain.yml里对应给出回复模板responses: utter_greet: - text: 你好我是医疗问答助手可以帮你查症状、找药、做初步判断。RulePolicy适合不需要槽位的固定链路比如查药品用法、查疾病百科、打招呼。这类请求如果丢给TEDPolicy去学反而可能因为训练数据不足产生随机行为。我一般把能写成规则的全写成规则只有真正依赖上下文和系统追问的才交给故事和策略模型。4.2 用Story串起症状追问和诊断症状查询天然是多轮的用户可能先说“头疼”得到答复后再补一句“还有点发烧”这时机器人要把新症状合并进上下文而不是当作新对话。stories.yml里的样本长这样stories: - story: 症状查询后继续追问 steps: - intent: ask_symptom - action: action_query_symptom - intent: ask_symptom - action: action_query_symptom - story: 多症状诊断链路 steps: - intent: diagnose - action: action_diagnose - intent: inform - action: action_continue_diagnose - intent: thankyou - action: utter_no_problem第一条story模拟用户在第一次答复后补充第二个症状第二条模拟用户直接发起诊断、再补充信息、最后道谢的完整链路。注意这里用了inform意图承接“还有发烧”“有点咳嗽”这类简短补充这个意图需要在nlu.yml里建训练样本否则模型接不住。槽位定义要保持能叠加slots: symptoms: type: list influence_conversation: truesymptoms用list类型每次新症状追加进列表而不是被覆盖。influence_conversation: true表示槽位变化会影响下一个动作选择这是多轮症状收集的关键配置。4.3 自定义Action接医疗知识库规则和故事只负责决定“下一步做什么”真正查知识库、算候选疾病的逻辑要写在actions.py里。一个最小的症状查询Action如下import json from typing import Any, Dict, Text, List from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher class ActionQuerySymptom(Action): def name(self) - Text: # 返回值必须与domain.yml里的action名完全一致 return action_query_symptom def run( self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) - List[Dict[Text, Any]]: symptoms tracker.get_slot(symptoms) or [] if not symptoms: dispatcher.utter_message(text你先描述一个症状比如头疼、发烧、拉肚子。) return [] db SimpleMedicalDB(data/medical.json) result db.query_symptoms(symptoms) dispatcher.utter_message(textf根据症状{、.join(symptoms)}的常见情况\n{result}) return []name()返回字符串必须和domain.yml、story里出现的action名完全一致拼错一个字符对话就会停住run()返回一个事件列表这里只回复用户、不修改槽位所以返回空列表[]tracker.get_slot(symptoms)取的是当前槽位的值list类型可以直接迭代。知识库检索先用一个简单的遍历版本保证逻辑先跑通class SimpleMedicalDB: def __init__(self, path: str): with open(path, encodingutf-8) as f: self.data json.load(f) def query_symptoms(self, symptoms): results [] for disease in self.data[diseases]: hit len(set(symptoms) set(disease[symptoms])) if hit 0: results.append((hit, disease[name])) results.sort(reverseTrue) return 、.join(name for _, name in results[:3])medical.json的结构是{ diseases: [ {name: 普通感冒, symptoms: [头疼, 发烧, 咳嗽, 流鼻涕]}, {name: 胃肠炎, symptoms: [拉肚子, 呕吐, 腹痛]} ] }查询逻辑按症状命中数倒排取前三个作为候选。这个小类适合数据量小的起步阶段几千条以后就要换成倒排索引第5章会讲到。写完Action后要单独启动Action Server对话主进程才能调用它rasa run actions另一个终端再跑rasa shellendpoints.yml里还要确认这一行是通的action_endpoint: url: http://localhost:5055/webhook不少新手只改了actions.py忘了起Action Server一调用自定义动作就静默失败这个坑值得先记下来。5. 医疗Rasa落地避坑分词、意图混淆与Action异常的五条血泪经验5.1 Jieba把“胃溃疡”切成“胃/溃疡”实体边界全乱现象训练时在nlu.yml里标注了[胃溃疡](disease)线上测试时识别出来的实体却是“溃疡”疾病名永远对不上知识库。原因Jieba默认词典没有“胃溃疡”这个词分词阶段把它切成了“胃”和“溃疡”两个字片段DIETClassifier只能基于碎片学习实体边界。解决给JiebaTokenizer配置自定义医疗词典路径指向一个目录pipeline: - name: JiebaTokenizer dictionary_path: resources/jieba_dict/目录下的医疗词典按jieba约定格式写每行一个词胃溃疡 5 nz 类风湿关节炎 5 nz 阿莫西林 5 nz三列分别是词、词频、词性nz表示专有名词。改完词典必须重新rasa train否则不生效。我一般在项目启动初期就把高频疾病词和药品词全部收进这个词典越早收后面实体F1的返工越少。5.2 “头疼是什么病”和“头疼吃什么药”意图互相打架现象混淆矩阵里find_medicine和ask_symptom大量互相串单个意图识别准确率跌到70%以下。原因两类样本都大量出现“头疼”“咳嗽”“发烧”模型只看到表面字符共性没有学到“是什么病/怎么回事”和“吃什么药/买什么药”这两类后缀的区分价值。解决在3.1的基础上把强信号词打散分布到每个意图的样本里。每个意图至少扩到40条且每条都用不同的后缀组合- intent: ask_symptom examples: | - 我[头疼](symptoms)怎么回事 - 一直[咳嗽](symptoms)是什么原因 - [头晕](symptoms)是啥问题 - intent: find_medicine examples: | - [头疼](symptoms)吃什么药好 - [咳嗽](symptoms)喝什么药 - 有[头晕](symptoms)该买什么药补完样本后重新训练再看confusion matrix确认这两个意图是否分开了。如果还缠在一起就把最容易混淆的几条query单独抽出来对比找第三个意图去承接而不是硬往一边塞。5.3 Action一抛异常整个对话像死了一样现象用户触发action_query_symptom后机器人不说话也不报错日志里只有一行Error occurred while running action后续消息全部无响应。原因Action Server里的Python异常会打断当前tracker状态对话策略拿不到结果也不知道下一步该干什么表现为“假死”。解决在Action的run()里整体包一层try/except任何异常都返回兜底话术并确保返回空事件列表def run(self, dispatcher, tracker, domain): try: result db.query_symptoms(symptoms) except Exception as e: dispatcher.utter_message(textf系统暂时没查到这个信息请换个说法试试{e}) return [] dispatcher.utter_message(textresult) return []return []表示不触发额外动作把控制权交回对话策略。绝不能把异常抛给Rasa核心进程。这个兜底看起来简单但能让线上故障从“全哑”变成“单条回复不可用”体验差别很大。5.4 症状追问没有终止条件用户被问烦现象用户说“头疼”机器人回“还有没有其他症状”用户回“没有了”机器人继续问“头晕吗”“恶心吗”没完没了。原因diagnose链路把症状收集写成必须集齐3个槽位才能给结论list槽位永远可以追加用户没有终止手段。解决在Action里限制最少症状数达到2条就出候选方向同时给“没有了”“就这些”配置一个独立意图触发后立即执行诊断动作if not symptoms or len(symptoms) 2: dispatcher.utter_message(text根据现有症状的候选方向普通感冒或上呼吸道感染建议线下就医进一步检查。) return []医疗对话和普通任务式对话不一样不是所有槽位都必须填满。及时给用户一个下一步动作比强行收集完整病史更符合实际场景。用户流失往往就发生在第三轮追问。5.5 知识库一大查询延迟以秒计现象medical.json里的疾病记录超过两三千条后每次症状查询要三四秒才返回用户以为机器人挂了。原因SimpleMedicalDB每次Action调用都遍历整个疾病列表逐个取症状集合求交集复杂度是O(N×M)N是疾病数M是症状数。解决启动时构建倒排索引把查询复杂度降到O(症状命中数)from collections import Counter class IndexedMedicalDB: def __init__(self, path: str): with open(path, encodingutf-8) as f: self.data json.load(f) self.index {} for disease in self.data[diseases]: for symptom in disease[symptoms]: self.index.setdefault(symptom, []).append(disease[name]) def query_symptoms(self, symptoms): counter Counter() for symptom in symptoms: counter.update(self.index.get(symptom, [])) return [name for name, _ in counter.most_common(3)]索引在Action Server启动时构建一次查询变成字典取值加计数排序几千条数据下响应能回到毫秒级。如果换成索引后仍然慢就去查是不是分词把词切碎了导致命中率低回到5.1看自定义词典有没有覆盖到位。6. 语音对话接入与端到端验证从ASR到TTS的延迟优化6.1 用REST接口把语音和Rasa接起来Rasa本身不带语音识别和语音合成标题里的“语音对话”通常是外挂方案本地ASR识别用户语音转成文字POST给Rasa的REST webhook拿到文本回复后再走TTS播放。这也是团队落地时最容易被接受的方案因为不捆绑任何一家语音厂商。一个最小的语音闭环脚本如下import requests import pyttsx3 def rasa_chat(text: str) - str: resp requests.post( http://localhost:5005/webhooks/rest/webhook, json{sender: voice_user, message: text}, timeout3, ) return .join(item[text] for item in resp.json() if text in item) engine pyttsx3.init() reply rasa_chat(头疼发热吃什么药) engine.say(reply) engine.runAndWait()sender是会话标识同一用户的多轮对话必须传同一个值否则Rasa会把每次请求当成新会话timeout3避免ASR文字已经准备好但Rasa返回慢时语音流程发空pyttsx3是离线TTSsay和runAndWait组合把字符串播出来。6.2 本地ASR选型与端到端验证习惯ASR部分我优先选本地离线方案Vosk或Whisper都可以。延迟优化的要点是识别过程要“边说边出文本”就选流式接口不要等用户停顿三秒再一次性交给Rasa把ASR结果按句号切分每句话单独POST能明显缩短“说完到听到回复”的体感延迟。TTS侧可以对高频问药的回复做音频缓存省掉每次合成的时间。我的端到端验证习惯是每改一次NLU或Action就跑一遍“麦克风→文字→Rasa→回复→播报”的五步闭环重点看两处。第一ASR识别出的文字是否带了奇怪标点或口水词这会直接干扰Rasa意图识别第二Rasa回复里的疾病名、药品名能不能被TTS正确朗读比如“阿莫西林”这类词经常被读错。这两处恰恰是语音医疗机器人最容易出现“答非所问”的地方。先按这个顺序调再去看模型本身的识别率犯错会少很多。希望帮到你。本文还有配套的精品资源点击获取