从 Neo4j 到 GraphRAG:用 Text-to-Cypher 构建图检索 RAG

文章来源声明: 原文作者:东风破_; 来源站点:掘金; 原文链接:https://juejin.cn/post/7693357497911459890; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

适合需要关系、层级与多跳推理的问答场景。GraphRAG 并非替代向量检索,而是补上关系检索这一环,为知识图谱类应用落地提供了清晰可复用的心智模型。

传统 RAG 通常通过向量检索或全文检索寻找相关文档。

Milvus 更擅长解决“哪些内容和问题语义相似”,Elasticsearch 更擅长解决“哪些内容包含指定关键词”。

但如果问题变成:

珍珠奶茶属于哪种类型?
珍珠奶茶包含哪些配料?
这些配料使用什么制作工艺?
台式奶茶下面的产品又包含哪些配料?

真正重要的就不再是文本相似度,而是实体之间的关系。

这正是 Neo4j 和 GraphRAG 擅长解决的问题。

整套流程可以先记成:

Question
   ↓
Text-to-Cypher
   ↓
Neo4j
   ↓
Graph Context
   ↓
LLM
   ↓
Answer

一、Neo4j 解决的是什么问题

可以把几种检索方式简单区分为:

Milvus
→ 语义相似
→ “意思像不像?”

Elasticsearch
→ 关键词匹配
→ “字面有没有?”

Neo4j
→ 实体关系 / 图路径
→ “它们怎么关联?”

例如:

珍珠奶茶
   │
   │ 包含
   ▼
珍珠
   │
   │ 使用
   ▼
煮制

这里表达的不再是三个孤立的数据,而是一条知识路径:

Product
   ↓ 包含
Ingredient
   ↓ 使用
Method

所以图数据库真正有价值的地方,不只是“存了哪些实体”,而是:

实体之间可以沿着什么关系继续走。


二、Neo4j 的基本心智模型

Neo4j 最核心的是节点、关系和属性。

例如:

(:Product {name: "珍珠奶茶"})

Product 是节点类型,name 是属性。

关系可以写成:

(:Product)-[:包含]->(:Ingredient)

表示:

Product ──包含──▶ Ingredient

在知识图谱里,可以定义:

(Product)-[:属于]->(Type)
(Product)-[:包含]->(Ingredient)
(Product)-[:适合]->(People)
(Ingredient)-[:使用]->(Method)

最终形成类似:

                         珍珠 ──使用──▶ 煮制
                         ▲
                         │ 包含
                         │
年轻人 ◀──适合── 珍珠奶茶 ──属于──▶ 台式奶茶
学生   ◀──适合──    │
                    ├──包含──▶ 果糖
                    ├──包含──▶ 红茶
                    └──包含──▶ 牛奶

这时知识不再只是“珍珠奶茶”“珍珠”“煮制”几个词,而是:

珍珠奶茶 ──包含──▶ 珍珠
珍珠 ──使用──▶ 煮制

也就是可以继续遍历的关系网络。


三、Cypher:用文本描述图

Neo4j 使用 Cypher 查询图。

例如:

MATCH (p:Product {name: "珍珠奶茶"})
      -[:包含]->
      (i:Ingredient)
RETURN i.name

不要把它理解成传统数据库里的 JOIN。

更自然的理解方式是直接看成:

Product
   │
   │ 包含
   ▼
Ingredient

MATCH 本质是在说:

在数据库中寻找符合这个图形模式的数据。

因此多跳查询也非常直观。

例如:

MATCH (p:Product {name: "珍珠奶茶"})
      -[:包含]->(i:Ingredient)
      -[:使用]->(m:Method)
RETURN p.name, i.name, m.name

表示:

Product
   ↓ 包含
Ingredient
   ↓ 使用
Method

而“台式奶茶有哪些产品配料”则需要走:

台式奶茶
   ▲
   │ 属于
Product
   │
   │ 包含
   ▼
Ingredient

对应:

MATCH
  (t:Type {name: "台式奶茶"})
  <-[:属于]-
  (p:Product)
  -[:包含]->
  (i:Ingredient)

RETURN DISTINCT i.name

这就是图数据库最核心的能力之一:沿关系完成多跳检索。


四、从 Cypher 到 Text-to-Cypher

真正的用户不会自己写 Cypher。

用户只会问:

珍珠奶茶有哪些配料?

所以需要 LLM 完成:

自然语言
   ↓
LLM
   ↓
Cypher

例如:

async function generateCypher(state) {
  const prompt = `
你是 Neo4j Cypher 生成器。

图谱结构:

(Product)-[:属于]->(Type)
(Product)-[:包含]->(Ingredient)
(Product)-[:适合]->(People)
(Ingredient)-[:使用]->(Method)

只返回可以直接执行的 Cypher。

用户问题:
${state.query}
`;

  const res = await llm.invoke([
    new HumanMessage(prompt),
  ]);

  return {
    cypher: res.content,
  };
}

用户输入:

珍珠奶茶有哪些配料?

LLM 可能生成:

MATCH (p:Product {name: "珍珠奶茶"})
      -[:包含]->
      (i:Ingredient)
RETURN i.name AS ingredient

注意,这时候 LLM 还没有回答问题。

它只是在完成:

Natural Language
→
Graph Query Language

也就是 Text-to-Cypher。

这里必须把图谱 Schema 告诉 LLM,因为模型本身并不知道数据库有哪些节点、关系以及关系方向。

可以把 Schema 理解成:

LLM 在图数据库里的地图。


五、Graph Retrieval:真正从知识图谱取事实

拿到 Cypher 后,就可以交给 Neo4j:

async function executeGraphQuery(state) {
  const result = await graph.query(
    state.cypher
  );

  return {
    context: JSON.stringify(result),
  };
}

例如 Neo4j 返回:

[  {"ingredient":"珍珠"},  {"ingredient":"果糖"},  {"ingredient":"红茶"},  {"ingredient":"牛奶"}]

这一步才是真正的 Retrieval。

也就是说:

Question
   ↓
LLM
   ↓
Cypher
   ↓
Neo4j
   ↓
Graph Facts

LLM 负责决定“怎么查”,Neo4j 负责回答“数据库里真实有什么”。


六、基于图谱结果生成答案

有了检索结果之后,再进行第二次 LLM 调用:

async function generateAnswer(state) {
  const prompt = `
请严格根据知识图谱检索结果回答问题。

检索结果:
${state.context}

用户问题:
${state.query}

不要补充检索结果中不存在的信息。
`;

  const res = await llm.invoke([
    new HumanMessage(prompt),
  ]);

  return {
    answer: res.content,
  };
}

数据流变成:

Question
   +
Graph Context
   ↓
LLM
   ↓
Answer

最终可能得到:

珍珠奶茶包含珍珠、果糖、红茶和牛奶。

因此同一个 LLM 实际扮演了两个角色:

第一次:
Question → Cypher
负责“怎么查”

第二次:
Question + Context → Answer
负责“怎么回答”

而 Neo4j 位于中间,负责提供真实的结构化知识。


七、用 LangGraph 把流程串起来

整个 GraphRAG 的 State 可以设计成:

const state = {
  messages: {
    value: (left, right) =>
      left.concat(
        Array.isArray(right)
          ? right
          : [right]
      ),
    default: () => [],
  },

  query: null,
  cypher: null,
  context: null,
  answer: null,
};

因此真正的数据流是:

messages
   ↓
query
   ↓
cypher
   ↓
context
   ↓
answer

工作流:

const workflow = new StateGraph({
  channels: state,
})
  .addNode("parse", parseQuestion)
  .addNode("generateCypher", generateCypher)
  .addNode("executeGraph", executeGraphQuery)
  .addNode("generateAnswer", generateAnswer)

  .addEdge(START, "parse")
  .addEdge("parse", "generateCypher")
  .addEdge("generateCypher", "executeGraph")
  .addEdge("executeGraph", "generateAnswer")
  .addEdge("generateAnswer", END);

对应:

START
  ↓
parse
  ↓
generateCypher
  ↓
executeGraph
  ↓
generateAnswer
  ↓
END

调用:

await app.invoke({
  messages: [
    new HumanMessage(question),
  ],
});

这里传进去的 { messages: [...] } 本身就是初始 State 的一部分。

parseQuestion():

async function parseQuestion(state) {
  const lastMessage =
    state.messages[state.messages.length - 1];

  return {
    query: lastMessage.content,
  };
}

返回的 { query } 是一次局部 State 更新,LangGraph 会自动合并,再把新的 State 传给下一个节点。

所以整套流程可以进一步压缩成:

messages
→ query
→ cypher
→ context
→ answer


八、GraphRAG 和传统 RAG 的区别

传统 RAG:

Question
   ↓
Embedding / BM25
   ↓
Vector DB / Elasticsearch
   ↓
Document Chunks
   ↓
LLM
   ↓
Answer

这里实现的 GraphRAG:

Question
   ↓
Text-to-Cypher
   ↓
Neo4j
   ↓
Entities / Relationships / Paths
   ↓
LLM
   ↓
Answer

两者本质上仍然都是:

Retrieval
   ↓
Augmented
   ↓
Generation

真正不同的是 Retrieval。

传统 RAG 更多是在找:

相关文本

图检索则可以直接找:

实体
关系
路径
多跳事实

所以 GraphRAG 并不是简单替代向量检索,而是在“需要关系、层级和多跳推理”的场景下提供另一种检索能力。


总结

如果以后忘记所有代码,只需要记住下面这一条:

Question
→ Cypher
→ Graph
→ Context
→ Answer

对应到各组件:

LLM
→ 理解问题、生成 Cypher、组织答案

Neo4j
→ 保存实体关系、执行图检索

LangGraph
→ 编排流程、管理 State

因此 GraphRAG 可以理解为:

让 LLM 把自然语言问题翻译成图查询,通过知识图谱检索实体关系和多跳路径,再将检索到的结构化事实作为上下文,生成最终答案。

这就是从 Neo4j 到 GraphRAG 最值得建立的心智模型。