MedMind RAG:为什么我在医疗场景用 GraphRAG 做知识扩展

写在前面

做 MedMind 这个项目之前,我对 RAG 的理解停留在“把文档切块、向量化、检索、生成”这套流程。

这套流程在菜谱问答那个项目里跑得挺顺,但切到医疗场景之后,我发现一个问题一直解决不好:用户问的是感冒,但他真正需要的答案里还涉及发热处理和布洛芬用法,而这两块知识在知识库里是三个独立的条目。

单纯靠相似度检索,召回的永远是最像“感冒”的那几条,关联知识捞不出来。

这篇文章记录我为什么引入 GraphRAG,怎么设计的,以及踩了哪些坑。

说明:这个项目只用于医疗知识问答和 RAG 检索能力演示,不替代医生诊断,也不提供个人用药建议。


问题是怎么暴露的

MedMind 的知识库按疾病/药物组织,每个条目是一个独立的 chunk。比如:

  • 内科-感冒:病因、症状、治疗原则
  • 内科-发热:分级、处理方法、就医指征
  • 药学-布洛芬:适应症、用法用量、注意事项

用户问“感冒了发烧怎么办”,理想的答案需要同时覆盖这三块内容。

但实际检索结果是:相似度最高的永远是 内科-感冒 这条,内科-发热 勉强能召回,药学-布洛芬 基本捞不到,因为用户的问题里没有出现“布洛芬”这个词。

多查询扩展能部分缓解这个问题,但治标不治本。用户问的是症状,不是药名,扩展出来的变体也大概率不包含具体药物名称。


为什么想到用图谱

医学知识本身就是有结构的:

感冒 -> 常见症状:发热 -> 退热用药:布洛芬 / 对乙酰氨基酚

这种关联关系不是语义相似,是明确的领域知识。向量检索擅长处理语义相似,但处理不了这种“A 会导致 B,B 的处理需要 C”的推理链。

图谱天然适合表达这种关系。


怎么实现的

我用 NetworkX 建了一个有向图,节点是知识库里的每个条目,边是条目之间的关联关系。

关联关系是我手动定义的,按照临床逻辑梳理:

1
2
3
4
5
6
7
8
MEDICAL_RELATIONS = [
("内科-普通感冒", "内科-发热", "常见并发症"),
("内科-发热", "药学-布洛芬", "退热用药"),
("内科-发热", "药学-对乙酰氨基酚", "退热用药"),
("内科-高血压", "心血管科/神经科-冠心病", "并发症"),
("内科-2型糖尿病", "内科-慢性肾脏病", "糖尿病肾病"),
# ...共 28 条关联边
]

图谱扩展的逻辑很简单:混合检索拿到初始候选集之后,对每个召回节点找它在图上的邻居节点,把邻居对应的知识条目也加进候选集,再一起进 LLM 重排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def expand_with_graph(docs, chunks, graph, max_expansion=3):
"""
基于知识图谱扩展候选集
docs: 混合检索初始结果
chunks: 完整知识库
graph: NetworkX 有向图
max_expansion: 每次最多扩展几个关联节点
"""
existing = {d["metadata"]["source"] for d in docs}
to_add = []

for doc in docs:
src = doc["metadata"]["source"]
if src in graph:
# 同时找后继节点(并发症/用药)和前驱节点(上级疾病)
neighbors = list(graph.successors(src)) + list(graph.predecessors(src))
for neighbor in neighbors:
if neighbor not in existing and neighbor not in to_add:
to_add.append(neighbor)
if len(to_add) >= max_expansion:
break

# 从知识库里找对应内容加入候选集
chunk_map = {c["metadata"]["source"]: c for c in chunks}
expanded = list(docs)
for source in to_add[:max_expansion]:
if source in chunk_map:
expanded.append({
**chunk_map[source],
"graph_expanded": True, # 标记来源,方便调试
})

return expanded

整个扩展在重排序之前完成,扩展进来的条目会和原始召回一起打分,由 LLM 决定最终保留哪几条。


效果怎么样

以“感冒发烧怎么办”为例,加图谱扩展之前:

召回顺序 条目
1 内科-普通感冒
2 内科-流行性感冒
3 内科-咳嗽

加图谱扩展之后,候选集里多了:

  • 内科-发热(感冒的常见并发症)
  • 药学-布洛芬(发热的退热用药)
  • 药学-对乙酰氨基酚(发热的退热用药)

经过 LLM 重排序,最终进入生成阶段的是感冒 + 发热 + 布洛芬三条,回答的完整度明显提升。


踩过的坑

图谱规模不能太大。 我一开始想把所有疾病关联都建进去,结果扩展出来的候选集太大,噪音也多,重排序的负担变重,反而影响了精度。后来控制在每次最多扩展 3 个节点,效果更稳。

关联关系要靠谱。 手动定义关系的好处是可控,坏处是要花时间梳理。我参考了内科、急诊科的临床路径,把“常见并发症”“首选用药”“鉴别诊断”这几类关系单独区分,避免把不相关的条目扯进来。

图谱扩展不能替代检索。 有几次我测试“布洛芬和对乙酰氨基酚有什么区别”这类直接问药的问题,图谱扩展帮不上什么忙,还是要靠 BM25 的关键词匹配把药名精确召回。两者是互补关系,不是替代关系。


现在的状态

目前知识库覆盖 8 个科室,53 个知识条目,图谱有 31 个节点、28 条关联边。

GraphRAG 这块还有很多可以做的,比如关联关系自动抽取(现在是手动定义)、多跳推理的深度控制、图谱和向量的联合评分。这些留着后面有时间继续探索。

项目 demo 部署在博客上,感兴趣可以直接体验:zxyblog.top/medical-rag


MedMind RAG:为什么我在医疗场景用 GraphRAG 做知识扩展
https://zxyblog.top/2026/05/19/MedMind-RAG-为什么我在医疗场景用GraphRAG做知识扩展/
作者
zxy
发布于
2026年5月19日
许可协议