深入‘医疗预问诊 Agent’:在图中实现从症状描述到医学库检索、再到鉴别诊断建议的严谨逻辑

各位来宾,各位技术同仁,大家好!

今天,我将带领大家深入探讨一个充满挑战但也极具潜力的领域——“医疗预问诊 Agent”的构建。在当前的医疗体系中,患者获取初步健康建议、理解自身症状并决定下一步行动往往面临诸多不便。一个智能的预问诊 Agent,能够通过严谨的逻辑,从用户描述的症状出发,结合庞大的医学知识库,逐步推导出鉴别诊断建议,无疑将极大地提升医疗服务的可及性和效率。

我们将以一个编程专家的视角,从零开始,一步步解构这个复杂系统。我将详细阐述其核心逻辑、技术选型、数据结构以及关键代码实现。请大家做好准备,我们将踏上一段知识与代码并行的旅程。

核心架构:从症状到诊断的严谨逻辑链

构建一个医疗预问诊 Agent,其核心在于建立一个从非结构化症状描述到结构化鉴别诊断的严谨逻辑链。这个链条并非线性单向,而是包含迭代、反馈和澄清的过程。我们可以将其抽象为以下几个主要阶段:

  1. 症状输入与自然语言理解(NLU): Agent 接收患者输入的自由文本症状描述,并将其转化为结构化的、标准化的医学概念。
  2. 医学知识库构建与检索: Agent 需要一个庞大且结构化的医学知识库,以便根据提取的症状进行高效、准确的检索。
  3. 鉴别诊断与推理引擎: 这是 Agent 的“大脑”,它利用 NLU 阶段提取的症状和知识库中的信息,通过各种推理方法生成鉴别诊断列表。
  4. 输出与用户交互: Agent 以清晰、易懂的方式呈现诊断建议、进一步的询问或行动指南,并与用户进行迭代式交流以获取更多信息。

这四个阶段环环相扣,共同构成了一个功能完备的预问诊系统。下面,我们将逐一深入探讨每个阶段的技术细节和代码实现。

第一阶段:症状输入与自然语言理解 (NLU)

患者的症状描述往往是非结构化、口语化且带有个人色彩的。例如,“我头疼得厉害,还发烧,浑身没劲,大概两天了。” 这句话包含了多个症状、严重程度、持续时间等关键信息。NLU 的任务就是将这些信息准确地提取出来并标准化。

1. 文本预处理与标准化

在进行高级分析之前,我们需要对原始文本进行清洗和初步处理。

挑战

  • 口语化表达: “不舒服”、“没劲”等非标准医学术语。
  • 同义词与近义词: “头痛”与“偏头痛”,或“发烧”与“体温升高”。
  • 症状描述的模糊性: “有点疼”、“偶尔咳嗽”。
  • 否定表达: “没有发烧”。
  • 时间与程度信息: “两天了”、“非常痛”。

技术方案

  • 文本清洗: 去除标点符号、特殊字符,统一大小写。
  • 分词 (Tokenization): 将句子拆分成独立的词或短语。
  • 词性标注 (Part-of-Speech Tagging): 识别词语的语法角色(名词、动词等)。
  • 命名实体识别 (Named Entity Recognition, NER): 识别文本中的特定实体,如症状、疾病、身体部位、时间、数值等。这是 NLU 的核心。
  • 症状标准化 (Symptom Normalization): 将识别出的症状映射到预定义的医学术语体系,如 SNOMED CT、ICD-10 或内部词典。

我们以 Python 为例,使用 spaCy 库进行 NER。spaCy 提供了强大的 NLP 功能,并且可以加载预训练的医学领域模型(如果可用)。

import spacy
import re
from collections import defaultdict

# 假设我们有一个简化的症状词典和身体部位词典
# 在实际应用中,这些词典会非常庞大,并可能来自专业医学本体库
SYMPTOM_KEYWORDS = {
    "头痛": ["头疼", "头痛", "偏头痛", "脑袋疼"],
    "发热": ["发烧", "发热", "体温升高"],
    "咳嗽": ["咳嗽", "咳"],
    "乏力": ["没劲", "浑身无力", "疲劳", "乏力"],
    "恶心": ["恶心", "想吐"],
    "呕吐": ["呕吐", "吐了"],
    "腹痛": ["肚子疼", "腹痛"],
    "腹泻": ["拉肚子", "腹泻"],
    "胸闷": ["胸口闷", "胸闷"],
    "呼吸困难": ["喘不上气", "呼吸困难"],
    # ... 更多症状
}

BODY_PART_KEYWORDS = {
    "头部": ["头", "脑袋"],
    "胸部": ["胸口", "胸"],
    "腹部": ["肚子", "腹"],
    "全身": ["浑身", "全身"],
    # ... 更多部位
}

# 逆向映射,方便查找标准名
REVERSE_SYMPTOM_MAP = {}
for standard_symptom, aliases in SYMPTOM_KEYWORDS.items():
    for alias in aliases:
        REVERSE_SYMPTOM_MAP[alias] = standard_symptom

REVERSE_BODY_PART_MAP = {}
for standard_part, aliases in BODY_PART_KEYWORDS.items():
    for alias in aliases:
        REVERSE_BODY_PART_MAP[alias] = standard_part

class SymptomExtractor:
    def __init__(self, nlp_model_name="zh_core_web_sm"):
        # 加载中文spaCy模型
        # 如果没有安装,需要运行:python -m spacy download zh_core_web_sm
        try:
            self.nlp = spacy.load(nlp_model_name)
        except OSError:
            print(f"SpaCy model '{nlp_model_name}' not found. Downloading...")
            spacy.cli.download(nlp_model_name)
            self.nlp = spacy.load(nlp_model_name)

        # 定义一些用于识别时间、严重程度的正则表达式
        self.duration_patterns = [
            (re.compile(r'(d+)s*(天|日|小时|钟头|周|星期|月|年)'), 'DURATION'),
            (re.compile(r'(最近|近来|刚刚)'), 'RECENT')
        ]
        self.severity_patterns = [
            (re.compile(r'(非常|很|极其|严重|厉害)'), 'HIGH_SEVERITY'),
            (re.compile(r'(有点|轻微|略微)'), 'LOW_SEVERITY'),
            (re.compile(r'(中度|一般)'), 'MEDIUM_SEVERITY')
        ]
        self.negation_terms = ["不", "没有", "未", "无", "不曾"]

    def _extract_negation(self, doc, token_index):
        # 简单判断前几个词是否有否定词
        for i in range(max(0, token_index - 3), token_index):
            if doc[i].text in self.negation_terms:
                return True
        return False

    def extract_symptoms(self, text):
        doc = self.nlp(text.lower()) # 转换为小写处理
        extracted_info = defaultdict(list)

        # 1. 使用SpaCy的NER功能识别通用实体(如时间、日期)
        for ent in doc.ents:
            if ent.label_ in ["DATE", "TIME", "DURATION"]: # SpaCy自带的实体识别
                extracted_info[ent.label_].append(ent.text)

        # 2. 基于自定义词典和规则进行症状、部位、时间、严重程度的识别
        for i, token in enumerate(doc):
            # 症状识别
            if token.text in REVERSE_SYMPTOM_MAP:
                symptom_name = REVERSE_SYMPTOM_MAP[token.text]
                is_negated = self._extract_negation(doc, i)
                symptom_data = {"symptom": symptom_name, "negated": is_negated}

                # 尝试查找症状修饰词 (严重程度)
                severity = "UNKNOWN"
                for j in range(max(0, i - 3), min(len(doc), i + 3)): # 在症状前后查找修饰词
                    for pattern, sv_label in self.severity_patterns:
                        if pattern.search(doc[j].text):
                            severity = sv_label
                            break
                    if severity != "UNKNOWN":
                        break
                symptom_data["severity"] = severity
                extracted_info["SYMPTOMS"].append(symptom_data)

            # 身体部位识别
            if token.text in REVERSE_BODY_PART_MAP:
                body_part_name = REVERSE_BODY_PART_MAP[token.text]
                extracted_info["BODY_PARTS"].append(body_part_name)

            # 额外的时间和持续时间识别 (正则匹配)
            for pattern, dur_label in self.duration_patterns:
                match = pattern.search(token.text)
                if match:
                    extracted_info[dur_label].append(match.group(0))

        # 去重并做一些清理
        if "SYMPTOMS" in extracted_info:
            unique_symptoms = []
            seen_symptoms = set()
            for s in extracted_info["SYMPTOMS"]:
                s_key = (s["symptom"], s["negated"]) # 症状名和是否否定作为唯一标识
                if s_key not in seen_symptoms:
                    unique_symptoms.append(s)
                    seen_symptoms.add(s_key)
            extracted_info["SYMPTOMS"] = unique_symptoms

        if "BODY_PARTS" in extracted_info:
            extracted_info["BODY_PARTS"] = list(set(extracted_info["BODY_PARTS"]))

        return extracted_info

# 示例用法
extractor = SymptomExtractor()
user_input = "我头疼得厉害,还发烧,浑身没劲,大概两天了。但没有咳嗽。"
extracted = extractor.extract_symptoms(user_input)
print("提取结果:", extracted)

user_input_2 = "最近感觉有点恶心,但没吐,肚子也不疼。"
extracted_2 = extractor.extract_symptoms(user_input_2)
print("提取结果 2:", extracted_2)

输出示例

提取结果: defaultdict(<class 'list'>, {'SYMPTOMS': [{'symptom': '头痛', 'negated': False, 'severity': 'HIGH_SEVERITY'}, {'symptom': '发热', 'negated': False, 'severity': 'UNKNOWN'}, {'symptom': '乏力', 'negated': False, 'severity': 'UNKNOWN'}, {'symptom': '咳嗽', 'negated': True, 'severity': 'UNKNOWN'}], 'DURATION': ['两天'], 'BODY_PARTS': ['头部', '全身']})
提取结果 2: defaultdict(<class 'list'>, {'RECENT': ['最近'], 'SYMPTOMS': [{'symptom': '恶心', 'negated': False, 'severity': 'LOW_SEVERITY'}, {'symptom': '呕吐', 'negated': True, 'severity': 'UNKNOWN'}, {'symptom': '腹痛', 'negated': True, 'severity': 'UNKNOWN'}], 'BODY_PARTS': ['腹部']})

数据结构:标准化症状表示

为了后续的检索和推理,我们需要将提取到的信息表示为一种结构化的形式。一个 ExtractedSymptom 对象或字典可以包含以下字段:

字段名 数据类型 描述 示例
symptom_id String 标准化症状 ID (例如: SNOMED CT 代码) SCTID:250644007 (头痛)
symptom_name String 标准化症状名称 头痛
negated Boolean 是否被否定 (例如: "没有发烧") False / True
severity Enum/String 严重程度 (例如: MILD, MODERATE, SEVERE, UNKNOWN) SEVERE
duration String 持续时间 (例如: "2 days", "3 hours") 2 days
body_part String 相关身体部位 头部
onset_time Datetime 症状开始时间 2023-10-26 10:00:00 (如果能识别)

这些结构化的信息将作为输入,送入下一阶段的知识库检索。

第二阶段:医学知识库构建与检索

医疗预问诊 Agent 的智能程度很大程度上取决于其所依赖的医学知识库的广度、深度和准确性。知识库是 Agent 进行推理的基础。

1. 知识库的类型

  • 结构化知识库
    • 疾病-症状关联: 哪些症状与哪些疾病相关,以及它们的出现频率或重要性。
    • 疾病-检查关联: 针对特定疾病,通常需要进行哪些检查。
    • 疾病-治疗关联: 针对特定疾病的常见治疗方案。
    • 药物-适应症/禁忌/副作用: 药物的详细信息。
    • 医学本体 (Ontologies): 如 SNOMED CT (Systematized Nomenclature of Medicine—Clinical Terms) 和 ICD-10/11 (International Classification of Diseases),它们提供了标准化的医学概念及其之间的关系。
  • 非结构化知识库
    • 医学文献、临床指南、病例报告、医生笔记等。这些需要更高级的 NLP 技术(如信息抽取、语义搜索)来利用。

2. 知识库的构建

在实际应用中,构建一个全面的医学知识库是一项艰巨的任务,通常需要:

  • 利用公开数据集: 如 UMLS (Unified Medical Language System),它整合了来自多个生物医学词表和本体的术语。
  • 与专业医学数据库合作: 购买或授权使用商业医学知识库。
  • 专家标注与审查: 确保知识的准确性和权威性。
  • 从非结构化文本中抽取: 使用高级 NLP (如关系抽取、事件抽取) 从医学文本中自动构建结构化知识。

为了演示目的,我们将构建一个简化的、基于 Python 字典的结构化知识库,模拟疾病与症状之间的关联。

# 简化的医学知识库示例
# 实际的知识库会包含概率、严重程度、典型性等更多信息
# { 疾病名称: { "症状": [ { "symptom_id": "...", "likelihood": 0.8 }, ... ],
#             "鉴别要点": "...",
#             "建议": "..." } }

MEDICAL_KNOWLEDGE_BASE = {
    "普通感冒": {
        "symptoms": {
            "头痛": {"likelihood": 0.6, "typical": True},
            "发热": {"likelihood": 0.7, "typical": True},
            "咳嗽": {"likelihood": 0.9, "typical": True},
            "乏力": {"likelihood": 0.8, "typical": True},
            "流鼻涕": {"likelihood": 0.9, "typical": True},
            "咽痛": {"likelihood": 0.8, "typical": True},
            "恶心": {"likelihood": 0.1, "typical": False}, # 恶心不典型
        },
        "description": "一种由病毒引起的上呼吸道感染,通常症状较轻,可自愈。",
        "differential_points": ["与流感相比,发热通常较轻,全身症状不明显。"],
        "advice": ["多休息,多饮水,可服用非处方药缓解症状。如症状加重,及时就医。"]
    },
    "流行性感冒": {
        "symptoms": {
            "头痛": {"likelihood": 0.8, "typical": True},
            "发热": {"likelihood": 0.9, "typical": True, "severity_usually": "HIGH"}, # 流感发热通常较高
            "咳嗽": {"likelihood": 0.8, "typical": True},
            "乏力": {"likelihood": 0.9, "typical": True, "severity_usually": "HIGH"},
            "肌肉酸痛": {"likelihood": 0.9, "typical": True},
            "咽痛": {"likelihood": 0.7, "typical": True},
            "恶心": {"likelihood": 0.2, "typical": False},
            "呼吸困难": {"likelihood": 0.1, "typical": False},
        },
        "description": "由流感病毒引起的急性呼吸道传染病,症状比普通感冒重。",
        "differential_points": ["起病急,全身症状重,常有高热、肌肉酸痛。"],
        "advice": ["及时就医,遵医嘱服用抗病毒药物。注意隔离,防止传播。"]
    },
    "细菌性肺炎": {
        "symptoms": {
            "咳嗽": {"likelihood": 0.9, "typical": True, "feature": "咳痰"},
            "发热": {"likelihood": 0.8, "typical": True, "severity_usually": "HIGH"},
            "胸闷": {"likelihood": 0.7, "typical": True},
            "呼吸困难": {"likelihood": 0.6, "typical": True},
            "乏力": {"likelihood": 0.7, "typical": True},
            "头痛": {"likelihood": 0.3, "typical": False},
            "恶心": {"likelihood": 0.1, "typical": False},
            "胸痛": {"likelihood": 0.5, "typical": True},
        },
        "description": "肺部感染,常由细菌引起,可能导致严重的呼吸系统问题。",
        "differential_points": ["常有咳痰,呼吸困难和胸痛,需通过影像学检查确诊。"],
        "advice": ["立即就医,可能需要抗生素治疗。"]
    },
    "偏头痛": {
        "symptoms": {
            "头痛": {"likelihood": 0.95, "typical": True, "feature": "搏动性, 偏侧"},
            "恶心": {"likelihood": 0.6, "typical": True},
            "呕吐": {"likelihood": 0.4, "typical": True},
            "畏光": {"likelihood": 0.7, "typical": True},
            "畏声": {"likelihood": 0.7, "typical": True},
            "发热": {"likelihood": 0.05, "typical": False},
            "乏力": {"likelihood": 0.3, "typical": False},
        },
        "description": "一种常见的慢性神经血管性头痛,常伴有其他神经系统症状。",
        "differential_points": ["头痛常为搏动性,多为偏侧,伴有恶心、呕吐、畏光、畏声等。无发热。"],
        "advice": ["避免诱因,按医嘱服用止痛药或偏头痛特异性药物。"]
    },
    # ... 更多疾病
}

3. 检索策略

当 Agent 接收到结构化的症状列表后,就需要从知识库中检索出最相关的疾病。

  • 关键词匹配: 最直接的方法,但精确度有限。例如,如果用户提到“头痛”,就匹配所有包含“头痛”的疾病。
  • 基于特征匹配的评分: 为每个疾病定义一套特征(症状、检查结果等),然后根据用户输入的症状与这些特征的匹配程度进行评分。
  • 语义相似度匹配: 使用词向量 (Word Embeddings) 或文档嵌入 (Document Embeddings) 来计算用户症状描述与知识库中疾病描述的语义相似度,即使词语不完全匹配也能找到相关性。这对于处理口语化、非标准化的症状描述非常有用。
  • 图遍历 (Graph Traversal): 如果知识库以知识图谱的形式构建,可以通过图算法(如最短路径、中心性分析)来发现症状与疾病之间的复杂关系。

为了简化演示,我们将使用基于特征匹配的评分策略。

class KnowledgeBaseRetriever:
    def __init__(self, knowledge_base):
        self.knowledge_base = knowledge_base

    def retrieve_diseases(self, extracted_symptoms):
        """
        根据提取的症状,从知识库中检索并初步排序可能的疾病。
        参数:
            extracted_symptoms (dict): 从NLU阶段提取的标准化症状信息。
                                       例如: {'SYMPTOMS': [{'symptom': '头痛', 'negated': False, 'severity': 'HIGH_SEVERITY'}, ...]}
        返回:
            list: 包含疾病名称和初步匹配分数的列表,按分数降序排列。
        """
        possible_diseases = {}
        user_positive_symptoms = {s['symptom'] for s in extracted_symptoms.get('SYMPTOMS', []) if not s['negated']}
        user_negative_symptoms = {s['symptom'] for s in extracted_symptoms.get('SYMPTOMS', []) if s['negated']}

        # 考虑症状严重程度
        user_symptom_details = {s['symptom']: s for s in extracted_symptoms.get('SYMPTOMS', []) if not s['negated']}

        for disease_name, disease_info in self.knowledge_base.items():
            score = 0
            matched_symptoms = set()

            # 1. 匹配阳性症状
            for user_symptom in user_positive_symptoms:
                if user_symptom in disease_info["symptoms"]:
                    symptom_data_in_kb = disease_info["symptoms"][user_symptom]

                    # 基础分数:症状在疾病中的可能性
                    score += symptom_data_in_kb.get("likelihood", 0.5) * 10 

                    # 加分项:如果症状是典型的
                    if symptom_data_in_kb.get("typical", False):
                        score += 3

                    # 加分项:如果用户描述的严重程度与疾病典型严重程度匹配
                    user_sev = user_symptom_details.get(user_symptom, {}).get("severity")
                    kb_sev = symptom_data_in_kb.get("severity_usually")
                    if user_sev and kb_sev and 
                       ((user_sev == "HIGH_SEVERITY" and kb_sev == "HIGH") or 
                        (user_sev == "LOW_SEVERITY" and kb_sev == "LOW")):
                        score += 2 # 匹配严重程度加分

                    matched_symptoms.add(user_symptom)

            # 2. 惩罚项:匹配阴性症状 (如果用户说没有某个症状,而这个症状是疾病的典型症状,则扣分)
            for user_symptom in user_negative_symptoms:
                if user_symptom in disease_info["symptoms"]:
                    symptom_data_in_kb = disease_info["symptoms"][user_symptom]
                    if symptom_data_in_kb.get("typical", False) or symptom_data_in_kb.get("likelihood", 0) > 0.5:
                        score -= 10 # 如果典型症状被否定,则大幅扣分

            # 3. 惩罚项:疾病的典型症状未被提及(可能信息不足,也可能不符合)
            #    这里简化处理,只对用户明确提及的症状打分,不因为未提及而大幅扣分,
            #    因为用户可能只是没有想到或没有描述出来。
            #    更复杂的系统会考虑未提及症状的“阴性证据”

            # 确保分数不为负
            possible_diseases[disease_name] = max(0, score)

        # 按分数降序排列
        sorted_diseases = sorted(possible_diseases.items(), key=lambda item: item[1], reverse=True)
        return sorted_diseases

# 示例用法
retriever = KnowledgeBaseRetriever(MEDICAL_KNOWLEDGE_BASE)
extracted_info_1 = extractor.extract_symptoms("我头疼得厉害,还发烧,浑身没劲,大概两天了。但没有咳嗽。")
retrieved_1 = retriever.retrieve_diseases(extracted_info_1)
print("n检索结果 1:", retrieved_1)

extracted_info_2 = extractor.extract_symptoms("最近感觉有点恶心,但没吐,肚子也不疼,头痛得很厉害,还畏光。")
retrieved_2 = retriever.retrieve_diseases(extracted_info_2)
print("检索结果 2:", retrieved_2)

输出示例

检索结果 1: [('流行性感冒', 28.0), ('普通感冒', 26.0), ('偏头痛', 18.0), ('细菌性肺炎', 14.0)]
检索结果 2: [('偏头痛', 33.0), ('流行性感冒', 13.0), ('普通感冒', 11.0), ('细菌性肺炎', 8.0)]

可以看到,流行性感冒 在第一个案例中得分最高,因为其高热、乏力、头痛的症状与用户描述匹配,且没有咳嗽进一步排除了普通感冒(普通感冒咳嗽典型)。第二个案例中,偏头痛 由于头痛剧烈、恶心、畏光等典型症状而被高度匹配。

第三阶段:鉴别诊断与推理引擎

这是 Agent 的核心智能所在。它不仅要检索相关疾病,更要根据已有的信息进行推理,给出最可能的诊断,并解释原因。

1. 推理方法

  • 基于规则的推理 (Rule-Based Reasoning)
    • 通过专家定义的 IF-THEN 规则进行推理。例如:如果 (高热 且 肌肉酸痛 且 乏力严重) 则 倾向于 流感
    • 优点:可解释性强,易于理解和维护(对于规则清晰的场景)。
    • 缺点:规则数量巨大时难以管理,难以处理不确定性和模糊性,无法从数据中学习。
  • 贝叶斯网络 (Bayesian Networks)
    • 处理不确定性推理的强大工具。它通过有向无环图表示变量(疾病、症状)之间的概率关系。
    • 优点:能够量化各种诊断的可能性(后验概率),处理缺失数据,提供概率解释。
    • 缺点:构建复杂的贝叶斯网络需要大量的先验概率和条件概率数据,获取这些数据可能很困难。
  • 机器学习模型 (Machine Learning Models)
    • 分类器: 将症状作为特征输入,训练模型(如决策树、随机森林、支持向量机 SVM、梯度提升树 XGBoost)直接预测疾病类别。
    • 深度学习: 利用循环神经网络 (RNN) 或 Transformer 模型处理序列化的症状数据,进行疾病分类或语义匹配。
    • 优点:可以从大量历史病例数据中学习,发现复杂的非线性关系。
    • 缺点:通常需要大量高质量的标注数据,模型可解释性可能较差(“黑箱”问题),难以处理未见过的新疾病。
  • 启发式算法 (Heuristic Algorithms)
    • 结合专家经验和统计规律,设计一些评分或筛选机制。例如,某种症状的出现会大幅提升某个疾病的可能性,而另一些症状的出现则会大幅降低。

在我们的 Agent 中,我们将结合基于规则的推理和基于特征匹配的评分,模拟一个简化的鉴别诊断过程。

class DiagnosisEngine:
    def __init__(self, knowledge_base):
        self.knowledge_base = knowledge_base

    def get_differential_diagnoses(self, extracted_symptoms, top_k=5):
        """
        根据提取的症状,进行鉴别诊断。
        此方法将结合检索结果和简单的推理规则。
        """
        # 1. 首先通过知识库检索得到初步排序的疾病列表
        retriever = KnowledgeBaseRetriever(self.knowledge_base)
        initial_candidates = retriever.retrieve_diseases(extracted_symptoms)

        final_diagnoses = []
        user_positive_symptoms = {s['symptom'] for s in extracted_symptoms.get('SYMPTOMS', []) if not s['negated']}
        user_negative_symptoms = {s['symptom'] for s in extracted_symptoms.get('SYMPTOMS', []) if s['negated']}
        user_symptom_details = {s['symptom']: s for s in extracted_symptoms.get('SYMPTOMS', []) if not s['negated']}

        for disease_name, initial_score in initial_candidates:
            if initial_score <= 0: # 过滤掉分数过低的疾病
                continue

            disease_info = self.knowledge_base[disease_name]
            final_score = initial_score # 从初始分数开始调整

            # 2. 结合更详细的推理逻辑进行分数调整和鉴别
            # 规则示例:
            # R1: 如果用户有高热且乏力严重,且疾病是流感,则加分。
            if disease_name == "流行性感冒":
                has_high_fever_and_fatigue = False
                for symp_detail in extracted_symptoms.get('SYMPTOMS', []):
                    if symp_detail['symptom'] == '发热' and not symp_detail['negated'] and symp_detail['severity'] == "HIGH_SEVERITY":
                        for symp_detail_fatigue in extracted_symptoms.get('SYMPTOMS', []):
                            if symp_detail_fatigue['symptom'] == '乏力' and not symp_detail_fatigue['negated'] and symp_detail_fatigue['severity'] == "HIGH_SEVERITY":
                                has_high_fever_and_fatigue = True
                                break
                        if has_high_fever_and_fatigue:
                            break
                if has_high_fever_and_fatigue:
                    final_score += 15 # 流感典型症状组合加分

            # R2: 如果用户有头痛、恶心、畏光且没有发热,则大幅倾向于偏头痛。
            if disease_name == "偏头痛":
                has_headache_nausea_photophobia = False
                if '头痛' in user_positive_symptoms and 
                   '恶心' in user_positive_symptoms and 
                   '畏光' in user_positive_symptoms and 
                   '发热' in user_negative_symptoms: # 注意:畏光未在示例词典中,这里假设能识别
                    final_score += 20 # 偏头痛典型组合大幅加分

            # R3: 如果用户明确否定了某个疾病的典型症状,则大幅降低该疾病的得分
            #    这部分已经在 retriever 中初步处理,但可以在此进一步强化
            for neg_symptom in user_negative_symptoms:
                if neg_symptom in disease_info["symptoms"] and disease_info["symptoms"][neg_symptom].get("typical", False):
                    final_score -= 15 # 再次强调否定典型症状的惩罚

            # R4: 考虑持续时间(简化:若持续时间很短,某些慢性病可能性降低)
            duration_info = extracted_symptoms.get('DURATION')
            if duration_info and any("小时" in d or "钟头" in d for d in duration_info) and disease_name in ["偏头痛"]: # 偏头痛发作时间短,与小时匹配
                 final_score += 5

            # ... 更多复杂的规则,可以处理症状的组合、排除条件等

            final_diagnoses.append({
                "disease": disease_name,
                "confidence_score": max(0, final_score), # 确保分数不为负
                "description": disease_info["description"],
                "differential_points": disease_info["differential_points"],
                "advice": disease_info["advice"]
            })

        # 再次排序并取前K个
        final_diagnoses = sorted(final_diagnoses, key=lambda x: x["confidence_score"], reverse=True)
        return final_diagnoses[:top_k]

# 示例用法
diagnosis_engine = DiagnosisEngine(MEDICAL_KNOWLEDGE_BASE)

# 案例 1
user_input_1 = "我头疼得厉害,还发烧,浑身没劲,大概两天了。但没有咳嗽。"
extracted_1 = extractor.extract_symptoms(user_input_1)
diagnoses_1 = diagnosis_engine.get_differential_diagnoses(extracted_1)
print("n--- 诊断结果 1 ---")
for diag in diagnoses_1:
    print(f"疾病: {diag['disease']}, 置信度: {diag['confidence_score']:.2f}")
    print(f"  描述: {diag['description']}")
    print(f"  鉴别要点: {', '.join(diag['differential_points'])}")
    print(f"  建议: {', '.join(diag['advice'])}")

# 案例 2
user_input_2 = "最近感觉有点恶心,但没吐,肚子也不疼,头痛得很厉害,还畏光。" # 假设 extractor 能够识别 "畏光"
# 为演示,手动添加畏光到 extracted_2
extracted_2 = extractor.extract_symptoms("最近感觉有点恶心,但没吐,肚子也不疼,头痛得很厉害,还畏光。")
extracted_2['SYMPTOMS'].append({'symptom': '畏光', 'negated': False, 'severity': 'UNKNOWN'})
diagnoses_2 = diagnosis_engine.get_differential_diagnoses(extracted_2)
print("n--- 诊断结果 2 ---")
for diag in diagnoses_2:
    print(f"疾病: {diag['disease']}, 置信度: {diag['confidence_score']:.2f}")
    print(f"  描述: {diag['description']}")
    print(f"  鉴别要点: {', '.join(diag['differential_points'])}")
    print(f"  建议: {', '.join(diag['advice'])}")

# 案例 3
user_input_3 = "我咳嗽,咳痰,还高烧,感觉胸口闷,有点喘不上气。"
extracted_3 = extractor.extract_symptoms(user_input_3)
# 为演示,手动添加咳痰和高烧的严重程度信息
extracted_3['SYMPTOMS'].append({'symptom': '咳痰', 'negated': False, 'severity': 'UNKNOWN'})
for s in extracted_3['SYMPTOMS']:
    if s['symptom'] == '发热':
        s['severity'] = 'HIGH_SEVERITY'
diagnoses_3 = diagnosis_engine.get_differential_diagnoses(extracted_3)
print("n--- 诊断结果 3 ---")
for diag in diagnoses_3:
    print(f"疾病: {diag['disease']}, 置信度: {diag['confidence_score']:.2f}")
    print(f"  描述: {diag['description']}")
    print(f"  鉴别要点: {', '.join(diag['differential_points'])}")
    print(f"  建议: {', '.join(diag['advice'])}")

输出示例

--- 诊断结果 1 ---
疾病: 流行性感冒, 置信度: 43.00
  描述: 由流感病毒引起的急性呼吸道传染病,症状比普通感冒重。
  鉴别要点: 起病急,全身症状重,常有高热、肌肉酸痛。
  建议: 及时就医,遵医嘱服用抗病毒药物。注意隔离,防止传播。
疾病: 普通感冒, 置信度: 26.00
  描述: 一种由病毒引起的上呼吸道感染,通常症状较轻,可自愈。
  鉴别要点: 与流感相比,发热通常较轻,全身症状不明显。
  建议: 多休息,多饮水,可服用非处方药缓解症状。如症状加重,及时就医。
疾病: 偏头痛, 置信度: 18.00
  描述: 一种常见的慢性神经血管性头痛,常伴有其他神经系统症状。
  鉴别要点: 头痛常为搏动性,多为偏侧,伴有恶心、呕吐、畏光、畏声等。无发热。
  建议: 避免诱因,按医嘱服用止痛药或偏头痛特异性药物。
疾病: 细菌性肺炎, 置信度: 14.00
  描述: 肺部感染,常由细菌引起,可能导致严重的呼吸系统问题。
  鉴别要点: 常有咳痰,呼吸困难和胸痛,需通过影像学检查确诊。
  建议: 立即就医,可能需要抗生素治疗。

--- 诊断结果 2 ---
疾病: 偏头痛, 置信度: 53.00
  描述: 一种常见的慢性神经血管性头痛,常伴有其他神经系统症状。
  鉴别要点: 头痛常为搏动性,多为偏侧,伴有恶心、呕吐、畏光、畏声等。无发热。
  建议: 避免诱因,按医嘱服用止痛药或偏头痛特异性药物。
疾病: 流行性感冒, 置信度: 13.00
  描述: 由流感病毒引起的急性呼吸道传染病,症状比普通感冒重。
  鉴别要点: 起病急,全身症状重,常有高热、肌肉酸痛。
  建议: 及时就医,遵医嘱服用抗病毒药物。注意隔离,防止传播。
疾病: 普通感冒, 置信度: 11.00
  描述: 一种由病毒引起的上呼吸道感染,通常症状较轻,可自愈。
  鉴别要点: 与流感相比,发热通常较轻,全身症状不明显。
  建议: 多休息,多饮水,可服用非处方药缓解症状。如症状加重,及时就医。
疾病: 细菌性肺炎, 置信度: 8.00
  描述: 肺部感染,常由细菌引起,可能导致严重的呼吸系统问题。
  鉴别要点: 常有咳痰,呼吸困难和胸痛,需通过影像学检查确诊。
  建议: 立即就医,可能需要抗生素治疗。

--- 诊断结果 3 ---
疾病: 细菌性肺炎, 置信度: 67.00
  描述: 肺部感染,常由细菌引起,可能导致严重的呼吸系统问题。
  鉴别要点: 常有咳痰,呼吸困难和胸痛,需通过影像学检查确诊。
  建议: 立即就医,可能需要抗生素治疗。
疾病: 流行性感冒, 置信度: 28.00
  描述: 由流感病毒引起的急性呼吸道传染病,症状比普通感冒重。
  鉴别要点: 起病急,全身症状重,常有高热、肌肉酸痛。
  建议: 及时就医,遵医嘱服用抗病毒药物。注意隔离,防止传播。
疾病: 普通感冒, 置信度: 16.00
  描述: 一种由病毒引起的上呼吸道感染,通常症状较轻,可自愈。
  鉴别要点: 与流感相比,发热通常较轻,全身症状不明显。
  建议: 多休息,多饮水,可服用非处方药缓解症状。如症状加重,及时就医。
疾病: 偏头痛, 置信度: 0.00
  描述: 一种常见的慢性神经血管性头痛,常伴有其他神经系统症状。
  鉴别要点: 头痛常为搏动性,多为偏侧,伴有恶心、呕吐、畏光、畏声等。无发热。
  建议: 避免诱因,按医嘱服用止痛药或偏头痛特异性药物。

可以看到,推理引擎在检索的基础上,通过更细致的规则调整了置信度,使诊断结果更符合逻辑。例如,案例3中,"咳嗽、咳痰、高烧、胸闷、呼吸困难" 这些关键症状使得 "细菌性肺炎" 得分大幅领先。

2. 迭代与澄清

一个优秀的预问诊 Agent 不仅仅是单向输出结果,更应该具备与用户进行多轮对话的能力。当 Agent 发现现有信息不足以做出明确诊断时,它可以主动向用户提问,以获取更多关键信息。

例如

  • 如果初步怀疑是流感或普通感冒,但用户没有提及肌肉酸痛,Agent 可以问:“您是否有全身肌肉酸痛的感觉?”
  • 如果怀疑是偏头痛,但用户没有提及畏光或畏声,Agent 可以问:“您是否对光线或声音特别敏感?”

这个过程可以通过维护一个对话状态 (dialogue state) 和一个“待确认症状”列表来实现。

class PreDiagnosisAgent:
    def __init__(self, extractor, diagnosis_engine):
        self.extractor = extractor
        self.diagnosis_engine = diagnosis_engine
        self.current_symptoms = defaultdict(list)
        self.dialogue_history = []

    def start_consultation(self):
        print("您好!我是您的医疗预问诊助手。请您描述一下您的症状。")
        self.current_symptoms = defaultdict(list)
        self.dialogue_history = []

    def process_user_input(self, user_input):
        self.dialogue_history.append({"speaker": "USER", "text": user_input})

        extracted = self.extractor.extract_symptoms(user_input)

        # 合并新提取的症状到当前症状列表
        for key, value in extracted.items():
            if isinstance(value, list):
                if key == "SYMPTOMS":
                    # 合并症状,避免重复,并更新细节(如严重程度)
                    existing_symptoms_map = {s['symptom']: s for s in self.current_symptoms[key]}
                    for new_symptom_data in value:
                        symptom_name = new_symptom_data['symptom']
                        is_negated = new_symptom_data['negated']

                        if symptom_name in existing_symptoms_map:
                            # 如果症状已存在,更新其否定状态或严重程度(如果新信息更具体)
                            existing_ss = existing_symptoms_map[symptom_name]
                            if existing_ss['negated'] != is_negated: # 否定状态改变,取最新
                                existing_ss['negated'] = is_negated
                            if new_symptom_data['severity'] != "UNKNOWN" and existing_ss['severity'] == "UNKNOWN":
                                existing_ss['severity'] = new_symptom_data['severity']
                        else:
                            self_copy = new_symptom_data.copy()
                            self.current_symptoms[key].append(self_copy)
                            existing_symptoms_map[symptom_name] = self_copy # 更新映射
                else:
                    self.current_symptoms[key].extend(value)
                    self.current_symptoms[key] = list(set(self.current_symptoms[key])) # 去重

        diagnoses = self.diagnosis_engine.get_differential_diagnoses(self.current_symptoms)

        response_text = self._generate_response(diagnoses)
        self.dialogue_history.append({"speaker": "AGENT", "text": response_text})
        return response_text

    def _generate_response(self, diagnoses):
        if not diagnoses or diagnoses[0]['confidence_score'] == 0:
            return "根据您提供的信息,我暂时无法给出明确的诊断建议。您能否提供更多细节?例如,症状持续了多久?是否有其他不适?"

        top_diagnosis = diagnoses[0]

        # 简单判断是否需要进一步提问
        # 理想情况下,这里会根据置信度差异、未提及的关键症状等进行智能提问
        if len(diagnoses) > 1 and (top_diagnosis['confidence_score'] - diagnoses[1]['confidence_score'] < 10): # 置信度相近
            question_suggestions = self._suggest_clarifying_questions(diagnoses)
            if question_suggestions:
                return f"根据您描述的症状,我初步判断可能是以下情况。为了更准确地判断,我还需要了解更多信息:n" 
                       f"最可能的疾病是:{top_diagnosis['disease']} (置信度: {top_diagnosis['confidence_score']:.2f})n" 
                       f"次要可能疾病包括:{diagnoses[1]['disease']} (置信度: {diagnoses[1]['confidence_score']:.2f})n" 
                       f"请问您是否有以下情况?{question_suggestions}"

        # 如果置信度较高且差异明显,直接给出建议
        return f"根据您的症状描述,最可能的疾病是 **{top_diagnosis['disease']}** (置信度: {top_diagnosis['confidence_score']:.2f})。n" 
               f"  简要描述: {top_diagnosis['description']}n" 
               f"  鉴别要点: {', '.join(top_diagnosis['differential_points'])}n" 
               f"  建议您: {', '.join(top_diagnosis['advice'])}n" 
               f"请注意,这仅是初步判断,不能替代专业的医疗诊断。建议您尽快就医。"

    def _suggest_clarifying_questions(self, diagnoses):
        # 这是一个非常简化的提问逻辑,实际系统会更复杂
        questions = []

        # 尝试从排名靠前的疾病中找出用户未提及的典型症状来提问
        all_known_symptoms = {s['symptom'] for s in self.current_symptoms.get('SYMPTOMS', [])}

        for diag in diagnoses[:2]: # 考虑前2个诊断
            disease_info = self.diagnosis_engine.knowledge_base[diag['disease']]
            for symptom, details in disease_info['symptoms'].items():
                if details.get('typical', False) and symptom not in all_known_symptoms:
                    questions.append(f"是否感到 {symptom}?")
                    # 仅提问一个,避免问题过多
                    return "您是否感到 " + symptom + "?"

        return "请问您是否有其他任何不适?"

# 完整的 Agent 流程示例
agent = PreDiagnosisAgent(extractor, diagnosis_engine)
agent.start_consultation()

# 对话 1
print("n--- 对话开始 ---")
response = agent.process_user_input("我头疼得厉害,还发烧,浑身没劲,大概两天了。但没有咳嗽。")
print("Agent:", response)

# 对话 2 (用户提供更多信息)
response = agent.process_user_input("是啊,我还有点肌肉酸痛。")
print("Agent:", response)

# 对话 3 (用户提供更多信息)
response = agent.process_user_input("没有畏光或畏声。")
print("Agent:", response)

# 对话 4 (换一个场景)
agent.start_consultation()
response = agent.process_user_input("我肚子疼,还拉肚子,有点恶心。")
print("Agent:", response)

输出示例

--- 对话开始 ---
Agent: 根据您描述的症状,我初步判断可能是以下情况。为了更准确地判断,我还需要了解更多信息:
最可能的疾病是:流行性感冒 (置信度: 43.00)
次要可能疾病包括:普通感冒 (置信度: 26.00)
请问您是否有以下情况?您是否感到 肌肉酸痛?
Agent: 根据您的症状描述,最可能的疾病是 **流行性感冒** (置信度: 58.00)。
  简要描述: 由流感病毒引起的急性呼吸道传染病,症状比普通感冒重。
  鉴别要点: 起病急,全身症状重,常有高热、肌肉酸痛。
  建议您: 及时就医,遵医嘱服用抗病毒药物。注意隔离,防止传播。
请注意,这仅是初步判断,不能替代专业的医疗诊断。建议您尽快就医。
Agent: 根据您的症状描述,最可能的疾病是 **流行性感冒** (置信度: 58.00)。
  简要描述: 由流感病毒引起的急性呼吸道传染病,症状比普通感冒重。
  鉴别要点: 起病急,全身症状重,常有高热、肌肉酸痛。
  建议您: 及时就医,遵医嘱服用抗病毒药物。注意隔离,防止传播。
请注意,这仅是初步判断,不能替代专业的医疗诊断。建议您尽快就医。

您好!我是您的医疗预问诊助手。请您描述一下您的症状。
Agent: 根据您提供的信息,我暂时无法给出明确的诊断建议。您能否提供更多细节?例如,症状持续了多久?是否有其他不适?

由于我们的知识库示例中没有包含腹痛和腹泻的疾病,所以最后一次对话 Agent 无法给出明确诊断,并提示用户提供更多信息,这符合预期行为。

第四阶段:输出与用户交互

Agent 的最终目标是为用户提供有价值的信息。输出应该清晰、专业且负责任。

1. 输出内容

  • 鉴别诊断列表: 按可能性高低排序,包含疾病名称和置信度得分。
  • 疾病简要描述: 帮助用户理解疾病。
  • 鉴别要点: 突出该疾病与相似疾病的区别,增加用户理解。
  • 建议的下一步行动
    • 就医建议: 何时就医、看什么科室(如“建议尽快去呼吸科就诊”)。
    • 居家观察: 症状较轻时的自我管理建议。
    • 急诊指征: 明确告知用户哪些症状需要立即就医(如“如果出现呼吸困难加重、意识模糊,请立即拨打急救电话”)。
  • 进一步的提问: 如前所述,用于澄清和缩小诊断范围。

2. 用户界面考虑

  • 清晰简洁: 避免医学术语,使用普通人能理解的语言。
  • 结构化呈现: 使用列表、加粗等格式,使信息易于阅读。
  • 免责声明: 明确告知 Agent 的建议仅供参考,不能替代专业医生的诊断。这是至关重要的一点,必须贯穿始终。

挑战与未来方向

构建一个实用的医疗预问诊 Agent 并非易事,面临诸多挑战,也蕴含着巨大的发展潜力。

  1. 数据稀疏性与偏见: 高质量的医学标注数据获取困难且成本高昂。现有数据可能存在偏见,导致 Agent 在面对某些群体或罕见病时表现不佳。
  2. 多病共存与复杂病例: 许多患者可能同时患有多种疾病,或症状表现不典型。Agent 需要处理这些复杂的交互关系。
  3. 领域知识的持续更新: 医学知识发展迅速,Agent 的知识库需要定期更新和维护,以保持其准确性和时效性。
  4. 可解释性 (Explainability): 尤其对于基于机器学习的模型,医生和患者都希望知道 Agent 为什么会给出某个诊断。提高模型的可解释性至关重要。
  5. 伦理与隐私: 医疗数据涉及高度敏感的个人隐私。Agent 的设计和部署必须严格遵守数据保护法规,并建立明确的伦理指导原则。
  6. 多模态输入: 结合图像(如皮疹照片、X光片)、声音(如咳嗽声、呼吸音)等多种模态信息,将极大地增强 Agent 的诊断能力。
  7. 个性化医疗: 整合患者的病史、用药情况、过敏史,甚至基因信息,实现更个性化的诊断和建议。

展望与期许

医疗预问诊 Agent 的发展,代表着人工智能在医疗健康领域深度应用的一个重要方向。它并非要取代医生,而是作为医生的有力助手,赋能患者,优化医疗资源分配,让高质量的初步医疗咨询触手可及。随着自然语言处理、知识图谱和机器学习技术的不断进步,我们有理由相信,未来的医疗预问诊 Agent 将更加智能、准确和人性化,为全球的健康福祉贡献更大的力量。持续的研究与开发,以及跨学科的紧密合作,将是推动这一领域向前发展的关键。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注