@qdrant_engine: 想要构建GraphRAG?从我们的明星Pavan的这份实用指南开始。在这份实践指南中,Pavan演示了……

X AI KOLs Timeline 新闻

摘要

本文提供了一份实用指南,介绍如何使用LangExtract、Neo4j、Qdrant和Ollama构建GraphRAG系统,结合实体提取、知识图谱和向量搜索,实现上下文感知的检索。

想要构建GraphRAG?从我们的明星Pavan的这份实用指南开始 在这份实践指南中,Pavan演示了如何使用LangExtract、@neo4j、Qdrant和@ollama构建GraphRAG架构。 你将学到如何: → 使用LangExtract从非结构化文本中提取实体和关系 → 在@neo4j中构建知识图谱 → 使用Qdrant进行语义检索 → 结合图遍历和向量搜索,获得更具上下文感知的响应 → 使用@ollama在本地运行整个流水线 如果你正在探索GraphRAG或构建生产级的检索系统,这是一份很好的逐步指导。 阅读文章: https://blog.stackademic.com/a-practical-graphrag-architecture-using-langextract-neo4j-qdrant-and-ollama-0e4c86908c41…
查看原文
查看缓存全文

缓存时间: 2026/07/02 04:18

想要构建GraphRAG?从我们明星贡献者Pavan的这份实用指南开始吧!在这份实用指南中,Pavan演示了如何使用LangExtract、@neo4j、Qdrant和@ollama构建GraphRAG架构。你将学到如何: → 使用LangExtract从非结构化文本中提取实体和关系 → 在@neo4j中构建知识图谱 → 使用Qdrant进行语义检索 → 结合图谱遍历与向量搜索,获得更贴合上下文的回答 → 在本地使用@ollama运行整个流水线

如果你正在探索GraphRAG或构建生产级检索系统,这绝对是一份出色的手把手教程。 阅读文章:https://blog.stackademic.com/a-practical-graphrag-architecture-using-langextract-neo4j-qdrant-and-ollama-0e4c86908c41…


使用LangExtract、Neo4j、Qdrant和Ollama的实用GraphRAG架构

来源:https://blog.stackademic.com/a-practical-graphrag-architecture-using-langextract-neo4j-qdrant-and-ollama-0e4c86908c41?gi=f57d40028cea
作者:M K Pavan Kumar (https://medium.com/@manthapavankumar11?source=post_page—byline–0e4c86908c41—————————————)

今天,我们将构建一个完整的GraphRAG系统,完全使用由Ollama、Neo4j、Qdrant和LangExtract驱动的本地LLM。与传统的检索文档片段并返回的RAG系统不同,该架构将原始文本转换为知识图谱,并通过关系检索语义相关的实体。本方案真正的亮点是LangExtract,它能自动从非结构化文本中提取实体和关系,并将其转换为可供图谱直接使用的知识。通过结合Qdrant的向量检索与Neo4j的图谱遍历,我们创建了一个既能理解含义又能理解上下文的检索流水线。让我们深入架构,看看每个组件如何协同工作,提供基于图谱的答案。

按回车或点击查看完整图片(由作者M K Pavan Kumar创建)

架构深度解析

该架构从非结构化原始文本进入LangExtract层开始。与常规RAG流水线不同(后者会立即将文档分割成块并生成嵌入),该系统首先将文本转换为结构化知识。由Ollama托管的语言模型驱动的LangExtract,直接从源文本中识别实体及其语义关系。在药物示例中,诸如药物名称、剂量、用药频率和疾病状况等实体被提取出来,同时通过公共分组机制保留它们之间的关系。此阶段有效地将自然语言转换为结构化表示,捕捉事实性连接,而不仅仅是存储文本段落。

提取完成后,生成的实体和关系被转换为图谱组件。每个提取的概念成为一个带有唯一标识符的图谱节点,而语义关系(如剂量、频率或疾病状况)则成为连接这些节点的显式边。这一转换过程创建了正式的知识表示,其中信息围绕实体及其关系组织,而非文档边界。图结构保留了在传统基于块的检索系统中通常会丢失的上下文含义。

提取出的图谱随后被导入Neo4j,作为系统的主要知识库。每个实体存储为图谱节点,每个语义关系存储为原生图谱边。Neo4j成为结构化知识的权威来源,并支持对连接概念进行高效遍历。系统不再检索孤立的文本片段,而是可以探索实体之间的多跳关系,从而发现超出原始提取点的上下文信息。

图谱构建完成后,架构为每个图谱节点生成嵌入。嵌入模型不处理整个文档或块,而是直接作用于实体名称和概念。每个实体获得一个向量表示,以在嵌入空间中捕捉其语义含义。这一设计选择创建了针对图谱实体的语义索引,而非文本内容,使检索能够聚焦于概念而非段落。这些实体嵌入随后与对应的Neo4j节点标识符一起存储在Qdrant中。Qdrant作为高性能语义检索层,而Neo4j仍然是结构化知识层。存储在Qdrant中的载荷包含Neo4j节点ID,从而在向量搜索结果与图谱实体之间建立直接映射。这种职责分离使得Qdrant擅长语义相似性搜索,而Neo4j负责关系遍历和图谱推理。

在查询时,用户提交一个自然语言问题。相同的嵌入模型将查询转换为向量表示。然后在Qdrant中搜索此查询嵌入,以识别语义最相关的图谱实体。与传统RAG系统返回文档块不同,检索过程返回对应于知识图谱中实体的节点标识符。这些节点作为图谱的语义入口点,代表与用户意图最密切相关的概念。

随后,检索到的节点标识符被用于查询Neo4j。系统不仅仅停留在检索到的实体上,而是通过遍历相邻节点和关系来进行图谱扩展。这种遍历会检索到包含匹配实体及其连接上下文的子图。扩展过程使得架构能够收集那些仅通过向量相似性可能无法直接检索到的支撑信息。结果,检索到的上下文同时包含语义相关性和结构关系。

最终的子图被转换为适合语言模型消费的格式。节点被收集到结构化列表中,而关系被转换为可读的三元组,表达实体之间的显式连接。例如,“Lisinopril剂量10mg”或“Metformin条件糖尿病”这样的关系成为结构化的上下文语句。这个格式化阶段通过将图结构转换为可解释的文本上下文,弥合了图数据库与语言模型之间的鸿沟。

最后,格式化的图谱上下文被提供给LLM。模型不再依赖检索到的文档块,而是接收到一个基于图谱的表示,包含实体和关系。这使得LLM能够基于显式事实和连接进行推理,而不是从零散的文本段落中推断关系。由于上下文来源于图谱遍历,模型获得的是结构化知识,本质上比标准的基于向量的检索更易于解释和追溯。

因此,整个架构可以被视为一个混合检索系统:LangExtract执行知识提取,Neo4j管理结构化关系,Qdrant提供语义实体检索,LLM执行基于图谱的推理。向量相似性与图谱遍历的结合创造了一个既具语义感知又具结构信息的检索机制,使系统能够使用连接的知识而非孤立的文本片段来回答问题。这种方法显著改进了关系信息的检索,并使其特别适用于医疗、金融、法律系统、企业知识管理和科学研究等领域,在这些领域中,理解实体之间的关系往往比检索单个文本段落更为重要。

实现演练

__init__()
构造函数是整个GraphRAG系统的入口。它加载所有所需的配置值,例如Neo4j凭据、Qdrant连接详情和来自环境变量的Ollama设置。该方法建立与Neo4j和Qdrant的连接,以便在整个流水线中重用。它还初始化了将用于实体提取、嵌入生成和答案合成的Ollama客户端。最后,它存储模型名称和向量维度,使架构保持灵活和可配置。

def __init__(self, env_path: str = ".env", ollama_model_extract: str = "gemma3:latest",
             ollama_model_answer: str = "gemma3:latest", ollama_embedding_model: str = "embeddinggemma:latest",
             ollama_host: str | None = None, vector_dimension: int = 768):
    load_dotenv(env_path)
    self.qdrant_key = os.getenv("QDRANT_KEY")
    self.qdrant_url = os.getenv("QDRANT_URL")
    self.neo4j_uri = os.getenv("NEO4J_URI")
    self.neo4j_username = os.getenv("NEO4J_USERNAME")
    self.neo4j_password = os.getenv("NEO4J_PASSWORD")
    self.neo4j_driver = GraphDatabase.driver(
        self.neo4j_uri, auth=(self.neo4j_username, self.neo4j_password)
    )
    self.qdrant_client = QdrantClient(
        url=self.qdrant_url, api_key=self.qdrant_key,
    )
    # 本地嵌入的Ollama客户端 (embeddinggemma:latest)
    self.ollama_client = ollama.Client(host=ollama_host) if ollama_host else ollama.Client()
    # langextract 需要纯URL字符串,而不是客户端对象
    self.ollama_url = ollama_host or os.environ.get("OLLAMA_HOST", "http://localhost:11434")
    # 模型/配置旋钮
    self.ollama_model_extract = ollama_model_extract
    self.ollama_model_answer = ollama_model_answer
    self.ollama_embedding_model = ollama_embedding_model
    self.vector_dimension = vector_dimension

extract_graph_components()
此方法负责将原始非结构化文本转换为结构化知识。它定义了提取指令并提供了示例,引导LangExtract识别实体及其关系。使用由Ollama托管的LLM,LangExtract处理输入文本并提取诸如药物名称、剂量、频率和疾病状况等概念。此阶段的输出仍然是提取元素的集合,而非图谱。然后该方法将这些提取结果转发用于图谱构建,为知识图谱奠定基础。

def extract_graph_components(self, raw_data: str):
    """使用langextract + Ollama提取药物实体和关系。"""
    prompt_description = textwrap.dedent("""
        提取药物及其详细信息,使用属性对相关信息进行分组:
        1. 按文本中出现顺序提取实体
        2. 每个实体必须有一个'medication_group'属性,将其链接到所属药物
        3. 关于同一药物的所有详细信息应共享相同的medication_group值
    """).strip()
    examples = [
        lx.data.ExampleData(
            text=("患者每天服用阿司匹林100mg以保护心脏健康,并在睡前服用辛伐他汀20mg。"),
            extractions=[
                lx.data.Extraction(
                    extraction_class="medication",
                    extraction_text="阿司匹林",
                    attributes={"medication_group": "阿司匹林"},
                ),
                lx.data.Extraction(
                    extraction_class="dosage",
                    extraction_text="100mg",
                    attributes={"medication_group": "阿司匹林"},
                ),
                lx.data.Extraction(
                    extraction_class="frequency",
                    extraction_text="每天",
                    attributes={"medication_group": "阿司匹林"},
                ),
                lx.data.Extraction(
                    extraction_class="condition",
                    extraction_text="心脏健康",
                    attributes={"medication_group": "阿司匹林"},
                ),
                lx.data.Extraction(
                    extraction_class="medication",
                    extraction_text="辛伐他汀",
                    attributes={"medication_group": "辛伐他汀"},
                ),
                lx.data.Extraction(
                    extraction_class="dosage",
                    extraction_text="20mg",
                    attributes={"medication_group": "辛伐他汀"},
                ),
                lx.data.Extraction(
                    extraction_class="frequency",
                    extraction_text="睡前",
                    attributes={"medication_group": "辛伐他汀"},
                ),
            ],
        )
    ]
    result = lx.extract(
        text_or_documents=raw_data,
        prompt_description=prompt_description,
        examples=examples,
        model_id=self.ollama_model_extract,
        model_url=self.ollama_url,
        resolver_params={"format_handler": lx_ollama.OLLAMA_FORMAT_HANDLER},
        max_char_buffer=4000,
        show_progress=True,
    )
    return self._convert_extractions_to_graph(result.extractions)

_convert_extractions_to_graph()
一旦实体被提取,此方法将它们转换为图谱友好的结构。它对相关信息进行分组,并识别作为锚节点的主要实体。为每个节点生成唯一标识符,以确保在Neo4j和Qdrant之间的一致性。然后在锚实体与其关联属性之间创建关系,保留语义含义。最终输出包括准备导入图数据库的图谱节点和边。

def _convert_extractions_to_graph(self, extractions: list):
    """将langextract的扁平分组提取结果转换为(节点, 关系)"""
    groups: dict[str, list] = {}
    for ext in extractions:
        if not ext.attributes or "medication_group" not in ext.attributes:
            continue
        group_name = ext.attributes["medication_group"]
        groups.setdefault(group_name, []).append(ext)
    nodes: dict[str, str] = {}
    relationships: list[dict] = []
    for group_name, group_extractions in groups.items():
        anchor_ext = next(
            (e for e in group_extractions if e.extraction_class == "medication"), None,
        )
        # 如果该组中没有显式的"medication"提取,则回退到组名本身,从而仍然获得锚点。
        anchor_text = anchor_ext.extraction_text if anchor_ext else group_name
        if anchor_text not in nodes:
            nodes[anchor_text] = str(uuid.uuid4())
        for ext in group_extractions:
            if ext is anchor_ext:
                continue
            target_text = ext.extraction_text
            if target_text not in nodes:
                nodes[target_text] = str(uuid.uuid4())
            relationships.append(
                {
                    "source": nodes[anchor_text],
                    "target": nodes[target_text],
                    "type": ext.extraction_class,
                }
            )
    return nodes, relationships

ingest_to_neo4j()
此方法将生成的图谱结构持久化到Neo4j中。每个提取的实体存储为图谱节点,而语义关系存储为图谱边。通过以这种格式存储数据,Neo4j可以后续执行高效的图谱遍历和关系探索。该方法确保每个节点保持唯一标识符,从而允许与向量搜索结果链接。完成后,图谱成为系统的结构化知识库。

def ingest_to_neo4j(self, nodes: dict, relationships: list):
    """将节点和关系导入Neo4j。"""
    with self.neo4j_driver.session() as session:
        # 在Neo4j中创建节点
        for name, node_id in nodes.items():
            session.run(
                "CREATE (n:Entity {id: $id, name: $name})",
                id=node_id, name=name,
            )
        # 在Neo4j中创建关系,使用语义类型
        # (dosage/frequency/condition等) 作为实际关系标签,
        # 而不是通用的"RELATIONSHIP"类型。
        for relationship in relationships:
            rel_type = self._sanitize_relationship_type(relationship["type"])
            session.run(
                "MATCH (a:Entity {id: $source_id}), (b:Entity {id: $target_id}) "
                f"CREATE (a)-[:{rel_type} {{type: $type}}]->(b)",
                source_id=relationship["source"],
                target_id=relationship["target"],
                type=relationship["type"],
            )
    return nodes

_sanitize_relationship_type()
由于关系名称源自LLM生成的提取结果,它们可能包含无效字符或格式。此方法在将关系标签插入Neo4j之前对其清理和标准化。转换将标签转换为符合Cypher要求的安全大写标识符。它还保护系统免受格式错误的关系名称和潜在查询问题的影响。虽然这是一个小方法,但它在维护图谱完整性方面起着重要作用。

@staticmethod
def _sanitize_relationship_type(raw_type: str) -> str:
    """
    Cypher关系类型不能作为查询参数传递,因此必须直接插入到查询字符串中。
    由于raw_type来自

相似文章