知识图谱增强RAG:基于dify和neo4j的关系问答Demo搭建
简介:面向人工智能应用开发者和知识图谱入门者,这份用Dify搭建基于知识图谱的RAG系统的演示课件材料,定位清晰,能帮助学习者快速理解GraphRAG的落地路径。资源包共3个文件,包含2份JSON格式的食谱语料数据集和1份YAML格式的GraphRAG编排配置,前者提供实体、关系标注的测试语料,后者定义了图谱构建与检索增强流程,压缩包整体约633MB,可直接用于复现演示环境。这套课件材料已有2586人学习,围绕真实食谱场景组织内容,读者通过数据集与配置文件的搭配,可以掌握Dify中知识图谱构建、实体关系抽取以及检索增强生成的关键环节,并进一步迁移到自己的业务场景中设计RAG方案。 做技术分享这几年,我越来越觉得普通的RAG演示很容易让听众产生一种错觉:好像只要把文档扔进知识库,大模型就能回答一切。但真正动手测过几个案例之后,就会发现纯粹靠向量检索的RAG在关系类问题上经常翻车,比如“A公司和B公司之间是什么关系”“某个项目的核心成员都有谁”。这类问题不是靠语义相似度就能解决的,它需要结构化的事实推理。后来我用dify搭了一套基于知识图谱的RAG Demo,把neo4j里的图谱查询接进了dify工作流,整套演示课件做下来,效果比单靠知识库检索好了一个量级。这篇文章就把这个Demo的完整搭建过程、工作流设计思路、课件材料组织方式以及我踩过的坑都梳理出来,给准备做RAG课程或技术分享的同学做个参考。
1. 为什么普通RAG在关系问答上会翻车
1.1 向量检索的天然短板
普通RAG的流程大家都熟:文档切片、向量化、根据用户问题做相似度召回、把命中的片段拼进上下文交给大模型生成答案。这个流程在处理“XX系统怎么配置”“XX功能是什么”这类有明确答案的问题时表现不错,因为答案通常就藏在某一段文字里,向量召回能把相关片段捞出来。
但关系类问题是另一回事。你问“甲公司和乙公司之间有没有股权关联”,这个问题需要抽取两个实体,再到关系网络里找它们之间的路径。文档切片之后,这些关系可能散落在不同段落甚至不同文件里,向量检索各自召回的内容根本无法拼出一条完整的关系链路。更别说“A通过什么关系与B产生联系”这种多跳问题,普通RAG基本只能靠大模型的训练知识瞎猜,猜错了也不会被发现。
我自己测过一个典型的例子:把一份虚构的公司图谱文档丢进dify知识库,然后问它“丙公司持有丁公司多少股份”,知识库检索出来的片段可能是大段经营描述,模型只能从文字缝隙里找线索,经常回答出“无法确定”或者干脆编造一个数字。这个问题的本质是,向量检索做的是语义匹配,不是结构化推理,它缺的是一张能精确表达实体关系的地图。
1.2 知识图谱恰好补的是这一环
知识图谱本质上就是把实体变成节点、把关系变成边,构成一张可查询的关系网络。在neo4j里,我们可以直接用Cypher语句查询“某个节点走几步能到另一个节点”“两个实体之间的全部路径”,这种能力是“语义相似”替代不了的。
我在这个Demo里的设计思路很简单:让dify负责两件事——理解用户意图和生成最终回答;让neo4j负责一件事——提供精确的图谱事实。dify工作流里先跑一个意图判断,如果识别到问题涉及实体关系,就调用图谱查询工具,把查询结果以结构化文本的形式拼进上下文,再由LLM组织成自然语言回答。这样一来,大模型不再需要从碎片文本里“猜关系”,它只需要把图谱返回的事实翻译成用户能看懂的话。
这套结构如果你要拿来给学员讲解,建议从“向量检索为什么不够用”切入,再引出图谱补充,逻辑上会顺很多。下一节先把环境搭起来。
2. 先把底子搭好:dify社区版部署与neo4j环境准备
2.1 用Docker Compose快速跑起dify
dify社区版是完全开源的,用Docker Compose部署是最省事的方式。官方仓库的docker目录里已经写好了编排文件,基本流程是:
- 把dify仓库克隆到服务器本地,我建议单独建一个目录,不要直接放在root下,方便管理和升级。
- 进入
docker目录,复制环境变量模板:cp .env.example .env。 - 检查
.env里的端口配置,默认情况dify的Web入口是80端口,如果你的服务器上已经有Nginx或其他服务占用80,需要改成别的端口。 - 执行
docker compose up -d拉取镜像并启动,这一步骤耗时取决于网络,等所有容器变成healthy状态就可以了。
启动之后打开浏览器访问服务器IP加端口,第一次进入会让你配置管理员账号。这里有一个细节:dify的模型供应商配置必须在管理员初始化阶段就完成,否则后续创建工作空间时没法选模型。我建议用兼容OpenAI接口的方式接入模型服务,方便换不同的模型做对比测试,如果用的是本地部署的推理服务,也要先确认dify能连通对应的接口地址。
部署完成后,去“设置-模型供应商”里添加至少一个对话模型和一个embedding模型。embedding模型用于知识库的向量化,如果你的知识库数据不复杂,用默认选项就行。别一上来就追求多模型配置,Demo阶段越简单越稳。
2.2 Neo4j环境与数据导入前的准备
Neo4j我直接用的官方容器镜像,一条命令就能跑起来:
docker run -d \
--name neo4j-demo \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/yourpassword \
neo4j:5-community
7474是浏览器管理界面,7687是Bolt协议端口,dify工作流里封装的自定义查询服务要通过7687连接数据库。需要注意新版本的Neo4j容器默认不能以root用户直接挂载数据卷,如果后面要挂载数据目录,记得加 --user=neo4j 或者调整目录权限。
数据库启动后,建议先在浏览器界面里创建一个测试节点,确认Cypher语句能正常执行。这里有个小技巧:Demo用的图谱数据不需要太大,几十个节点、几十条关系就足够了,关键是关系类型要清晰。比如我常用来上课的例子是“公司-项目-人员”三类节点,关系包括“投资”“参与”“任职”,这种规模的数据既能在页面上展示图谱全貌,又不会让查询逻辑复杂到学员看不懂。
3. 知识图谱从零构建:实体、关系、质量与索引
3.1 从数据到三元组的转化方法
很多教程一上来就让你用大模型自动抽取实体关系,但在Demo场景里完全没必要这么搞。我建议直接用CSV文件导入,既可控又能讲清楚知识图谱的基本概念。
先准备三份数据:节点文件一份是公司、一份是人员,关系文件一份记录投资关系、一份记录任职关系。举个例子,人员CSV长这样:
id,name,position
p1,张伟,CEO
p2,李娜,CTO
p3,王强,产品总监
公司CSV:
id,name,industry
c1,星河科技,人工智能
c2,蓝海资本,投资
c3,云图数据,大数据
然后用Cypher的 LOAD CSV 语句批量导入。先导入节点:
LOAD CSV WITH HEADERS FROM 'file:///companies.csv' AS row
CREATE (:Company {id: row.id, name: row.name, industry: row.industry});
导入关系的时候要确保两端的节点已存在,所以节点导入要放在前面。接着导入投资关系:
LOAD CSV WITH HEADERS FROM 'file:///invest_relations.csv' AS row
MATCH (c1:Company {id: row.from_id})
MATCH (c2:Company {id: row.to_id})
CREATE (c1)-[:INVESTS {amount: toInteger(row.amount)}]->(c2);
这样图谱就有了雏形。把CSV文件放到Neo4j容器的 import 目录里,文件才能被 LOAD CSV 读取。实际操作中我最常犯的错就是忘记确认CSV表头和Cypher里的字段名一致,字段对不上导入就会静默失败,连报错都很隐晦。
3.2 约束、索引与Cypher查询验证
数据导入只是第一步,真正影响Demo稳定性的其实是约束和索引。如果每次重复导入数据时没有唯一约束,图谱里会出现大量重复节点,后续查询路径结果会变得异常混乱。所以导入前一定要先建立唯一约束:
CREATE CONSTRAINT company_name_unique FOR (c:Company) REQUIRE c.name IS UNIQUE;
CREATE CONSTRAINT person_name_unique FOR (p:Person) REQUIRE p.name IS UNIQUE;
约束建立之后,再用 MERGE 代替 CREATE 来导入节点,这样即使重复执行导入脚本,也不会产生重复数据。我每次构建完图谱都会跑一遍验证查询,比如看看“星河科技一共投资了哪些公司”:
MATCH (c:Company {name: '星河科技'})-[:INVESTS]->(target:Company)
RETURN target.name, target.industry;
如果返回结果正确,说明图谱基本可以用了。我再加一个多跳查询来测试路径能力,比如“张伟和蓝海资本之间有什么关系”:
MATCH p = shortestPath((a:Person {name: '张伟'})-[:*..4]-(b:Company {name: '蓝海资本'}))
RETURN p;
这类路径查询是知识图谱相对向量检索的典型优势,也是我在课件里必展示的对比点。
4. 在dify工作流里接入图谱查询:编排思路与工具封装
4.1 先想清楚两条检索路径如何合并
工作流设计上,最简单的结构是:用户问题进来之后,先用一个LLM节点判断是否需要图谱查询,判断结果是“需要”就走图谱查询分支,否则走普通知识库检索分支,最后把结果汇总到回答节点。这个“路由”设计在dify里实现起来并不复杂,可以选择参数提取节点,或者用直接对话节点让它输出一个JSON标志位。
我自己偏好的做法是先把用户问题交给一个带指令的LLM节点,让它提取“实体名称”和“问题类型”,输出成结构化字段。然后根据实体名称构造Cypher查询语句,调工具节点取回图谱数据。这样做的好处是,即使学员提问的措辞千变万化,只要实体被正确提取出来,图谱查询就能稳定命中。不好的地方是增加了延迟,但Demo阶段完全能接受。
两条路径合并时,我习惯让图谱查询结果优先进入上下文,因为图谱数据是精确的,知识库检索片段作为补充即可。用dify工作流里的变量聚合器把两段内容拼成一个 context 变量,传给最后的回答节点。
4.2 用API工具节点把Cypher查询封装成服务
dify社区版本身不直接内置Neo4j连接器,但工作流里提供工具节点,可以调用自定义API。所以我在Demo里写了一个很小的FastAPI服务,专门接收实体名称和查询类型,内部拼Cypher去查neo4j,返回结果JSON,这样一个服务就能同时支撑多种查询场景。
from fastapi import FastAPI, Query
from neo4j import GraphDatabase
app = FastAPI()
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "yourpassword"))
def query_relations(tx, entity_name):
result = tx.run(
"MATCH p = (n)-[*..3]-(m) "
"WHERE n.name = $entity "
"RETURN p LIMIT 10",
entity=entity_name
)
return [str(record["p"]) for record in result]
@app.get("/graph_query")
def graph_query(entity: str = Query(...)):
with driver.session() as session:
relations = session.execute_read(query_relations, entity)
return {"entity": entity, "relations": relations}
然后在dify工作流里添加一个OpenAPI工具,把FastAPI自动生成的openapi.json内容导入进去,工具节点就可以直接调用 /graph_query 接口了。这里有一个很容易被忽略的点:dify所在容器要能访问到FastAPI服务的地址,如果dify和服务都跑在同一台机器上但用了不同容器,要用 host.docker.internal 或者容器网络的别名,不能用 localhost 。
4.3 提示词与结果融合的小技巧
图谱查询返回的是路径文本,通常比较生硬,如果不做加工直接丢给LLM,回答会很干。我试过几种处理方式,效果最好的是在回答节点的提示词里加上一段说明:
你是一个基于知识图谱的问答助手。以下是知识图谱查询返回的事实路径,请基于这些路径组织答案。如果路径信息不足以回答问题,明确说明缺少哪些信息,不要编造关系。
这样LLM就会把路径“翻译”成人话,而不是把Cypher路径原样复述出来。另外要注意图谱查询结果为空的情况,需要设置兜底逻辑,比如判断返回值是否为空,为空就走纯知识库检索,避免学员的问题不在图谱覆盖范围内时拿到一个空白回答。
5. Demo课件的核心体验点与演示节奏控制
5.1 对比问题的设计是成败关键
课件材料里最重要的不是架构图,也不是代码截图,而是对比问题。我强烈建议每个Demo课件里预埋两到三类问题:
第一类是纯事实型问题,比如“云图数据的CEO是谁”,普通RAG和知识图谱RAG都能答对,用来展示基础问答能力。第二类是关系型问题,比如“星河科技投资了哪些公司”“张伟和蓝海资本有什么关系”,这类问题普通RAG基本答不准,而知识图谱RAG能给出完整路径。第三类是多跳问题,比如“张三投资的公司里,哪些公司和云图数据有业务往来”,这类问题最能体现图谱的路径查询能力。
演示顺序上,先跑普通RAG版本,把错误答案摆在台面上,再切到知识图谱RAG版本,让学员直观看到差异。这个对比冲击力远大于对着架构图讲十分钟原理。我建议课件里把问题和预期答案都写成备注页,演示时直接照着走,不用现编。
5.2 课件页面的结构安排与演示话术要点
我一般会把课件控制在8到10页左右,开场页交代“为什么需要知识图谱”,然后放一张整体架构图,中间留两页讲数据准备和工作流设计,之后是演示环节,最后留一页列出扩展思路。整个演讲控制在15分钟以内效果最好,超过20分钟学员注意力容易涣散。
架构图一定要自己画,不要直接用网上截下来的图。图上要体现“用户问题 -> dify工作流 -> 路由判断 -> 图谱查询/向量检索 -> LLM生成”这条主线,旁边用小字标注每个环节的输入输出。讲课的时候我会反复强调“图谱查询返回的是结构化事实,不是文本片段”,这句话基本能帮学员快速建立画面感。
演示环节建议提前录一份备用视频,万一遇到现场网络波动或者模型接口超时,可以直接切到视频,避免长时间等待的尴尬。即便没有录视频,也至少要准备好一个大模型的备用Key,dify里切换模型供应商只需要改配置,非常快。
5.3 预演时容易翻车的地方
预演翻车点我最少碰到过三次,而且每次都发生在最不该出现问题的地方。一个是neo4j服务没启动,工作流里图谱查询就超时,应对方式是开场前用 docker ps 检查所有容器状态。另一个是embedding模型效果不稳定,有的问题语义改写后召回结果变化很大,应对方式是把演示要问的问题固定下来,不要现场自由发挥。还有一个是dify的API Key过期,工作流节点调用会报401,这个很难排查,所以我通常演示前会去“密钥管理”里把用到的key重新生成一遍。
这些都是小问题,但在台上任何一个都能毁掉整场演示。
6. 实操中踩过的坑:从部署到演示的完整复盘
6.1 部署阶段的坑
dify部署最烦的是版本升级。社区版经常更新,升级之后工作流里之前配置好的工具接口偶尔会失效,不是接口变了,而是API密钥或服务地址被重置过。我现在的做法是:在 .env 里注释清楚每个变量的用途,升级前先备份这个文件,升级完成后对照检查。
另一个坑是Docker资源限制。dify全家桶启动后,内存占用轻松超过4GB,如果服务器内存只有8GB,同时再跑neo4j和FastAPI服务,很容易触发OOM。我建议至少给Docker分配6GB内存,并且把neo4j的堆内存限制一下,尤其不要用默认值直接跑。
6.2 图谱Query和向量检索打架的问题
当图谱查询结果和知识库召回结果冲突时,比如知识库片段里写着某公司持股比例是20%,图谱里存的是30%,工作流会优先把图谱结果放进上下文,但LLM看到两段矛盾信息后可能会自己“综合”出一个错误答案。这个问题我查了很久,最终解决办法是在回答节点的提示词里加一句:如果图谱事实和知识库片段冲突,以图谱事实为准,不要糅合两个来源。
这种处理方式在演示阶段足够用,但如果你想把这个Demo扩展成生产系统,还需要加一层冲突检测和人工审核流程,只是课件材料里不必讲那么深。
6.3 课件素材准备的坑
课件素材最尴尬的是截图和当前版本不一致。dify工作流界面升级很快,旧版截图放出去,学员照着操作却发现按钮位置不一样,信任感会大打折扣。解决方法很笨但有效:截图必须是最后一遍完整走通时重新截,不能翻旧图。同理,neo4j Browser的界面也建议用深色主题,投屏时对比度高,台下看得更清楚。
顺带提一句,课件里的代码块一定要标注语言类型,无论是Cypher还是Python,投屏时代码高亮能帮学员快速定位关键语法,这个细节对现场体验的提升非常明显。
最后再分享一个我每次演示前必做的事:手动删掉neo4j里的测试数据,完全按课件里的导入脚本重建一遍图谱。这样能保证路径查询的结果和课件里记录的完全一致,不会出现台上面试出的数据跟PPT对不上的情况。这个小操作只需要五分钟,但能帮你规避掉演示中最尴尬的“数据不一致”问题。
更多推荐


所有评论(0)