GraphRAG落地实战:知识图谱与向量混合检索怎么补上召回短板

向量检索撑不起企业级问答的两个场景:全局概括(「这套系统一共有多少个降级策略」)和多跳关联(「订单超时关闭的链路涉及哪些服务」)。前者 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

评论0

请先
显示验证码
没有账号?注册  忘记密码?