RAG

RAG优化技术:Graph RAG

前面我们在介绍查询路由的时候,提到过可以用图数据库来增强RAG的效果,同时在提示词改写的时候也提到过,通过问题拆解,可以解决多跳问题回答效果不好的问题。 这些都和我们这一期要讲的Graph RAG有关系。 Graph RAG 是一种…

TL;DR

前面我们在介绍查询路由的时候,提到过可以用图数据库来增强RAG的效果,同时在提示词改写的时候也提到过,通过问题拆解,可以解决多跳问题回答效果不好的问题。 这些都和我们这一期要讲的Graph RAG有关系。 Graph RAG 是一种…

前面我们在介绍查询路由的时候,提到过可以用图数据库来增强RAG的效果,同时在提示词改写的时候也提到过,通过问题拆解,可以解决多跳问题回答效果不好的问题。 这些都和我们这一期要讲的Graph RAG有关系。 Graph RAG 是一种结合了图结构(Graph)与RAG,技术的新型人工智能架构。它在传统 RAG 的基础上引入了知识图谱或由文本构建的图结构,以更有效地组织、检索和利用信息,从而提升大语言模型(LLM)在复杂问答、推理和知识密集型任务中的表现。 传统的 RAG 方法通常包括两个步骤: 1. 检索(Retrieval):从大规模文档库中用向量相似度(如 embedding)检索出与用户查询最相关的若干段落。 2. 生成(Generation):将检索到的内容与原始问题一起输入 LLM,生成答案。 但这种方法存在一些问题: - 上下文碎片化:检索到的段落彼此孤立,缺乏语义关联。 - 难以处理多跳推理:例如“爱因斯坦在哪所大学工作过?该校位于哪个国家?”需要两步推理,传统 RAG 难以有效支持。(虽然问题改写可以解决一部分,但是跳数过多,或者逻辑过于复杂的未必能解决) - 重复/冗余信息:不同文档可能包含相同事实,导致信息冗余或冲突。 - 缺乏全局知识结构:无法利用实体之间的关系进行推理。 Graph RAG 的核心思想是:将非结构化文本转化为结构化的图,并在图上进行智能检索与推理。 GraphRAG和传统RAG的主要区别就是会借助图数据库和知识图谱技术,抽取文档中的实体之间的关系,构建一个图结构,不再依赖相似度检索,而是改用图的拓扑结构来定位相关信息。

知识图谱

知识图谱是一种以图结构表示实体及其之间关系的知识库。它将现实世界中的事物(如人、地点、事件、概念等)建模为节点(实体),将它们之间的语义关系建模为边(关系)。 典型三元组形式:(实体, 关系, 实体) 例如:(吴京, 参演过, 战狼)

图数据库

图数据库是一种专门用于存储、查询和管理图结构数据的 NoSQL 数据库。它以节点(Node)、边(Edge/Relationship) 和 属性(Property) 为核心模型,天然适合表达和处理高度互联的数据。 - 节点(Node):表示实体,如“用户”、“商品”、“城市”。 - 边(Edge / Relationship):表示节点之间的关系,如“购买”、“关注”、“位于”。边是有方向的(可选),并可携带属性。 - 属性(Property):键值对,用于描述节点或边的特征,如 {name: "张三", age: 30}。 图数据库如果只能推荐一个的话,那一定是Neo4j 接下来,我们就以Neo4J为例,介绍一个通过图数据库+知识图谱做检索的例子,使我们的RAG系统能够回答这样的多跳问题:电影《十面埋伏》的导演,还导演过什么电影? 这是一个经典的多跳图查询,我们需要在 Neo4j 中执行以下逻辑: 1. 找到电影《十面埋伏》 2. 找到导演了这部电影的导演。 3. 找出该导演还导演了哪些其他电影。

Neo4J 部署

docker run \
  --name neo4j \
  -p 7474:7474 -p 7687:7687 \
  -v $HOME/neo4j/data:/data \
  -v $HOME/neo4j/logs:/logs \
  -v $HOME/neo4j/conf:/conf \
  -e NEO4J_AUTH=neo4j/neo4j666 \
  -e NEO4JLABS_PLUGINS='["apoc"]' \
  -d neo4j:5.22-community

部署成功后,使用http://localhost:7474 访问

Neo4J接入

添加依赖

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

增加配置:

spring:
  neo4j:
    uri: bolt://localhost:7687
    authentication:
      username: neo4j
      password: your_password

定义实体类

@Node("Movie")
public class Movie {
    @Id
    private String title;

    private int year;

    public Movie() {
    }

    public Movie(String title, int year) {
        this.title = title;
        this.year = year;
    }

    // Getters
    public String getTitle() {
        return title;
    }

    public int getYear() {
        return year;
    }
}
@Node("Director")
public class Director {
    //导演
    private String director;
    @Id
    private String name;

    public Director() {
    }

    public Director(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

创建 Repository

@Repository
public interface MovieGraphRepository extends Neo4jRepository<Movie, String> {
    @Query("""
            MATCH (m:Movie {title: $title}) <-[:DIRECTED]- (d:Director) -[:DIRECTED]-> (other:Movie)
            WHERE other.title <> $title
            RETURN d.name AS director, collect(other.title + ' (' + other.year + ')') AS otherMovies
            """)
    List<DirectorMoviesDto> findOtherMoviesBySameDirector(String title);

}

这里面返回值我们封装成DirectorMoviesDto:

public class DirectorMoviesDto {
    private String director;
    private List<String> otherMovies;

    public DirectorMoviesDto() {
    }

    public DirectorMoviesDto(String director, List<String> otherMovies) {
        this.director = director;
        this.otherMovies = otherMovies;
    }

    public String getDirector() {
        return director;
    }

    public void setDirector(String director) {
        this.director = director;
    }

    public List<String> getOtherMovies() {
        return otherMovies;
    }

    public void setOtherMovies(List<String> otherMovies) {
        this.otherMovies = otherMovies;
    }
}

接着,定义Service

@Service
public class GraphService {

    @Autowired
    private MovieGraphRepository repository;

    public String retrieveContext(String movieName) {
        List<Map<String, Object>> results = repository.findOtherMoviesBySameDirector(movieName);

        if (results.isEmpty()) {
            return "未找到导演过《" + movieName + "》的导演的其他作品。";
        }

        StringBuilder sb = new StringBuilder();
        for (Map<String, Object> row : results) {
            String director = (String) row.get("director");
            @SuppressWarnings("unchecked")
            List<String> movies = (List<String>) row.get("otherMovies");
            sb.append(String.format("- 导演 %s 还执导了:%s\n", director, String.join("、", movies)));
        }
        return sb.toString().trim();

    }

}

定义Controller,先做数据初始化:

@RequestMapping("/rag/graph")
@RestController
public class GraphRagController {

    @Autowired
    private Neo4jTemplate neo4jTemplate;

    @Autowired
    private Neo4jClient neo4jClient;


    @GetMapping("/init")
    public String initData() {
        // 保存节点
        neo4jTemplate.save(new Director("张艺谋"));
        neo4jTemplate.save(new Director("陈思诚"));
        neo4jTemplate.save(new Movie("十面埋伏", 2004));
        neo4jTemplate.save(new Movie("影", 2016));
        neo4jTemplate.save(new Movie("英雄", 2002));
        neo4jTemplate.save(new Movie("误杀", 2019));

        neo4jClient.query("""
                        MATCH (p:Director {name: $name}), (m:Movie {title: $title})
                        MERGE (p)-[:DIRECTED]->(m)
                        """)
                .bind("张艺谋").to("name")
                .bind("十面埋伏").to("title")
                .run();
        neo4jClient.query("""
                        MATCH (p:Director {name: $name}), (m:Movie {title: $title})
                        MERGE (p)-[:DIRECTED]->(m)
                        """)
                .bind("张艺谋").to("name")
                .bind("影").to("title")
                .run();

        neo4jClient.query("""
                        MATCH (p:Director {name: $name}), (m:Movie {title: $title})
                        MERGE (p)-[:DIRECTED]->(m)
                        """)
                .bind("张艺谋").to("name")
                .bind("英雄").to("title")
                .run();
        neo4jClient.query("""
                        MATCH (p:Director {name: $name}), (m:Movie {title: $title})
                        MERGE (p)-[:DIRECTED]->(m)
                        """)
                .bind("陈思诚").to("name")
                .bind("误杀").to("title")
                .run();

        return "Data initialized successfully";
    }
}

neo4jTemplate.save可以直接来保存节点,但是关系需要使用neo4jClient.query("...").bind(...).to(...).run(); ,他是使用Neo4J的Cypher语言进行数据库操作的,实现数据的初始化。 做图数据库检索及回答:

@RequestMapping("/rag/graph")
@RestController
public class GraphRagController {

    @Autowired
    private GraphService graphService;

    @Autowired
    private ChatModel chatModel;

    @GetMapping("/ask")
    public String ask(@RequestBody String movieName) {

        String context = graphService.retrieveContext(movieName);

        String prompt = """
                你是一个电影知识助手,请根据以下上下文回答问题。
                如果上下文没有足够信息,请回答“我不知道”。

                上下文:
                %s

                问题:%s
                回答:
                """.formatted(context, movieName + "的导演还执导过哪些电影?");

        return chatModel.call(prompt);
    }
}
版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。