GraphRAG 实战:用 Neo4j 知识图谱,让 RAG 从“找货“进化到“梳理关系“

引子:你可能被"传统 RAG"骗了

做 RAG 的人几乎都从这套组合拳开始:向量库(Milvus/PgVector)做语义检索 + ES 的 BM25 做关键词检索 + ReRank 精排。

它们很强,但有个共同的"盲区"------只认"单个文档",不认"文档之间的关系" 。

  • Milvus 是"语义相近就捞出来",本质是拿着你的问题去比相似度;
  • ES 的 BM25 是"分词后从倒排索引里字面命中",本质是找包含关键词的那几段;
  • ReRank 是"在捞出来的候选里再排个序"。

你会发现,它们全都停留在 "单点式检索" :能告诉你"这篇文档里有珍珠奶茶的做法",却回答不了"珍珠奶茶 → 用了哪种珍珠 → 这种珍珠怎么煮 → 适合谁喝"这种连锁问题。

向量和倒排索引是"精准找货"的工具,但搞不清"货与货的来龙去脉"。

而这恰恰是 Agent 场景下 RAG 最需要的能力:基于关联去推导答案、梳理知识体系。传统 RAG 拿到的是一堆"信息孤岛",碎片化、无结构、无关联------想让它"能推理、能关联、能解释",光靠向量是不够的。

这就是 GraphRAG 要解决的问题:在"语义匹配 + 关键词匹配"之外,补上一块"图谱关联匹配"。

复制代码
传统 RAG:问题 → 向量/关键词 → 一堆孤立文档 → 拼答案
GraphRAG:问题 → 图谱多跳关联 → 一条关系链 → 有据可依的答案

一、为什么是 Neo4j?

说到知识图谱,绕不开 Neo4j ------ 一款原生图数据库。

它和 MySQL 的差别,不是"换个数据库",而是换了一种世界观:

MySQL Neo4j
存储核心:表、行、列 存储核心:节点、关系
查询语言:SQL 查询语言:Cypher
擅长的事:查记录 擅长的事:查关系、多跳、因果链路

用一句话区分它们的心态:MySQL 关注"这条数据是什么",Neo4j 关注"这条数据和别的数据是什么关系"。

把散落的文档、实体、关键词像知网那样串起来,就形成了一张清晰的知识图谱。举个奶茶的例子,你在 Neo4j 里可以这样一路走通:

复制代码
珍珠奶茶 --包含--> 珍珠 --制作--> 煮制 --适合--> ...

一条多跳的全链路检索就此打通。这正是 Milvus 和 ES 的短板:它们能检索"点",却串不起"线",更画不出"网"。

二、三分钟把 Neo4j 跑起来

别急着写代码,先把数据库用 Docker 拉起来。这套 docker-compose.yml 已经把关键配置都备好了:

复制代码
services:
  neo4j:
    image: neo4j:latest
    container_name: neo4j-container
    ports:
      - "7474:7474"   # Web 管理界面(浏览器打开就能点着玩 Cypher)
      - "7687:7687"   # Bolt 协议(代码连接用的就是这个端口)
    environment:
      - NEO4J_AUTH=neo4j/12345678          # 账号 neo4j / 密码 12345678
      - NEO4J_PLUGINS=["apoc"]             # 装上 APOC 插件,后面扩展好用
      - NEO4J_dbms_security_procedures_unrestricted=apoc.*
    volumes:
      - ./volumes/neo4j/data:/data         # 数据持久化,别让容器重启就丢图
      - ./volumes/neo4j/logs:/logs
      - ./volumes/neo4j/conf:/conf
    restart: unless-stopped

两个端口记住就够:7474 是给人看的(Web UI),7687 是给代码连的(Bolt) 。

复制代码
docker compose up -d
# 浏览器打开 http://localhost:7474 ,用 neo4j / 12345678 登录

登录后你可以直接在 Web 界面里敲 Cypher,边写边看图谱实时长出来------这是学习图数据库最爽的地方:所见即所得。

三、Cypher 实战:手搓一张奶茶知识图谱

理论说再多,不如亲手建一张图。下面这套语句,直接抄进 Neo4j 就能跑。

1. 建节点:给每种"实体"一个标签

复制代码
// 奶茶品类与品牌
CREATE (product: Product {name: '珍珠奶茶'})
CREATE (type1: Type {name: "台湾奶茶"})
CREATE (type2: Type {name: "港式奶茶"})
CREATE (brand1: Brand {name: "蜜雪冰城"})
CREATE (brand2: Brand {name: "古茗"})

// 常见配料
CREATE (ing1: Ingredient {name: "珍珠"})
CREATE (ing2: Ingredient {name: "芋圆"})
CREATE (ing3: Ingredient {name: "果糖"})
CREATE (ing4: Ingredient {name: "红茶"})
CREATE (ing5: Ingredient {name: "牛奶"})

// 制作工艺 & 适宜人群
CREATE (method1: Method {name: "煮制"})
CREATE (method2: Method {name: "冲泡"})
CREATE (people1: People {name: "年轻人"})
CREATE (people2: People {name: "学生"})
CREATE (people3: People {name: "甜食爱好者"})

(变量: 标签 {属性}) 就是 Cypher 里"造一个节点"的语法。标签(Label)相当于给节点分类,检索时靠它筛选。

2. 建关系:知识图谱真正的灵魂

节点只是"珠子",关系才是把珠子串成项链的"线"。图的价值,90% 在关系上:

复制代码
// 珍珠奶茶 属于 台湾奶茶
MATCH (p:Product {name: "珍珠奶茶"}), (t:Type {name: "台湾奶茶"})
CREATE (p)-[:属于]->(t)

// 珍珠奶茶 包含 各种配料
MATCH (p:Product {name: "珍珠奶茶"}), (i:Ingredient {name: "珍珠"})
CREATE (p)-[:包含]->(i)
MATCH (p:Product {name: "珍珠奶茶"}), (i:Ingredient {name: "牛奶"})
CREATE (p)-[:包含]->(i)
// ......芋圆、果糖、红茶同理

// 珍珠奶茶 适合 各类人群
MATCH (p:Product {name: "珍珠奶茶"}), (people:People {name: "学生"})
CREATE (p)-[:适合]->(people)

注意这里的套路:先 MATCH 找到两个已存在的节点,再 CREATE 一条关系 。-[:关系名]-> 里的箭头方向非常重要,它是图语义的一部分。

3. 查询验证:从"看全图"到"多跳推理"

复制代码
// 看全图(调试用,数据量大时别乱跑)
MATCH (n)-[r]->(m)
RETURN n, r, m

// 多跳关联:珍珠奶茶 → 配料 → 制作工艺
MATCH (p:Product {name: "珍珠奶茶"})-[:包含]->(i)-[:制作]->(m)
RETURN p.name, i.name, m.name

// 珍珠奶茶适合哪些人
MATCH (p:Product {name: "珍珠奶茶"})-[:适合]->(people)
RETURN p.name, people.name

看到那条 -[:包含]->(i)-[:制作]->(m) 了吗?两个关系连起来,就是一次"多跳查询" ------这就是 GraphRAG 推理能力的来源。向量检索永远做不到这种"顺藤摸瓜"。

4. 更新与删除:图也是要维护的

复制代码
// 更新属性
MATCH (p:Product {name: "珍珠奶茶"})
SET p.calorie = "中高热量", p.taste = "甜香"

// 删关系(只断开,不删节点)
MATCH (p:Product {name: "珍珠奶茶"})-[r:适合]->(s:People {name: "学生"})
DELETE r

// 删节点前必须先删它的关系,否则会报错"节点仍有关系"
MATCH (i:Ingredient {name: "芋圆"})-[r]-()
DELETE r, i

// 清空全图(慎用)
MATCH (n) DELETE n

踩坑预警:Neo4j 里删节点前必须先删掉它的所有关系,否则会报 Cannot delete node, because it still has relationships。上面 -[r]-() 不写方向,就是为了"匹配任意方向的所有关系",一次删干净。

四、代码里怎么连 Neo4j:neo4j-driver 上手

Web UI 玩熟了,就得让代码连上。最底层的 neo4j-driver 长这样:

复制代码
import neo4j from 'neo4j-driver';

const driver = neo4j.driver(
  "bolt://localhost:7687",
  neo4j.auth.basic("neo4j", "12345678"),
);
const session = driver.session();

// 建节点
await session.run(`
  CREATE (p:Product {name: "珍珠奶茶"})
  CREATE (i:Ingredient {name: "珍珠"})
`);

// 建关系
await session.run(`
  MATCH (p:Product {name: "珍珠奶茶"}), (i:Ingredient {name: "珍珠"})
  CREATE (p)-[:包含]->(i)
`);

// 查询并读取结果
const result = await session.run(`
  MATCH (p:Product {name: "珍珠奶茶"})-[r]->(i)
  RETURN p, r, i
`);
result.records.forEach(record => {
  console.log('奶茶', record.get('p').properties.name);
  console.log('关系', record.get('r').type);
  console.log('配料', record.get('i').properties.name);
});

// 用完记得关,不然连接泄漏
await session.close();
await driver.close();

关键就三个动作:session.run(cypher) 执行、record.get('别名') 取结果、close() 收尾。RETURN 里起的别名,就是你在代码里 get() 的钥匙。

五、重头戏:用 LangGraph 搭一条 GraphRAG 流水线

底层会了,接下来把整套 text2cypher 的 GraphRAG 工作流拼起来------这是整篇最有含金量的部分。

我们用的是 @langchain/community 的 Neo4jGraph + @langchain/openai 的 LLM + @langchain/langgraph 编排。整体是一条四节点的直线流水线:

翻译成人话就是:解析问题 → 生成 Cypher → 查图谱 → 组织答案。

第一步:定义状态(State)

LangGraph 是"状态机"思维,先把流转中的数据槽位定义好:

复制代码
const state = {
  messages: {
    // 自定义 reducer:新消息永远 append,不覆盖
    value: (left, right) => left.concat(Array.isArray(right) ? right : [right]),
    default: () => [],
  },
  query: null,
  cypher: null,   // LLM 生成的 Cypher(text2cypher)
  context: null,  // 图谱检索结果
  answer: null,
}

messages 那个 value 函数就是 reducer ------ 决定"新值怎么合并进旧值"。这里定成"追加",这样多轮对话的消息不会被吃掉。

第二步:四个节点,各司其职

节点 1 · parse:把最后一句用户问题拎出来
复制代码
async function parseQuestion(state) {
  const lastMessage = state.messages[state.messages.length - 1]
  return { query: lastMessage.content }
}
节点 2 · generateCypher:让 LLM 把自然语言翻译成 Cypher

这一步是整个 GraphRAG 的心脏。Prompt 里必须把图谱的 schema 喂给 LLM,否则它就是在黑盒里瞎猜:

复制代码
async function generateCypher(state) {
  const prompt = `
    你是一个专业的 Neo4j Cypher 生成器。
    严格按照下面的结构生成正确语句,只返回 Cypher 代码,不要任何解释、不要标点、不要 markdown。

    节点:
    - Product: 奶茶品牌
    - Ingredient: 配料
    - Type: 奶茶类型
    - Method: 制作工艺
    - People: 适合人群

    关系方向 (必须严格遵守):
    - (Product)-[:属于]->(Type)
    - (Product)-[:包含]->(Ingredient)
    - (Product)-[:适合]->(People)
    - (Ingredient)-[:使用]->(Method)

    规则:
    1. 关系方向绝对不能反
    2. 多跳查询使用多个 MATCH,不能连错路径
    3. 只返回最终可运行的 Cypher 语句

    用户问题: ${state.query}
  `
  const res = await llm.invoke([new HumanMessage(prompt)])
  return { cypher: res.content }
}
节点 3 · executeGraphQuery:拿 Cypher 去查图,查不到要兜底
复制代码
async function executeGraphQuery(state) {
  try {
    const res = await graph.query(state.cypher)
    return { context: JSON.stringify(res) }
  } catch (err) {
    return { context: "未查询到相关知识" }   // 关键:别让异常炸掉整条链
  }
}
节点 4 · generateAnswer:基于图谱上下文组织人话答案
复制代码
async function generateAnswer(state) {
  const prompt = `
    你是奶茶专家,根据下方[检索结果]回答用户问题,检索结果为空或不足时简要说明无法从图谱得到答案,不要编造。
    回答要求:
    - 直接列出事实,不要推断图谱中未出现的配料(如水、冰、添加剂等)。

    检索结果: ${state.context}
    用户问题: ${state.query}
  `
  const res = await llm.invoke([new HumanMessage(prompt)])
  return { answer: res.content }
}

注意这两处 prompt 里的"防幻觉"设计:检索为空就老实说查不到 、别脑补图谱里没写的配料。这是让 RAG 可信的关键所在。

第三步:把节点连起来,编译成可执行工作流

复制代码
const workflow = new StateGraph({ channels: state })
  .addNode("parse", parseQuestion)
  .addNode("generateCypher", generateCypher)
  .addNode("executeGraphQuery", executeGraphQuery)
  .addNode("generateAnswer", generateAnswer)
  .addEdge(START, "parse")
  .addEdge("parse", "generateCypher")
  .addEdge("generateCypher", "executeGraphQuery")
  .addEdge("executeGraphQuery", "generateAnswer")
  .addEdge("generateAnswer", END)

const app = workflow.compile()

编译后还能自动吐出 Mermaid 拓扑图,顺手校验一下流程对不对:

复制代码
const drawable = await app.getGraphAsync()
const mermaid = drawable.drawMermaid({ withStyles: true })
console.log(mermaid)

最后把它跑起来,批量问三个问题:

复制代码
async function runGraphRAG(question) {
  const res = await app.invoke({ messages: [new HumanMessage(question)] })
  console.log('用户问题:', question)
  console.log('生成Cypher:', res.cypher)
  console.log('检索结果:', res.context)
  console.log('最终回答:', res.answer)
  return res.answer
}

await Promise.all([
  runGraphRAG('我们这款珍珠奶茶有哪些配料?'),
  runGraphRAG('台式奶茶的饮品都有哪些配料?'),
  runGraphRAG('珍珠奶茶适合哪些人群饮用?'),
])

把「生成 Cypher → 查图 → 出答案」三个环节都打印出来,每一步都透明可查 ------这正是 GraphRAG 优于黑盒向量 RAG 的地方:答案从哪来,一目了然,可信、可解释。

六、踩坑实录:GraphRAG 最容易翻车的三个地方

这套东西跑通不难,但有几个坑,我踩过,希望你别再踩。

坑一:图谱 schema 必须"内外一致"

我笔记里建关系用的是 (Ingredient)-[:制作]->(Method),但 generateCypher 的 prompt 里写成了 (Ingredient)-[:使用]->(Method)。

你建图用"制作",让 LLM 生成"使用",查出来当然是空。这是 text2cypher 最隐蔽的 bug------不报错,只是永远查不到。

text2cypher 的命门,是"你告诉 LLM 的 schema"和"你真实建图的 schema"必须严丝合缝。差一个字,全盘皆输。

坑二:让 LLM"只吐 Cypher",别吐废话

prompt 里那句"只返回 Cypher 代码,不要任何解释、不要 markdown"不是客套。LLM 天生爱解释,一旦它给你返回:

复制代码
```cypher
MATCH (p:Product) RETURN p
```

这就带着 markdown 代码块围栏,直接丢进 graph.query() 会语法报错。约束它的输出格式,比事后清洗便宜一百倍。

坑三:查询失败要有兜底,整个链路别崩

executeGraphQuery 里那个 try/catch 不能省。LLM 生成的 Cypher 语法错误是家常便饭,一旦抛异常没接住,整条 LangGraph 流水线就断了。兜一句"未查询到相关知识",让流程优雅降级。

金句:RAG 工程的一半功力,在"查不到时怎么体面地退场"。

七、选型对照:向量、关键词、图谱,谁也别想独吞

最后来张"选型速查表"。它们不是替代关系,而是互补关系,各有不可替代的场景。

检索方式 底层原理 适合场景
Milvus 向量语义检索 语义相似度 用户提问没有明确关键词(大白话)、意思相近即可、模糊泛化推荐、非结构化文档(笔记/手册/FAQ)
ElasticSearch BM25 分词 + 倒排索引 有明确专有名词/编号/文件名、需精准字面命中与高亮、官方文档/规章制度/接口文档检索、字段过滤排序
Neo4j 知识图谱 Cypher 关系推理 需实体关联、多跳推理、"A 和 B 什么关系"、逻辑推理与脉络梳理

一句话决策:

  • 要"意思相近" → 向量(Milvus)
  • 要"字面精准" → 关键词(ES)
  • 要"关系推理" → 图谱(Neo4j)

真正的下一代 RAG,是图谱关联 + 语义匹配 + 关键词匹配的多重检索组合拳------让答案更精准、更具解释性。

结语:RAG 的下半场是"结构"

回头看这一路,从 docker-compose 起库,到手搓奶茶知识图谱,再到 LangGraph 编排 text2cypher 四节点流水线------你其实只做了一件事:

把"信息孤岛"重新连成一张"知识网络"。

向量库帮你找到"点",ES 帮你锚定"点",而知识图谱帮你把点连成"线"和"网"。当 RAG 能顺着关系链推理、能对每个答案给出依据,它才算真正跨过了"能查"到"能想"的门槛。

最后送一句:上一代 RAG 拼的是"谁能检索到",下一代 RAG 拼的是"谁能把关系讲清楚"。而 GraphRAG + Neo4j,就是把这句话变成代码的最短路径。

下次再想加个"品牌 → 门店 → 区域"的新关系?在 Cypher 里补几条边的事------图的扩展性,就是让你加关系时,不用重构任何东西。

相关推荐
敖老师20 分钟前
Dify 连局域网 Ollama 的两个坑:docker0 网段撞车,和请求里少掉的那个参数
人工智能
MocMoc37 分钟前
从 0 到 1:把 Dify Chatflow 接进一个 qiankun 中后台
人工智能
民乐团扒谱机1 小时前
【微实验】NSGA-II(非支配排序遗传算法)——解决多目标优化问题的“自然选择模拟器”
linux·人工智能·算法
怎么全是重名2 小时前
VerdictAD
人工智能·异常检测·few-shot·zsas·异常分割
Bode_20022 小时前
具身智能机构设计的质量控制
人工智能
阿里云大数据AI技术2 小时前
EMR Serverless StarRocks × Lance:多模态分析与检索的云上实践
人工智能
SelectDB2 小时前
数据不能出境的数仓选型与迁移落地:四问框架、可复算成本与迁移三段式
人工智能
李青菜2 小时前
一个人 + AI,用 Tauri 2 和 Rust 写了个开源的 Windows 效率工具:提醒 + 笔记 + 启动器
人工智能