RAG met Neo4j en LLMs - AI Verbeteren met Kennisgrafen
Leer hoe je Retrieval-Augmented Generation implementeert met Neo4j als kennisbasis om taalmodelresponsen te verbeteren


Wat is RAG en Waarom Combineren met Neo4j?
RAG (Retrieval-Augmented Generation) is een techniek die de antwoorden van grote taalmodellen (LLM’s) verbetert door ze relevante context te geven die is opgehaald uit een externe kennisbasis. In plaats van alleen te vertrouwen op kennis in modelparameters, zoekt RAG naar specifieke informatie voordat het een antwoord genereert.
Neo4j, als grafendatabase, is ideaal voor RAG omdat:
- ✅ Relationele context: Legt complexe relaties tussen entiteiten vast
- ✅ Efficiënte navigatie: Maakt snelle traversal van relaties mogelijk
- ✅ Gestructureerde kennis: Organiseert informatie semantisch
- ✅ Contextueel zoeken: Vindt relevante informatie rekening houdend met verbindingen
RAG Architectuur met Neo4j
Gebruiker → Vraag
↓
1. Vraag embedding
↓
2. Zoeken in Neo4j (Vector Search)
↓
3. Ophalen van relationele context
↓
4. Prompt constructie met context
↓
5. LLM genereert verrijkt antwoord
↓
Gecontextualiseerd antwoord → Gebruiker
Vereisten
# Installeer benodigde afhankelijkheden
pip install neo4j langchain openai tiktoken
Je hebt nodig:
- Neo4j 5.11+ (met Vector Index ondersteuning)
- OpenAI API key (of elk compatibel LLM)
- Python 3.9+
Stap 1: Configureer Neo4j met Vectoren
Maak eerst een vector index in Neo4j om embeddings op te slaan:
// Maak vector index voor documenten
CREATE VECTOR INDEX document_embeddings IF NOT EXISTS
FOR (d:Document)
ON d.embedding
OPTIONS {
indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
}
}
Stap 2: Laad Kennis in de Graaf
from neo4j import GraphDatabase
import openai
class KnowledgeGraphRAG:
def __init__(self, uri, user, password, openai_key):
self.driver = GraphDatabase.driver(uri, auth=(user, password))
openai.api_key = openai_key
def close(self):
self.driver.close()
def create_embedding(self, text):
"""Genereer embedding met OpenAI"""
response = openai.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def add_document(self, title, content, metadata=None):
"""Voeg document toe aan graaf met embedding"""
embedding = self.create_embedding(content)
with self.driver.session() as session:
session.run("""
CREATE (d:Document {
title: $title,
content: $content,
embedding: $embedding,
metadata: $metadata,
created_at: datetime()
})
""", title=title, content=content,
embedding=embedding, metadata=metadata)
def add_relationship(self, doc1_title, doc2_title, rel_type):
"""Maak relatie tussen documenten"""
with self.driver.session() as session:
session.run("""
MATCH (d1:Document {title: $doc1})
MATCH (d2:Document {title: $doc2})
CREATE (d1)-[r:%s]->(d2)
""" % rel_type, doc1=doc1_title, doc2=doc2_title)
Stap 3: Implementeer Vector Search
def retrieve_context(self, query, top_k=3):
"""Haal relevante context op met vector search"""
query_embedding = self.create_embedding(query)
with self.driver.session() as session:
result = session.run("""
CALL db.index.vector.queryNodes(
'document_embeddings',
$top_k,
$query_embedding
) YIELD node, score
// Haal ook gerelateerde documenten op
OPTIONAL MATCH (node)-[r:RELATES_TO|REFERENCES]-(related:Document)
RETURN
node.title as title,
node.content as content,
score,
collect(DISTINCT {
title: related.title,
type: type(r)
}) as related_docs
ORDER BY score DESC
""", query_embedding=query_embedding, top_k=top_k)
return [dict(record) for record in result]
Stap 4: Genereer Antwoord met LLM
def generate_response(self, query, context_docs):
"""Genereer antwoord met opgehaalde context"""
# Bouw context van Neo4j
context_text = "\n\n".join([
f"Document: {doc['title']}\n{doc['content']}\n"
f"Gerelateerde documenten: {', '.join([r['title'] for r in doc['related_docs']])}"
for doc in context_docs
])
# Maak verrijkte prompt
prompt = f"""Gebaseerd op de volgende context, beantwoord de vraag van de gebruiker.
Context:
{context_text}
Vraag: {query}
Gedetailleerd antwoord:"""
# Roep OpenAI aan
response = openai.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "Je bent een expert assistent die antwoordt op basis van de gegeven context."},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=500
)
return response.choices[0].message.content
def ask(self, query):
"""Hoofd RAG methode"""
# 1. Haal context op
context_docs = self.retrieve_context(query, top_k=3)
# 2. Genereer antwoord
answer = self.generate_response(query, context_docs)
# 3. Retourneer met bronnen
return {
"answer": answer,
"sources": [doc['title'] for doc in context_docs],
"relevance_scores": [doc['score'] for doc in context_docs]
}
Stap 5: Volledig Systeemgebruik
# Initialiseer
rag = KnowledgeGraphRAG(
uri="bolt://localhost:7687",
user="neo4j",
password="password",
openai_key="sk-..."
)
# Voeg kennis toe aan de graaf
rag.add_document(
title="Introductie tot Neo4j",
content="Neo4j is een native grafendatabase die gegevens opslaat in nodes en relaties...",
metadata={"category": "database", "level": "beginner"}
)
rag.add_document(
title="Cypher Query Language",
content="Cypher is Neo4j's declaratieve querytaal, ontworpen om met grafen te werken...",
metadata={"category": "query-language", "level": "intermediate"}
)
# Maak relaties tussen documenten
rag.add_relationship(
"Introductie tot Neo4j",
"Cypher Query Language",
"PREREQUISITE"
)
# Stel vragen
result = rag.ask("Hoe kan ik gegevens opvragen in Neo4j?")
print("Antwoord:", result['answer'])
print("\nBronnen:", result['sources'])
print("Relevantie:", result['relevance_scores'])
# Sluit verbinding
rag.close()
Voorbeeld Output
Antwoord: Om gegevens in Neo4j op te vragen, gebruik je Cypher Query Language,
de declaratieve taal specifiek ontworpen voor werken met grafen.
Cypher stelt je in staat om MATCH te gebruiken voor patronen, WHERE om te filteren,
en RETURN om te specificeren welke gegevens je wilt ophalen. Bijvoorbeeld:
MATCH (n:Person)-[:KNOWS]->(friend)
WHERE n.name = 'Alice'
RETURN friend.name
Deze query vindt alle vrienden van Alice in de graaf.
Bronnen: ['Cypher Query Language', 'Introductie tot Neo4j']
Relevantie: [0.89, 0.76]
Voordelen van RAG met Neo4j vs Eenvoudige Vector Search
| Kenmerk | Eenvoudige Vectoren | Neo4j RAG |
|---|---|---|
| Relationele context | ❌ Nee | ✅ Ja |
| Kennisnavigatie | ❌ Beperkt | ✅ Compleet |
| Uitlegbaarheid | ⚠️ Laag | ✅ Hoog |
| Updates | ⚠️ Complex | ✅ Eenvoudig |
| Multi-hop redenering | ❌ Nee | ✅ Ja |
Geavanceerde Gebruiksscenario’s
1. Multi-hop RAG (Meerstaps Redenering)
def multi_hop_retrieve(self, query, depth=2):
"""Haal context op door de graaf in diepte te navigeren"""
query_embedding = self.create_embedding(query)
with self.driver.session() as session:
result = session.run("""
CALL db.index.vector.queryNodes(
'document_embeddings',
3,
$query_embedding
) YIELD node, score
// Navigeer tot 2 niveaus diep
CALL apoc.path.subgraphAll(node, {
maxLevel: $depth,
relationshipFilter: 'RELATES_TO|REFERENCES|PREREQUISITE'
})
YIELD nodes, relationships
RETURN node.title as main_doc,
[n IN nodes | n.title] as related_docs,
[r IN relationships | type(r)] as relationship_types,
score
""", query_embedding=query_embedding, depth=depth)
return [dict(record) for record in result]
2. RAG met Temporeel Filteren
def retrieve_recent_context(self, query, days=30):
"""Haal alleen recente context op"""
query_embedding = self.create_embedding(query)
with self.driver.session() as session:
result = session.run("""
CALL db.index.vector.queryNodes(
'document_embeddings',
5,
$query_embedding
) YIELD node, score
WHERE node.created_at > datetime() - duration({days: $days})
RETURN node.title, node.content, score
ORDER BY score DESC
""", query_embedding=query_embedding, days=days)
return [dict(record) for record in result]
3. RAG met Gebruikerspersonalisatie
def personalized_retrieve(self, query, user_id):
"""Haal gepersonaliseerde context op op basis van gebruikersvoorkeuren"""
query_embedding = self.create_embedding(query)
with self.driver.session() as session:
result = session.run("""
// Vind relevante documenten
CALL db.index.vector.queryNodes(
'document_embeddings',
10,
$query_embedding
) YIELD node, score
// Boost documenten gerelateerd aan gebruiker
OPTIONAL MATCH (u:User {id: $user_id})-[:INTERESTED_IN]->(t:Topic)
OPTIONAL MATCH (node)-[:ABOUT]->(t)
WITH node, score,
CASE WHEN t IS NOT NULL THEN 1.5 ELSE 1.0 END as user_boost
RETURN node.title, node.content,
(score * user_boost) as adjusted_score
ORDER BY adjusted_score DESC
LIMIT 5
""", query_embedding=query_embedding, user_id=user_id)
return [dict(record) for record in result]
Optimalisatie en Best Practices
1. Intelligent Chunking
def smart_chunk_document(self, long_text, chunk_size=500, overlap=50):
"""Splits lange documenten in chunks met overlap"""
words = long_text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = ' '.join(words[i:i + chunk_size])
chunks.append(chunk)
return chunks
def add_long_document(self, title, content):
"""Voeg lang document toe in gerelateerde chunks"""
chunks = self.smart_chunk_document(content)
for i, chunk in enumerate(chunks):
chunk_title = f"{title} - Deel {i+1}"
self.add_document(chunk_title, chunk)
# Relateer opeenvolgende chunks
if i > 0:
prev_chunk = f"{title} - Deel {i}"
self.add_relationship(prev_chunk, chunk_title, "NEXT_CHUNK")
2. Embedding Cache
from functools import lru_cache
@lru_cache(maxsize=1000)
def create_embedding_cached(self, text):
"""Cached versie van create_embedding"""
return self.create_embedding(text)
3. Relevantie Monitoring
def ask_with_confidence(self, query, min_score=0.7):
"""RAG met vertrouwensdrempel"""
context_docs = self.retrieve_context(query, top_k=3)
# Controleer minimale relevantie
if not context_docs or context_docs[0]['score'] < min_score:
return {
"answer": "Ik heb geen voldoende relevante informatie gevonden om met vertrouwen te antwoorden.",
"confidence": "low",
"sources": []
}
answer = self.generate_response(query, context_docs)
return {
"answer": answer,
"confidence": "high",
"sources": [doc['title'] for doc in context_docs],
"avg_score": sum(d['score'] for d in context_docs) / len(context_docs)
}
Conclusie
RAG met Neo4j combineert het beste van twee werelden: de generatiecapaciteit van LLM’s en gestructureerde kennis in grafen. Deze architectuur maakt mogelijk:
- 🎯 Nauwkeurigere antwoorden gebaseerd op specifieke kennis
- 🔍 Volledige traceerbaarheid van bronnen en redenering
- 🔄 Dynamische updates van kennis zonder modellen opnieuw te trainen
- 🌐 Relationele context die eenvoudige vectoren niet kunnen vastleggen
- 📊 Uitlegbaarheid van waarom bepaalde antwoorden werden gegenereerd
De toekomst van conversationele AI ligt in het combineren van taalmodellen met gestructureerde kennisbases, en Neo4j is het perfecte platform voor het bouwen van deze systemen.