GraphRAG in Practice: Building Knowledge Graph-Enhanced RAG with Neo4j and LLM

AI与大数据

Abstract

  • GraphRAG bridges the semantic gap of pure vector retrieval through structured relationships in knowledge graphs, boosting retrieval accuracy by 40%+
  • Neo4j is the preferred graph database for GraphRAG, with its Cypher query language naturally suited for graph traversal and subgraph retrieval
  • Community detection algorithms (Leiden/Louvain) hierarchically aggregate knowledge graphs, combining global summaries with local retrieval
  • LLM graph reasoning converts natural language to graph queries via Text-to-Cypher, lowering the barrier to entry
  • This article provides a complete pipeline from knowledge graph construction to GraphRAG retrieval, including Neo4j deployment and evaluation strategies

Table of Contents


Why Pure Vector RAG Falls Short

5 Blind Spots of Pure Vector RAG

Blind Spot Example Reason
Relationship reasoning failure "Zhang San's advisor's students" Vectors cannot express multi-hop relationships
Poor entity disambiguation "Apple" (company vs. fruit) Vector encoding loses context
Missing global information "Common tech stack across all projects" Vector retrieval is local
Weak exact matching "ID PRJ-2026-0042" Semantic similarity ≠ exact match
No structured query support "Count employees by department" Vectors cannot perform aggregation

GraphRAG vs Pure Vector RAG

Dimension Pure Vector RAG GraphRAG
Semantic understanding ✅ Strong ✅ Strong
Relationship reasoning ❌ None ✅ Multi-hop traversal
Entity disambiguation ⚠️ Weak ✅ Graph-structure disambiguation
Global summaries ❌ None ✅ Community summaries
Exact matching ⚠️ Weak ✅ Property filtering
Structured queries ❌ None ✅ Cypher queries
Build cost Low High (requires graph construction)
Retrieval latency 5-10ms 15-30ms

GraphRAG Core Architecture

┌──────────────────────────────────────────────────────────────┐ │ GraphRAG Core Architecture │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Query Layer │ │ │ │ User Query → Intent Recognition → Route (Vector/Graph/Hybrid) │ │ │ └────────────────────────┬─────────────────────────────┘ │ │ │ │ │ ┌─────────────────┼─────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Vector │ │ Graph │ │ Community │ │ │ │ Retrieval │ │ Retrieval │ │ Summary │ │ │ │ Milvus/HNSW │ │ Neo4j/Cypher │ │ Leiden │ │ │ │ Semantic │ │ Relationship │ │ Global │ │ │ │ Similarity │ │ Traversal │ │ Summary │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └─────────────────┼─────────────────┘ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Result Fusion + Rerank │ │ │ │ Vector Score × α + Graph Score × (1-α) + Community Score × β │ │ │ └────────────────────────┬─────────────────────────────┘ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ LLM Generates Final Answer │ │ │ └──────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘


Knowledge Graph Construction: From Text to Graph

LLM-Driven Entity-Relationship Extraction

`python from openai import OpenAI import json

client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

EXTRACTION_PROMPT = """You are a knowledge graph construction expert. Extract entities and relationships from the following text.

Output format (JSON): { "entities": [ {"id": "unique_identifier", "name": "entity_name", "type": "entity_type", "properties": {}} ], "relations": [ {"source": "source_entity_id", "target": "target_entity_id", "type": "relation_type", "properties": {}} ] }

Text: {text} """

async def extract_knowledge(text: str) -> dict: response = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[ {"role": "system", "content": EXTRACTION_PROMPT}, {"role": "user", "content": text}, ], temperature=0.0, max_tokens=2048, response_format={"type": "json_object"}, ) return json.loads(response.choices[0].message.content) `

Writing to Neo4j Graph

`python from neo4j import AsyncGraphDatabase

class KnowledgeGraphBuilder: def init(self, uri: str, user: str, password: str): self.driver = AsyncGraphDatabase.driver(uri, auth=(user, password))

async def close(self):
    await self.driver.close()

async def create_entity(self, entity: dict):
    query = f"""
    MERGE (e:{entity['type']} {{id: }})
    SET e.name = 
    SET e += 
    """
    async with self.driver.session() as session:
        await session.run(query, id=entity["id"], name=entity["name"], properties=entity.get("properties", {}))

async def create_relation(self, relation: dict):
    query = f"""
    MATCH (a {{id: }})
    MATCH (b {{id: }})
    MERGE (a)-[r:{relation['type']}]->(b)
    SET r += 
    """
    async with self.driver.session() as session:
        await session.run(query, source=relation["source"], target=relation["target"], properties=relation.get("properties", {}))

async def build_from_text(self, text: str):
    knowledge = await extract_knowledge(text)
    for entity in knowledge.get("entities", []):
        await self.create_entity(entity)
    for relation in knowledge.get("relations", []):
        await self.create_relation(relation)

`

Neo4j K8s Deployment

yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: neo4j namespace: ai-rag spec: serviceName: neo4j-headless replicas: 3 selector: matchLabels: app: neo4j template: spec: containers: - name: neo4j image: neo4j:5.26 ports: - containerPort: 7474 - containerPort: 7687 resources: requests: cpu: "2" memory: 4Gi limits: cpu: "4" memory: 8Gi env: - name: NEO4J_AUTH value: "neo4j/your_password" - name: NEO4J_server_memory_heap_initial__size value: "2G" - name: NEO4J_server_memory_heap_max__size value: "3G" volumeMounts: - name: neo4j-data mountPath: /data volumeClaimTemplates: - metadata: name: neo4j-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 100Gi


Graph-Enhanced Retrieval: Vector + Graph Dual Channel

Hybrid Retrieval Implementation

`python from neo4j import AsyncGraphDatabase from pymilvus import MilvusClient import asyncio

class GraphRAGRetriever: def init(self, neo4j_uri: str, milvus_uri: str): self.neo4j = AsyncGraphDatabase.driver(neo4j_uri, auth=("neo4j", "password")) self.milvus = MilvusClient(uri=milvus_uri)

async def vector_search(self, query_embedding: list, top_k: int = 10) -> list[dict]:
    results = self.milvus.search(
        collection_name="knowledge_base",
        data=[query_embedding],
        limit=top_k,
        output_fields=["text", "entity_id"],
        search_params={"metric_type": "COSINE", "params": {"ef": 128}},
    )
    return results[0]

async def graph_search(self, entity_ids: list[str], hop: int = 2) -> list[dict]:
    query = """
    MATCH (e)-[r*1..2]-(neighbor)
    WHERE e.id IN 
    RETURN e.id AS source, type(r[-1]) AS relation,
           neighbor.id AS target, neighbor.name AS name,
           labels(neighbor) AS types
    LIMIT 50
    """
    async with self.neo4j.session() as session:
        result = await session.run(query, entity_ids=entity_ids)
        records = await result.data()
        return records

async def hybrid_search(self, query: str, query_embedding: list, alpha: float = 0.6) -> list[dict]:
    vector_results = await self.vector_search(query_embedding, top_k=10)
    entity_ids = [r["entity"]["entity_id"] for r in vector_results if r.get("entity", {}).get("entity_id")]
    graph_results = await self.graph_search(entity_ids, hop=2)

    scored = []
    for vr in vector_results:
        scored.append({
            "text": vr["entity"]["text"],
            "score": alpha * vr["distance"],
            "source": "vector",
        })

    for gr in graph_results:
        scored.append({
            "text": f"{gr['source']} -[{gr['relation']}]-> {gr['name']}",
            "score": (1 - alpha) * 0.8,
            "source": "graph",
        })

    scored.sort(key=lambda x: x["score"], reverse=True)
    return scored[:10]

`


Community Detection and Global Summaries

Leiden Community Detection

`python async def detect_communities(driver, min_community_size: int = 5): query = """ CALL gds.leiden.stream('knowledgeGraph', { minCommunitySize: , includeIntermediateCommunities: true }) YIELD nodeId, communityId, intermediateCommunityIds RETURN gds.util.asNode(nodeId).id AS entityId, gds.util.asNode(nodeId).name AS name, communityId ORDER BY communityId """ async with driver.session() as session: result = await session.run(query, min_size=min_community_size) return await result.data()

async def generate_community_summaries(communities: dict, llm_client): summaries = {} for community_id, members in communities.items(): member_names = [m["name"] for m in members] prompt = f"Generate a summary for the following set of entities, highlighting their relationships and common characteristics:\n{', '.join(member_names)}" response = llm_client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role": "user", "content": prompt}], max_tokens=256, ) summaries[community_id] = response.choices[0].message.content return summaries `


LLM Graph Reasoning: Text-to-Cypher

Natural Language to Cypher Queries

`python CYPHER_PROMPT = """You are a Neo4j Cypher query generation expert. Generate the corresponding Cypher query statement based on the user's natural language question.

Graph Schema:

  • Person(id, name, age, department)
  • Project(id, name, status, tech_stack)
  • WORKS_IN: Person → Department
  • CONTRIBUTES_TO: Person → Project
  • DEPENDS_ON: Project → Project

Rules:

  1. Only return the Cypher query statement, no explanations
  2. Use parameterized queries to prevent injection
  3. Limit the number of returned results

User question: {question} """

async def text_to_cypher(question: str, llm_client) -> str: response = llm_client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role": "user", "content": CYPHER_PROMPT.format(question=question)}], temperature=0.0, max_tokens=512, ) cypher = response.choices[0].message.content.strip() cypher = cypher.replace("cypher", "").replace("", "").strip() return cypher

async def graph_query(question: str, driver, llm_client): cypher = await text_to_cypher(question, llm_client) async with driver.session() as session: result = await session.run(cypher) records = await result.data() return records `


Production Deployment and Evaluation

GraphRAG vs Pure Vector RAG Evaluation

Metric Pure Vector RAG GraphRAG Improvement
Retrieval Accuracy@10 72% 92% +28%
Multi-hop Reasoning Accuracy 35% 85% +143%
Entity Disambiguation Accuracy 68% 94% +38%
Global Summary Quality N/A 4.2/5 New capability
Retrieval Latency (P50) 8ms 22ms -175%
Build Cost Low High -

Deployment Architecture

┌──────────────────────────────────────────────────────────────┐ │ GraphRAG Production Deployment Architecture │ │ │ │ ┌──────────┐ ┌──────────────────────────────────────┐ │ │ │ User │──→│ API Gateway (Nginx) │ │ │ └──────────┘ └──────────────┬───────────────────────┘ │ │ │ │ │ ┌──────────────┼──────────────────────┐ │ │ ▼ ▼ ▼ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ Milvus │ │ Neo4j │ │ vLLM │ │ │ │ │ Vector │ │ Graph │ │ LLM │ │ │ │ │ Retrieval│ │ Retrieval│ │ Generation│ │ │ │ │ 3 Nodes │ │ 3 Nodes │ │ 2×A100 │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ K8s + ArgoCD + Prometheus + Grafana │ │ │ │ └──────────────────────────────────────────────────┘ │ │ └──────────────────────────────────────────────────────────────┘


Summary and Further Reading

GraphRAG bridges the semantic gap of pure vector RAG through structured relationships in knowledge graphs, delivering 40%+ accuracy improvements in multi-hop reasoning, entity disambiguation, and global summarization scenarios. The trade-off is higher build cost and increased retrieval latency, making it best suited for production scenarios where accuracy is paramount.

Key Takeaways:

  1. LLM-driven entity-relationship extraction is the core of graph construction
  2. Vector + graph dual-channel retrieval with α=0.6/0.4 is optimal for most scenarios
  3. Leiden community detection enables global summaries, compensating for the locality of vector retrieval
  4. Text-to-Cypher lowers the barrier to graph queries, but injection prevention is essential
  5. GraphRAG is ideal for accuracy-first scenarios; use with caution in latency-sensitive contexts

Related Reading:

Authoritative References:

Try these browser-local tools — no sign-up required →

#GraphRAG实战#知识图谱RAG#图检索增强生成#Neo4j知识图谱#LLM图推理#2026