✦ 大道至简 · 时光是画在卷上的河流 · 行到水穷处,坐看云起时

langchainneo4jcypher2026.09.04 · 25 分钟阅读

传统检索答不了「关系题」:Neo4j 知识图谱做 GraphRAG

摘要: 问「A 和 B 什么关系」「A 的配料有哪些」「A 属于哪一类」,靠向量/关键词检索只能捞回零散文本。本文以奶茶知识图谱为例,讲清楚哪些场景该交给图数据库,并用 LangChain + LangGraph 搭一条「LLM 写 Cypher → 执行图查询 → 生成答案」的 GraphRAG 流水线

L

Leo

2026.09.04 · 更新于 2026.09.10

1 次浏览
传统检索答不了「关系题」:Neo4j 知识图谱做 GraphRAG

传统检索答不了「关系题」:Neo4j 知识图谱做 GraphRAG 一次吃透

你问「珍珠奶茶里加了什么」,传统 RAG 可能从十几篇测评文里拼出「珍珠、糖、红茶」; 但当你问「台式奶茶都用了哪些配料、这些配料又靠什么工艺做出来、适合谁喝」——零散文本就答不动了,因为它要的不是「找相似」,而是沿着一条链路把事实串起来。

这种"关系、层级、脉络、多跳推理"的题,正是 知识图谱(GraphRAG) 的用武之地。本篇用一个奶茶菜单做例子,讲清它的适用场景,并搭一条完整可跑的 GraphRAG 流水线。


为什么:先分清「查内容」和「查关系」

把大模型 + 检索库想象成一个「有无限记忆的助理」。它解决两类完全不同的问题:

第一类:找内容。「给我推荐一篇讲奶茶减糖的攻略」「红茶和绿茶口感有什么差别」——这类题的关键是语义相似,答案藏在某一段文本里,捞出来拼给大模型就行。向量检索、关键词检索干的就是这活。

第二类:找关系。「珍珠奶茶属于哪类?它和芋圆奶茶有什么共同配料?这些配料是用什么工艺做的?」——答案不在任何一段话里,而在于实体之间的连接结构。这类题有几个共同特征:

特征人话版本例子
实体关联 / 关系查询问「A 和 B 什么关系」珍珠奶茶 ↔ 台式奶茶
从属 / 包含问「A 包含哪些」「A 属于哪类」一杯奶茶有哪些配料
层级 / 分类体系 / 链路顺着一条线往下捋品类 → 产品 → 配料 → 工艺
多跳推理中间隔了几层也能绕出来台式奶茶里的珍珠靠什么煮的
脉络梳理把散落事实按逻辑串成完整回答从「属于什么」到「适合谁」

一句话记住:传统检索给你的是零散文本,知识图谱给你的是关系和脉络。 前者擅长"模糊找相似",后者擅长"精确找结构"。

下面这组题最典型——每个单独的事实都不难,但要把它们串起来,只有图能做:

这台珍珠奶茶的配料都有哪些?
台式奶茶的饮品都用了哪些配料?
珍珠、芋圆这类配料是靠什么工艺做出来的?

铺垫:把菜单建成一张图——节点、关系、属性

图数据库的思维只有三样东西,理解了就不会迷路:

  • 节点(Node):一个实体,有标签(Label)表示"它是哪类东西"。比如 Product(奶茶)、Ingredient(配料)。
  • 关系(Relationship):两个节点之间有方向的语义连线。图数据库最值钱的就是这个——关系本身还带类型和方向。
  • 属性(Property):节点上的键值字段,比如 name。

奶茶菜单的模型一目了然:产品属于某个品类、包含若干配料、适合某类人群;配料使用某种工艺。画出来是这样一张图:

注意每根线上都写着关系类型和方向。方向一旦反了(比如 属于 写反),整条链就断了——这是后面让大模型写 Cypher 时最容易踩的坑。

最小可运行:先起一个 Neo4j(Docker 一条命令),浏览器打开 http://localhost:7474 就能执行 Cypher:

docker run --name neo4j \
  -p 7474:7474 -p 7687:7687 \
  -e NEO4J_AUTH=neo4j/12345678 \
  -d neo4j:latest

浏览器端是给人和 Cypher 用的;7687 是 Bolt 协议,给代码连库用的。后面 GraphRAG 连的就是 7687。

把上面这一小段菜单建成图,只需一段 Cypher(在浏览器里执行即可):

// 建节点
CREATE (珍珠奶茶:Product {name:'珍珠奶茶'})
CREATE (台式奶茶:Type    {name:'台式奶茶'})
CREATE (珍珠:Ingredient  {name:'珍珠'})
CREATE (红茶:Ingredient  {name:'红茶'})
CREATE (牛奶:Ingredient  {name:'牛奶'})
CREATE (煮制:Method      {name:'煮制'})
CREATE (年轻人:People    {name:'年轻人'})

// 建关系(关系要指明方向)
CREATE (珍珠奶茶)-[:属于]->(台式奶茶)
CREATE (珍珠奶茶)-[:包含]->(珍珠)
CREATE (珍珠奶茶)-[:包含]->(红茶)
CREATE (珍珠奶茶)-[:包含]->(牛奶)
CREATE (珍珠)-[:使用]->(煮制)
CREATE (珍珠奶茶)-[:适合]->(年轻人)

要点

  • 节点 = 实体,关系 = 有方向的语义,关系类型和方向是图谱的灵魂;
  • 图能回答"结构性"问题,是因为它把关系和实体一起存下来了,而不是临时拼;
  • 建库阶段不用急着想怎么答,先把领域模型(谁连谁、往哪个方向)画对。

第一关:把「图」查出来——Cypher 的本质是「画一条路径」

Cypher 查询最直观,它长得就像在图上描路线。三段式语法先记下:

MATCH (起点)-[关系]->(终点)   -- ① 你要走什么样的路径
WHERE ...                    -- ② 过滤(可选)
RETURN ...                   -- ③ 把路径上哪些字段还给我

一条"珍珠奶茶包含哪些配料"是这样描的——从 珍珠奶茶 这个节点出发,顺着 包含 关系走到配料节点:

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

多跳才是图谱的杀手锏:想查"珍珠是靠什么工艺做的",路径就多画一段:

MATCH (p:Product {name:'珍珠奶茶'})-[:包含]->(i:Ingredient)-[:使用]->(m:Method)
RETURN i.name AS 配料, m.name AS 工艺
┌────────┬───────┐
│ 配料   │ 工艺   │
├────────┼───────┤
│ 珍珠   │ 煮制   │
└────────┴───────┘

上面那张图的每一类"链路题",本质都是把一条路径写对:

问题路径多跳
它属于哪类?(p)-[:属于]->(type)1 跳
它包含哪些配料?(p)-[:包含]->(i)1 跳
它适合谁喝?(p)-[:适合]->(people)1 跳
台式奶茶都用了哪些配料?(type)<-[:属于]-(p)-[:包含]->(i)2 跳
这些配料靠什么工艺做?(p)-[:包含]->(i)-[:使用]->(m)2 跳

注意最后一组里"台式奶茶的配料",方向是反着走的:奶茶把 属于 指向类型,所以要找"属于台式奶茶的产品",路径要从 Type 节点逆着箭头往回:

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

要点

  • Cypher 就是把问题翻译成一段路径:起在哪、顺哪根关系走、要哪几层;
  • 方向要盯死,逆着箭头走就写 <--,路径才能串对;
  • 多跳查询是"这段路中间隔了关系,但图依然一次走得完"——这正是它区别于文本检索的核心能力。

第二关:让大模型写 Cypher——Text2Cypher

手写 Cypher 只够验证思路。真实业务里用户问法千变万化,总不能让系统工程师每条都手写。于是有了 Text2Cypher:把"中文问题 → Cypher 语句"这步翻译交给大模型。

听起来简单,实际有个大坑:模型不知道你的图长什么样。它必须被告知 schema(节点类型、关系类型、方向),否则会脑补出不存在的标签和关系。

所以这一步的 prompt 铁律是:把"领域说明书"写进 prompt,并让它只回 Cypher、别回解释:

你是一个专业的 Neo4j Cypher 生成器,只返回纯 Cypher,不要解释、不要 markdown。

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

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

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

用户问题:{这里填用户问题}

把它喂给「台式奶茶的饮品都有哪些配料?」这类问题,模型就能对着上面的"地图"写出 MATCH (t:Type {name:'台式奶茶'})<-[:属于]-(p:Product)-[:包含]->(i:Ingredient) RETURN DISTINCT i.name。

要点

  • Text2Cypher 的正确率几乎等于 schema 写得有多清楚;
  • 把节点标签、关系方向、甚至"禁止编造标签"写死进 prompt,能把幻觉压到最低;
  • 让模型只输出代码,方便直接执行(实际工程里建议顺手剥掉 ``` 围栏,正则清洗一下再跑)。

第三关:GraphRAG 三段流水线——生成 → 执行 → 作答

Text2Cypher 只解决了"怎么写查询"。整条 GraphRAG 链路是标准的三段式:

  1. 生成 Cypher:LLM 看 schema,把问题翻译成图查询;
  2. 执行图查询:拿 Cypher 打 Neo4j,取回结构化结果;
  3. 生成答案:LLM 基于检索结果组织人话回答,查不到就明说,绝不编造。

它和普通 RAG 的分水岭在第 2 步:普通 RAG 召回的是"相似文本片段",GraphRAG 召回的是"一串带关系的结构化事实"——宁可少,不要瞎补。

最小可运行:用 LangChain 连接 Neo4j、LangGraph 串这三步(依赖:@langchain/community、@langchain/openai、@langchain/langgraph、@langchain/core;环境变量 OPENAI_BASE_URL、MODEL_NAME 指向你的模型):

import 'dotenv/config'
import { Neo4jGraph } from '@langchain/community/graphs/neo4j_graph'
import { ChatOpenAI } from '@langchain/openai'
import { StateGraph, END, START } from '@langchain/langgraph'
import { HumanMessage } from '@langchain/core/messages'

// 连库:浏览器端是 7474,代码走 Bolt 协议 7687
const graph = new Neo4jGraph({
  url: 'bolt://localhost:7687',
  username: 'neo4j',
  password: '12345678',
})

const llm = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  temperature: 0,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
})

// 状态:本轮问题、生成的 cypher、检索到的上下文、最终答案
const state = {
  messages: { value: (l, r) => l.concat(Array.isArray(r) ? r : [r]), default: () => [] },
  cypher: null,
  context: null,
  answer: null,
}
const userQuery = (s) => s.messages[s.messages.length - 1].content

// ① 生成 Cypher:把「图长什么样」写进 prompt
async function generateCypher(s) {
  const prompt = `
    你是专业的 Neo4j Cypher 生成器,只返回纯 Cypher,不要解释、不要 markdown。
    节点:Product=奶茶产品,Ingredient=配料,Type=奶茶类型,Method=制作工艺,People=适合人群。
    关系方向(必须严格遵守,绝对不能反):
    (Product)-[:属于]->(Type)
    (Product)-[:包含]->(Ingredient)
    (Product)-[:适合]->(People)
    (Ingredient)-[:使用]->(Method)
    规则:1.方向不能反  2.多跳用多个 MATCH 连对路径  3.只返回可运行的 Cypher
    用户问题:${userQuery(s)}
  `
  const res = await llm.invoke([new HumanMessage(prompt)])
  return { cypher: res.content.replace(/^```(?:cypher)?|```$/g, '').trim() }
}

// ② 执行图查询:跑 Cypher,命中失败就把检索结果置空
async function executeGraph(s) {
  try {
    const res = await graph.query(s.cypher)
    return { context: JSON.stringify(res) }
  } catch (e) {
    return { context: '未查询到相关知识' }
  }
}

// ③ 生成答案:只依据检索结果,宁可说不知道也不编
async function generateAnswer(s) {
  const prompt = `
    你是奶茶专家,根据下方「检索结果」回答用户问题。
    检索结果为空或不足时,简要说明无法从图谱得到答案,不要编造。
    不要推断图谱里没出现的配料(如水、冰、添加剂等)。

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

// 串成三步流水线
const app = new StateGraph({ channels: state })
  .addNode('generateCypher', generateCypher)
  .addNode('executeGraph', executeGraph)
  .addNode('generateAnswer', generateAnswer)
  .addEdge(START, 'generateCypher')
  .addEdge('generateCypher', 'executeGraph')
  .addEdge('executeGraph', 'generateAnswer')
  .addEdge('generateAnswer', END)
  .compile()

async function ask(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)
}

ask('珍珠奶茶适合哪些人群饮用?')

对着上面那几张表建好的小图,问一句"珍珠奶茶适合哪些人群饮用?",会依次看到三段中间产物:

问题: 珍珠奶茶适合哪些人群饮用?
Cypher: MATCH (p:Product {name:'珍珠奶茶'})-[:适合]->(peo:People) RETURN peo.name
检索结果: [{"peo.name":"年轻人"}]
回答: 珍珠奶茶适合年轻人饮用。

这就是 GraphRAG 的完整闭环:它把一次查询拆成"写图查询 → 拿结构化事实 → 组织语言",中间每一步都可以观察、可以替换。而"查不到就明说、绝不瞎编"这条兜底,正好压住了大模型最爱犯的幻觉。

要点

  • 三段式的每一步都返回中间产物写进 state,跑挂了好排查、想换执行器也好换;
  • 模型答"关系题"之所以可信,是因为答案是顺着库里真实存在的路径来的,不是拍脑袋;
  • 检索失败要显式兜底成"未查到相关知识",否则模型会拿上下文缺口当发挥空间。

结尾:三大引擎各司其职,组合起来互相兜底

收工前把三种检索引擎摆在一起看——没有谁是万能的,它们各有天生短板,而这恰恰是"组合"的前提:

引擎最擅长天生短板
Milvus(向量检索)语义模糊匹配,找"意思相近"的内容没有结构、不懂关系
ES(关键词检索)关键词精准命中、分词倒排、过滤筛选只是"文本孤岛",不会串关系、不会推理
Neo4j(知识图谱)实体关联、多跳推理、层级脉络不擅长模糊语义,也不适合海量全文文档检索

单独看,各自都有答不了的题;组合起来之后,互相兜底、互相增强:向量负责"圈出候选",关键词负责"精确锁定",图谱负责"讲清关系和脉络"——各干各擅长的,短板让队友补上。一个典型的按需路由就是这样:

更进一步,向量 + 图谱可以这样协作:先用 Milvus 从文档里召回实体("珍珠奶茶"出现在哪几篇),再把这几个实体送进 Neo4j 展开上下游关系,最后连同原文一起交给大模型——既拿到相关文本,又拿到结构脉络。

那"该不该上图谱"到底怎么判断?最后一张决策表:

你的问题长什么样该用谁为什么
"帮我找意思相近的文章 / 语义相似的描述"向量检索模糊相似是它的主场
"匹配某个精确词 / 商品名 / 编号"关键词检索精确命中,不走向量
"A 和 B 什么关系、A 包含哪些、A 属于哪类"知识图谱结构和关系在库里
"产品 → 配料 → 工艺 的整条链路"知识图谱多跳沿路径一次走完
"先召回实体,再展开它上下游关系"向量 + 图谱混合向量定位实体,图谱展开脉络

图谱不是万能的:存十万杯奶茶的"口感描述"它不如向量库;回答"哪杯更接近你想要的风味"这种模糊题,向量反而更灵。但只要问题是奔着"关系、层级、脉络、多跳"去的,就该把题交给图数据库——这正是知识图谱存在的理由:别让大模型在零散文本里脑补关系,把关系直接摆到它面前。而真正复杂的产品,往往是向量、关键词、图谱三兄弟各司其职,再由路由层把它们捏成一个答案。

下次再遇到"这台奶茶的配料靠什么工艺做、台式奶茶里哪些人群爱喝"这类一串三问的题,你已经知道答案了:交给知识图谱。

L

Leo

博主

独立开发者 / Blogger,原博客「大道至简」维护者。正在把 WordPress 上攒了几年的文章与拾语迁移到 Next.js。

读者留言

COMMENTS · 0

发表留言

评论经审核后展示 · 请友善发言0/100