Wie ich meinen eigenen AI-Chatbot baute - Neo4j, RAG und der Wolf
Eric Lenk
Der Wolf ist geboren
Auf meiner Website sitzt unten rechts ein kleiner Wolf. Er sieht harmlos aus, aber hinter ihm steckt mehr als nur ein Button mit GPT-Anbindung. Er kennt mich – meine Services, meine Projekte, meine Geschichte. Und er antwortet, ohne zu halluzinieren.
Wie das funktioniert? Mit einer Graph-Datenbank, RAG (Retrieval Augmented Generation) und einigen Lektionen, die ich gerne früher gewusst hätte.
Warum nicht einfach ChatGPT anbinden?
Klar, man könnte einen LLM einfach als API aufrufen und fertig. Das Problem:
- Halluzinationen: GPT erfindet Dinge, die nicht stimmen
- Kein Kontext: Das Modell weiß nichts über meine Website
- Keine Kontrolle: Was antwortet es auf "Was kostet ein Projekt?"
Die Lösung: RAG (Retrieval Augmented Generation)
Statt dem LLM blind zu vertrauen, geben wir ihm nur die Informationen, die es braucht. Der Bot fragt erst die Datenbank, dann formuliert er die Antwort.
Die Architektur
User-Frage
↓
WolfChatController (ASP.NET)
↓
Neo4jService → Holt relevanten Kontext aus Graph-DB
↓
WolfChatService → Baut Prompt + ruft LLM auf
↓
Antwort mit korrekten Informationen
Die Komponenten:
| Komponente | Aufgabe |
|---|---|
| Neo4j | Graph-Datenbank mit meinem gesamten Website-Wissen |
| Neo4jService | Synct Content → Graph, holt Kontext für Fragen |
| WolfChatService | System-Prompt, LLM-Call, Antwort-Formatierung |
| wolf-chat.js | Frontend Widget mit Alpine.js |
Warum eine Graph-Datenbank?
Relationale Datenbanken (SQL) sind top für strukturierte Daten. Aber für Wissensnetze sind sie umständlich.
Beispiel: Welche Skills braucht man für welchen Service?
SQL:
SELECT s.name, sk.name
FROM services s
JOIN service_skills ss ON s.id = ss.service_id
JOIN skills sk ON ss.skill_id = sk.id
WHERE ... -- 3 Tabellen, 2 Joins
Neo4j (Cypher):
MATCH (s:Service)-[:REQUIRES]->(sk:Skill)
RETURN s.name, sk.name
Ein Graph bildet Beziehungen natürlich ab. Und genau das braucht ein Chatbot, der Zusammenhänge verstehen soll.
Das Knowledge-Schema
Mein Wolf kennt diese Knoten-Typen:
(Person {name, role, contact})
(Service {name, description})
(Project {title, status, tech})
(BlogPost {title, excerpt})
(Feature {name, trigger, instructions})
(Skill) (Technology)
Und diese Beziehungen:
(Person)-[:OFFERS]->(Service)
(Service)-[:USES]->(Technology)
(Project)-[:USES]->(Technology)
(Feature)-[:NAVIGATES_TO]->(Page)
Der Content-Sync
Jedes Mal, wenn die App startet, synct ein ContentSyncService alle Inhalte in die Graph-DB:
public async Task SyncAllContentAsync()
{
await SyncPersonInfoAsync();
await SyncServicesAsync();
await SyncFeaturesAsync();
// Blog-Posts und Projekte aus Markdown-Dateien
foreach (var post in blogPosts)
await SyncBlogPostAsync(post);
}
Das bedeutet: Immer aktuell, keine manuelle Pflege.
Der RAG-Flow in Aktion
Wenn ein User fragt: "Was kostet ein Website-Projekt?"
- Wolf empfängt Frage (via
WolfChatController) - Neo4j-Query: Hole alles über Services, Preise, Prozess
- Prompt-Building: System-Prompt + Kontext + User-Frage
- LLM-Call: Gemini/GPT generiert Antwort
- Antwort geht zurück mit korrekten Infos
var context = await _neo4jService.GetRelevantContextAsync(userQuestion);
var systemPrompt = BuildSystemPrompt(context);
var response = await _llm.GenerateAsync(systemPrompt, userQuestion);
Learnings
1. Immer ALLES laden, nicht filtern
Ursprünglich wollte ich "relevante" Nodes per Keyword-Matching laden. Das führte zu:
- Lücken im Kontext
- Inkonsistente Antworten
Lösung: Ich lade einfach alles. Bei meiner Website-Größe (~50 Nodes) ist das kein Problem und verhindert Halluzinationen.
2. System-Prompt ist King
Der wichtigste Code ist nicht der Graph-Query, sondern der System-Prompt:
Du bist der Wolf-Assistent von Lupus Malus Deviant.
Du antwortest NUR basierend auf dem folgenden Kontext.
Wenn du etwas nicht weißt, sag es ehrlich.
Du bist freundlich, direkt und hilfst gerne.
3. Fehlerfall = Ehrlichkeit
Wenn Neo4j nicht erreichbar ist oder das LLM spinnt, antwortet der Wolf:
"Awoo! Da ist was schiefgelaufen. Schreib Eric lieber direkt an..."
Besser als ein kaputter Bot.
Tech-Stack
- Backend: ASP.NET Core 8
- Graph-DB: Neo4j (Docker)
- LLM: Google Gemini (via API)
- Frontend: Alpine.js, Tailwind CSS
- Deployment: Docker + GitHub Actions
Fazit
Ein guter Chatbot ist nicht der, der am meisten weiß – sondern der, der nur das sagt, was er wirklich weiß.
Mit Neo4j + RAG habe ich einen Assistenten gebaut, der:
- ✅ Korrekte Informationen gibt
- ✅ Meine Website kennt
- ✅ Nicht halluziniert
- ✅ Persönlichkeit hat (er ist ein Wolf!)
Hast du Fragen zum Wolf oder willst einen eigenen Chatbot? Schreib mir – ich helfe gerne!
Eric Lenk
Web-Entwickler mit Fokus auf ASP.NET, moderne Frontends und KI-Integration. Immer auf der Jagd nach dem nächsten spannenden Projekt.