传统检索答不了「关系题」:Neo4j 知识图谱做 GraphRAG
摘要: 问「A 和 B 什么关系」「A 的配料有哪些」「A 属于哪一类」,靠向量/关键词检索只能捞回零散文本。本文以奶茶知识图谱为例,讲清楚哪些场景该交给图数据库,并用 LangChain + LangGraph 搭一条「LLM 写 Cypher → 执行图查询 → 生成答案」的 GraphRAG 流水线
Leo
2026.09.04 · 更新于 2026.09.10
传统检索答不了「关系题」: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 链路是标准的三段式:
- 生成 Cypher:LLM 看 schema,把问题翻译成图查询;
- 执行图查询:拿 Cypher 打 Neo4j,取回结构化结果;
- 生成答案: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 属于哪类" | 知识图谱 | 结构和关系在库里 |
| "产品 → 配料 → 工艺 的整条链路" | 知识图谱 | 多跳沿路径一次走完 |
| "先召回实体,再展开它上下游关系" | 向量 + 图谱混合 | 向量定位实体,图谱展开脉络 |
图谱不是万能的:存十万杯奶茶的"口感描述"它不如向量库;回答"哪杯更接近你想要的风味"这种模糊题,向量反而更灵。但只要问题是奔着"关系、层级、脉络、多跳"去的,就该把题交给图数据库——这正是知识图谱存在的理由:别让大模型在零散文本里脑补关系,把关系直接摆到它面前。而真正复杂的产品,往往是向量、关键词、图谱三兄弟各司其职,再由路由层把它们捏成一个答案。
下次再遇到"这台奶茶的配料靠什么工艺做、台式奶茶里哪些人群爱喝"这类一串三问的题,你已经知道答案了:交给知识图谱。
读者留言
COMMENTS · 0发表留言