Zum Inhalt springen

LLMs und Ontologien: Wie eine Ontologie die Antwortqualität verbessert

    Einleitung

    Spätestens mit dem Aufkommen großer Sprachmodelle (LLMs) wie GPT stellt sich vielen die Frage, wie man solchen Modellen strukturierte, präzise Informationen zur Verfügung stellen kann. Denn obwohl LLMs in der Lage sind, Fragen sehr überzeugend zu beantworten, beruhen viele ihrer Antworten lediglich auf sprachstatistischen Wahrscheinlichkeiten, nicht auf logischem Schließen oder explizitem Faktenwissen. An dieser Stelle bietet der Einsatz einer Ontologie einen systematischen Mehrwert.

    Im folgenden Beitrag wird anhand einer fiktiven Mission im „Herr der Ringe“-Universum gezeigt, wie eine Ontologie ein LLM bei der Beantwortung komplexer Fragen unterstützen kann.

    Was ist eine Ontologie?

    Eine Ontologie ist ein formales Modell zur strukturierten Repräsentation von Wissen. Sie dient dazu, reale Sachverhalte, Zusammenhänge und Abhängigkeiten durch Konzepte und deren Beziehungen abzubilden. Technisch basiert sie in der Regel auf dem RDF-Modell (Resource Description Framework), bei dem Wissen in Form von Tripeln dargestellt wird: ein Subjekt, ein Prädikat und ein Objekt. Diese Tripel bilden die grundlegende Einheit zur Beschreibung von Fakten und Beziehungen in einer Wissensdomäne.

    Statt Begriffe einfach aufzulisten, modelliert eine Ontologie die Realität in ihrer Abhängigkeit. Dabei wird beschrieben, wie Entitäten zueinander in Beziehung stehen. So kann etwa festgehalten werden, dass ein Subjekt („Aragorn“) über ein bestimmtes Merkmal („Schwertkampf“) verfügt. Diese Beziehung wird durch ein Prädikat („hatSkill“) ausgedrückt. Aragorn, ein Hauptcharakter aus Herr der Ringe, ist ein erfahrener Kämpfer mit ausgeprägten Fähigkeiten im Schwertkampf und in der Führung.

    (Aragorn, hatSkill, Schwertkampf)
    (Gollum, istIn, Mordor)

    Dieses strukturierte Wissen kann durch maschinelle Systeme wie ein LLM (Large Language Model) genutzt werden, um präzisere Antworten zu geben, vor allem dann, wenn das Modell nicht auf rein statistische Wahrscheinlichkeiten zurückgreift, sondern auf explizit modelliertes Hintergrundwissen.
    Solche Tripel lassen sich in Graphdatenbanken wie Neo4j speichern und mit Python-Bibliotheken wie py2neo, rdflib oder networkx analysieren und visualisieren.

    Beispiel Ontologie:

    Nehmen wir an, wir modellieren eine Ontologie mit Charakteren aus Herr der Ringe. In dieser Ontologie sind folgende Informationen enthalten:

    triples = [
    # Kämpfer und Fähigkeiten
    ("Aragorn", "hatSkill", "Schwertkampf"),
    ("Aragorn", "hatSkill", "Führung"),
    ("Aragorn", "hatSkillLevel", "Experte"),

    ("Legolas", "hatSkill", "Bogenschießen"),
    ("Legolas", "hatSkill", "Beweglichkeit"),
    ("Legolas", "hatSkillLevel", "Experte"),

    ("Gimli", "hatSkill", "Axtkampf"),
    ("Gimli", "hatSkill", "Standfestigkeit"),
    ("Gimli", "hatSkillLevel", "Experte"),

    ("Frodo", "hatSkill", "Heimlichkeit"),
    ("Frodo", "hatSkill", "Willensstärke"),
    ("Frodo", "hatSkillLevel", "Fortgeschritten"),

    ("Samweis", "hatSkill", "Heimlichkeit"),
    ("Samweis", "hatSkill", "Durchhaltevermögen"),
    ("Samweis", "hatSkillLevel", "Fortgeschritten"),

    # Treue
    ("Aragorn", "loyalität", "Mittelerde"),
    ("Legolas", "loyalität", "Elbenallianz"),
    ("Gimli", "loyalität", "Zwerge"),
    ("Frodo", "loyalität", "Frodo selbst"),
    ("Samweis", "loyalität", "Frodo"),

    # Missionen und Anforderungen
    ("Schattenlager Infiltration", "erfordertSkill", "Heimlichkeit"),
    ("Schattenlager Infiltration", "missionstyp", "Sabotage"),
    ("Orks am Fluss bekämpfen", "erfordertSkill", "Fernkampf"),
    ("Orks am Fluss bekämpfen", "missionstyp", "Kampf"),
    ("Verteidigung des Tors", "erfordertSkill", "Axtkampf"),
    ("Verteidigung des Tors", "missionstyp", "Verteidigung"),

    # Erfahrung der Kämpfer
    ("Legolas", "hatErfahrungMit", "Orks am Fluss bekämpfen"),
    ("Gimli", "hatErfahrungMit", "Verteidigung des Tors"),
    ("Frodo", "hatErfahrungMit", "Schattenlager Infiltration"),
    ("Samweis", "hatErfahrungMit", "Schattenlager Infiltration"),

    # Optional: Verletzungsschwere
    ("Gimli", "verletzungsgrad", "leicht"),
    ]
    data.py

    Visuelle Darstellung der obigen Ontologie:

    Frodo Detailansicht:

    Was leistet eine Ontologie im Zusammenspiel mit einem LLM?

    Statt einem LLM einfach eine offene Frage zu stellen (und damit dem Modell zu überlassen, ob es auf Halluzinationen oder Fragmentwissen zurückgreift), kann man das LLM so steuern, dass es zunächst strukturiertes Wissen aus einer Ontologie berücksichtigt. Das hat mehrere Vorteile:

    • Konsistenz
      Das LLM bezieht sich nur auf definierte und überprüfbare Fakten.
    • Einschränkung des Suchraums
      Die Antwortsuche wird gezielt geführt.
    • Explainability
      Entscheidungen können über den zugrunde liegenden Graph nachvollzogen werden.
    • Kombination von Logik und Sprache
      Die Ontologie liefert die Fakten, das LLM formuliert elegant.

    Beispiel: Eine Mission im Herr-der-Ringe-Universum

    Angenommen, wir haben die folgende Frage und fragen ein LLM nach der Antwort:

    Welcher Charakter aus 'Herr Der Ringe' verfügt über die höchste Fähigkeit in Heimlichkeit und Willenskraft für eine gefährliche Mission in Mordor?

    ChatGPT bzw. das LLM Antwort auf diese Frage wie folgt:

    Im gezeigten Beispiel wird deutlich, wie ein LLM, in diesem Fall ChatGPT, zwar inhaltlich nachvollziehbare Antwort liefert, dabei jedoch nicht zwingend korrekt auf eine spezifische Faktenlage reagiert. Die gegebene Antwort basiert auf einem allgemeinen Weltwissen, das das Modell während seines Trainings aus einer Vielzahl öffentlich zugänglicher Quellen gelernt hat. Sie reflektiert damit eine wahrscheinliche Sichtweise, aber nicht notwendigerweise die tatsächlichen, im aktuellen Kontext geltenden Fakten.

    LLMs arbeiten im Kern auf der Basis von Wahrscheinlichkeiten. Sie berechnen, welche Antwort auf eine Eingabe statistisch am wahrscheinlichsten erscheint, basierend auf dem Sprach- und Wissensraum, in dem sie trainiert wurden. In vielen Fällen führt das zu plausiblen Ergebnissen. Doch sobald die Fragestellung auf domänenspezifisches Wissen abzielt, etwa internes Fachwissen, projektbezogene Zusammenhänge oder speziell kuratierte Fakten, stoßen solche Modelle ohne zusätzliche Steuerung an Grenzen.

    Hier kommt der Mehrwert einer Ontologie ins Spiel. Eine Ontologie ermöglicht es, solches Spezialwissen strukturiert zu modellieren, also welche Entitäten existieren, welche Eigenschaften sie haben und wie sie zueinander stehen. In einer Ontologie kann exakt festgelegt werden, dass Frodo sowohl über die Fähigkeit Heimlichkeit als auch über Willensstärke verfügt, während Samweis zwar Heimlichkeit, aber keine Willensstärke besitzt. Dieses Wissen ist explizit hinterlegt und maschinenlesbar.

    ....
    ("Frodo", "hatSkill", "Heimlichkeit"),
    ("Frodo", "hatSkill", "Willensstärke"),
    ("Frodo", "hatSkillLevel", "Fortgeschritten"),

    ("Samweis", "hatSkill", "Heimlichkeit"),
    ("Samweis", "hatSkill", "Durchhaltevermögen"),
    ("Samweis", "hatSkillLevel", "Fortgeschritten"),
    ....

    Durch die Einbindung eines solchen strukturierten Wissensmodells in die LLM-Verarbeitung wird die ursprüngliche Wahrscheinlichkeitsverteilung gezielt beeinflusst. Das LLM muss nicht mehr erraten, wer geeignet sein könnte, sondern es erhält klare Fakten, auf deren Basis die Antwort generiert wird. Die Wahrscheinlichkeit für eine korrekte und spezialisierte Antwort steigt deutlich, weil das Modell nicht mehr ausschließlich auf seinem generalisierten Trainingswissen basiert, sondern zusätzlich auf spezialisierte, kontextspezifische Informationen zugreift.

    Die Ontologie wirkt somit als semantische Leitstruktur. Sie erhöht die faktische Treffsicherheit und zwingt das Modell, innerhalb eines Bedeutungskontexts zu argumentieren. Besonders in realen Anwendungsszenarien, etwa in Unternehmen, medizinischen Systemen oder technischen Assistenzlösungen, kann diese Integration über den Unterschied zwischen plausibel und korrekt entscheiden.

    Erweiterung von Prompts durch Ontologie-Wissen zur präziseren LLM-Antwortgenerierung

    Bevor eine Frage an das Sprachmodell gesendet wird, wird der ursprüngliche Prompt durch strukturiertes Wissen aus der Ontologie ergänzt. Dieses Wissen besteht aus Tripeln, die relevante Informationen über die Domäne enthalten, zum Beispiel über Eigenschaften von Figuren oder Zusammenhänge zwischen Konzepten. Durch die Einbettung dieser Fakten kann das Sprachmodell nicht nur auf statistische Muster zurückgreifen, sondern gezielt auf das bereitgestellte Spezialwissen zugreifen. Das führt zu besseren, nachvollziehbaren und domänenspezifisch korrekten Antworten.

    INFO - ------ LLM Prompt Start ------
    INFO - Nutze die folgenden Fakten, um die Frage m�glichst pr�zise zu beantworten.
    INFO -
    INFO - Fakten:
    INFO - Legolas HATSKILL Beweglichkeit
    INFO - Legolas HATSKILLLEVEL Experte
    INFO - Gimli HATSKILL Axtkampf
    INFO - Gimli HATSKILL Standfestigkeit
    INFO - Gimli HATSKILLLEVEL Experte
    INFO - Frodo HATSKILL Heimlichkeit
    INFO - Frodo HATSKILL Willensst�rke
    INFO - Frodo HATSKILLLEVEL Fortgeschritten
    INFO - Samweis HATSKILL Heimlichkeit
    INFO - Samweis HATSKILL Durchhalteverm�gen
    INFO - Samweis HATSKILLLEVEL Fortgeschritten
    INFO - Aragorn LOYALIT�T Mittelerde
    INFO - Legolas LOYALIT�T Elbenallianz
    INFO - Gimli LOYALIT�T Zwerge
    INFO - Frodo LOYALIT�T Frodo selbst
    INFO - Samweis LOYALIT�T Frodo
    INFO - Schattenlager Infiltration ERFORDERTSKILL Heimlichkeit
    INFO - Schattenlager Infiltration MISSIONSTYP Sabotage
    INFO - Orks am Fluss bek�mpfen ERFORDERTSKILL Fernkampf
    INFO - Orks am Fluss bek�mpfen MISSIONSTYP Kampf
    INFO - Verteidigung des Tors ERFORDERTSKILL Axtkampf
    INFO - Verteidigung des Tors MISSIONSTYP Verteidigung
    INFO - Legolas HATERFAHRUNGMIT Orks am Fluss bek�mpfen
    INFO - Gimli HATERFAHRUNGMIT Verteidigung des Tors
    INFO - Frodo HATERFAHRUNGMIT Schattenlager Infiltration
    INFO - Samweis HATERFAHRUNGMIT Schattenlager Infiltration
    INFO - Gimli VERLETZUNGSGRAD leicht
    INFO - Aragorn HATSKILL Schwertkampf
    INFO - Aragorn HATSKILL F�hrung
    INFO - Aragorn HATSKILLLEVEL Experte
    INFO - Legolas HATSKILL Bogenschie�en
    INFO -
    INFO - Frage:
    INFO - Welcher Charakter aus 'Herr Der Ringe' verfügt über die höchste Fähigkeit in Heimlichkeit und Willenskraft für eine gefährliche Mission in Mordor?
    INFO -
    INFO - Antwort:
    INFO - ------ LLM Prompt Ende ------

    Nach der Erweiterung des Prompts durch die Metadaten erhalten wir als Antwort „Frodo“ statt wie zuvor „Samweis“.

    INFO - Antwort des LLM: Frodo

    Zur Beantwortung der Frage wurde im aktuellen Beispiel die gesamte Ontologie in den Prompt eingebunden. Alle verfügbaren Fakten wurden dem LLM vollständig mitgegeben, um sicherzustellen, dass die Antwort ausschließlich auf dem modellierten Wissen basiert. Dieses Vorgehen funktioniert in kleineren Beispielen, eignet sich aber nicht für reale, umfangreiche Anwendungen.

    In professionellen Umgebungen enthalten Ontologien oft zehntausende oder sogar Millionen Einträge. Es wäre ineffizient, jedes Mal alle Informationen zu übertragen, da

    • die maximale Eingabelänge des Modells überschritten werden kann
    • unnötige Informationen die Rechenzeit verlängern
    • viele irrelevante Fakten im Prompt enthalten wären

    Deshalb wird in der Praxis nur ein relevanter Ausschnitt aus der Ontologie verwendet, der zur aktuellen Frage passt.

    Wie eine sinnvolle Auswahl funktioniert

    Eine Ontologie ist ein strukturiertes Modell aus Subjekten, Prädikaten und Objekten. Die Auswahl der relevanten Informationen zur Beantwortung einer konkreten Frage erfolgt nicht durch statistische Ähnlichkeit oder semantisches Raten, sondern durch strukturierte Traversierung der modellierten Beziehungen.

    Entscheidend ist, dass man aus der Ontologie gezielt jene Teilmengen auswählt, die inhaltlich in Abhängigkeit zur Fragestellung stehen. Dafür werden keine ML-Modelle benötigt, sondern regelbasierte Mechanismen:

    • Beziehungsorientierte Navigation
      Über Abfragen wie „MATCH (c:Charakter)-[:HATSKILL]->(s:Skill) WHERE s.name = ‚Heimlichkeit’“ kann man gezielt alle Entitäten finden, die mit dem gesuchten Konzept in Verbindung stehen.
    • Pfadbasierte Selektion
      Es kann definiert werden, dass nur Entitäten berücksichtigt werden, die mit bestimmten Missionsanforderungen über mehrere Kanten hinweg verknüpft sind, z. B. über „erfordertSkill“ und „hatSkill“.
    • Einschränkung nach Attributen
      Durch gezielte Filterung auf Knotenattribute wie „hatSkillLevel = Experte“ wird die Auswahl weiter eingegrenzt.
    • Graphorientierte Nachbarschaftsanalyse
      Man kann mit festen Regeln entscheiden, welche benachbarten Knoten entlang definierter Prädikate betrachtet werden sollen, um zum Beispiel Erfahrungen, Loyalitäten oder Verfügbarkeiten in die Auswahl einzubeziehen.

    Diese Form der Selektion nutzt die Struktur der Ontologie, also ihre expliziten Kanten und Knoten, um genau die Wissenselemente zu extrahieren, die für die Fragestellung notwendig sind.

    Ein LLM kommt erst nachgelagert ins Spiel, um auf Basis dieser verdichteten Faktenlage eine sprachliche Antwort zu formulieren. Die Qualität der Antwort hängt daher wesentlich von der Qualität und Relevanz der zuvor ausgewählten Teilstruktur der Ontologie ab.

    neo4j

    Neo4j ist eine Graphdatenbank, die speziell dafür entwickelt wurde, Beziehungen zwischen Daten effizient abzubilden und abzufragen. Im Gegensatz zu klassischen relationalen Datenbanken speichert Neo4j Informationen nicht in Tabellen, sondern in Form von Knoten (Entities), Kanten (Beziehungen) und Eigenschaften.

    Mit Neo4j lassen sich komplexe, vernetzte Strukturen modellieren und analysieren:

    • Ontologien und Wissensgraphen (z. B. welche Person hat welche Fähigkeit und welche Aufgabe erfordert sie)
    • Soziale Netzwerke (z. B. wer kennt wen)
    • Empfehlungssysteme (z. B. welche Produkte passen zum Nutzerverhalten
    • Betrugserkennung (z. B. ungewöhnliche Muster in Zahlungsströmen)
    • IT-Architekturen (z. B. welche Systeme sind wie miteinander verbunden)

    Unsere Ontologie sieht innerhalb von neo4j wie folgt aus:

    Installation via Docker

    docker run --name neo4j -p7474:7474 -p7687:7687 -d -v neo4j_data:/data -e NEO4J_AUTH=neo4j/secret123 neo4j:5

    Navigation via neo4j

    Finde alle Charaktere (oder allgemein: Entitäten), die den Skill „Heimlichkeit“ besitzen.

    MATCH (c:Entity)-[:HATSKILL]->(s:Entity) 
    WHERE s.name = 'Heimlichkeit'
    RETURN c

    Durch diese Abfrage betrachten wir gezielt nur den für die Fragestellung relevanten Ausschnitt der Ontologie, um das Sprachmodell gezielt mit passenden Metadaten anzureichern.

    Kontextuelle Reduktion der Ontologie für LLM-Abfragen

    Nachdem die Ontologie gezielt nach relevanten Zusammenhängen durchsucht wurde, können dem Sprachmodell anstelle der vollständigen Ontologie nur die inhaltlich relevanten Abhängigkeiten als Kontext übergeben werden.

    Lade Konfiguration und initialisiere OpenAI-Client.
    Initialisiere Neo4j-Treiber.
    ===== Starte intelligente RAG-Pipeline (Version: Final mit Synonym-Matching) =====
    Bereinige alte Daten, säubere neue Tripel und füge sie in Neo4j ein.
    Bestehende Graphen-Datenbank wurde bereinigt.
    31 bereinigte Tripel wurden erfolgreich eingefügt.
    Ontologie-Wissensbasis: Verfügbare Fähigkeiten sind ['Axtkampf', 'Beweglichkeit', 'Bogenschießen', 'Durchhaltevermögen', 'Führung', 'Heimlichkeit', 'Schwertkampf', 'Standfestigkeit', 'Willensstärke']
    Extrahiere und matche Fähigkeiten aus der Frage: 'Welcher Charakter aus 'Herr Der Ringe' verfügt über die höchste Fähigkeit in Heimlichkeit und Willenskraft für eine gefährliche Mission in Mordor?'
    HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
    Vom LLM extrahierte UND korrigierte Fähigkeiten: ['Heimlichkeit', 'Willensstärke']

    Lese relevanten Teilgraph mit einem mehrstufigen, robusten Query-Ansatz.
    Führe Sub-Query aus für Fähigkeit: 'Heimlichkeit'
    -> Gefundene Charaktere: {'Samweis', 'Frodo'}
    Führe Sub-Query aus für Fähigkeit: 'Willensstärke'
    -> Gefundene Charaktere: {'Frodo'}
    Schnittmenge der Charaktere, die alle Fähigkeiten besitzen: {'Frodo'}
    Hole alle Fakten für die finalen Charaktere: ['Frodo']
    Anzahl relevanter Fakten für den Kontext gefunden: 5
    Erzeuge Graphvisualisierung.
    Suche nach pyvis-Vorlage im Verzeichnis: C:\sources\ontology\venv\Lib\site-packages\pyvis\templates
    Lade Vorlagendatei: template.html
    Graph als HTML gespeichert: ontology_graph.html

    Sende Frage an LLM mit dynamisch erstelltem Kontext.

    ------ Finaler LLM Prompt Start ------
    Du bist ein 'Herr der Ringe'-Experte. Nutze AUSSCHLIESSLICH die folgenden Fakten, um die Frage präzise zu beantworten. Begründe deine Antwort kurz basierend auf den Fakten.

    Fakten:
    - Frodo haterfahrungmit Schattenlager Infiltration
    - Frodo loyalität Frodo selbst
    - Frodo hatskilllevel Fortgeschritten
    - Frodo hatskill Willensstärke
    - Frodo hatskill Heimlichkeit

    Frage: Welcher Charakter aus 'Herr Der Ringe' verfügt über die höchste Fähigkeit in Heimlichkeit und Willenskraft für eine gefährliche Mission in Mordor?

    Antwort:
    ------ Finaler LLM Prompt Ende ------

    HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
    Antwort des LLM:
    Basierend auf den gegebenen Fakten ist Frodo der Charakter aus 'Herr der Ringe', der über die höchste Fähigkeit in Heimlichkeit und Willenskraft für eine gefährliche Mission in Mordor verfügt. Dies wird durch seine Erfahrung mit Schattenlager Infiltration, sein fortgeschrittenes Skilllevel und seine spezifischen Fähigkeiten in Heimlichkeit und Willensstärke belegt.

    Schließe Datenbankverbindung.
    ===== Pipeline-Durchlauf beendet =====

    LLM Antwort

    Basierend auf den gegebenen Fakten ist Frodo der Charakter aus 'Herr der Ringe', der über die höchste Fähigkeit in Heimlichkeit und Willenskraft für eine gefährliche Mission in Mordor verfügt. Dies wird durch seine Erfahrung mit Schattenlager Infiltration, sein fortgeschrittenes Skilllevel und seine spezifischen Fähigkeiten in Heimlichkeit und Willensstärke belegt.

    Code Beispiel

    pip install neo4j networkx jinja2
    pip install git https://github.com/WestHealth/pyvis.git
    Installation

    # Import der notwendigen Bibliotheken
    from neo4j import GraphDatabase
    from pyvis.network import Network
    import networkx as nx
    import os
    from jinja2 import Environment, FileSystemLoader
    from data import triples
    import json
    from openai import OpenAI
    import logging
    import pyvis

    # --- Logging-Konfiguration ---
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    if logger.hasHandlers():
    logger.handlers.clear()
    file_handler = logging.FileHandler("logfile.txt", mode='w', encoding='utf-8')
    formatter = logging.Formatter('%(message)s')
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    # --- Ende der Logging-Konfiguration ---

    # Konfiguration und OpenAI-Client initialisieren
    logging.info("Lade Konfiguration und initialisiere OpenAI-Client.")
    with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)
    client = OpenAI(api_key=config["openai_api_key"])

    # Verbindungskonfiguration zu Neo4j
    uri = config["neo4j_uri"]
    user = config["neo4j_user"]
    password = config["neo4j_password"]

    # Initialisierung des Neo4j-Treibers
    logging.info("Initialisiere Neo4j-Treiber.")
    driver = GraphDatabase.driver(uri, auth=(user, password))

    def insert_data(triples_data):
    """Löscht alte Daten, bereinigt sie von Leerzeichen und fügt sie dann in Neo4j ein."""
    logging.info("Bereinige alte Daten, säubere neue Tripel und füge sie in Neo4j ein.")
    with driver.session() as session:
    session.run("MATCH (n) DETACH DELETE n")
    logging.info("Bestehende Graphen-Datenbank wurde bereinigt.")

    cleaned_triples = 0
    for s, p, o in triples_data:
    s_clean, p_clean, o_clean = s.strip(), p.strip(), o.strip()
    p_upper = p_clean.upper()
    session.run(
    "MERGE (a:Entity {name: $s}) MERGE (b:Entity {name: $o}) MERGE (a)-[r:`" p_upper "`]->(b)",
    s=s_clean, o=o_clean
    )
    cleaned_triples = 1
    logging.info(f"{cleaned_triples} bereinigte Tripel wurden erfolgreich eingefügt.")

    def get_all_skills_from_ontology(triples_data):
    """Extrahiert eine eindeutige Liste aller Fähigkeiten aus den Rohdaten."""
    all_skills = set()
    for s, p, o in triples_data:
    if p.strip().lower() == 'hatskill':
    all_skills.add(o.strip())
    logging.info(f"Ontologie-Wissensbasis: Verfügbare Fähigkeiten sind {sorted(list(all_skills))}")
    return sorted(list(all_skills))

    def extract_and_match_skills(question, available_skills):
    """
    Nutzt das LLM, um Fähigkeiten zu extrahieren UND sie mit den verfügbaren
    Fähigkeiten aus der Ontologie abzugleichen.
    """
    logging.info(f"Extrahiere und matche Fähigkeiten aus der Frage: '{question}'")

    available_skills_str = ", ".join(available_skills)
    prompt = (
    "Du bist ein Experte für die Analyse von Anfragen. Deine Aufgabe besteht aus zwei Schritten:\n"
    "1. Extrahiere die Schlüssel-Fähigkeiten (skills), die ein Charakter aus der Frage besitzen muss.\n"
    "2. Vergleiche jede extrahierte Fähigkeit mit der folgenden Liste von verfügbaren Fähigkeiten und finde die exakte Übereinstimmung. "
    "Korrigiere Synonyme oder kleine Tippfehler (z.B. wenn der Nutzer 'Willenskraft' sagt, solltest du 'Willensstärke' zurückgeben).\n\n"
    f"Verfügbare Fähigkeiten in der Datenbank: [{available_skills_str}]\n\n"
    f"Frage des Nutzers: \"{question}\"\n\n"
    "Gib das Ergebnis als ein JSON-Objekt zurück, das einen einzigen Schlüssel namens 'korrigierte_faehigkeiten' hat, der ein Array der exakten, korrigierten Fähigkeitsnamen aus der Liste ist."
    )

    try:
    response = client.chat.completions.create(
    model="gpt-4-turbo",
    response_format={"type": "json_object"},
    messages=[
    {"role": "system", "content": "You are a helpful assistant designed to output JSON."},
    {"role": "user", "content": prompt}
    ],
    temperature=0
    )

    extracted_text = response.choices[0].message.content
    data = json.loads(extracted_text)
    skills = data.get("korrigierte_faehigkeiten", [])

    if not isinstance(skills, list) or not all(isinstance(s, str) for s in skills):
    logging.error(f"LLM hat kein valides JSON-Array von Strings zurückgegeben. Antwort: {extracted_text}")
    return []

    logging.info(f"Vom LLM extrahierte UND korrigierte Fähigkeiten: {skills}")
    return skills
    except Exception as e:
    logging.error(f"Fehler bei der Extraktion/Matching der Fähigkeiten mit dem LLM: {e}")
    return []

    def get_relevant_facts(skills):
    """Fragt die Datenbank mit dem robusten, mehrstufigen Ansatz ab."""
    if not skills:
    logging.info("Keine Fähigkeiten zur Abfrage vorhanden.")
    return []

    logging.info("\nLese relevanten Teilgraph mit einem mehrstufigen, robusten Query-Ansatz.")
    character_sets = []
    with driver.session() as session:
    for skill in skills:
    query = "MATCH (c:Entity)-[:HATSKILL]->(:Entity {name: $skill_param}) RETURN c.name AS name"
    logging.info(f"Führe Sub-Query aus für Fähigkeit: '{skill}'")
    result = session.run(query, skill_param=skill)
    characters_with_skill = {record["name"] for record in result}
    logging.info(f" -> Gefundene Charaktere: {characters_with_skill if characters_with_skill else 'Keine'}")
    character_sets.append(characters_with_skill)

    if not character_sets:
    final_characters = set()
    else:
    final_characters = character_sets[0].intersection(*character_sets[1:])
    logging.info(f"Schnittmenge der Charaktere, die alle Fähigkeiten besitzen: {final_characters if final_characters else 'Keine'}")

    if not final_characters:
    logging.info("Anzahl relevanter Fakten für den Kontext gefunden: 0")
    return []

    with driver.session() as session:
    query = "MATCH (c:Entity)-[r]->(o:Entity) WHERE c.name IN $characters_param RETURN c.name AS source, type(r) AS rel, o.name AS target"
    logging.info(f"Hole alle Fakten für die finalen Charaktere: {list(final_characters)}")
    result = session.run(query, characters_param=list(final_characters))
    triples = [(r["source"], r["rel"], r["target"]) for r in result]
    logging.info(f"Anzahl relevanter Fakten für den Kontext gefunden: {len(triples)}")
    return triples

    def visualize_graph(triples_data):
    """Erstellt eine HTML-Visualisierung des Graphen mit expliziter Template-Pfad-Logik."""
    if not triples_data:
    logging.info("Keine Daten zur Visualisierung vorhanden. Es wird keine Graph-Datei erzeugt.")
    return
    logging.info("Erzeuge Graphvisualisierung.")
    G = nx.DiGraph()
    for s, p, o in triples_data:
    G.add_edge(s, str(o), label=p)

    net = Network(notebook=False, directed=True, height="750px", width="100%")
    net.from_nx(G)

    # FINALE KORREKTUR: Explizites Laden der pyvis-Vorlage zur Behebung des 'NoneType'-Fehlers.
    # Diese Methode ist robust und stellt sicher, dass die HTML-Vorlage gefunden wird,
    # unabhängig von der Installationsumgebung des Pakets.
    try:
    template_dir = os.path.join(os.path.dirname(pyvis.__file__), 'templates')
    logging.info(f"Suche nach pyvis-Vorlage im Verzeichnis: {template_dir}")

    env = Environment(loader=FileSystemLoader(template_dir))

    # In neueren pyvis-Versionen heißt die Hauptvorlage 'index.html'.
    # Wir prüfen deren Existenz und greifen für die Kompatibilität auf 'template.html' zurück.
    template_filename = "index.html"
    if not os.path.exists(os.path.join(template_dir, template_filename)):
    template_filename = "template.html"

    logging.info(f"Lade Vorlagendatei: {template_filename}")
    net.template = env.get_template(template_filename)

    except Exception as e:
    logging.error(f"Konnte pyvis-Vorlage nicht laden, Visualisierung könnte fehlschlagen. Fehler: {e}")

    net.show_buttons(filter_=['physics'])
    output_file = "ontology_graph.html"
    net.show(output_file)
    logging.info(f"Graph als HTML gespeichert: {output_file}")


    def answer_question_with_ontology(question, context_triples):
    logging.info("\nSende Frage an LLM mit dynamisch erstelltem Kontext.")
    if not context_triples:
    context = "Keine spezifischen Fakten in der Wissensdatenbank gefunden, die exakt zur Frage passen. Antworte basierend auf deinem Allgemeinwissen."
    else:
    context = "\n".join([f"- {s} {p.replace('_', ' ').lower()} {o}" for s, p, o in context_triples])
    prompt = (f"Du bist ein 'Herr der Ringe'-Experte. Nutze AUSSCHLIESSLICH die folgenden Fakten, um die Frage präzise zu beantworten. Begründe deine Antwort kurz basierend auf den Fakten.\n\nFakten:\n{context}\n\nFrage: {question}\n\nAntwort:")
    logging.info("\n------ Finaler LLM Prompt Start ------")
    for line in prompt.splitlines(): logging.info(line)
    logging.info("------ Finaler LLM Prompt Ende ------\n")
    response = client.chat.completions.create(model="gpt-4", messages=[{"role": "user", "content": prompt}], temperature=0)
    antwort = response.choices[0].message.content
    logging.info(f"Antwort des LLM:\n{antwort}")
    print("\n--- Antwort des LLM ---\n")
    print(antwort)
    print("\n-----------------------\n")

    # Hauptausführung
    if __name__ == "__main__":
    try:
    logging.info("===== Starte intelligente RAG-Pipeline (Version: Final mit Synonym-Matching) =====")
    insert_data(triples)

    # NEUER SCHRITT: Hole alle verfügbaren Fähigkeiten aus der Ontologie
    available_skills = get_all_skills_from_ontology(triples)

    frage = config["frage"]
    print(f"Analysiere die Frage: '{frage}'")

    # ÜBERARBEITETER SCHRITT: Extrahiere und korrigiere Fähigkeiten
    skills = extract_and_match_skills(frage, available_skills)

    graph_data = get_relevant_facts(skills)

    visualize_graph(graph_data)
    answer_question_with_ontology(frage, graph_data)

    except Exception as e:
    logging.error(f"Ein schwerwiegender Fehler ist aufgetreten: {e}", exc_info=True)
    finally:
    logging.info("\nSchließe Datenbankverbindung.")
    driver.close()
    logging.info("===== Pipeline-Durchlauf beendet =====")
    app.py
    {
    "neo4j_uri": "bolt://localhost:7687",
    "neo4j_user": "neo4j",
    "neo4j_password": "secret123",
    "cypher_query": "MATCH (c:Entity)-[r:HATSKILL]->(s:Entity) WHERE s.name IN ['Heimlichkeit', 'Willensstärke'] WITH c, collect(s.name) AS skills WHERE 'Heimlichkeit' IN skills AND 'Willensstärke' IN skills MATCH (c)-[r2:HATSKILL]->(s2:Entity) RETURN c.name AS source, type(r2) AS rel, s2.name AS target",
    "frage": "Welcher Charakter aus 'Herr Der Ringe' verfügt über die höchste Fähigkeit in Heimlichkeit und Willenskraft für eine gefährliche Mission in Mordor?",
    "openai_api_key": "sk-...oF"
    }
    config.json

    OWL (Web Ontology Language)

    In den vorherigen Abschnitten haben wir gesehen, wie eine Ontologie in Form eines Graphen mit einfachen Tripeln (Subjekt-Prädikat-Objekt) die Qualität der Antworten eines LLMs drastisch verbessern kann. Wir haben Entitäten wie Frodo und Heimlichkeit und die Beziehung hatSkill zwischen ihnen modelliert.

    Dieser Ansatz ist bereits sehr mächtig. Allerdings weiß unser System bisher nur, dass eine Verbindung besteht, aber nicht, was diese Verbindung logisch bedeutet oder welchen Regeln sie unterliegt. Um diese Lücke zu schließen und unserer Ontologie eine tiefere Ebene der „Intelligenz“ zu verleihen, nutzen wir die Web Ontology Language (OWL).

    OWL ist ein vom W3C standardisiertes Format zur Erstellung von umfassenden, formalen Ontologien. Man kann es sich als eine Erweiterung von RDF (dem Framework, auf dem unser Tripel-Modell basiert) vorstellen.

    Wenn unser bisheriger Graph das Vokabular und die einfachen Sätze liefert, dann ist OWL die Grammatik und das Regelwerk, das die logischen Zusammenhänge dazwischen beschreibt. OWL erlaubt es uns, die Bedeutung der Begriffe und Beziehungen in unserer „Herr der Ringe“-Welt so zu definieren, dass eine Maschine sie nicht nur lesen, sondern auch verstehen und darüber schlussfolgern kann. Die Umstellung oder Erweiterung unserer Ontologie mit OWL würde uns mehrere entscheidende Vorteile bringen, die die Qualität der an das LLM übergebenen Fakten nochmals deutlich steigern.

    • Problem heute
      In unserem Graphen sind Frodo, Heimlichkeit und Schattenlager Infiltration allesamt Knoten vom gleichen Typ (Entity). Das System weiß nicht, dass es sich um einen Charakter, eine Fähigkeit und eine Mission handelt.
    • Lösung mit OWL
      Wir können formale Klassen definieren: Charakter, Faehigkeit und Mission. Anschließend können wir sogar Hierarchien bilden, z.B. könnten Hobbit und Elb Unterklassen von Charakter sein. Frodo wäre dann eine Instanz der Klasse Hobbit.
    • Vorteil
      Das System kann viel präzisere Abfragen durchführen („Finde alle Hobbits, die die Fähigkeit ‚Heimlichkeit‘ haben“). Logische Fehler, wie eine Mission einer Loyalität zuzuweisen, werden unmöglich, und der Kontext für das LLM wird unmissverständlich klar.

    Indem wir OWL verwenden, verwandeln wir unseren bestehenden Wissensgraphen von einer reinen Datensammlung in ein dynamisches, logisches Modell unserer „Herr der Ringe“-Welt. Die Fakten, die wir dann an das LLM übergeben, sind nicht nur präziser und kontextreicher, sondern können auch Wissen enthalten, das durch logische Schlussfolgerungen erst generiert wurde. Dies ist der nächste, entscheidende Schritt zur Maximierung der Antwortqualität.

    Ontologie Beispiel

    @prefix lotr: <http://aaron.de/ontology/lotr#> .
    @prefix owl: <http://www.w3.org/2002/07/owl#> .
    @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
    @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

    lotr:Aragorn a lotr:Charakter ;
    lotr:hatSkill lotr:Führung,
    lotr:Schwertkampf ;
    lotr:hatSkillLevel "Experte"^^xsd:string ;
    lotr:loyalität lotr:Mittelerde .

    lotr:Gimli a lotr:Charakter ;
    lotr:hatErfahrungMit lotr:Verteidigung_des_Tors ;
    lotr:hatSkill lotr:Axtkampf,
    lotr:Standfestigkeit ;
    lotr:hatSkillLevel "Experte"^^xsd:string ;
    lotr:loyalität lotr:Zwerge ;
    lotr:verletzungsgrad "leicht"^^xsd:string .

    lotr:Legolas a lotr:Charakter ;
    lotr:hatErfahrungMit lotr:Orks_am_Fluss_bekämpfen ;
    lotr:hatSkill lotr:Beweglichkeit,
    lotr:Bogenschießen ;
    lotr:hatSkillLevel "Experte"^^xsd:string ;
    lotr:loyalität lotr:Elbenallianz .

    lotr:Samweis a lotr:Charakter ;
    lotr:hatErfahrungMit lotr:Schattenlager_Infiltration ;
    lotr:hatSkill lotr:Durchhaltevermögen,
    lotr:Heimlichkeit ;
    lotr:hatSkillLevel "Fortgeschritten"^^xsd:string ;
    lotr:loyalität lotr:Frodo .

    lotr:erfordertSkill a owl:ObjectProperty ;
    rdfs:domain lotr:Mission ;
    rdfs:range lotr:Faehigkeit .

    lotr:hatErfahrungMit a owl:ObjectProperty ;
    rdfs:domain lotr:Charakter ;
    rdfs:range lotr:Mission .

    lotr:hatSkill a owl:ObjectProperty ;
    rdfs:domain lotr:Charakter ;
    rdfs:range lotr:Faehigkeit .

    lotr:hatSkillLevel a owl:DatatypeProperty ;
    rdfs:domain lotr:Charakter ;
    rdfs:range xsd:string .

    lotr:loyalitaet a owl:ObjectProperty ;
    rdfs:domain lotr:Charakter ;
    rdfs:range lotr:Loyalitaetsziel .

    lotr:missionstyp a owl:DatatypeProperty ;
    rdfs:domain lotr:Mission ;
    rdfs:range xsd:string .

    lotr:verletzungsgrad a owl:DatatypeProperty ;
    rdfs:domain lotr:Charakter ;
    rdfs:range xsd:string .

    lotr:Beweglichkeit a lotr:Faehigkeit .

    lotr:Bogenschießen a lotr:Faehigkeit,
    lotr:Fernkampffaehigkeit .

    lotr:Durchhaltevermögen a lotr:Faehigkeit .

    lotr:Elbenallianz a lotr:Loyalitaetsziel .

    lotr:Fernkampf a lotr:Faehigkeit .

    lotr:Fernkampffaehigkeit rdfs:subClassOf lotr:Kampffaehigkeit .

    lotr:Frodo a lotr:Charakter,
    lotr:Loyalitaetsziel ;
    lotr:hatErfahrungMit lotr:Schattenlager_Infiltration ;
    lotr:hatSkill lotr:Heimlichkeit,
    lotr:Willensstärke ;
    lotr:hatSkillLevel "Fortgeschritten"^^xsd:string ;
    lotr:loyalität lotr:Frodo_selbst .

    lotr:Frodo_selbst a lotr:Loyalitaetsziel .

    lotr:Führung a lotr:Faehigkeit .

    lotr:Mittelerde a lotr:Loyalitaetsziel .

    lotr:Orks_am_Fluss_bekämpfen a lotr:Mission ;
    lotr:erfordertSkill lotr:Fernkampf ;
    lotr:missionstyp "Kampf"^^xsd:string .

    lotr:Schwertkampf a lotr:Faehigkeit,
    lotr:Nahkampffaehigkeit .

    lotr:Standfestigkeit a lotr:Faehigkeit,
    lotr:Nahkampffaehigkeit .

    lotr:Verteidigung_des_Tors a lotr:Mission ;
    lotr:erfordertSkill lotr:Axtkampf ;
    lotr:missionstyp "Verteidigung"^^xsd:string .

    lotr:Willensstärke a lotr:Faehigkeit .

    lotr:Zwerge a lotr:Loyalitaetsziel .

    lotr:Axtkampf a lotr:Faehigkeit,
    lotr:Nahkampffaehigkeit .

    lotr:Kampffaehigkeit rdfs:subClassOf lotr:Faehigkeit .

    lotr:Schattenlager_Infiltration a lotr:Mission ;
    lotr:erfordertSkill lotr:Heimlichkeit ;
    lotr:missionstyp "Sabotage"^^xsd:string .

    lotr:Heimlichkeit a lotr:Faehigkeit .

    lotr:Nahkampffaehigkeit rdfs:subClassOf lotr:Kampffaehigkeit .

    lotr:Loyalitaetsziel a owl:Class .

    lotr:Mission a owl:Class .

    lotr:Charakter a owl:Class .

    lotr:Faehigkeit a owl:Class .

    Visualisierung via Protegé