尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

知识代码:从MMC临床规则到CDSS推理引擎的全流程构建

知识代码:从MMC临床规则到CDSS推理引擎的全流程构建 简介瑞金医院MMC人工智能辅助构建知识代码是一套面向医疗AI与知识图谱开发者的轻量级Python工具资源。它以多学科医疗中心真实项目为背景解决医疗数据预处理、知识抽取、关系构建与模型评估等环节中的基础编码问题。整个资源包共3个Python文件压缩后大小约5KB涵盖数据加载与清洗、评估逻辑及模块化封装几类功能结构简洁便于直接阅读和改造。目前已有860人学习浏览适合具备一定Python基础、希望快速了解医疗知识图谱构建流程的读者。通过这份代码使用者可以掌握从电子病历、医学文献中抽取实体关系并构造知识网络的实现思路学习数据清洗与格式转换的常见技巧同时参考其数据接口设计和评估方法为自己的科研或工程项目节省初期探索时间。尽管资源体量不大但对于理解瑞金医院MMC在AI辅助诊疗方向的落地实践仍具有直接参考价值。1. 瑞金医院MMC的人工智能知识代码建的是一套可计算的临床规则资产一个再普通不过的场景内分泌科医生接诊随访满3个月的2型糖尿病患者系统根据近三次糖化血红蛋白和低血糖记录自动刷新下阶段用药建议与复查清单。要让系统具备这个判断力前提是把“糖化血红蛋白≥9%且近3个月无低血糖则提示启动胰岛素方案讨论”这类临床知识编译成带唯一编号、可被程序调用的知识代码。瑞金医院牵头的MMC国家标准化代谢性疾病管理中心把代谢病管理做成标准化模式知识代码就是产品化路径上的一环把指南和真实诊疗经验变成数据结构而不是躺在Word里的文档段落。人工智能辅助构建知识代码本质是在知识工程流水线上加入实体识别、关系抽取与自动映射让机器先产出候选规则人只做审阅和修订。这套方法适合临床信息工程师、知识图谱开发者和CDSS后端工程师也适合要做临床知识库但不想从零发明格式的团队。下面按数据建模、AI抽取、质量校验和推理应用四个环节推进代码按通用技术栈写可以直接搬进内部平台再改造成自己的服务。2. MMC知识代码的数据建模字段设计、标准映射与入库校验2.1 一条知识代码的字段设计为什么采用“前提-动作-证据”三段一条知识能不能被程序稳定调用取决于结构是否统一。常见做法是把临床规则拆成“前置条件precondition、建议动作action、证据溯源evidence”三段再为整条规则分配唯一标识。这样拆的理由很直接CDSS引擎查找规则时可以只扫precondition字段触发后再读action派发随访任务而evidence字段让人工审阅时能追溯到指南原文避免出现“AI改了一条规则却没人说得清依据”的局面。建议的MMC知识代码表字段结构如下。字段示例值说明code_idKNO-DM-2024-000127唯一知识代码业务域年份流水号domaindiabetes_mellitusMMC业务域糖尿病、肥胖、并发症、随访preconditionlab.hba1c9.0 AND flag.no_hypoglycemia_3mtrue触发条件表达式必须可被程序解析actionrecommend_insulin_therapy_discussion建议动作对应MMC随访任务模板evidence中国2型糖尿病防治指南2023版 第7.2节来源定位statusactivedraft/active/superseded三态version2同一规则代码的修订版本这里拒绝直接用自然语言存precondition。自然语言适合给人阅读但推理引擎要的是可解析的表达式。把阈值和逻辑操作符标准化后后续做规则冲突检测、单元测试都会顺手很多。code_id里带业务域缩写和年份能一眼看出规则归属也不会在跨科室引用时重号。2.2 ICD-10、LOINC与本地扩展码共存的映射策略MMC场景里诊断要用ICD-10的E11这类标准码实验室项目要尽量对齐LOINC但具体到治疗方案和随访动作标准编码覆盖不到必须自己造本地扩展码。一个常见误区是企图把所有本地码一次性翻译成SNOMED CT结果映射表半年都没建完。更稳妥的做法是让标准码和本地码共存先保业务跑通再慢慢补映射质量。标准体系标准码本地扩展码映射类型ICD-10E11.9MMC-DX-20351:1LOINC4548-4MMC-LAB-HBA1C1:NSNOMED CT46635009MMC-ENT-82081:N映射表单独建一张mapping表记录映射类型和置信度。置信度低于0.9的映射不进生产环境只进人工复核队列映射表每天对比上游标准码更新一次避免本地码长期脱离标准体系。2.3 入库前校验用Pydantic把格式错误挡在数据库外既然知识代码要进数据库、进推理引擎入库前就必须挡住格式错误和空字段。用Pydantic定义规则对象会把校验逻辑和数据结构放在一起新增字段时不会漏掉规则是知识工程里比较省心的做法。from pydantic import BaseModel, Field, field_validator, model_validator from enum import Enum from datetime import datetime class RuleStatus(str, Enum): DRAFT draft ACTIVE active SUPERSEDED superseded class KnowledgeRule(BaseModel): code_id: str Field(patternr^KNO-[A-Z]{2,8}-\d{4}-\d{6}$) domain: str Field(min_length2, max_length64) precondition: str Field(min_length4, max_length1024) action: str Field(min_length2, max_length128) evidence: str Field(default) status: RuleStatus RuleStatus.DRAFT version: int Field(default1, ge1) created_at: datetime Field(default_factorydatetime.now) field_validator(precondition) classmethod def precondition_whitelist(cls, v: str) - str: allowed set(abcdefghijklmnopqrstuvwxyz0123456789_.()!|- ) if any(ch not in allowed for ch in v.lower()): raise ValueError(precondition包含非法字符) return v model_validator(modeafter) def active_rules_need_evidence(self): if self.status RuleStatus.ACTIVE and not self.evidence.strip(): raise ValueError(ACTIVE规则必须提供evidence) return self代码里有三个参数值得留意。pattern约束code_id只能是指定格式KNO后是业务域缩写、年份和六位流水号可以防止不同团队各写一套编号field_validator里的precondition_whitelist对表达式做字符级白名单过滤避免规则表达式里混入分号、引号这类可能干扰下游解析器的字符model_validator保证只有带证据来源的规则才能置为ACTIVE这是知识代码能长期维护的底线。批量上传时逐条实例化这个模型异常信息里自带code_id可以直接定位哪条规则不合格。for item in load_knowledge_rules_from_file(knowledge_rules.json): try: rule KnowledgeRule(**item) insert_into_mmc_knowledge_db(rule) except ValidationError as exc: log_error(item.get(code_id, unknown), exc.errors())入库代码只处理通过校验的数据数据库端不用再堆一堆触发器做防御排错时日志里也有明确的条目标识。这里的load和insert函数按各自技术栈实现即可核心是把校验模型作为唯一入口。3. 人工智能辅助构建从MMC临床语料抽取知识代码3.1 语料预处理切句时别漏掉小数点之间的句号任何一份临床文本都不是天生适合抽规则的。MMC里常见的输入包括出院小结、门诊记录、随访表单里的自由文本混着表格、括号和计量单位。第一步是统一脱掉患者姓名、住院号这类隐私字段再做句子切分。切分只按句号、分号、换行处理不按逗号切因为一条治疗建议经常跨越逗号。import re def split_into_sentences(text: str) - list[str]: text re.sub(r[\r\n], \n, text) text re.sub(r(?\d)[.。](?\d), ., text) # 保留1.5中的点号 parts re.split(r[。\n], text) return [p.strip() for p in parts if len(p.strip()) 6]这里的第二个re.sub是典型的坑中文病历里“1.5mmol/L”这种数值带点号直接按句号切会拦腰切断数字。先守住数字之间的句点再按句末标点切分后面实体识别的召回率会稳定不少。最小长度设为6可以过滤掉“无特殊”这类无信息碎片。3.2 医学NER实体识别把指标、药品和阈值从句子中圈出来知识代码的三元组里主体通常是检验指标或症状客体是数值或药品。第一层抽取用基于BERT的中文医疗NER模型最合适前提是模型在医疗语料上做过预训练并在自有数据上微调过不要直接拿通用BERT跑临床文本。from transformers import AutoTokenizer, AutoModelForTokenClassification import torch tokenizer AutoTokenizer.from_pretrained(your_medical_ner_path) model AutoModelForTokenClassification.from_pretrained(your_medical_ner_path) def extract_entities(text: str, id2tag: dict) - list[dict]: inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): logits model(**inputs).logits pred_ids logits.argmax(dim-1)[0].tolist() tokens tokenizer.convert_ids_to_tokens(inputs[input_ids][0]) entities, cur [], None for tok, tag_id in zip(tokens[1:-1], pred_ids[1:-1]): tag id2tag[tag_id] if tag.startswith(B-): if cur: entities.append(cur) cur {text: tok.lstrip(##), type: tag[2:]} elif tag.startswith(I-) and cur: cur[text] tok.lstrip(##) else: if cur: entities.append(cur) cur None if cur: entities.append(cur) return entities预训练NER模型通常输出BIO标签B表示一个实体开始I表示延续O表示实体外。上面代码把标签还原成连续文本块同时记录实体类型。max_length128对单条句子够用句子过长时先做截断临床建议的结论往往出现在句尾所以要保留句首和句尾信息。实体类型先只保留三类指标、药品、数值阈值其余类型留给关系抽取阶段再定。以“糖化血红蛋白9.2%建议加用二甲双胍缓释片0.5g每晚一次”为例NER阶段会产出“糖化血红蛋白(指标)、9.2%(阈值)、二甲双胍缓释片(药品)”三个候选。阈值“0.5g”不能单独成为实体要在关系抽取阶段和前面的药品绑定。3.3 LLM关系抽取temperature调低输出受限JSON实体识别解决了“谁在句子里”还没解决“他们之间是什么关系”。关系抽取用大模型最省力但必须控制自由发挥。常见做法是把任务定义成受限的JSON数组输出用system提示词限定可接受的谓词集合。LLM_SYSTEM 你是代谢病知识抽取助手。把用户给出的临床句子转成JSON数组。 每个元素格式 {subject: 实体, relation: 高于|低于|建议使用|禁忌|随访, object: 实体或数值, advice: 动作或空字符串} 要求 1. subject只能来自实体词表不能自己造概念 2. 数值和单位保持原文不做四舍五入 3. 句子中没有明确临床建议时返回空数组。 def extract_rule_triples(sentences: list[str], llm_call) - list[dict]: results [] for sent in sentences: resp llm_call( systemLLM_SYSTEM, userf句子{sent}, temperature0.1, top_p0.9, max_tokens1024 ) results.extend(json.loads(resp)) return results关系抽取任务里反复要调整的是三个参数具体作用如下表。参数设定值作用与调整建议temperature0.1抽取要复现指南原意温度越低重复性越好想提高召回可提到0.3超过0.4会出现改写原文top_p0.9核采样截断尾部低概率词。temperature和top_p不需要同时调得很高二选一控制即可max_tokens1024单句三元组JSON一般几十到几百token1024足够覆盖多三元组句子设太小会导致JSON被截断这里的llm_call是对内部模型服务接口的封装无论是自建还是商业模型统一成system和user两个参数即可方便切换后端。LLM输出不能直接入库返回的JSON里每个三元组只能作为半成品候选后续还要过实体链接和人工复核。3.4 实体链接把“HbA1c”和“糖化血红蛋白”收拢到同一词表LLM返回的“糖化血红蛋白”和库里维护的“HbA1c”可能是同一概念实体链接解决的就是归一化。先在本地维护一张synonym表覆盖缩写、中英文写法和表述差异用最快速度做字符级匹配匹配不上的再考虑向量相似度。synonyms { 糖化血红蛋白: [hba1c, 糖化血红蛋白a1c], 空腹血糖: [fpg, 空腹血浆葡萄糖], } def link_entity(text: str) - str: text text.replace( , ).lower() for canonical, variants in synonyms.items(): if text canonical.lower() or text in [v.lower() for v in variants]: return canonical return UNMAPPED: text实体链接失败的条目带UNMAPPED前缀统一进入工单队列不自动落库。映射不上的原因绝大多数是词表不全补充synonym后重跑一遍即可。整个AI辅助构建的关键在于把模型输出当“初稿”而不是“定稿”人工审阅环节始终保留这也是知识代码和纯模型生成内容的本质区别。4. 知识代码质检冲突检测、相似规则发现与版本演进4.1 用分组key做冲突检测同一前提不能指向不同动作知识代码数量过千后最先暴露的问题是重复和冲突。两条规则的domain和precondition完全相同action却不一样推理引擎就不知道该听谁的。检测方法不复杂只要按domain加precondition做分组key去聚合。from collections import defaultdict def find_conflicts(rules) - list[dict]: groups defaultdict(list) for r in rules: key (r[domain], r[precondition]) groups[key].append(r) conflicts [] for key, items in groups.items(): actions {i[action] for i in items} if len(actions) 1: conflicts.append({ key: key, code_ids: [i[code_id] for i in items], actions: sorted(actions) }) return conflicts这里用tuple作为分组key把domain和precondition拼在一起天然保证两个规则只有在业务域和触发条件都一致时才会被分到同一组。action集合超过1说明存在歧义必须人工裁定。注意这里没有比较version因为同一code的不同version本来就是前后关系不属于冲突只有不同code_id之间才有冲突问题。4.2 向量相似度挖掘疑似同义规则字符串完全相同的冲突好查但“hba1c9.0”和“糖化血红蛋白数值不低于9%”这种写法不同、语义相同的情况靠SQL查不出来。可以给规则表达式计算向量再两两比较余弦相似度阈值定在0.93以上进入人工复核而不是直接自动合并。from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(your_similarity_model_path) express [r[precondition] for r in rules] embs model.encode(express, normalize_embeddingsTrue) review_candidates [] for i in range(len(embs)): for j in range(i 1, len(embs)): cos float(np.dot(embs[i], embs[j])) if cos 0.93: review_candidates.append((rules[i][code_id], rules[j][code_id], cos))这里把embedding做了归一化余弦相似度直接等于点积省掉一次除法。0.93这个阈值来自经验低于它会有太多误报高于0.96基本就是同一句换皮需要结合业务判断。向量相似度只用来缩小人工范围不该替代人工决策因为表达式的数值不同但文本结构相似的情况相似度反而会高比如“hba1c8.0”和“hba1c9.0”。4.3 版本演进新指南发布时旧代码如何超迁指南会更新MMC的管理路径也会调整知识代码必须支持多版本共存。创建新版本时把旧版本置为superseded而不是物理删除是这里最值得说清楚的一点。原因是历史病历可能按旧规则执行过后续要复盘时还得查得到当时依据。版本指南年份变更摘要受影响code_id数处理方式v12022初建120保留v22024血压控制目标调整34旧规则置superseded新建规则并挂新证据批量超迁时可以用一条更新语句统一处理但前提是必须记录operated_by和operated_at两个审计字段。人工误操作在知识工程里很常见没有审计字段就无法回滚。建议每次发版前导出全部conflict报告发版后第二天再拉一次告警确认没有新增冲突才关闭工单。5. 知识代码上线在MMC随访场景中做规则评估与缓存失效5.1 极简表达式解释器一条规则如何被实时触发知识代码最终要在随访系统里跑起来输入是化验值和患者状态输出是建议动作。这里用一个极简表达式解释器把precondition字段里的文本转成布尔结果适合规则数量在千级以内的场景再往上就需要换成AST解析器。import re import operator OPS { : operator.ge, : operator.le, : operator.eq, : operator.gt, : operator.lt, !: operator.ne, } def eval_precondition(expr, lab_values, flags): def eval_one(cond): for op in OPS: k, _, v cond.partition(op) k k.strip() if k.startswith(lab.): return OPS[op](lab_values[k[4:]], float(v)) if k.startswith(flag.): return OPS[op](flags.get(k[5:], False), v.lower() true) raise ValueError(f无法解析条件: {cond}) parts re.split(r\s(AND|OR)\s, expr) result, cur_op None, AND for part in parts: if part in (AND, OR): cur_op part continue val eval_one(part) result val if result is None else ( result and val if cur_op AND else result or val ) return bool(result)执行流程按空格切分AND和OR逐段求值后合并labor开头的条件读lab_values字典flag开头的条件读患者状态布尔值。这段代码只处理扁平逻辑表达式不支持嵌套括号生产环境要扩展时建议换成熟表达式引擎但接口可以保持eval_precondition(expr, lab_values, flags)不动。示例调用lab {hba1c: 9.2, bps: 145} flags {no_hypoglycemia_3m: True} expr lab.hba1c 9.0 AND flag.no_hypoglycemia_3m true print(eval_precondition(expr, lab, flags)) # True调用结果True表示这条知识代码被触发CDSS任务调度器就可以读action字段去创建随访任务。5.2 缓存命中与软删除上线后最容易忽略的两个细节规则评估本身很快但MMC门诊高峰时段并发高每次查询都全表扫一遍规则不可取。缓存键建议用sha1(patient_id code_id)拼接值存评估结果和评估时间key里带上code_id可以让某条规则超迁时精确失效不用清空整个缓存。失效时机选在4.3节的批次更新任务完成后代码里删除对应key即可。另一个细节是软删除。知识代码表上保留deleted_at字段只有确认为错误创建的记录才真正删除正常消亡流程一律走superseded状态。消费端所有查询统一加WHERE deleted_at IS NULL历史规则只做追溯不做下发线上行为和审计记录两边都能解释清楚。这两条配合前面章节的质检流程知识代码从创建、上线到回收的每一步都能定位到人才算真正具备资产管理能力。本文还有配套的精品资源点击获取
返回列表