向量库答不出谁连着谁?Neo4j 把知识图谱做成可遍历的上下文

向量库答不出谁连着谁?Neo4j 把知识图谱做成可遍历的上下文

向量库答不出谁连着谁?Neo4j 把知识图谱做成可遍历的上下文

源微信页触发了验证码拦截(secitptpage/verify.html)。正文以 Neo4j 官方文档:什么是图数据库、Cypher MATCH 手册、Docker Compose 单机部署、neo4j-graphrag Python RAG 指南 与 What is GraphRAG 为准,不沿用源标题的「必学数据库」口号,也不臆造未公开能力。

多数 RAG 把「检索」理解成:把文档切成块、嵌入、按余弦相似度捞 Top-K。这对「这段话大概在讲什么」够用。痛点出在多跳事实:谁给谁写过、这个 API 依赖哪份协议、故障单和配置项隔了两层关系。向量库没有边,JOIN 要自己在应用层拼。Neo4j 的立场相反:关系是一等公民,存在节点旁边,查询语言 Cypher 直接画路径。

封面:关系才是上下文


一、切块检索为什么答不出「谁连着谁」

把 PDF 切成 512 Token 的块再嵌入,本质是在做最近邻搜索。块与块之间只剩「向量空间里的距离」,原来的章节层级、作者署名、引用边、配置依赖全被切碎。用户问「这个函数的调用方还依赖哪份协议」,检索器可能捞到函数定义那一块,却捞不到两跳之外的协议节点。不是模型笨,是上下文从来没被连上。

关系型库能连,但代价是 JOIN。两张表还好,五跳、可变路径、还要按关系类型过滤时,执行计划会迅速变丑。官方把这点写得很直白:图数据库不用 JOIN,关系与节点一起原生存储,遍历可以按深度和广度走,而不是按外键反复探测。

A Neo4j graph database stores data as nodes, relationships, and properties instead of in tables or documents. This means you can organize your data in a similar way as when sketching ideas on a whiteboard.

白板类比不是营销话。知识图谱本来就是「实体圆圈 + 箭头」。硬塞进行列模型,等于先把图拍扁,查询时再临时拼回来。AI 场景里这更致命:LLM 需要的不是一张宽表,而是可解释的邻域——命中一个 Chunk 之后,把它连着的 Entity、Document、下一跳邻居一起塞进 Prompt。

向量库和文档库仍然有位置:海量非结构化原文、纯语义召回、跨语言近义。Neo4j 不是来替换它们的,而是把「命中之后怎么走」做成数据库能力。后面会看到,官方 neo4j-graphrag 包甚至允许向量仍放在 Weaviate / Pinecone / Qdrant,图关系留在 Neo4j——承认两套存储可以并存。

纯向量 RAG 还有第二个缺口:不可解释。近似最近邻返回的是分数,说不清「为什么这块和那块有关」。图上的路径本身就是证据:(Chunk)-[:MENTIONS]->(API)-[:DEPENDS_ON]->(Protocol) 可以原样打印给用户,也可以给评测脚本做忠实度检查。


二、属性图:节点、关系、属性才是一等公民

Neo4j 用的是属性图(Property Graph),不是 RDF 三元组那套必须把一切拆成主谓宾的模型。三个构件就够画大多数 AI 知识图:

构件 官方定义要点 知识图谱里典型长什么样
节点 Node 实体或离散对象;可打多个标签;可挂任意键值属性 (:Chunk)、(:Entity:Tool)、(:Document)
关系 Relationship 必须有起点、终点、恰好一个类型、必须有方向;也可挂属性 [:MENTIONS]、[:HAS_CHUNK]、[:ACTED_IN {role:'Bud Fox'}]
属性 Property 节点和关系上的键值对 name、embedding、year、role
标签 Label 把节点划进可索引的命名子集;一个节点可以多个标签 :Person:Actor、:Person:Director

关系不能悬空:没有起点或终点的边不存在。方向是一等语义,(:Person)-[:DIRECTED]->(:Movie) 和反过来不是一回事。节点却可以同时戴多顶帽子——Charlie Sheen 既是 Person 又是 Actor。这对 AI 抽取很友好:同一实体可以既是 Tool 又是 OpenSourceProject,不必立刻做僵硬的单表继承。

属性挂在边上,是图模型相对「只有三元组」的关键增量。电影图里 ACTED_IN 带 role,知识图里 DEPENDS_ON 可以带 since、optional。把角色塞进节点名(CharlieSheenAsBudFox)是建模失败;角色是关系的限定,不是实体本身。

约束和索引是生产底盘,不是可选项。没有唯一约束,LLM 抽实体时「Neo4j」和「neo4j」会变成两个节点,后面所有遍历都在噪声上走。官方还强调:标签、关系类型、字符串值都大小写敏感。抽取管道里必须先做规范化,再 MERGE。

一张面向 RAG 的最小数据模型,通常长这样:

Mermaid Diagram

左边是词汇图(Document / Chunk,给语义检索当入口),右边是领域图(Entity 与类型化关系,给多跳当骨架)。两边缺一不可:只有切块,退化成普通向量 RAG;只有实体没有原文,模型生成时缺少可引用的句子。


三、Cypher:用模式匹配代替 JOIN 地狱

Cypher 的核心不是「SELECT 哪些列」,而是「图上长什么样」。ASCII 艺术就是语法:(节点:标签 {属性})-[关系:类型]->(节点)。官方 MATCH 手册用电影图示范,下面这组语句可以直接在空库里复现。

CREATE (charlie:Person:Actor {name: 'Charlie Sheen'}),
(martin:Person:Actor {name: 'Martin Sheen'}),
(michael:Person:Actor {name: 'Michael Douglas'}),
(oliver:Person:Director {name: 'Oliver Stone'}),
(rob:Person:Director {name: 'Rob Reiner'}),
(wallStreet:Movie {title: 'Wall Street'}),
(thePresident:Movie {title: 'The American President'}),
(charlie)-[:ACTED_IN {role: 'Bud Fox'}]->(wallStreet),
(martin)-[:ACTED_IN {role: 'Carl Fox'}]->(wallStreet),
(michael)-[:ACTED_IN {role: 'Gordon Gekko'}]->(wallStreet),
(oliver)-[:DIRECTED]->(wallStreet),
(martin)-[:ACTED_IN {role: 'A.J. MacInerney'}]->(thePresident),
(michael)-[:ACTED_IN {role: 'President Andrew Shepherd'}]->(thePresident),
(rob)-[:DIRECTED]->(thePresident)

问「谁演了 Wall Street」不需要演员表 JOIN 电影表:

MATCH (:Movie {title: 'Wall Street'})<-[:ACTED_IN]-(actor:Person)
RETURN actor.name AS actor

问「Charlie Sheen 演过的片子是谁导的」,一条模式就能跨两跳:

MATCH (:Person {name: 'Charlie Sheen'})-[:ACTED_IN]->(movie:Movie)<-[:DIRECTED]-(director:Person)
RETURN movie.title AS movieTitle, director.name AS director

CREATE 和 MERGE 必须分清。官方写得很硬:CREATE 每次都新建,重复执行会复制节点;MERGE 先匹配,没有才创建。LLM 抽实体的写入路径只能走 MERGE,再配唯一约束,否则知识图谱会在一夜之间变成重影森林。

WHERE 不是事后过滤那么简单。手册强调:谓词是模式的一部分,要跟对应的 MATCH 放在一起。复杂一点可以用 EXISTS 子查询,例如「Martin Sheen 演过、但不是 Oliver Stone 导的片子」:

MATCH (martin:Person)-[:ACTED_IN]->(movie:Movie)
WHERE martin.name = 'Martin Sheen' AND NOT EXISTS {
MATCH (movie)<-[:DIRECTED]-(director:Person {name: 'Oliver Stone'})
}
RETURN movie.title AS movieTitle

参数化是防注入的底线。用户问题、实体名一律走 $param,不要用字符串拼接 Cypher。新版本还支持动态标签 MATCH (n:$(expr)),用来降低注入面,但性能有版本差异:5.26 到 2025.07 动态标签可能退化成全节点扫描;2025.11 之后才能较好利用属性索引。Agent 自动生成 Cypher 时,优先固定标签,动态值放属性。

变量作用域也容易踩坑。连续两个 MATCH 可以靠引用把中间结果传下去;既没被引用、又没被 WITH 显式带上的变量会丢掉。写 GraphRAG 的 retrieval_query 时,官方约定作用域里有 node(向量命中的节点)和 score,你的查询必须从这两个变量出发去遍历,而不是再写一个无关的 MATCH (n)。


四、知识图谱怎么从文档长出来

GraphRAG 要好用,图的形状必须先对。官方把构图拆成两步,并且允许反复迭代:先建模相关节点和关系,再导入、抽取或计算出符合该模型的图结构。数据来源通常混在一起——结构化库、CSV、API,再加上非结构化文本。

LOAD CSV 适合中小规模结构化导入。它把每一行先读成字符串,数字和日期要自己转:

LOAD CSV WITH HEADERS
FROM 'https://data.neo4j.com/importing-cypher/people.csv' AS row
WITH toInteger(row.personId) AS personId, date(row.birthYear) AS birthYear, row.name AS name
RETURN personId, birthYear, name

更大的文件走 neo4j-admin import,不要指望 LOAD CSV 扛亿级初始化。导入后立刻核对行数:RETURN count(row) 和源文件行数对不上,后面所有 RAG 评测都是空中楼阁。

非结构化文本走抽取管道。neo4j-graphrag 的 Knowledge Graph Builder 把步骤写死了:数据加载 → 切块 →(可选)切块嵌入 → Schema → 词汇图(Document / Chunk)→ 实体关系抽取 → 按 Schema 修剪 → 写入 → 实体消解。SimpleKGPipeline 是带默认值的快捷方式,生产里至少要显式提供三样东西:LLM、嵌入模型、节点/关系类型 Schema。

没有 Schema 的抽取,模型会发明一堆近义关系:USES、USED_BY、DEPENDS、REQUIRES。图马上不可查询。Schema 不是限制创意,是给 LLM 一张合法边类型清单。实体消解是另一步:Writer 只管创建,不假设两个「Neo4j Inc.」是同一个节点;Resolver 负责合并。跳过消解,多跳查询会在重复实体之间「看起来很忙、什么都走不通」。

词汇图不要省。Document 和 Chunk 以及它们之间的 FROM_DOCUMENT / NEXT_CHUNK 关系,是「回答必须带原文」的退路。领域实体再漂亮,生成阶段仍需要能引用的句子。官方示例常把 Chunk 的 embedding 建向量索引,实体的 name 建全文索引,两套入口以后可以杂交。

构图不是一次 ETL。图谱算法可以往上叠:社区发现后的主题摘要(微软 Query Focused Summarization 那一路)、相似度边、Personalized PageRank。这些是计算出来的边,和 LLM 抽出来的边要分开标签,免得评测时分不清「模型抽的」和「算法算的」。


五、GraphRAG:向量只是起点,遍历才是上下文

Michael Hunger 在官方博客里把 GraphRAG 定义成:利用图结构里的丰富上下文,改进生成式应用的检索机制。基线 RAG 只靠向量库做语义搜索,返回的是彼此孤立的文本碎片,对付多跳问题先天不足。GraphRAG 用知识图谱把数据点连起来,检索器可以顺着关系把「不明显但关键」的邻居一并带回。

一次典型检索不是「向量 OR 图」,而是「向量当探针,图当放大镜」:

CALL db.index.vector.queryNodes('chunk_embedding', 5, $embedding)
YIELD node AS doc, score
RETURN score, doc, COLLECT {
MATCH path = (doc)-[rel]-(neighbor)
RETURN path
} AS paths
ORDER BY score DESC
LIMIT 10

先在切块向量索引上找 5 个起点,再把每个命中节点的一跳邻域收成路径。Prompt 里不再只有五段孤立文字,而是「这段话 + 它提到的实体 + 实体之间的关系」。可解释性来自路径本身:你可以告诉用户,答案经过了哪条 MENTIONS / DEPENDS_ON。

官方归纳了几类检索器,实际项目几乎总是组合使用:

类型 做什么 适合问什么
向量 / 全文 / 空间索引 用问题里的信号找图上的起点 「这段文档大概在讲什么」
邻域遍历 取节点的直接或间接邻居 「这个 API 还连着谁」
路径遍历 找实体之间的路径并扩展 「A 和 B 隔了几跳、中间是什么」
全局查询 预计算的跨主题摘要 「整体上这批文档在谈哪些社区」
查询模板 领域专家写好的分类查询 高频、结构稳定的业务问题
Text2Cypher LLM 按 Schema 生成 Cypher 明确的结构化计数/过滤
Agent 遍历 把检索器当工具,迭代选下一步 开放域、需要多步收集证据
图嵌入检索 用邻域「气质」做模糊拓扑匹配 结构相似、文本字面不像

Agent 化时,这些检索器就是工具。模型先向量定位,发现缺一跳关系,再调路径查询,最后才生成。不要一上来就把 Text2Cypher 当唯一入口:生成的 Cypher 会幻觉不存在的关系类型,空结果被模型圆成「知识库里没有」,其实是查询写错了。

插图:向量命中后沿关系扩展邻域


六、四种检索器怎么选:Vector、VectorCypher、Hybrid、Text2Cypher

neo4j-graphrag 把上面的模式收成可实例化的类。最小 GraphRAG 只要三件套:Neo4j Driver、Retriever、LLM。

from neo4j import GraphDatabase
from neo4j_graphrag.retrievers import VectorRetriever
from neo4j_graphrag.llm import OpenAILLM
from neo4j_graphrag.generation import GraphRAG
from neo4j_graphrag.embeddings import OpenAIEmbeddings

URI = "neo4j://localhost:7687"
AUTH = ("neo4j", "change-me-now")
INDEX_NAME = "chunk_embedding"

driver = GraphDatabase.driver(URI, auth=AUTH)
embedder = OpenAIEmbeddings(model="text-embedding-3-large")
retriever = VectorRetriever(driver, INDEX_NAME, embedder)
llm = OpenAILLM(model_name="gpt-4.1-mini")
rag = GraphRAG(retriever=retriever, llm=llm)

response = rag.search(
query_text="How do I do similarity search in Neo4j?",
retriever_config={"top_k": 5},
)
print(response.answer)

这是基线:只返回命中节点和分数,等价于「向量库住在 Neo4j 里」。真正用上图,要换成 VectorCypherRetriever。它的 retrieval_query 作用域里有 node 和 score,官方建议返回属性而不是整个节点对象,方便拼进 Prompt:

from neo4j_graphrag.retrievers import VectorCypherRetriever

# 作用域里已有 node(向量命中节点)和 score;从 Chunk 沿 MENTIONS 扩展邻域
retrieval_query = """
RETURN node.text AS chunk,
score AS similarity,
collect {
MATCH (node)-[:MENTIONS]->(e)
OPTIONAL MATCH (e)-[r]->(n2)
RETURN e.name AS src, type(r) AS rel, n2.name AS dst
} AS neighborhood
"""
retriever = VectorCypherRetriever(
driver,
index_name=INDEX_NAME,
embedder=embedder,
retrieval_query=retrieval_query,
)

HybridRetriever / HybridCypherRetriever 同时打向量索引和全文索引。产品名、错误码、API 符号这类「字面必须命中」的查询,纯向量经常被近义噪声挤掉;全文能拉回来。专有名词多的技术语料,杂交通常比单向量稳。

Text2CypherRetriever 把问题翻译成 Cypher 再跑。只适合 Schema 稳定、问题能落成过滤/计数/路径的场景,例如「有多少个 Tool 依赖 Protocol X」。必须把节点标签和关系类型清单塞进提示,并限制生成的子句。空结果不要直接交给生成模型编故事,先校验查询是否引用了不存在的类型。

过滤是另一个版本陷阱。filters={"year": {"$gte": 2000}} 在 2026.01 之前会绕过向量索引、改走暴力扫描,过滤条件不够狠就会把库打满。2026.01 起,库会自动走 Cypher SEARCH,把兼容谓词推进索引内过滤;前提是建索引时声明 filterable_properties,且只用 $eq/$ne/$lt/$lte/$gt/$gte/$between 以及隐式 $and。$or、$in、$like 仍会回退。升级了服务器却没改索引定义,等于没升级。

向量仍可外置:包里有 WeaviateNeo4jRetriever、PineconeNeo4jRetriever、QdrantNeo4jRetriever。已经为向量库付过钱的团队,不必把 embedding 再搬一次;Neo4j 负责关系,外部库负责近邻。代价是一次检索变两次网络往返,超时和一致性要自己兜。

LLM 侧不要被示例里的 gpt-5 带跑。官方自己注明:推理模型会拒 max_tokens、温度只能是 1,推理 Token 还计入 max_completion_tokens。固定温度的示例改用 gpt-4.1-mini 这类非推理模型。本地可以用 OllamaLLM 指到 127.0.0.1:11434,密钥不要写进仓库。


七、Docker 起一套,把迷你知识图跑起来

官方镜像在 Docker Hub,社区版与企业版都有。单机 Compose 的骨架是:数据卷持久化、7474 给浏览器、7687 给 Bolt、NEO4J_AUTH 设初始账号。不要把密码写进会进 Git 的明文;示例里用环境变量占位。

services:
neo4j:
image: neo4j:2026.08
ports:
- "7474:7474"
- "7687:7687"
environment:
NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-change-me-now}
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
restart: unless-stopped

volumes:
neo4j_data:
neo4j_logs:

docker compose up -d 之后,用容器内的 cypher-shell 跑种子,不要在应用里反复 CREATE:

docker compose up -d
docker compose exec neo4j cypher-shell -u neo4j -p change-me-now --file /var/lib/neo4j/import/seed.cypher

下面这张迷你图模拟「AI 工具知识图谱」:文档切块、工具实体、协议依赖。它不是电影示例的翻版,但建模手法一样——标签分类、关系类型化、属性挂在边上。

CREATE CONSTRAINT tool_name IF NOT EXISTS
FOR (t:Tool) REQUIRE t.name IS UNIQUE;

CREATE CONSTRAINT proto_name IF NOT EXISTS
FOR (p:Protocol) REQUIRE p.name IS UNIQUE;

MERGE (doc:Document {title: 'GraphRAG 实践笔记'})
MERGE (c1:Chunk {id: 'c1', text: 'VectorCypherRetriever 先向量命中 Chunk,再沿 MENTIONS 扩展实体邻域。'})
MERGE (c2:Chunk {id: 'c2', text: 'Bolt 是客户端与 Neo4j 之间的协议,默认端口 7687。'})
MERGE (neo4j:Tool {name: 'Neo4j'})
MERGE (graphrag:Tool {name: 'neo4j-graphrag'})
MERGE (bolt:Protocol {name: 'Bolt'})
MERGE (doc)-[:HAS_CHUNK]->(c1)
MERGE (doc)-[:HAS_CHUNK]->(c2)
MERGE (c1)-[:MENTIONS]->(graphrag)
MERGE (c1)-[:MENTIONS]->(neo4j)
MERGE (c2)-[:MENTIONS]->(bolt)
MERGE (c2)-[:MENTIONS]->(neo4j)
MERGE (graphrag)-[:RUNS_ON]->(neo4j)
MERGE (neo4j)-[:SPEAKS {port: 7687}]->(bolt);

验证多跳是否通:从一段切块走到协议,中间不必在文本里同时出现「Retriever」和「7687」。

MATCH (c:Chunk {id: 'c1'})-[:MENTIONS]->(t:Tool)-[:RUNS_ON]->(n:Tool)-[:SPEAKS]->(p:Protocol)
RETURN c.text AS startChunk, t.name AS mentioned, n.name AS runtime, p.name AS protocol

浏览器打开 http://localhost:7474 能看见节点和箭头,比任何架构 PPT 都有效。cypher-shell 也可以 --file 跑整份脚本,或在交互里 :source seed.cypher。官方还警告:无持久化卷的 docker run 一重启数据就没了;--restart always 只保证进程起来,不保证你挂了 /data。

Python 驱动连的是 Bolt,不是 HTTP。URI 用 neo4j://localhost:7687(单机)或 neo4j+s://(Aura)。账号密码走环境变量。页面包 practice/ 里放了 Compose、种子 Cypher 和最小检索脚本,本地可以先跑通图,再决定要不要接嵌入模型。

插图:属性图三件套与 GraphRAG 检索器


八、排坑:约束、索引、幻觉查询和 schema 漂移

1. 重复实体。 没有唯一约束就 CREATE,抽取管道每跑一次就多一排重影。先 CREATE CONSTRAINT ... REQUIRE n.name IS UNIQUE,写入一律 MERGE。消解步骤不能省,别指望后期用模糊匹配手工合并。

2. 向量过滤把库打满。 2026.01 之前 filters 会关闭近似最近邻。升级后仍要在索引上声明 filterable_properties,否则库会打日志并走旧路径。只把真正会过滤的字段放进去,乱加一串属性会让索引膨胀。

3. Text2Cypher 幻觉。 模型会写出图上不存在的关系类型,空结果被生成阶段圆成「没有这个事实」。把 Schema 当白名单,生成后先 EXPLAIN,再跑;空结果返回「查询未命中」而不是让模型自由发挥。动态标签查询在旧版本可能全表扫,Agent 自动写 Cypher 时优先静态标签。

4. CREATE 当幂等。 演示脚本跑两遍,节点翻倍,关系更乱。种子数据必须 MERGE + 约束;CI 里先清空测试库再导入,不要在共享实例上反复执行 CREATE。

5. Schema 漂移。 抽取提示改了一次,图上多出 USES 和 DEPENDS_ON 两种近义边,旧查询全哑火。关系类型变更要当迁移做:写 Cypher 把旧类型收拢,同时改检索模板和 Text2Cypher 的 Schema 描述。词汇图(Chunk)和领域图(Entity)的关系不要混用一个类型名。

向量库答不出谁连着谁?Neo4j 把知识图谱做成可遍历的上下文

6. 密钥与端口。 NEO4J_AUTH、OpenAI Key 只进环境变量。对外暴露 7474 等于把浏览器控制台公开;生产至少改密码、绑内网、需要时再上 Aura。Bolt 7687 才是应用该连的端口,不要让后端去爬 Browser 的 HTTP 接口。

选型可以收成一张对照表,避免把 Neo4j 说成「万能库」:

维度 关系型库 纯向量库 Neo4j 属性图
一等公民 行与表 嵌入向量 节点、类型化关系、属性
多跳查询 JOIN,跳数一多就痛 基本没有边 Cypher 模式匹配 / 变长路径
语义召回 要外挂检索 本职工作 向量索引 + 可选外部向量库
可解释性 SQL 计划 分数黑盒 路径即证据
Schema 先定表结构 几乎无 标签/类型可演进,仍需约束
典型代价 熟悉、生态大 运维简单 要建模、要管约束与抽取质量
AI 场景 事务与报表 切块 RAG GraphRAG、知识图谱、规则+语义

关系库继续管订单和权限,向量库继续管海量原文近邻。Neo4j 接手的是「实体之间怎么连、命中之后怎么走」。把这三层叠在同一条 RAG 流水线里,比争论「哪一个才是 AI 圈必学数据库」有用得多。

💻 配套实训环境与动手练习

本文涉及的相关技术指令、开发环境与工具链已内置在边学边练在线实验室中,无需繁琐安装配置,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章