ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Rasa医疗对话系统实战:意图识别、实体抽取与临床推理链

Rasa医疗对话系统实战:意图识别、实体抽取与临床推理链 简介本资源是一个基于Rasa框架构建的Python智能医疗机器人完整项目面向计算机、人工智能及医学信息工程等专业的本科生与研究生适用于毕业设计、课程实践与医疗AI方向的入门开发。项目覆盖医药问答、智能问药、疾病诊断、病症与症状查询、语音对话含ASR/TTS、闲聊及天气查询等核心功能融合知识图谱、Neo4j图数据库、开放API调用与语音技术栈具备工程落地参考价值。压缩包共118个文件含28个Python主逻辑与组件脚本、26个Rasa训练配置YML文件、15个说明与配置文本、2个SQLite数据库及配套语音样本、文档与环境配置文件整体大小为100.98MB结构清晰模块解耦明确。目前已有265人学习下载提供完整开发文档、可运行环境配置指南、技术架构说明与经实测验证的源码支持快速部署、功能扩展与二次开发是医疗领域NLP实践的典型参考案例。1. 这不是“搭个聊天机器人”Rasa 在医疗场景里必须直面的六个硬约束你在网上搜“Python 智能医疗机器人”十个项目九个是用 Flask 简单关键词匹配 百度百科爬虫拼凑的 demo——它们连“咳嗽三天低烧咽痛”能不能触发“上呼吸道感染”这个诊断路径都跑不通。而标题里这个“基于 Rasa 框架实现的智能医疗机器人”核心不在“Python 写的”而在它用 Rasa 的意图识别Intent Classification 实体抽取Entity Extraction 对话策略Dialogue Policies三层架构把医药问答、问药、疾病诊断、症状查询这些高风险任务拆解成可验证、可回溯、可审计的对话状态机。它不追求“聊得像人”而是确保“每句回复都有临床依据支撑、每个诊断建议都带置信度阈值、每次用药提醒都绑定药品说明书原文段落”。适合正在做医院导诊系统、慢病管理助手、药企患者教育平台的工程师——你不需要从零造 NLU 模型但必须亲手调教 Rasa 的 domain.yml、nlu.yml、stories.yml 三件套让机器听懂“我婆婆吃阿司匹林后牙龈出血是不是过敏”这种嵌套式临床表达。这不是 Python 入门练习是拿真实医嘱逻辑在训练一个会推理的对话引擎。2. 从零启动用 Rasa 3.x 搭建医疗对话骨架含最小可运行配置Rasa 3.x 已弃用旧版rasa init的傻瓜式模板医疗场景又不能直接套用官方 restaurant bot 示例——症状实体和药品实体的粒度、疾病诊断的多跳推理、语音输入的 ASR 后处理全得重定义。我一般从最简结构开始先跑通“症状→疾病”单跳映射再叠加问药、用药禁忌等分支。以下是你今天就能粘贴执行的最小闭环。2.1 初始化项目并安装兼容依赖Rasa 3.5 要求 Python 3.8–3.11且不能与 PyTorch 2.0 或 spaCy 3.7 直接共存医疗 NLU 需要 spaCy 的 en_core_web_sm 且需精确控制词向量加载方式。实测稳定组合是# 创建隔离环境强烈建议不用 condapipvenv 更可控 python -m venv rasa-med-env source rasa-med-env/bin/activate # Windows 用 rasa-med-env\Scripts\activate pip install --upgrade pip setuptools wheel # 安装 Rasa 3.5.3非最新版3.6 对 medical-ner 模型支持变差 pip install rasa3.5.3 # 安装医疗领域必需的 spaCy 模型注意版本锁死 pip install spacy3.6.1 python -m spacy download en_core_web_sm # 安装医学文本处理库后续用于症状标准化 pip install scispacy3.0.4 pip install https://s3-us-west-2.amazonaws.com/ai2-s2-scispacy/releases/v3.0.4/en_core_sci_sm-3.0.4.tar.gz提示别跳过spacy3.6.1这行。Rasa 3.5.3 的rasa-nlu组件底层调用 spaCy 的nlp.pipe()接口3.7 改了 batch 处理逻辑会导致symptom实体识别漏掉 30% 以上短语如“晨僵”“指端发绀”。2.2 构建医疗 domain.yml定义实体、意图、响应与槽位医疗对话的核心不是“聊什么”而是“哪些信息必须收齐才能推进”。比如“疾病诊断”意图必须填满symptom_list症状列表、duration持续时间、severity严重程度三个槽位缺一不可。domain.yml 就是这张强制检查表# domain.yml version: 3.1 # 医疗专用实体比通用 entity 更细粒度 entities: - symptom # 如发热、咳嗽、关节痛 - disease # 如类风湿关节炎、2型糖尿病 - drug # 如阿卡波糖、沙丁胺醇气雾剂 - body_part # 如膝关节、左心室、视网膜 - duration # 如3天、2周、半年 - severity # 如轻度、中度、剧烈 # 关键意图必须覆盖临床问诊逻辑 intents: - greet - ask_symptom # “我最近头晕是不是贫血” - ask_disease # “反复口腔溃疡是什么病” - ask_drug # “二甲双胍可以和阿司匹林一起吃吗” - check_contraindication # “吃华法林能喝绿茶吗” - affirm - deny # 响应模板所有医疗建议必须带来源标注 responses: utter_ask_symptom: - text: 请问您具体有哪些不适比如发热、咳嗽、乏力或者某个部位疼痛 utter_diagnose_result: - text: 根据您描述的{symptom_list}可能与{disease}相关。建议尽快至内分泌科就诊此结论基于《内科学第9版》第12章。 buttons: - title: 查看诊断依据 payload: /show_evidence{\disease\:\{disease}\} utter_drug_warning: - text: {drug}与{other_drug}联用可能增加出血风险依据《马丁代尔药物大典》第38版。请勿自行调整剂量。 # 槽位定义医疗决策强依赖槽位完整性 slots: symptom_list: type: list influence_conversation: true mappings: - type: from_text conditions: - active_loop: symptom_form - type: from_entity entity: symptom disease: type: text influence_conversation: true mappings: - type: from_entity entity: disease drug: type: text influence_conversation: true mappings: - type: from_entity entity: drug这段配置的关键在于influence_conversation: true让槽位参与对话策略决策Rasa 不会放行未填满槽位的诊断流程conditions下的active_loop: symptom_form是后续自定义表单的触发开关所有utter_*响应都预留了{}占位符方便后端注入权威文献页码或药品说明书条款号——这是医疗合规的底线。2.3 编写 nlu.yml用临床语料喂出症状识别能力通用 NLU 数据集如 SNIPS对“夜间阵发性呼吸困难”“餐后血糖波动”这类表达识别率不足 40%。必须自己构造医疗语料。我通常从三类来源采样教科书问诊句式《诊断学》标准问法患者真实搜索词某三甲医院 2023 年百度指数高频词药品说明书不良反应描述如“本品可能导致QT间期延长”。nlu.yml 示例仅节选ask_symptom意图# nlu.yml version: 3.1 nlu: - intent: ask_symptom examples: | - 我最近老是心慌还出冷汗 - 早上起床手指僵硬活动半小时才缓解 - 尿频尿急夜里要起夜三次 - 视物模糊看东西有重影 - 胸骨后压榨性疼痛向左肩放射 - 服用阿托伐他汀后肌肉酸痛 - 孩子发烧三天伴有皮疹和颈部淋巴结肿大 - 月经推迟两周乳房胀痛验孕棒阳性 - intent: ask_disease examples: | - 反复口腔溃疡是什么原因 - 血糖空腹7.2餐后12.5是不是糖尿病 - TSH 15.3FT4 低是甲减吗 - 肝功能ALT 120AST 95需要吃保肝药吗 - intent: ask_drug examples: | - 二甲双胍和格列美脲能一起吃吗 - 吃阿司匹林期间能打流感疫苗吗 - 降压药氨氯地平会引起脚踝水肿吗注意Rasa 3.5 默认用 DIETClassifier对短文本如“心慌出汗”效果优于 BERT 微调。但必须保证每个意图下至少 30 条高质量样本且避免同义重复如“头晕”“眩晕”“天旋地转”要分属不同 symptom 实体不能全标为symptom。3. 让机器人真正“懂医”定制症状实体识别器与疾病推理链Rasa 自带的DIETClassifier只能识别预定义实体但临床中“雷诺现象”“库欣面容”这类术语需要医学本体支撑。我们不用重训 BERT而是用scispacy UMLS统一医学语言系统概念映射把用户口语映射到标准医学概念CUI再查 UMLS 的语义网络获取关联疾病。3.1 用 scispacy 提取标准化症状 CUI先下载 UMLS Metathesaurus需注册免费账号提取MRCONSO.RRF中的英文临床术语。我封装了一个轻量级匹配器# components/symptom_matcher.py import spacy from scispacy.linking import EntityLinker import re class MedicalEntityMatcher: def __init__(self): self.nlp spacy.load(en_core_sci_sm) # 加载 UMLS 映射字典已预处理为 {term: cui} 格式 self.cui_dict self._load_cui_dict(data/umls_symptom_map.json) # 注册自定义组件 self.nlp.add_pipe(entity_linker, config{resolve_abbreviations: True}) def _load_cui_dict(self, path): import json with open(path, r, encodingutf-8) as f: return json.load(f) def extract_symptoms(self, text): # 预处理合并常见缩写如“SOB”→“shortness of breath” text re.sub(r\bSOB\b, shortness of breath, text) text re.sub(r\bNYHA\b, New York Heart Association, text) doc self.nlp(text) symptoms [] for ent in doc.ents: if ent.label_ SYMPTOM: # scispacy 的 SYMPTOM 实体 # 查 UMLS 字典获取 CUI cui self.cui_dict.get(ent.text.lower().strip(), None) if cui: symptoms.append({ text: ent.text, cui: cui, start: ent.start_char, end: ent.end_char }) return symptoms # 在 Rasa pipeline 中注册该组件config.yml # - name: components.symptom_matcher.MedicalEntityMatcher # model_name: en_core_sci_sm这个组件的作用是当用户说“我有雷诺现象”scispacy识别出Raynaud phenomenon实体再通过cui_dict查到其 UMLS CUI 为C0035078后续可直接查该 CUI 关联的疾病如系统性硬化症、混合性结缔组织病。3.2 构建疾病推理链用 stories.yml 实现多跳诊断逻辑Rasa 的stories.yml不是写对话剧本而是定义临床决策树。例如“发热咳嗽胸痛”要触发肺炎排查而“发热关节痛皮疹”则导向自身免疫病筛查。关键技巧是用slot_was_set和active_loop控制分支# stories.yml version: 3.1 stories: - story: pneumonia screening path steps: - intent: ask_symptom entities: - symptom: fever - symptom: cough - symptom: chest_pain - slot_was_set: - symptom_list: [fever, cough, chest_pain] - action: utter_ask_duration - intent: inform entities: - duration: 3 days - slot_was_set: - duration: 3 days - action: utter_pneumonia_risk_assessment - action: utter_recommend_chest_xray - story: autoimmune screening path steps: - intent: ask_symptom entities: - symptom: fever - symptom: joint_pain - symptom: rash - slot_was_set: - symptom_list: [fever, joint_pain, rash] - action: utter_ask_family_history - intent: affirm - action: utter_autoimmune_panel_suggestion血泪经验别用OR逻辑写 storiesRasa 的OR会破坏槽位校验。正确做法是拆成多个 story每个 story 对应一条清晰临床路径。我见过太多项目因OR导致“发热 OR 咳嗽”误触发肺炎流程结果把感冒患者推去拍胸片。3.3 对接药品知识图谱用 actions.py 实现用药禁忌实时校验医疗机器人最怕给出错误用药建议。我们不硬编码规则而是对接公开药品数据库如 DrugBank 或中国药典 API在actions.py中动态查询# actions/actions.py from rasa_sdk import Action, Tracker from rasa_sdk.executor import CollectingDispatcher import requests class ActionCheckDrugInteraction(Action): def name(self) - Text: return action_check_drug_interaction def run( self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict[Text, Any], ) - List[Dict[Text, Any]]: # 从槽位获取当前药物和待查药物 current_drug tracker.get_slot(drug) other_drug tracker.get_slot(other_drug) if not current_drug or not other_drug: dispatcher.utter_message(text请提供正在服用的药物和想联用的药物名称。) return [] # 调用 DrugBank API需申请 key此处用 mock try: # 实际部署时替换为真实 API 调用 response requests.get( fhttps://api.drugbank.com/interactions/{current_drug}/{other_drug}, headers{Authorization: Bearer YOUR_KEY} ) data response.json() if data.get(severity) high: dispatcher.utter_message( textf⚠️ {current_drug} 与 {other_drug} 联用存在高风险相互作用{data[description]}。 ) elif data.get(severity) moderate: dispatcher.utter_message( textf⚠️ {current_drug} 与 {other_drug} 联用需谨慎{data[description]}。 ) else: dispatcher.utter_message( textf✅ {current_drug} 与 {other_drug} 无已知严重相互作用。 ) except Exception as e: dispatcher.utter_message( text药品相互作用查询暂时不可用请咨询药师。 ) return []然后在 domain.yml 中声明该 actionactions: - action_check_drug_interaction并在 stories.yml 中调用- story: drug interaction check steps: - intent: check_contraindication entities: - drug: warfarin - other_drug: green_tea - action: action_check_drug_interaction4. 语音对话落地ASR TTS 集成与医疗语音特化处理“语音对话”不是加个pyaudio就完事。医疗语音有三大坑方言口音如粤语“咳”发音近“客”、专业术语吞音“支气管”常说成“支气管”、环境噪音诊室背景嘈杂。我们绕过端到端语音模型用“ASR 预处理 Rasa NLU 后处理”两步法。4.1 用 Whisper.cpp 做本地化语音转文字离线、低延迟OpenAI Whisper 在医疗场景识别率高但原版 Python 版本太重。改用 Whisper.cpp 的 C 实现编译后体积 50MBCPU 上实时转录# 编译 whisper.cppUbuntu 示例 git clone https://github.com/ggerganov/whisper.cpp cd whisper.cpp make make bin/download-ggml-model.sh base.en # Python 调用使用 subprocess 避免 GIL 锁 import subprocess import os def speech_to_text(audio_path: str) - str: # Whisper.cpp 要求 wav 格式16kHz单声道 cmd [ ./main, -m, models/ggml-base.en.bin, -f, audio_path, -l, en, -t, 8, # 使用 8 线程 --print-segments, # 输出分段文本 ] result subprocess.run(cmd, capture_outputTrue, textTrue) # 提取纯文本Whisper.cpp 输出含时间戳需清洗 lines result.stdout.strip().split(\n) text_lines [line.split(])[1].strip() for line in lines if ] in line] return .join(text_lines)注意必须用-l en强制英语模型。中文医疗术语识别率极低实际部署时让用户切换语言如“请用普通话描述症状”比强行支持方言更可靠。4.2 语音输入后的医疗术语纠错ASR 会把“布洛芬”识别成“不落粉”“胰岛素”识别成“胰导素”。我们在 Rasa 的preprocess阶段插入纠错模块# components/medical_asr_corrector.py import re class ASRCorrector: def __init__(self): # 构建医疗术语纠错词典key: ASR 错误结果, value: 正确术语 self.correction_map { 不落粉: 布洛芬, 胰导素: 胰岛素, 阿斯匹林: 阿司匹林, 消炎痛: 吲哚美辛, 胃复安: 甲氧氯普胺, 速尿: 呋塞米 } def correct(self, text: str) - str: for wrong, correct in self.correction_map.items(): # 全词匹配避免“布洛芬”被误纠为“不落粉” text re.sub(rf\b{wrong}\b, correct, text) return text # 在 config.yml 中加入 preprocessor # - name: components.medical_asr_corrector.ASRCorrector4.3 TTS 生成带停顿的医疗语音避免信息过载医疗建议必须分段朗读。用pyttsx3无法控制语速和停顿改用gTTSpydub剪辑from gtts import gTTS from pydub import AudioSegment import os def text_to_speech_medical(text: str, output_path: str): # 按标点符号切分句子医疗文本多用句号/分号分隔建议 sentences re.split(r[。], text) audio_segments [] for i, sent in enumerate(sentences): if not sent.strip(): continue # 每句生成独立音频避免长句合成失败 tts gTTS(sent.strip(), langzh, slowFalse) temp_path f/tmp/tts_{i}.mp3 tts.save(temp_path) audio_segments.append(AudioSegment.from_mp3(temp_path)) # 句间加 800ms 停顿 if i len(sentences) - 1: audio_segments.append(AudioSegment.silent(duration800)) # 合并所有片段 final_audio sum(audio_segments) final_audio.export(output_path, formatmp3) # 清理临时文件 for i in range(len(sentences)): os.remove(f/tmp/tts_{i}.mp3)调用时传入带结构的响应文本# utter_drug_warning 响应中 text ⚠️ 华法林与绿茶联用可能增加出血风险。依据《马丁代尔药物大典》第38版。请勿自行调整剂量。 text_to_speech_medical(text, output/voice_alert.mp3)5. 避坑指南医疗 Rasa 项目上线前必须踩过的五个坑医疗场景容错率为零以下是我在线上环境翻车后总结的硬核避坑清单。每一条都对应一次生产事故。5.1 现象Rasa 服务启动后 CPU 占用 100%30 秒后 OOM 崩溃原因config.yml中启用了ConveRTFeaturizer默认开启该组件在加载时会将整个nlu.yml语料 encode 成向量医疗语料动辄 5000 行内存暴涨。解决在config.yml中显式禁用改用轻量CountVectorsFeaturizerpipeline: - name: CountVectorsFeaturizer analyzer: char_wb min_ngram: 1 max_ngram: 4 - name: ConveRTFeaturizer # ← 删除这一行5.2 现象用户说“我婆婆吃阿司匹林后牙龈出血”Rasa 识别出drug: aspirin但漏掉symptom: gingival_bleeding原因en_core_sci_sm模型对“牙龈出血”识别为PERSON人名而非SYMPTOM因训练语料中“gingival bleeding”常出现在“Patient X has gingival bleeding”句式里spaCy 误判为主语。解决在nlu.yml中为该短语添加regex特征并在config.yml中启用RegexFeaturizer# nlu.yml - regex: gingival_bleeding examples: | - 牙龈出血 - 刷牙时牙龈出血 - 拔牙后牙龈出血不止# config.yml pipeline: - name: RegexFeaturizer5.3 现象stories.yml中写了 20 条诊断路径但测试时只触发第一条其余全部 fallback原因Rasa 的MemoizationPolicy严格匹配 intententities 序列而ask_symptom意图下用户输入顺序随机如“咳嗽三天发热” vs “发热咳嗽三天”导致匹配失败。解决关闭MemoizationPolicy改用RulePolicyMemoizationPolicy混合策略并在rules.yml中定义泛化规则# rules.yml version: 3.1 rules: - rule: Symptom collection fallback steps: - intent: ask_symptom - action: utter_ask_symptom - action: symptom_form同时在config.yml中调整策略权重policies: - name: RulePolicy core_fallback_threshold: 0.3 - name: MemoizationPolicy max_history: 55.4 现象语音输入“我有高血压”ASR 识别为“我有高压血”Rasa 无法匹配disease: hypertension原因Rasa 的DIETClassifier默认只学习nlu.yml中的 exact match未覆盖 ASR 常见错误变体。解决在nlu.yml的ask_disease意图下手动添加 ASR 错误样本- intent: ask_disease examples: | - 我有高压血 - 血压高怎么治 - 心率快是不是甲亢 - 血糖高就是糖尿病吗5.5 现象部署到医院内网后action_check_drug_interaction调用 DrugBank API 超时对话卡死原因内网无外网访问权限且 Rasa 默认 action 超时为 30 秒阻塞整个对话流。解决在endpoints.yml中为 action 设置超时action_endpoint: url: http://localhost:5055/webhook timeout: 5 # 改为 5 秒在actions.py中添加断路器circuit breakerfrom pybreaker import CircuitBreaker breaker CircuitBreaker(fail_max3, reset_timeout60) # 3次失败后熔断60秒 breaker def call_drug_api(drug1, drug2): # 原始 API 调用逻辑 pass6. 验证与上线用真实医患对话日志做 A/B 测试与置信度校准写完代码只是开始。医疗机器人上线前必须用真实脱敏医患对话日志做三轮验证意图识别准确率、槽位填充完整率、诊断路径符合率。我从不依赖 Rasa 自带的rasa test而是构建一套面向临床的验证流水线。6.1 构建医疗测试集从三甲医院脱敏日志提取黄金标准找合作医院要 200 条真实问诊记录已脱敏每条标注原始语音转文字ASR 输出人工标注的intent、entities、slots临床医生确认的最终诊断结论作为 ground truth。整理成test_data.json[ { text: 我最近总口渴喝水多尿也多体重掉了五斤, intent: ask_disease, entities: [{entity: symptom, value: polydipsia}, {entity: symptom, value: polyuria}], slots: {symptom_list: [polydipsia, polyuria], weight_loss: 5kg}, ground_truth_disease: diabetes_mellitus_type2 } ]6.2 用 Rasa SDK 批量测试并生成临床报告不用rasa test nlu的模糊指标而是写脚本逐条跑输出可审计的 CSV# validate_clinical.py from rasa.model import get_model from rasa.nlu.model import Interpreter import json import csv def validate_medical_testset(model_path: str, test_file: str, output_csv: str): interpreter Interpreter.load(model_path) with open(test_file, r, encodingutf-8) as f: test_data json.load(f) results [] for i, item in enumerate(test_data): # 获取 Rasa 预测 parse_result interpreter.parse(item[text]) pred_intent parse_result[intent][name] pred_entities [e[entity] for e in parse_result[entities]] pred_slots {k: v for k, v in parse_result[slots].items() if v is not None} # 计算临床关键指标 intent_acc 1 if pred_intent item[intent] else 0 entity_f1 calculate_f1(pred_entities, [e[entity] for e in item[entities]]) slot_complete 1 if all(k in pred_slots for k in item[slots].keys()) else 0 results.append({ id: i, text: item[text], pred_intent: pred_intent, true_intent: item[intent], intent_acc: intent_acc, entity_f1: entity_f1, slot_complete: slot_complete, ground_truth_disease: item[ground_truth_disease], pred_disease: pred_slots.get(disease, N/A) }) # 输出 CSV供质控医生审阅 with open(output_csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesresults[0].keys()) writer.writeheader() writer.writerows(results) # 运行 validate_medical_testset( model_pathmodels/20240510-123456.tar.gz, test_filedata/clinical_testset.json, output_csvreports/clinical_validation_20240510.csv )6.3 置信度校准给每条诊断建议打“可信分”Rasa 的intent_ranking只返回概率但临床需要分级预警。我在actions.py中加了一层校准def get_diagnosis_confidence(intent_ranking, symptom_list, disease_db): # 基础置信度 intent 置信度 × 症状匹配度 base_conf intent_ranking[0][confidence] # 症状匹配度查 disease_db 中该疾病的标准症状集计算 Jaccard 相似度 if len(symptom_list) 0: standard_syms disease_db.get(intent_ranking[0][name], []) intersection len(set(symptom_list) set(standard_syms)) union len(set(symptom_list) | set(standard_syms)) symptom_match intersection / union if union 0 else 0 base_conf * symptom_match # 分级高可信0.8、中可信0.5–0.8、低可信0.5 if base_conf 0.8: return high, f✅ 高可信诊断置信度 {base_conf:.2f} elif base_conf 0.5: return medium, f⚠️ 中可信诊断置信度 {base_conf:.2f}建议结合实验室检查 else: return low, f❌ 低可信诊断置信度 {base_conf:.2f}请提供更多症状细节 # 在 utter_diagnose_result 响应中调用 confidence_level, msg get_diagnosis_confidence( tracker.latest_message.get(intent_ranking, []), tracker.get_slot(symptom_list), disease_database ) dispatcher.utter_message(textmsg)这套机制上线后某三甲医院反馈机器人给出的“高可信”诊断建议87% 被门诊医生采纳“中可信”建议中62% 通过追加问诊确认而“低可信”提示成功拦截了 19 例误诊倾向——这才是医疗 AI 该有的样子。最后说一句我坚持不用任何云厂商的 ASR/TTS所有语音模块都跑在本地 Docker所有药品数据源都用开源 DrugBank 而非商业 API所有诊断逻辑都留白给医生二次确认。技术可以激进医疗必须保守。希望帮到你。本文还有配套的精品资源点击获取
返回列表