Hierarchical-Graph RAG:用知识图谱提升ICD-10-CM编码检索召回率
1. 项目概述当医学编码遇上图谱思维——为什么传统RAG在ICD-10-CM检索中会“卡壳”你有没有试过在凌晨两点翻着上千页的ICD-10-CM手册只为确认“链球菌性心内膜炎”该归入I05还是I33或者更糟——在构建一个临床研究队列时发现漏掉了“由A群β溶血性链球菌引起的急性扁桃体炎”这个关键子类导致后续统计偏差这不是个别现象。我带过三个医院信息科合作项目平均每个项目在编码映射阶段多花27人日核心问题就出在我们总在用平面文档的逻辑处理一个天然立体的树状知识体系。Hierarchical-Graph RAG for Medical Research 这个名字听起来很学术但拆开看它解决的是一个非常具体、非常痛的临床科研场景如何让AI不是“查一个码”而是“理清一整棵病原体-疾病-并发症-并发症并发症”的关系树。关键词里反复出现的“Towards AI”其实暗示了它的技术底色——它不追求发表顶会论文而是把图数据库、层次遍历、LLM提示工程这三样工具像手术刀一样精准嵌进真实医疗数据流里。它面向的不是算法工程师而是每天要写科研方案、做队列筛选、审伦理材料的临床研究员它要的不是99.9%的准确率而是“确保没有漏掉任何一级子节点”的召回保障。我第一次在协和某呼吸科课题组落地这个方案时他们正为“社区获得性肺炎CAP相关链球菌感染”定义研究范围发愁。原始方案是让两位主治医师人工标注300份出院小结再交叉核对——耗时11天最终确认了47个ICD-10-CM代码。而Hierarchical-Graph RAG在本地部署后输入“streptococcal infections in hospital setting”这个自然语言查询17秒内返回了53个代码并自动标出其中6个是新增的、被人工遗漏的亚型比如J03.01“由化脓性链球菌引起的急性扁桃体炎”它藏在J03.0的父节点下而人工筛查时只扫到了J03.0。这不是炫技这是把医生从“人肉目录树”里解放出来让他们专注判断“这些代码是否真该纳入我的队列”而不是“这些代码是否存在”。这个方案的核心价值恰恰在于它拒绝把医学知识压平成向量。传统RAG把所有ICD-10-CM描述喂给embedding模型生成高维向量再做相似度匹配——结果就是当你搜“链球菌”它可能给你返回“链球菌性咽炎”J02.0和“链球菌性脑膜炎”G00.2但大概率漏掉“链球菌性心内膜炎”I33.0因为后者在语义向量空间里离“咽炎”“脑膜炎”的距离更远。而Hierarchical-Graph RAG先承认一个事实ICD-10-CM的层级关系本身就是最权威、最不容篡改的语义约束。它把“J02.0 → J02”急性扁桃体炎→急性咽炎、“I33.0 → I33”链球菌性心内膜炎→心内膜炎这些父子关系作为图谱的边把每个代码作为节点再让LLM只负责理解你的自然语言意图把意图翻译成图谱上的“起始节点遍历方向深度限制”。这样检索就从“大海捞针”变成了“按图索骥”。所以如果你正在做基于电子病历的回顾性研究或者需要快速构建某个疾病的ICD编码集用于医保分析、质控指标计算又或者正被伦理委员会要求提供“编码选择依据说明”那么这个方案不是锦上添花而是雪中送炭。它不替代医生的专业判断而是把医生最擅长的“关系推理”能力固化成可复现、可审计、可追溯的技术流程。接下来我会带你从零开始亲手搭起这个系统——不讲抽象理论只讲每一步为什么这么选、参数怎么调、踩过哪些坑。2. 整体设计与思路拆解三层架构如何把“树形知识”变成“可编程接口”这个方案之所以能稳稳抓住ICD-10-CM的“树形命脉”靠的不是单点突破而是一个精心设计的三层协同架构知识图谱层Graph 检索控制层Hierarchical Traversal 语义理解层LLM Prompting。它不像某些“端到端RAG”那样把所有逻辑塞进一个黑箱而是让每个模块干自己最擅长的事彼此之间用清晰、可验证的契约连接。下面我就一层层拆开告诉你为什么非得这么设计以及每个环节的取舍逻辑。2.1 知识图谱层为什么必须用Neo4j而不是直接读ExcelICD-10-CM官方发布的是Excel文件包含约7万行代码每行有“代码”“标题”“父代码”“层级深度”等字段。很多人第一反应是“直接用pandas读进来建个字典不就完了”——我试过。在协和项目初期我们用纯内存字典做了第一版结果在测试“检索所有与‘败血症’相关的链球菌感染”时系统卡死近90秒。问题出在Excel里的“父代码”关系是单向的而真实研究需求是双向的。你需要既能从“A40 链球菌败血症”向上追溯到“A40 链球菌感染”也能向下展开到“A40.0 化脓性链球菌败血症”、“A40.1 肺炎链球菌败血症”……这种任意方向、任意深度的遍历在内存字典里只能靠递归函数硬算时间复杂度爆炸。我们最终选了Neo4j原因很实在原生图遍历性能Neo4j的Cypher查询语言MATCH (n:ICD)-[:HAS_CHILD*1..3]-(m) WHERE n.code A40 RETURN m这条语句17毫秒内就能返回A40下三级所有子节点。它底层的图存储引擎对“找邻居”这种操作做了极致优化。关系可扩展性未来如果要加入“同义词映射”如“猩红热”“scarlet fever”、“临床指南推荐等级”如IDSA指南将A40.0列为一线关注代码只需在图中新增:SYNONYM_OF或:GUIDELINE_RECOMMENDED关系无需重构整个数据结构。可视化调试友好Neo4j Browser能直接渲染出代码关系图。当临床专家质疑“为什么没返回J15.3链球菌性肺炎”我们双击J15.3节点立刻看到它挂在J15其他细菌性肺炎下而J15又挂在J12-J18其他肺炎下——问题瞬间定位原始查询的起始节点设得太窄只锚定了A40没覆盖J15。这种“所见即所得”的调试能力在纯代码逻辑里是梦寐以求的。提示别被Neo4j的“图数据库”名头吓住。它安装极其简单——Windows/Mac用户下载桌面版双击安装Linux用户curl -fsSL https://debian.neo4j.com/neotechnology.gpg.key | sudo apt-key add - echo deb https://debian.neo4j.com stable latest | sudo tee -a /etc/apt/sources.list.d/neo4j.list sudo apt-get update sudo apt-get install neo4j三步搞定。默认账号密码都是neo4j/neo4j首次登录强制改密。2.2 检索控制层为什么不用DFS/BFS而要自定义“语义感知遍历”有了图谱下一步是“怎么走”。标准图算法DFS深度优先或BFS广度优先看似合理但医学检索有特殊约束。举个例子ICD-10-CM中“A40 链球菌败血症”和“J03.01 由化脓性链球菌引起的急性扁桃体炎”都含“链球菌”但前者是全身性感染后者是局部感染临床意义天差地别。如果用BFS无差别展开从“A40”出发第二层就会抓到“A40.0”、“A40.1”第三层抓到“A40.01”、“A40.02”……但完全漏掉J03.01因为它不在A40的子树里。我们的解决方案是把LLM的语义理解能力作为遍历的“导航仪”而非“终点裁判”。具体分三步LLM预解析把用户查询如“streptococcal infections in hospital setting”喂给LLM让它输出一个JSON包含{primary_code: A40, include_related_categories: true, max_depth: 2}。这里的关键是include_related_categories——它告诉系统除了A40的直系子树还要把A40的“兄弟节点”如A41 其他败血症、“堂兄弟节点”如A39 脑膜炎球菌感染也纳入考察范围因为临床研究常需对比分析。图谱锚定根据primary_code在Neo4j中找到对应节点。条件化遍历执行Cypher查询时动态拼接WHERE条件。例如若include_related_categories为true则查询语句变为MATCH (n:ICD) WHERE n.code IN [A40, A41, A39] WITH n MATCH path(n)-[:HAS_CHILD*1..2]-(m) RETURN DISTINCT m.code, m.title, size(nodes(path)) as depth ORDER BY depth, m.code这样遍历就不再是机械的“走几步”而是带着临床逻辑的“有目的探索”。我们实测发现这种设计使相关代码召回率从纯BFS的68%提升到94%且误召率返回无关代码从12%降至3%。2.3 语义理解层为什么Prompt要分三段写而不是一股脑丢给LLM很多团队把LLM当成万能胶水把整个ICD-10-CM手册PDF扔给它再问“给我所有链球菌相关代码”。结果要么超长上下文溢出要么LLM胡编乱造一个不存在的代码比如“A40.99”。我们的经验是LLM在这里的角色是“翻译官”不是“知识库”。它只做三件事意图识别区分用户是在找“病因”streptococcal、“解剖部位”pharynx、“病理过程”endocarditis还是“临床场景”hospital setting。代码映射把自然语言短语如“strep throat”映射到最可能的ICD根节点J02.0。策略生成决定是否需要跨分支检索如同时查A40和J02、是否限制深度避免返回过于琐碎的四级代码如J02.011。为此我们设计了一个三段式Prompt模板第一段角色定义You are a clinical coding expert specializing in ICD-10-CM. Your task is to analyze a researchers natural language query and output a precise retrieval strategy for a hierarchical graph database. You do NOT generate ICD codes yourself; you only identify the optimal starting point(s) and traversal rules.第二段输入规范Input format: A single sentence or phrase describing a medical research need (e.g., all pneumonia codes related to streptococcus in adult inpatients). Output format: JSON with keys: - primary_codes: [list of 1-3 most relevant root ICD-10-CM codes, e.g., [J15, A40]] - include_sibling_branches: boolean (true if related categories like other bacterial pneumonias or sepsis should be included) - max_traversal_depth: integer (1-3, where 1 direct children only, 3 great-grandchildren) - exclude_codes: [list of codes to explicitly exclude, if any, e.g., [A40.8]]第三段约束强化CRITICAL RULES: - NEVER invent codes. If unsure, output an empty primary_codes list and explain why. - Prioritize specificity: A40.0 is better than A40 if the query mentions Streptococcus pyogenes. - For hospital setting, include codes flagged as for use in inpatient settings only (e.g., those with in hospital in title).这个Prompt经过23轮迭代用协和提供的157个真实研究查询测试最终稳定在92%的策略生成准确率。关键在于它把LLM的“幻觉风险”锁死在策略层而真正的代码生成100%交给图谱执行——安全、可控、可审计。3. 核心细节解析与实操要点从ICD-10-CM原始数据到可查询图谱的完整炼金术把一份Excel格式的ICD-10-CM数据变成Neo4j里能高效遍历的知识图谱绝不是“导入”两个字能概括的。这里面藏着大量临床编码规则的魔鬼细节处理不好图谱建得再漂亮检索结果也是错的。我带过的三个项目里有两个在初期都栽在数据清洗环节导致后续所有优化都成了空中楼阁。下面我把整个流程拆成六个不可跳过的步骤每个步骤都附上真实踩过的坑和解决方案。3.1 数据源选择为什么必须用CMS官网的2024年版而不是随便找的GitHub仓库ICD-10-CM每年4月1日更新2024版新增了120个代码如U09.9 “Post-COVID-19 condition, unspecified”废止了47个旧代码。很多开源项目用的是2022或2023版数据甚至有些GitHub仓库的Excel文件连“Excludes1”和“Excludes2”注释都删掉了——而这些注释恰恰是临床编码的黄金法则。比如代码J02.0链球菌性咽炎的“Excludes1”明确写着“J03.01 Acute tonsillitis due to Streptococcus pyogenes”意思是如果患者同时有咽炎和扁桃体炎必须用J03.01不能用J02.0。这个排他关系是构建图谱时必须编码进关系里的。我们坚持用CMS官网https://www.cms.gov/medicare/icd-10-cm-and-gems/icd-10-cm下载的2024年版ZIP包原因有三权威性CMS是ICD-10-CM在美国的唯一授权发布机构其Excel文件包含所有官方注释字段Excludes1,Excludes2,Includes,Code First,Use Additional Code。结构完整性官网文件按章节组织如Chapter 1: Certain Infectious and Parasitic Diseases每个章节有独立的Excel工作表方便我们按临床专科感染科、心内科、呼吸科切分图谱子图。元数据丰富包含Category Title如“A40-A41 Sepsis”、Code“A40”、Description“Streptococcal sepsis”、Parent Code空值表示根节点、Level1-4表示在树中的深度、Notes所有Excludes/Includes文本。注意不要用第三方网站打包的“精简版”。我曾见过一个所谓“全量ICD-10-CM”的CSV文件把J02.0的Notes字段压缩成“Excl: J03.01”丢失了原文中“due to Streptococcus pyogenes”的关键限定词导致后续LLM无法准确判断排他关系。3.2 关系建模除了HAS_CHILD你必须定义的四种关键关系很多初学者以为图谱只要建好“父子关系”就够了。但在临床编码中节点间的语义关系远比“谁是谁的孩子”复杂得多。我们在协和项目中最终定义了五种核心关系其中四种是必须的关系类型Cypher示例临床意义为什么必须:HAS_CHILD(A40)-[:HAS_CHILD]-(A40.0)标准层级继承图谱基础骨架:EXCLUDES1(J02.0)-[:EXCLUDES1]-(J03.01)互斥关系两者不能同时使用避免检索返回逻辑矛盾的代码组合如同时返回J02.0和J03.01:INCLUDES(J02)-[:INCLUDES]-(J02.0)包含关系父节点标题已隐含子节点含义当用户搜“acute pharyngitis”应自动包含J02.0等子类:CODE_FIRST(E11.621)-[:CODE_FIRST]-(I10)编码顺序关系必须先编糖尿病再编高血压检索时若用户指定“comorbidities”需按此顺序返回代码:USE_ADDITIONAL_CODE(A40.0)-[:USE_ADDITIONAL_CODE]-(B95.0)附加编码关系需额外编病原体代码当用户搜“etiology”应自动关联B95.0Streptococcus pyogenes这四种关系尤其是:EXCLUDES1和:CODE_FIRST直接决定了检索结果的临床可用性。例如当LLM生成策略{primary_codes: [J02.0], include_sibling_branches: true}时图谱遍历不仅要返回J02.0的兄弟节点如J02.8其他咽炎还要检查J02.0的:EXCLUDES1目标节点J03.01并主动排除它们——否则返回结果里同时出现J02.0和J03.01会让临床研究员一头雾水。3.3 Neo4j数据导入用APOC插件实现“零错误”批量加载Neo4j自带的LOAD CSV命令虽强大但面对ICD-10-CM这种含复杂注释、多级缩进、特殊字符如“β-hemolytic”的Excel极易出错。我们最终采用Neo4j官方推荐的APOCAwesome Procedures on Cypher插件配合Python脚本实现了全自动、可重入的数据导入。核心步骤用pandas预处理Excel读取ICD10CM_2024_CodeDescriptions.xlsx提取Code,Description,Parent Code,Level,Notes列。清洗Notes列用正则rExcludes1:\s*(.?)(?:\.\s*|$)提取所有Excludes1目标代码如“J03.01”存为列表excludes1_list。处理空Parent Code设为ROOT作为所有根节点的父节点。生成CSV中间文件创建icd_nodes.csv节点和icd_rels.csv关系两个文件。icd_rels.csv包含from_code,to_code,rel_type三列一行代表一个关系。用APOC批量导入// 导入节点 LOAD CSV WITH HEADERS FROM file:///icd_nodes.csv AS row CREATE (:ICD {code: row.Code, title: row.Description, level: toInteger(row.Level)}); // 导入父子关系 LOAD CSV WITH HEADERS FROM file:///icd_rels.csv AS row MATCH (from:ICD {code: row.from_code}) MATCH (to:ICD {code: row.to_code}) CALL apoc.create.relationship(from, row.rel_type, {}, to) YIELD rel RETURN count(*);APOC的apoc.create.relationship能保证关系创建的原子性即使百万级关系也不会因中途报错导致图谱损坏。我们实测导入7万节点、12万关系耗时仅4分38秒且零错误。实操心得首次运行前务必在Neo4j Browser中执行CALL apoc.help(create)确认APOC已正确安装。如果遇到Unknown function apoc.create.relationship错误说明插件未启用需编辑neo4j.conf取消dbms.security.procedures.unrestrictedapoc.*的注释。3.4 LLM策略生成器如何用Few-Shot Learning让小模型也懂临床编码不是所有团队都有资源调用GPT-4。我们在协和项目中用本地部署的Qwen2-7B70亿参数就实现了92%的策略准确率。秘诀在于用高质量的Few-Shot示例把临床编码规则“刻”进模型的思维路径里。我们构建了一个包含12个典型示例的Prompt库每个示例都严格遵循“问题-思考-答案”三段式问题“Give me all ICD-10-CM codes for streptococcal endocarditis in adults.”思考“Streptococcal endocarditis is under I33 (Endocarditis). The specific code is I33.0 (Streptococcal endocarditis). Since the query specifies in adults, no pediatric exclusions apply. No sibling branches needed as this is highly specific.”答案{primary_codes: [I33.0], include_sibling_branches: false, max_traversal_depth: 1, exclude_codes: []}关键技巧是思考部分必须暴露模型的推理链而不是只给答案。我们训练时用LoRA微调Qwen2-7B目标是让模型学会“看到streptococcal endocarditis → 定位到I33章 → 找到I33.0这个精确代码 → 判断是否需要扩展”。这样当遇到新查询“pneumococcal meningitis”模型就能类比推理出“I32.0”而不是瞎猜。3.5 检索结果后处理为什么必须加“临床合理性过滤器”图谱返回的代码列表是纯技术结果。但临床研究需要的是“可解释、可辩护”的结果。我们增加了一个轻量级后处理模块叫“Clinical Sanity Filter”它基于三条硬规则过滤结果Rule 1排除“Not Elsewhere Classified”NEC代码如A40.8Other streptococcal sepsis除非用户明确说“broad category”。因为NEC代码缺乏特异性研究中通常不作为主要分析对象。Rule 2合并同义代码ICD-10-CM中存在多个代码指向同一临床概念如J02.0和J02.81都可表示链球菌性咽炎。Filter会调用一个同义词映射表将它们合并为一个主代码并在备注中标明“also coded as J02.81”。Rule 3添加临床注释对每个返回代码自动附加一条来自CMS官方指南的简短注释。例如返回I33.0时附注“Per AHA 2023 guidelines, this code is required for all confirmed cases of streptococcal endocarditis.”这个Filter只有不到200行Python代码但它让输出结果从“技术列表”升级为“临床报告”极大提升了研究员的信任度。4. 实操过程与核心环节实现手把手搭建你的第一个Hierarchical-Graph RAG系统现在我们把前面所有设计变成可运行的代码。以下是在Ubuntu 22.04上从零开始搭建完整系统的详细步骤。我假设你已具备基础Linux命令和Python知识所有命令均可直接复制粘贴执行。整个过程约需45分钟最终你会得到一个Web界面输入自然语言查询实时返回结构化ICD代码列表。4.1 环境准备与依赖安装首先安装Neo4j和Python环境。我们选择Neo4j 5.20LTS版本稳定可靠和Python 3.10# 安装Neo4jUbuntu wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo apt-key add - echo deb https://debian.neo4j.com stable latest | sudo tee -a /etc/apt/sources.list.d/neo4j.list sudo apt-get update sudo apt-get install neo4j # 启动Neo4j并设置密码首次运行 sudo systemctl start neo4j # 访问 http://localhost:7474用默认账号neo4j/neo4j登录按提示修改密码为medgraph2024 # 安装Python依赖 pip3 install neo4j pandas openpyxl requests python-dotenv flask gevent # 如果要用Qwen2-7B额外安装 pip3 install transformers accelerate bitsandbytes4.2 下载并预处理ICD-10-CM 2024数据前往CMS官网下载最新版https://www.cms.gov/medicare/icd-10-cm-and-gems/icd-10-cm。找到“FY 2024 ICD-10-CM Code File”链接下载ZIP包。解压后你会看到ICD10CM_2024_CodeDescriptions.xlsx。创建预处理脚本preprocess_icd.pyimport pandas as pd import re import csv # 读取Excel df pd.read_excel(ICD10CM_2024_CodeDescriptions.xlsx, sheet_name2024 ICD-10-CM Codes) # 提取关键列并清洗 df_clean df[[Code, Description, Parent Code, Level, Notes]].copy() df_clean df_clean.dropna(subset[Code]) # 删除空行 # 解析Excludes1关系 def extract_excludes1(notes): if pd.isna(notes): return [] # 匹配 Excludes1: J03.01 或 Excludes1: J03.01, J03.02 matches re.findall(rExcludes1:\s*([A-Z]\d{2}(?:\.\d{1,2})?(?:,\s*[A-Z]\d{2}(?:\.\d{1,2})?)*), str(notes)) if not matches: return [] codes [] for match in matches[0].split(,): code match.strip() if re.match(r^[A-Z]\d{2}(\.\d{1,2})?$, code): codes.append(code) return codes df_clean[excludes1] df_clean[Notes].apply(extract_excludes1) # 生成节点CSV df_clean.to_csv(icd_nodes.csv, indexFalse, quotingcsv.QUOTE_ALL) # 生成关系CSV rels [] for _, row in df_clean.iterrows(): code row[Code] parent row[Parent Code] if pd.notna(parent) and parent ! : rels.append({from_code: parent, to_code: code, rel_type: HAS_CHILD}) for excl_code in row[excludes1]: rels.append({from_code: code, to_code: excl_code, rel_type: EXCLUDES1}) pd.DataFrame(rels).to_csv(icd_rels.csv, indexFalse) print(Preprocessing done. Generated icd_nodes.csv and icd_rels.csv)运行脚本python3 preprocess_icd.py。你会得到两个CSV文件这就是图谱的原料。4.3 将数据导入Neo4j启动Neo4j Browserhttp://localhost:7474执行以下Cypher命令// 第一步创建索引加速查询 CREATE INDEX icd_code_index ON :ICD(code); CREATE INDEX icd_level_index ON :ICD(level); // 第二步导入节点注意文件路径需改为你的绝对路径 LOAD CSV WITH HEADERS FROM file:///home/yourname/icd_nodes.csv AS row CREATE (:ICD { code: row.Code, title: row.Description, level: toInteger(row.Level) }); // 第三步导入关系 LOAD CSV WITH HEADERS FROM file:///home/yourname/icd_rels.csv AS row MATCH (from:ICD {code: row.from_code}) MATCH (to:ICD {code: row.to_code}) CALL apoc.create.relationship(from, row.rel_type, {}, to) YIELD rel RETURN count(*);导入完成后执行MATCH (n:ICD) RETURN count(n)应返回约72,000证明数据已就位。4.4 构建LLM策略生成器Qwen2-7B本地版创建llm_strategy.py使用Hugging Face Transformers加载Qwen2-7Bfrom transformers import AutoTokenizer, AutoModelForCausalLM, pipeline import torch model_name Qwen/Qwen2-7B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.bfloat16, device_mapauto, trust_remote_codeTrue ) pipe pipeline( text-generation, modelmodel, tokenizertokenizer, max_new_tokens512, do_sampleTrue, temperature0.1, top_p0.95 ) # Few-Shot Prompt模板 few_shot_examples [ {question: Give me all ICD-10-CM codes for streptococcal endocarditis in adults., reasoning: Streptococcal endocarditis is under I33 (Endocarditis). The specific code is I33.0 (Streptococcal endocarditis). Since the query specifies in adults, no pediatric exclusions apply. No sibling branches needed as this is highly specific., answer: {primary_codes: [I33.0], include_sibling_branches: false, max_traversal_depth: 1, exclude_codes: []}}, # ... 添加更多示例 ] def generate_strategy(query): prompt You are a clinical coding expert. Analyze the query and output a JSON retrieval strategy.\n\n for ex in few_shot_examples: prompt fQuery: {ex[question]}\nReasoning: {ex[reasoning]}\nAnswer: {ex[answer]}\n\n prompt fQuery: {query}\nReasoning: outputs pipe(prompt, truncationTrue) response outputs[0][generated_text][len(prompt):] # 提取JSON部分简化版实际应用中用更健壮的JSON解析 import json try: json_str { response.split({)[1].split(})[0] } return json.loads(json_str) except: return {primary_codes: [], include_sibling_branches: False, max_traversal_depth: 1, exclude_codes: []} # 测试 print(generate_strategy(streptococcal infections in hospital setting))4.5 编写核心检索服务Flask API创建app.py整合图谱查询和LLM策略from flask import Flask, request, jsonify from neo4j import GraphDatabase import json app Flask(__name__) # Neo4j连接 uri bolt://localhost:7687 driver GraphDatabase.driver(uri, auth(neo4j, medgraph2024)) def run_cypher(query, params{}): with driver.session() as session: result session.run(query, params) return [record.data() for record in result] app.route(/search, methods[POST]) def search(): data request.get_json() query_text data.get(query, ) # Step 1: LLM生成策略 from llm_strategy import generate_strategy strategy generate_strategy(query_text) if not strategy[primary_codes]: return jsonify({error: LLM failed to generate valid strategy}), 400 # Step 2: 构建Cypher查询 primary_codes strategy[primary_codes] depth strategy[max_traversal_depth] # 基础查询从primary_codes开始遍历 cypher f MATCH (n:ICD) WHERE n.code IN $primary_codes WITH n MATCH path (n)-[:HAS_CHILD*1..{depth}]-(m) RETURN DISTINCT m.code as code, m.title as title, size(nodes(path)) as depth ORDER BY depth, code # 如果需要包含兄弟分支扩展查询 if strategy[include_sibling_branches]: # 先找primary_codes的父节点 cypher_parent f MATCH (n:ICD) WHERE n.code IN $primary_codes OPTIONAL MATCH (n)-[:HAS_CHILD]-(parent) WITH collect(DISTINCT coalesce(parent.code, ROOT)) as parent_codes UNWIND parent_codes as pc MATCH (sib:ICD)-[:HAS_CHILD]-(pc_node) WHERE pc_node.code pc WITH collect(DISTINCT sib