一、为什么需要GraphRAG

传统RAG的局限非常明显:它只能找到文本相似的内容,无法理解实体间的关联

场景 Naive RAG GraphRAG
“Spring Boot的作者还创建了哪些项目” 只能搜到Spring Boot文档 通过图谱找到Rod Johnson→Spring Framework→其它项目
“调用链中Service A挂了会影响哪些API” 语义搜索不精确 图遍历精确找到所有下游依赖
“这个Bug和三个月前的问题一样吗” 向量相似不一定相关 通过实体(Bug→模块→开发者)关联判断

本文用Java+Spring Boot 3 + Neo4j + Spring AI,从零搭建一个GraphRAG Agent。


二、架构设计

+-------------------------------------------------+
|                   REST API                       |
|           AIGraphController                      |
+-------------------------------------------------+
|  GraphRAGService                                 |
|  +----------------+ +----------+ +-------------+ |
|  |EntityExtractor  | |GraphRepo | |VectorSearch | |
|  |  (LLM解析)      | |(Neo4j)  | |(Embedding)  | |
|  +----------------+ +----------+ +-------------+ |
+-------------------------------------------------+
|  Spring AI ChatClient + EmbeddingClient          |
+-------------------------------------------------+

三、依赖与配置

pom.xml 关键依赖:

<dependencies>
    <!-- Spring Boot 3 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI (OpenAI兼容) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        <version>1.0.0-M6</version>
    </dependency>

    <!-- Neo4j -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-neo4j</artifactId>
    </dependency>

    <!-- 向量存储 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-neo4j-store-spring-boot-starter</artifactId>
        <version>1.0.0-M6</version>
    </dependency>
</dependencies>

application.yml

spring:
  neo4j:
    uri: bolt://localhost:7687
    authentication:
      username: neo4j
      password: ${NEO4J_PASSWORD}
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini
      embedding:
        options:
          model: text-embedding-3-small

四、图实体模型

package com.demo.graphrag.model;

import org.springframework.data.neo4j.core.schema.*;
import java.util.Set;

/**
 * 知识图谱中的实体节点
 * 例如:技术名词"Spring Boot"、人名"Rod Johnson"、项目"PetClinic"
 */
@Node("Entity")
public class KnowledgeEntity {

    @Id
    @GeneratedValue
    private Long id;

    @Property("name")
    private String name;           // 实体名称

    @Property("type")
    private String type;           // 实体类型: PERSON/PROJECT/TECHNOLOGY/CONCEPT

    @Property("description")
    private String description;    // LLM生成的实体描述

    @Property("embedding")
    private double[] embedding;    // 向量嵌入(1536维)

    @Relationship(type = "RELATED_TO", direction = Relationship.Direction.OUTGOING)
    private Set<EntityRelation> relations;

    // constructors
    public KnowledgeEntity() {}

    public KnowledgeEntity(String name, String type) {
        this.name = name;
        this.type = type;
    }

    // getters/setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public double[] getEmbedding() { return embedding; }
    public void setEmbedding(double[] embedding) { this.embedding = embedding; }
    public Set<EntityRelation> getRelations() { return relations; }
    public void setRelations(Set<EntityRelation> relations) { this.relations = relations; }
}
package com.demo.graphrag.model;

import org.springframework.data.neo4j.core.schema.*;

/**
 * 实体之间的关系边
 */
@RelationshipProperties
public class EntityRelation {

    @RelationshipId
    private Long id;

    @TargetNode
    private KnowledgeEntity target;

    @Property("type")
    private String type;           // 关系类型: CREATED_BY, DEPENDS_ON, PART_OF, SIMILAR_TO

    @Property("weight")
    private double weight;         // 关系权重 0.0~1.0

    public EntityRelation() {}

    public EntityRelation(KnowledgeEntity target, String type, double weight) {
        this.target = target;
        this.type = type;
        this.weight = weight;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public KnowledgeEntity getTarget() { return target; }
    public void setTarget(KnowledgeEntity target) { this.target = target; }
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
    public double getWeight() { return weight; }
    public void setWeight(double weight) { this.weight = weight; }
}

五、Neo4j Repository层

package com.demo.graphrag.repository;

import com.demo.graphrag.model.KnowledgeEntity;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;

public interface EntityRepository extends Neo4jRepository<KnowledgeEntity, Long> {

    /**
     * 按名称精确查找实体
     */
    KnowledgeEntity findByName(String name);

    /**
     * 查找某实体的N度邻居
     * MATCH (e:Entity {name: $name})-[r:RELATED_TO*1..$depth]-(related:Entity)
     */
    @Query("MATCH (e:Entity {name: $name})-[r:RELATED_TO*1..$depth]-(related:Entity) " +
           "RETURN DISTINCT related LIMIT $limit")
    List<KnowledgeEntity> findNeighbors(
        @Param("name") String name,
        @Param("depth") int depth,
        @Param("limit") int limit
    );

    /**
     * 找出两个实体之间的最短路径
     */
    @Query("MATCH p = shortestPath(" +
           "(a:Entity {name: $from})-[*..5]-(b:Entity {name: $to})) " +
           "RETURN nodes(p)")
    List<KnowledgeEntity> findShortestPath(
        @Param("from") String from,
        @Param("to") String to
    );

    /**
     * 向量相似搜索(需要neo4j-graph-data-science插件)
     * 按余弦相似度排序返回最相似的实体
     */
    @Query("MATCH (e:Entity) WHERE e.embedding IS NOT NULL " +
           "WITH e, gds.similarity.cosine(e.embedding, $queryEmbedding) AS similarity " +
           "WHERE similarity > $threshold " +
           "RETURN e, similarity ORDER BY similarity DESC LIMIT $limit")
    List<KnowledgeEntity> findSimilarByEmbedding(
        @Param("queryEmbedding") double[] queryEmbedding,
        @Param("threshold") double threshold,
        @Param("limit") int limit
    );

    /**
     * 按类型过滤实体
     */
    List<KnowledgeEntity> findByType(String type);
}

六、核心服务:实体抽取 + 图谱构建

package com.demo.graphrag.service;

import com.demo.graphrag.model.EntityRelation;
import com.demo.graphrag.model.KnowledgeEntity;
import com.demo.graphrag.repository.EntityRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

@Service
public class GraphRAGService {

    private final ChatClient chatClient;
    private final EmbeddingClient embeddingClient;
    private final EntityRepository entityRepository;
    private final ObjectMapper objectMapper;

    public GraphRAGService(ChatClient.Builder chatBuilder,
                           EmbeddingClient embeddingClient,
                           EntityRepository entityRepository,
                           ObjectMapper objectMapper) {
        this.chatClient = chatBuilder.build();
        this.embeddingClient = embeddingClient;
        this.entityRepository = entityRepository;
        this.objectMapper = objectMapper;
    }

    /**
     * 从文本中抽取实体和关系,并写入Neo4j
     */
    @Transactional
    public List<KnowledgeEntity> ingestDocument(String documentText) {
        // 1. 用LLM抽取实体和关系
        String extractionResult = extractEntitiesAndRelations(documentText);

        // 2. 解析JSON结果
        List<ExtractedEntity> extracted;
        try {
            extracted = objectMapper.readValue(extractionResult,
                new TypeReference<List<ExtractedEntity>>() {});
        } catch (Exception e) {
            throw new RuntimeException("LLM抽取结果解析失败", e);
        }

        // 3. 持久化到Neo4j(含向量嵌入)
        List<KnowledgeEntity> saved = new ArrayList<>();
        for (ExtractedEntity ee : extracted) {
            KnowledgeEntity entity = upsertEntity(ee);
            saved.add(entity);

            // 4. 创建关系
            if (ee.relations != null) {
                for (ExtractedRelation rel : ee.relations) {
                    KnowledgeEntity target = upsertEntity(
                        new ExtractedEntity(rel.targetName, rel.targetType, null, null));
                    entity.getRelations().add(
                        new EntityRelation(target, rel.type, rel.weight));
                }
            }
            // 保存关系和嵌入
            entityRepository.save(entity);
        }
        return saved;
    }

    /**
     * 用LLM抽取实体和关系(返回JSON格式)
     */
    private String extractEntitiesAndRelations(String text) {
        String prompt = "你是一个知识图谱构建助手。从以下文本中提取所有关键实体和它们之间的关系。\n" +
            "以JSON数组格式返回,每个实体包含:\n" +
            "{\n" +
            "  \"name\": \"实体名称\",\n" +
            "  \"type\": \"PERSON|PROJECT|TECHNOLOGY|CONCEPT|ORGANIZATION\",\n" +
            "  \"description\": \"一句话描述\",\n" +
            "  \"relations\": [\n" +
            "    {\"targetName\": \"关联实体名\", \"targetType\": \"类型\", " +
            "\"type\": \"CREATED_BY|DEPENDS_ON|PART_OF|USES|SIMILAR_TO\", \"weight\": 0.8}\n" +
            "  ]\n" +
            "}\n\n" +
            "文本:\n" + text;

        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }

    /**
     * 插入或更新实体(含向量嵌入生成)
     */
    private KnowledgeEntity upsertEntity(ExtractedEntity ee) {
        KnowledgeEntity existing = entityRepository.findByName(ee.name);
        if (existing != null) {
            if (ee.description != null) {
                existing.setDescription(ee.description);
            }
            return existing;
        }

        KnowledgeEntity entity = new KnowledgeEntity(ee.name, ee.type);
        entity.setDescription(ee.description);

        // 生成向量嵌入
        List<double[]> embeddings = embeddingClient.embed(
            new EmbeddingRequest(List.of(ee.name + ": " + ee.description), null));
        if (!embeddings.isEmpty()) {
            entity.setEmbedding(embeddings.get(0));
        }

        entity.setRelations(new HashSet<>());
        return entityRepository.save(entity);
    }

    // 内部DTO类
    public static class ExtractedEntity {
        public String name;
        public String type;
        public String description;
        public List<ExtractedRelation> relations;

        public ExtractedEntity() {}
        public ExtractedEntity(String name, String type, String description,
                                List<ExtractedRelation> relations) {
            this.name = name;
            this.type = type;
            this.description = description;
            this.relations = relations;
        }
    }

    public static class ExtractedRelation {
        public String targetName;
        public String targetType;
        public String type;
        public double weight;
    }
}

七、混合检索:向量 + 图遍历

纯向量检索可能召回语义相似但逻辑无关的实体,GraphRAG结合图结构做多跳推理

package com.demo.graphrag.service;

import com.demo.graphrag.model.KnowledgeEntity;
import com.demo.graphrag.repository.EntityRepository;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class HybridSearchService {

    private final EntityRepository entityRepository;
    private final EmbeddingClient embeddingClient;

    public HybridSearchService(EntityRepository entityRepository,
                                EmbeddingClient embeddingClient) {
        this.entityRepository = entityRepository;
        this.embeddingClient = embeddingClient;
    }

    /**
     * 混合检索:向量相似 + 图扩展
     * 1. 向量检索找到种子实体
     * 2. 图遍历扩展关联实体
     * 3. 合并去重排序
     */
    public SearchResult hybridSearch(String query, int depth, int limit) {
        // Step 1: 生成查询向量
        List<double[]> queryEmb = embeddingClient.embed(
            new EmbeddingRequest(List.of(query), null));
        double[] queryVector = queryEmb.get(0);

        // Step 2: 向量检索Top-K种子实体
        List<KnowledgeEntity> seeds = entityRepository
            .findSimilarByEmbedding(queryVector, 0.7, limit);

        // Step 3: 图扩展——每个种子实体向外辐射N度
        Set<KnowledgeEntity> allEntities = new LinkedHashSet<>(seeds);
        for (KnowledgeEntity seed : seeds) {
            List<KnowledgeEntity> neighbors = entityRepository
                .findNeighbors(seed.getName(), depth, limit);
            allEntities.addAll(neighbors);
        }

        // Step 4: 构建上下文文本
        String context = buildContext(allEntities, seeds);

        return new SearchResult(allEntities, context);
    }

    /**
     * 将图数据转化为LLM可读的上下文文本
     */
    private String buildContext(Set<KnowledgeEntity> entities,
                                 List<KnowledgeEntity> seeds) {
        StringBuilder sb = new StringBuilder();
        sb.append("=== 核心相关实体 ===\n");
        for (KnowledgeEntity seed : seeds) {
            sb.append(formatEntity(seed)).append("\n");
        }
        sb.append("\n=== 关联实体 ===\n");
        for (KnowledgeEntity entity : entities) {
            if (!seeds.contains(entity)) {
                sb.append(formatEntity(entity)).append("\n");
            }
        }
        return sb.toString();
    }

    private String formatEntity(KnowledgeEntity e) {
        StringBuilder sb = new StringBuilder();
        sb.append("- ").append(e.getName())
          .append(" [").append(e.getType()).append("]");
        if (e.getDescription() != null) {
            sb.append(": ").append(e.getDescription());
        }
        return sb.toString();
    }

    public record SearchResult(Set<KnowledgeEntity> entities, String context) {}
}

八、REST API层

package com.demo.graphrag.controller;

import com.demo.graphrag.model.KnowledgeEntity;
import com.demo.graphrag.service.GraphRAGService;
import com.demo.graphrag.service.HybridSearchService;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/graphrag")
public class AIGraphController {

    private final GraphRAGService graphRAGService;
    private final HybridSearchService hybridSearchService;

    public AIGraphController(GraphRAGService graphRAGService,
                              HybridSearchService hybridSearchService) {
        this.graphRAGService = graphRAGService;
        this.hybridSearchService = hybridSearchService;
    }

    /**
     * 文档摄入:从文本中自动构建知识图谱
     */
    @PostMapping("/ingest")
    public List<KnowledgeEntity> ingest(@RequestBody IngestRequest request) {
        return graphRAGService.ingestDocument(request.text());
    }

    /**
     * 混合检索:结合向量和图的语义查询
     */
    @PostMapping("/search")
    public HybridSearchService.SearchResult search(@RequestBody SearchRequest request) {
        return hybridSearchService.hybridSearch(
            request.query(),
            request.depth() != 0 ? request.depth() : 2,
            request.limit() != 0 ? request.limit() : 10
        );
    }

    public record IngestRequest(String text) {}
    public record SearchRequest(String query, int depth, int limit) {}
}

九、端到端使用示例

# 1. 摄入技术文档构建知识图谱
curl -X POST http://localhost:8080/api/graphrag/ingest \
  -H "Content-Type: application/json" \
  -d '{"text": "Spring Boot是由Pivotal团队开发的Java框架。Rod Johnson是Spring框架的创始人。它内置了Tomcat并集成了Spring Data JPA和Spring Security。"}'

# 2. 图查询:谁是Spring框架的创始人?
curl -X POST http://localhost:8080/api/graphrag/search \
  -H "Content-Type: application/json" \
  -d '{"query": "Spring Boot的创始人和相关技术栈", "depth": 2, "limit": 10}'

响应示例:

{
  "entities": [
    {"name": "Spring Boot", "type": "TECHNOLOGY"},
    {"name": "Rod Johnson", "type": "PERSON"},
    {"name": "Spring Framework", "type": "TECHNOLOGY"},
    {"name": "Spring Data JPA", "type": "TECHNOLOGY"},
    {"name": "Spring Security", "type": "TECHNOLOGY"},
    {"name": "Pivotal", "type": "ORGANIZATION"},
    {"name": "Tomcat", "type": "TECHNOLOGY"}
  ],
  "context": "=== 核心相关实体 ===\n- Spring Boot [TECHNOLOGY]: ..."
}

十、工程化要点

关注点 方案 细节
实体去重 Neo4j MERGE语义 findByName先查再插,防止重复节点
向量存储 Neo4j原生数组属性 无需额外向量数据库,减少架构复杂度
事务一致性 @Transactional 实体+关系原子写入
LLM容错 JSON解析+try/catch LLM输出格式不稳定时降级处理
扩展性 分片+索引 Neo4j支持集群部署,支持亿级实体

更多推荐