
向量检索撑不起企业级问答的两个场景:全局概括(「这套系统一共有多少个降级策略」)和多跳关联(「订单超时关闭的链路涉及哪些服务」)。前者 Top-K 覆盖不了全库细碎节点,后者 A→B、B→C 分散在两份文档里,相似度检索根本看不见 A→C 这条路。GraphRAG 的思路是用图谱固化实体关系、用社区摘要撑全局概括,和向量检索互补,而不是替掉它。
双路召回的架构怎么搭
我们基于 Spring Boot 3 + LangChain4j + Neo4j + Milvus 做了工程落地,核心是双路召回:向量相似度一路,知识图谱子图挖掘一路,最后 RRF 融合重排。离线侧,文档分块后并行走两条管道——Embedding 进 Milvus,LLM 实体关系抽取经消歧后写入 Neo4j,图谱之上跑 Leiden 社区发现、分层生成社区摘要;在线侧,Query 先做意图分析和实体识别,再同时打向量库和图谱,结果融合后装配 Prompt。

三方案对比,选型时心里要有这张表:
| 维度 | Naive RAG | Advanced RAG | GraphRAG |
|---|---|---|---|
| 底层存储 | 纯向量库 | 向量库+BM25 | 向量库+Neo4j |
| 检索方式 | 单一向量相似度 | 向量+关键字 | Top-K+多跳+社区摘要 |
| 跨文档多跳 | 极差 | 较弱 | 极强 |
| 全局概括 | 几乎不支持 | 弱 | 极强 |
| 构建成本 | 低 | 中 | 较高(LLM 抽三元组+离线聚类) |
| 线上延迟 | 50~200ms | 100~400ms | 200~600ms |
结论很直接:文档更新频繁、预算有限、查询以单点事实为主,Naive RAG 加重排就够;多跳和全景查询占比高的场景(微服务拓扑、合规审计、故障复盘库),GraphRAG 的构建成本才花得值。
图谱检索的核心代码
按实体查两跳子图、查所属社区摘要,Cypher 是这么写的:
@Service
public class GraphRetrievalService {
private final Driver neo4jDriver;
public GraphRetrievalService(Driver neo4jDriver) {
this.neo4jDriver = neo4jDriver;
}
public List<String> retrieveSubGraph(List<String> entities) {
String cypherQuery =
"MATCH (e:Entity) WHERE e.name IN $entityNames " +
"MATCH path = (e)-[r:RELATION*1..2]-(target:Entity) " +
"RETURN DISTINCT " +
"head(nodes(path)).name + ' --[' + type(relationships(path)[0]) + ']-> ' + last(nodes(path)).name AS triplet " +
"LIMIT 50";
List<String> triplets = new ArrayList<>();
try (Session session = neo4jDriver.session()) {
var result = session.run(cypherQuery, Values.parameters("entityNames", entities));
while (result.hasNext()) {
triplets.add(result.next().get("triplet").asString());
}
}
return triplets;
}
public List<String> retrieveCommunitySummaries(List<String> entities) {
String communityCypher =
"MATCH (e:Entity)-[:BELONGS_TO]->(c:Community) " +
"WHERE e.name IN $entityNames " +
"RETURN DISTINCT c.title AS title, c.summary AS summary " +
"LIMIT 5";
List<String> summaries = new ArrayList<>();
try (Session session = neo4jDriver.session()) {
var result = session.run(communityCypher, Values.parameters("entityNames", entities));
while (result.hasNext()) {
var record = result.next();
summaries.add("【社区: " + record.get("title").asString() + "】: " + record.get("summary").asString());
}
}
return summaries;
}
}
在线侧两路并行召回后合成上下文,图谱三元组和社区摘要放前面、文本碎片放后面,System Prompt 里明确「检索不到就实事求是回答,严禁臆造」。

一个容易漏掉的依赖:GDS 插件
社区发现这一步实际落地时有个源码示例不会告诉你的坑:Neo4j 5.x 跑 Leiden 要装 GDS(Graph Data Science)插件,社区划分结果是内存中的算法结果,必须用 gds.leiden.write 把 communityId 写回节点属性,再由离线任务把 Community 节点和 BELONGS_TO 关系物化出来。漏了这步,retrieveCommunitySummaries 永远返回空列表,还不容易定位——Cypher 语法没错,数据就没落库。装完插件记得核对 GDS 版本与 Neo4j 主版本的兼容矩阵,5.x 对 GDS 2.x 的次版本有硬要求。
三个必须提前规避的坑
实体消歧缺失,图谱直接爆炸。文档 A 写 OrderService,文档 B 写「订单服务」,文档 C 写 trade-order-srv,直接抽取就是三个孤立节点。离线管道必须加消歧层:同义词表映射加小模型向量聚类,同义实体绑到同一个节点上。
Leiden 全量重算拖垮吞吐。社区聚类极其耗时,每上传一篇文档就全局重算是自杀。冷热分区:主干图谱周级全量聚类生成社区报告,日常新增走局部子图合并、小范围重聚类。
超级节点引发查询雪崩。在 MySQL、SpringCloud 这类度数上万的节点上跑无方向 *1..3 发散,几万条边瞬间拉出,Neo4j 内存飙升。对策:Cypher 严格带 LIMIT,度数超 200 的通用实体只保留与查询上下文共现的子边,Java 侧再设降级超时兜底。

效果与验收口径
1200 份技术文档、100 组跨链路与全景查询的评测集上,对比结果:
| 指标 | Naive RAG | GraphRAG |
|---|---|---|
| 检索命中率 Hit@5 | 62.4% | 91.8% |
| 全局回答完整性(LLM-as-a-Judge) | 2.8/5.0 | 4.6/5.0 |
| 多跳推理准确度 | 38.0% | 84.5% |
| P99 端到端延迟 | 420ms | 890ms |
延迟增加换准确率翻倍,企业知识库场景这笔账通常划算。验收要立住口径:Hit@5 的命中判定建议双人标注加仲裁,LLM-as-a-Judge 评分要固定评委模型和评分 Prompt 版本,否则前后不可比。回归评测集 freeze 起来,图谱结构一变就重跑,防止「优化」实际是退化。
一句话收束:图谱定骨架、向量充血肉、LLM 做表达。多跳和全景查询为主的知识库,这套架构值得尽早预研;但别把它当万金油,构建成本和延迟是真金白银的代价。

评论0