版本:Spring AI 2.0.1
目标:弄清知识图谱和向量 RAG 各解决什么问题;用 Neo4j + 结构化抽取 + 受限 Cypher Tool,把「写图 → 查图」接到 ChatClient。
说明: Spring AI 2.0.1 没有独立的 GraphRAG 模块。本章做的是:在图库上自己建 schema、自己写图、自己用
@Tool查图。

向量 RAG 按语义相似度找文本片段。下面这类问题,只靠向量往往不够稳:
| 问题类型 | 例子 | 向量 RAG 容易怎样 |
|---|---|---|
| 多跳 | Acme 的供应商合规等级是什么? | 两段文档可能分别提到供应商和合规,但拼不成路径 |
| 约束 | 谁同时在项目 X,又不能接触数据 Y? | 相似段落有了,交集/排除难做准 |
| 消歧 | 两个「张三」各在哪家公司? | 同名实体容易混 |
知识图谱把事实写成节点 + 边,查询用 Cypher 沿关系走,而不是只比向量距离。
11.1 和向量 RAG 怎么选
| 向量 RAG(第 10 章) | 知识图谱(本章) | |
|---|---|---|
| 存什么 | 文本 chunk + embedding | 实体节点、关系边、属性 |
| 怎么查 | 向量相似度 | 图模式匹配 / Cypher |
| 适合 | FAQ、手册、非结构化叙述 | 多跳、约束、关系校验 |
| 你要付的成本 | 切分、embedding、召回调参 | schema、抽取质量、图数据治理 |
| Spring AI 里用什么 | VectorStore + QA / Modular Advisor | Neo4jVectorStore(向量)、entity(...)(抽取)、@Tool(查图) |
判断口诀:
只要「找相关段落」→ 向量 RAG
必须「沿关系走两步以上」→ 图谱(或图 + 向量一起用)
先上线问答 → 先 RAG;关系类问题变多再补图
11.2 Neo4jVectorStore 是什么、不是什么
依赖与配置:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-neo4j</artifactId>
</dependency>
spring:
ai:
vectorstore:
neo4j:
database-name: neo4j
index-name: spring-ai-index
neo4j:
uri: bolt://localhost:7687
authentication:
username: neo4j
password: ${NEO4J_PASSWORD}
Neo4jVectorStore 的含义很具体:把向量检索放在 Neo4j 里做。 它不会自动帮你建业务实体、建 WORKS_AT 边,也不会自动生成 GraphRAG Agent。
业务知识图谱还要你自己做三件事:
-
定 schema(有哪些 Label、Relationship、主键、租户字段)
-
从文档抽出三元组,用参数化 Cypher 写入
-
用预置
@Tool查图,而不是让模型自由拼 Cypher
另外还有 Neo4jChatMemoryRepository:那是对话记忆存在 Neo4j,和业务知识图谱不是一回事。
11.3 落地路径:抽 → 写 → 查

整条链路就是图上那五步:文档 → entity() 抽三元组 → MERGE 写 Neo4j → 受限 @Tool → ChatClient 回答。
1)先定 Schema
关系名、主键、租户字段先写死,再让模型填值。Schema 漂了,抽取和查询一起漂。
(:Person {id, name, tenantId})
(:Company {id, name, tenantId})
(:Document {id, title})
(Person)-[:WORKS_AT]->(Company)
(Company)-[:SUPPLIES]->(Company)
(Document)-[:MENTIONS]->(Person|Company)
建议同时建唯一约束,例如 Person(id, tenantId)、Company(id, tenantId),避免重复节点。
2)用 entity() 抽三元组
public record KgTriple(String subjectId, String subjectName,
String relation,
String objectId, String objectName) {}
public record KgExtractResult(List<KgTriple> triples) {}
KgExtractResult extracted = chatClient.prompt()
.system("""
从文本中抽取实体关系三元组。
relation 只能是:WORKS_AT、SUPPLIES、MENTIONS。
subjectId / objectId 用稳定业务 id;没有 id 时用规范化名称生成可复现的 key。
""")
.user(documentText)
.call()
.entity(KgExtractResult.class);
kgWriter.mergeTriples(tenantId, documentId, extracted.triples());
要点:
-
relation限制在白名单,减少模型乱造边类型 -
抽出的结果要校验(空 id、非法 relation 直接丢掉)
-
本章目标是「抽得出、写得进」;更重的自纠流水线以后再加
3)参数化 MERGE 写图
以 WORKS_AT 为例:
public void mergeWorksAt(String tenantId,
String personId, String personName,
String companyId, String companyName,
String documentId) {
String cypher = """
MERGE (p:Person {id: $personId, tenantId: $tenantId})
SET p.name = $personName
MERGE (c:Company {id: $companyId, tenantId: $tenantId})
SET c.name = $companyName
MERGE (p)-[:WORKS_AT]->(c)
MERGE (d:Document {id: $documentId})
MERGE (d)-[:MENTIONS]->(p)
MERGE (d)-[:MENTIONS]->(c)
""";
neo4jClient.query(cypher)
.bind(tenantId).to("tenantId")
.bind(personId).to("personId")
.bind(personName).to("personName")
.bind(companyId).to("companyId")
.bind(companyName).to("companyName")
.bind(documentId).to("documentId")
.run();
}
mergeTriples 里按 relation 映射到固定方法,不要把模型吐出的关系名直接拼进 Cypher:
public void mergeTriples(String tenantId, String documentId, List<KgTriple> triples) {
for (KgTriple t : triples) {
switch (t.relation()) {
case "WORKS_AT" -> mergeWorksAt(
tenantId, t.subjectId(), t.subjectName(),
t.objectId(), t.objectName(), documentId);
case "SUPPLIES" -> mergeSupplies(
tenantId, t.subjectId(), t.subjectName(),
t.objectId(), t.objectName(), documentId);
case "MENTIONS" -> mergeMentions(tenantId, documentId, t);
default -> { /* 非法关系:丢弃并记日志 */ }
}
}
}
4)查询:受限 Cypher Tool
模型只负责选「调哪个预置查询」并填业务参数;Cypher 模板和 tenantId 都在服务端。
@Service
public class KgQueryTools {
private final KgQueryService kgQueryService;
public KgQueryTools(KgQueryService kgQueryService) {
this.kgQueryService = kgQueryService;
}
@Tool(description = "查询某人任职的公司")
public String companiesOfPerson(
@ToolParam(description = "人员业务 id") String personId,
ToolContext ctx) {
String tenantId = String.valueOf(ctx.getContext().get("tenantId"));
return kgQueryService.findCompanies(tenantId, personId);
}
@Tool(description = "查询公司两跳供应商的合规等级")
public String supplierCompliance(
@ToolParam(description = "公司业务 id") String companyId,
ToolContext ctx) {
String tenantId = String.valueOf(ctx.getContext().get("tenantId"));
return kgQueryService.supplierComplianceTwoHop(tenantId, companyId);
}
}
服务端查询仍然参数化:
MATCH (p:Person {id: $personId, tenantId: $tenantId})-[:WORKS_AT]->(c:Company)
RETURN c.id AS id, c.name AS name
两跳供应商示例:
MATCH (a:Company {id: $companyId, tenantId: $tenantId})
-[:SUPPLIES]->(b:Company {tenantId: $tenantId})
OPTIONAL MATCH (b)-[:HAS_COMPLIANCE]->(g:Compliance)
RETURN b.id AS supplierId, b.name AS supplierName, g.level AS level
挂到 ChatClient:
chatClient.prompt()
.user("张三现在在哪家公司?")
.tools(kgQueryTools)
.toolContext(Map.of("tenantId", tenantId))
.call()
.content();
tenantId 放进 ToolContext,不要做成模型可随意填写的 @ToolParam。
11.4 和向量 RAG 一起用
常见拼法:
用户问题
→ 向量 RAG 召回相关文档 / 实体线索(人名、公司名、文档 id)
→ 图谱 Tool 做多跳扩展或约束校验
→ 模型综合回答(注明依据:文档 id / 图路径)
Advisor 顺序通常仍是:记忆 → RAG → Tool(图谱查询挂在 Tool 层)。 Spring AI 没有「打开一个开关就自动 GraphRAG」的 Advisor;混合就是同一次对话里同时挂上向量检索和图谱 Tool。
11.5 安全与数据质量
| 风险 | 做法 |
|---|---|
| 模型自由拼 Cypher | 只暴露预置查询 Tool;模板 + $param |
| 跨租户读到别人的图 | 节点带 tenantId;查询强制过滤;tenantId 来自 ToolContext |
| 抽出不存在的边 | 关系白名单;抽检;关键关系二次确认 |
| 图越写越脏 | 幂等 MERGE;带来源 Document id,方便回溯 |
| 对话里误删图 | 写图 Tool 和读图 Tool 分开;对用户开放的对话默认只挂读 |
11.6 小结
-
向量 RAG:找相近段落。
-
知识图谱:沿实体关系回答多跳和约束问题。
-
Neo4jVectorStore:只是「向量存在 Neo4j」;业务图要自己建。 -
本章闭环:定 schema →
entity()抽取 → 参数化MERGE→ 受限 Cypher@Tool→(可选)和 RAG 一起用。
3283



被折叠的 条评论
为什么被折叠?



