Blog

Dein AI Agent braucht mehr als Guardrails. Er braucht eine Welt mit Regeln.

Johannes Hayer
Johannes Hayer
·11 Min. Lesezeit·de
Ein probabilistischer AI Agent innerhalb eines Ontologie-Graphen, dessen Tool-Vorschläge durch einen deterministischen Validator gefiltert werden.

Alle beschweren sich darüber, dass LLMs halluzinieren.

Aber was, wenn das kein Bug ist, den wir irgendwann wegpatchen?

Ein LLM ruft keine Wahrheit aus einer Datenbank ab. Es erzeugt die nächste plausible Fortsetzung. Genau dadurch kann es Code schreiben, neue Lösungswege vorschlagen und sich etwas vorstellen, das vorher nicht existiert hat.

Menschen können das auch. Wir nennen es Fantasie, Hypothese oder Idee.

Nur: Wenn ein Agent einen Refund ausführt, einen Vertrag prüft oder Daten in deinem System verändert, reicht „plausibel“ nicht mehr.

Dann brauchst du Zuverlässigkeit.

Wir versuchen dieses Problem heute mit besseren Prompts, mehr Context, RAG und Agentic Loops zu lösen. Das macht Agents leistungsfähiger. Aber nicht automatisch verlässlicher.

Ein Loop gibt dem Agenten mehr Möglichkeiten, seinen Fehler zu korrigieren. Er gibt ihm aber auch mehr Möglichkeiten, vom Ziel abzudriften, Tokens zu verbrennen oder denselben falschen Gedanken dreimal auszuführen.

Mehr Context hilft dem Agenten, mehr über die Aufgabe zu wissen. Aber Context allein definiert noch keine Welt.

Wenn ein Agent in deiner Domäne handeln soll, muss dein System explizit beschreiben:

  • Welche Entitäten existieren?
  • Wie hängen sie zusammen?
  • Welche Zustände sind erlaubt?
  • Welche Aktionen dürfen niemals passieren?

Der Agent braucht nicht weniger Fantasie.

Er braucht ein maschinenlesbares Verständnis der Welt, in der diese Fantasie handeln darf.

Genau hier werden Ontologien interessant.

Der Gedanke kam aus Frank Coyles Talk „Why Agentic Systems Need Ontologies“. Seine Kernthese trifft einen echten Production-Pain: Ein LLM kann einen zweiten Refund auf dieselbe Order vorschlagen, Geld an die falsche Rolle senden oder einen Status wie probably_shipped erfinden. Ein längerer System-Prompt verhindert das nicht zuverlässig.

Zwei Welten, die wir zusammenbringen müssen#

Coyle beschreibt zwei Entwicklungslinien der AI.

Auf der einen Seite stehen die AI Agents.

Sie nehmen ihre Umgebung wahr, entscheiden über den nächsten Schritt und schlagen eine Aktion vor. Unter der Haube bleibt das LLM probabilistisch. Es berechnet nicht die eine logisch richtige Aktion. Es erzeugt die Aktion, die im aktuellen Context am plausibelsten erscheint.

Genau deshalb ist es so flexibel.

Der Agent kann auf neue Situationen reagieren, zwischen Tools wählen und einen Plan anpassen, den wir nicht vollständig vorprogrammiert haben.

Auf der anderen Seite steht die Ontologie.

Der Begriff kommt ursprünglich aus der Philosophie. Ontologie ist die Lehre des Seins: Was existiert überhaupt? Welche Arten von Dingen gibt es? Und in welchen Beziehungen stehen sie zueinander?

In der Informatik wird daraus ein formales Modell einer Domäne.

Für ein Payment-System könnte dieses Modell beschreiben:

  • Customer, SupportRepresentative, Order und Refund sind unterschiedliche Entitäten.
  • Ein Refund gehört zu genau einer Order.
  • Der Empfänger eines Refunds muss der Käufer dieser Order sein.
  • Eine bereits vollständig erstattete Order darf nicht erneut erstattet werden.

Der Agent erzeugt Möglichkeiten.

Die Ontologie beschreibt die Welt, in der diese Möglichkeiten bewertet werden.

Verbindest du beide Ansätze, entsteht das, was oft neuro-symbolische AI genannt wird:

text
LLM / Agent
  → schlägt eine Aktion probabilistisch vor
 
Ontologie
  → liefert das formale Domänenmodell und mögliche Inferenzen
 
Runtime-Validator
  → prüft den Vorschlag gegen Modell, Policies und aktuellen Zustand

Der Agent darf weiterhin kreativ planen. Aber er entscheidet nicht allein, was in der echten Welt passieren darf.

Die Ontologie gibt ihm nicht automatisch menschliches Verständnis. Sie gibt dem System einen expliziten Rahmen, gegen den es seinen Vorschlag prüfen kann.

Zusammenspiel aus probabilistischem AI Agent, Ontologie und deterministischem Runtime-Validator

Warum das gerade in Agentic Loops wichtig wird#

Ein Agent beantwortet nicht einfach nur einen Prompt.

Er arbeitet in einem Loop:

text
LLM schlägt einen Tool Call vor
  → Tool wird ausgeführt
  → Ergebnis kommt als Observation zurück
  → LLM entscheidet über den nächsten Schritt
  → Loop beginnt erneut

Genau dieser Loop macht aus einem LLM einen Agenten.

Das Modell kann Informationen nachladen, auf Fehler reagieren und seinen Plan während der Ausführung anpassen. Es muss nicht beim ersten Versuch richtigliegen.

Aber der Loop verstärkt nicht nur die Fähigkeiten des Modells. Er verstärkt auch seine Fehler.

Wenn der Agent eine falsche Annahme trifft, kann er auf dieser Annahme weitere Tool Calls aufbauen. Wenn ein Tool fehlschlägt, kann er dieselbe Aktion in leicht veränderter Form immer wieder versuchen. Mit jedem Durchlauf wächst der Context, die ursprüngliche Aufgabe rückt weiter nach hinten und der Token-Verbrauch steigt.

Aus einem falschen Vorschlag wird so schnell:

  • ein endloser Reparaturversuch,
  • Context Drift über mehrere Tool Calls,
  • dreimal dieselbe unerlaubte Aktion,
  • oder eine überraschend hohe API-Rechnung.

Ein max_steps = 6 verhindert die Endlosschleife.

Es macht die ersten fünf Schritte aber noch nicht richtig.

Genau hier wird die Ontologie praktisch. Wir können die Regeln unserer Domäne als Entitäten, Beziehungen und erlaubte Zustände im Graphen ausdrücken und jeden Tool Call gegen diese Welt prüfen.

Der Agent darf weiterhin probabilistisch vorschlagen:

json
{
  "id": "call_01",
  "tool": "refund_order",
  "arguments": {
    "order_id": "order_42",
    "recipient_id": "support_17",
    "amount_cents": 12900
  }
}

Im Graphen steht jedoch:

text
customer_9  ── owns ──>  order_42
order_42    ── paid_by ──> customer_9
refund      ── must_go_to ──> payer(order_42)

Aber wie kommt diese Regel jetzt konkret zum Tool Call?

In der Praxis würde ich die Ontologie um eine operative Policy-Ebene erweitern. Das Tool selbst ist dabei ebenfalls ein Knoten im Graphen:

text
(:ToolAction {name: "refund_order"})
  ├──[:CHECK_BEFORE]──> (:Policy {key: "recipient_is_order_payer"})
  ├──[:CHECK_BEFORE]──> (:Policy {key: "order_has_refundable_balance"})
  └──[:CHECK_AFTER]───> (:Policy {key: "refund_matches_approved_request"})
 
(:Policy {key: "recipient_is_order_payer"})
  └──[:USES_RELATION]─> (:Relation {name: "PAID_BY"})
 
(:Order {id: "order_42"})
  └──[:PAID_BY]───────> (:Customer {id: "customer_9"})

Damit ist maschinenlesbar hinterlegt:

  • welche Policies zu refund_order gehören,
  • wann sie geprüft werden,
  • und auf welche Begriffe und Beziehungen der Domäne sie sich beziehen.

Das ist streng genommen nicht mehr nur eine reine OWL-Ontologie. Es ist eine operative Policy-Schicht auf demselben Knowledge Graph. Genau diese Trennung ist wichtig: Der Graph beschreibt und verknüpft die Regeln. Deterministischer Code führt sie aus.

So sieht der Check im Code aus#

Zuerst muss aus dem probabilistischen LLM-Output überhaupt ein gültiger Tool Call werden. Dafür ist Pydantic zuständig:

python
from typing import Literal
from pydantic import BaseModel, Field
 
 
class RefundArguments(BaseModel):
    order_id: str = Field(pattern=r"^order_[0-9]+$")
    recipient_id: str = Field(pattern=r"^(customer|support)_[0-9]+$")
    amount_cents: int = Field(gt=0)
 
 
class RefundToolCall(BaseModel):
    id: str
    tool: Literal["refund_order"]
    arguments: RefundArguments

Damit kann das Backend den rohen Vorschlag des LLMs parsen:

python
call = RefundToolCall.model_validate(llm_output)

Pydantic beantwortet dabei Fragen wie:

  • Ist amount_cents wirklich eine positive Ganzzahl?
  • Hat order_id das erwartete Format?
  • Existieren alle benötigten Argumente?
  • Hat das LLM überhaupt refund_order ausgewählt?

Pydantic kann aber nicht beantworten, ob customer_9 diese Order bezahlt hat oder ob noch ein erstattbarer Betrag vorhanden ist.

Das ist die nächste Schicht.

Wenn der Agent refund_order aufrufen will, lädt der Runtime-Code zuerst alle CHECK_BEFORE-Policies des Tool-Knotens:

python
def load_policies(graph, tool_name: str, phase: str) -> list[str]:
    relation = {
        "before": "CHECK_BEFORE",
        "after": "CHECK_AFTER",
    }[phase]
 
    rows = graph.query(
        f"""
        MATCH (:ToolAction {{name: $tool_name}})
              -[:{relation}]->(policy:Policy)
        RETURN policy.key AS key
        ORDER BY policy.key
        """,
        tool_name=tool_name,
    )
 
    return [row["key"] for row in rows]

Für jede Policy existiert eine deterministische Implementierung:

python
def recipient_is_order_payer(call, graph) -> bool:
    return graph.exists(
        """
        MATCH (:Order {id: $order_id})
              -[:PAID_BY]->(:Customer {id: $recipient_id})
        """,
        order_id=call.arguments.order_id,
        recipient_id=call.arguments.recipient_id,
    )
 
 
def order_has_refundable_balance(call, graph) -> bool:
    order = graph.get_order(call.arguments.order_id)
 
    requested = call.arguments.amount_cents
    remaining = order.paid_cents - order.refunded_cents
 
    return requested > 0 and requested <= remaining
 
 
POLICY_EVALUATORS = {
    "recipient_is_order_payer": recipient_is_order_payer,
    "order_has_refundable_balance": order_has_refundable_balance,
}

Der Runtime-Validator verbindet beide Seiten:

python
def validate_before(call, graph) -> list[str]:
    policy_keys = load_policies(
        graph,
        tool_name=call.tool,
        phase="before",
    )
 
    violations = []
 
    for key in policy_keys:
        evaluator = POLICY_EVALUATORS[key]
 
        if not evaluator(call, graph):
            violations.append(key)
 
    return violations

Für den Vorschlag des Agenten:

json
{
  "id": "call_01",
  "tool": "refund_order",
  "arguments": {
    "order_id": "order_42",
    "recipient_id": "support_17",
    "amount_cents": 12900
  }
}

läuft der erste Check auf diese Graph-Abfrage hinaus:

text
Gibt es diesen Pfad?
 
(order_42:Order)-[:PAID_BY]->(support_17:Customer)

Die Antwort ist false.

Der Runtime-Code ruft das Payment-Tool deshalb überhaupt nicht auf. Er gibt dem Agentic Loop stattdessen eine strukturierte Observation zurück:

json
{
  "status": "rejected",
  "failed_policy": "recipient_is_order_payer",
  "message": "The refund recipient must be the customer who paid the order.",
  "allowed_recipient_id": "customer_9"
}

Das LLM muss im nächsten Durchlauf nicht blind raten. Es weiß, welche Regel verletzt wurde und welcher Pfad in der Domäne zulässig ist.

Und was wird nach dem Tool Call geprüft?#

Auch das Ergebnis eines Tools kann falsch oder unvollständig sein.

Ein vorbereitetes Refund-Ergebnis könnte zum Beispiel so aussehen:

json
{
  "refund_id": "refund_88",
  "order_id": "order_42",
  "recipient_id": "customer_9",
  "amount_cents": 12900,
  "status": "prepared"
}

Die CHECK_AFTER-Policy refund_matches_approved_request prüft, ob das Tool-Ergebnis noch exakt zum validierten Vorschlag passt:

python
def refund_matches_approved_request(call, result, graph) -> bool:
    return all(
        [
            result.order_id == call.arguments.order_id,
            result.recipient_id == call.arguments.recipient_id,
            result.amount_cents == call.arguments.amount_cents,
            result.status == "prepared",
        ]
    )

Auch hier bestimmt die Kante im Graphen, welcher Check ausgeführt wird:

python
AFTER_POLICY_EVALUATORS = {
    "refund_matches_approved_request": (
        refund_matches_approved_request
    ),
}
 
 
def validate_after(call, result, graph) -> list[str]:
    policy_keys = load_policies(
        graph,
        tool_name=call.tool,
        phase="after",
    )
 
    return [
        key
        for key in policy_keys
        if not AFTER_POLICY_EVALUATORS[key](call, result, graph)
    ]

Der vollständige Ablauf sieht damit so aus:

python
def rejected_observation(violations):
    return {
        "status": "rejected",
        "failed_policies": violations,
    }
 
 
async def execute_agent_tool(call, graph, tools):
    before_violations = validate_before(call, graph)
 
    if before_violations:
        return rejected_observation(before_violations)
 
    # Noch keine irreversible Aktion:
    prepared_result = await tools[call.tool].prepare(
        call.arguments.model_dump()
    )
 
    after_violations = validate_after(call, prepared_result, graph)
 
    if after_violations:
        await tools[call.tool].discard(prepared_result)
        return rejected_observation(after_violations)
 
    # Erst jetzt darf der externe Side Effect passieren:
    receipt = await tools[call.tool].commit(
        prepared_result,
        idempotency_key=call.id,
    )
 
    return {
        "status": "success",
        "tool_result": receipt,
    }

Bis hierhin haben wir einen einzelnen Tool Call abgesichert.

Das allein verhindert aber noch keinen endlosen Agentic Loop. Dafür braucht der Orchestrator eigene, deterministische Abbruchbedingungen:

python
from collections import Counter
from pydantic import ValidationError
 
 
MAX_STEPS = 6
MAX_SAME_POLICY_FAILURES = 2
 
 
async def run_agent(task, llm, graph, tools):
    observations = []
    policy_failures = Counter()
 
    for step in range(MAX_STEPS):
        raw_proposal = await llm.propose(
            task=task,
            observations=observations,
        )
 
        # 1. Syntaktische Grenze
        try:
            call = RefundToolCall.model_validate(raw_proposal)
        except ValidationError as error:
            observation = {
                "status": "rejected",
                "reason": "INVALID_TOOL_SCHEMA",
                "details": error.errors(),
            }
            observations.append(observation)
            continue
 
        # 2. Semantische Grenze und Tool-Ausführung
        observation = await execute_agent_tool(
            call,
            graph,
            tools,
        )
        observations.append(observation)
 
        if observation["status"] == "success":
            return observation
 
        # 3. Circuit Breaker für wiederholte Regelverletzungen
        for policy in observation.get("failed_policies", []):
            policy_failures[policy] += 1
 
            if (
                policy_failures[policy]
                >= MAX_SAME_POLICY_FAILURES
            ):
                return {
                    "status": "human_review_required",
                    "reason": "REPEATED_POLICY_VIOLATION",
                    "failed_policy": policy,
                    "steps_used": step + 1,
                }
 
    # 4. Globales Limit gegen Drift und Token-Verbrauch
    return {
        "status": "stopped",
        "reason": "MAX_STEPS_EXCEEDED",
        "steps_used": MAX_STEPS,
    }

Jetzt sind die Verantwortlichkeiten sauber getrennt:

text
Pydantic
  → Ist der Tool Call strukturell gültig?
 
BEFORE-Policies am Tool-Knoten
  → Ist diese Aktion im aktuellen Domänenzustand erlaubt?
 
AFTER-Policies am Tool-Knoten
  → Entspricht das vorbereitete Ergebnis dem validierten Auftrag?
 
Circuit Breaker im Orchestrator
  → Wie oft darf der Agent scheitern, bevor der Loop endet?

Scheitert eine Policy einmal, bekommt der Agent strukturiertes Feedback und darf seinen Plan korrigieren.

Scheitert dieselbe Policy wiederholt, stoppt der Circuit Breaker den Loop. Wird unabhängig davon das globale Step-Limit erreicht, endet der Loop ebenfalls.

Damit verhindert die Ontologie nicht selbst Context Drift oder Token-Verbrauch. Ihre Policy-Knoten liefern dem Orchestrator aber präzise Fehlersignale, auf deren Basis er gezielt entscheiden kann: noch ein Versuch, anderer Pfad oder Hard Stop.

Das ist der entscheidende Punkt:

text
Agent schlägt Tool Call vor
  → Runtime lädt BEFORE-Policies vom Tool-Knoten
  → deterministischer Code prüft Graph und aktuellen Zustand
  → Tool bereitet Ergebnis ohne Side Effect vor
  → Runtime lädt AFTER-Policies vom Tool-Knoten
  → deterministischer Code prüft das Ergebnis
  → erst danach wird committed

Nicht jedes externe Tool bietet ein echtes prepare und commit. Dann brauchst du je nach System Transaktionen, Idempotency Keys, eine Saga oder einen Approval-Schritt. Ein Post-Check allein kann einen bereits ausgeführten Refund nicht zurückholen.

Die Ontologie beendet den Loop also nicht von selbst.

Sie gibt dem deterministischen Code aber die Regeln, mit denen er einen Loop kontrollieren, falsche Aktionen früh blockieren und dem Agenten bessere Wege zurück in den erlaubten Raum zeigen kann.

Was Ontologien wirklich beitragen#

Eine Ontologie gibt deiner Domäne ein gemeinsames, maschinenlesbares Modell:

  • Welche Entitäten existieren?
  • Welche Eigenschaften haben sie?
  • Wie hängen sie zusammen?
  • Welche Schlussfolgerungen folgen aus diesen Beziehungen?

Mit RDFS kannst du zum Beispiel ausdrücken, dass jemand, der etwas unterrichtet, ein Teacher ist und dass jeder Teacher auch eine Person ist.

Mit OWL kannst du stärkere Aussagen modellieren:

  • ancestorOf ist transitiv.
  • Customer und SupportRepresentative sind disjunkte Klassen.
  • hasFather ist eine funktionale Eigenschaft.

Damit kann ein Reasoner neue Fakten ableiten oder Widersprüche im Modell erkennen.

Das ist mehr als eine Graph-Datenbank. Der Graph speichert Beziehungen. Die Ontologie gibt ihnen Bedeutung.

Aber hier wird es wichtig.

OWL ist nicht automatisch dein Validator#

Geminis Erklärung macht aus OWL schnell eine Datenbank-Constraint:

Eine Functional Property verhindert Duplikate und erzwingt Konsistenz.

So funktioniert OWL nicht.

OWL arbeitet ohne die übliche Unique-Name-Annahme. Wenn Peter und Peter_Griffin beide als Vater derselben Person eingetragen sind und hasFather funktional ist, kann ein Reasoner daraus ableiten, dass beide Namen dieselbe Person bezeichnen. Er wirft nicht automatisch einen Validation Error.

Auch fehlende Daten sind in OWL nicht automatisch falsch. Unter der Open-World-Assumption bedeutet „nicht im Graphen“ zunächst nur „unbekannt“.

Für Inferenz ist das nützlich.

Für einen Runtime-Check wie „jede Auszahlung braucht exakt einen verifizierten Empfänger“ reicht es nicht.

Wenn du RDF-Daten tatsächlich gegen geschlossene Bedingungen validieren willst, brauchst du zum Beispiel SHACL. SHACL prüft einen Data Graph gegen definierte Shapes und liefert einen strukturierten Validation Report.

Die Rollen sind also verschieden:

text
Pydantic  → Ist der Tool Call syntaktisch wohlgeformt?
OWL       → Welche Bedeutung und welche Inferenzen folgen aus dem Modell?
SHACL     → Entspricht der aktuelle Graph den erwarteten Datenbedingungen?
Policy    → Darf dieser Actor diese Aktion in diesem Zustand ausführen?

Alles davon „Ontology Validator“ zu nennen, klingt einfacher. Es versteckt aber genau die Architektur, die du in Production verstehen musst.

Die Ontologie kennt nicht das Ziel des Agenten#

Der nächste Denkfehler ist subtiler.

Eine Ontologie verhindert nicht automatisch Context Drift.

Sie weiß, dass ein Customer kein SupportRepresentative ist. Sie weiß aber nicht, ob der Agent nach sechs Tool Calls noch an der ursprünglichen Aufgabe arbeitet.

Dafür muss dein System das Ziel explizit repräsentieren:

python
task = {
    "goal": "prepare_refund_proposal",
    "actor_id": "user_42",
    "allowed_tools": ["get_order", "get_payment", "propose_refund"],
    "max_steps": 6,
    "max_cost_cents": 20,
    "requires_approval": True,
}

Jetzt kann der Loop harte Fragen beantworten:

  • Ist das Tool für diesen Task erlaubt?
  • Passt der aktuelle State noch zur nächsten Transition?
  • Wurde dieselbe fehlgeschlagene Aktion schon wiederholt?
  • Ist das Step-, Zeit- oder Token-Budget aufgebraucht?
  • Muss ein Mensch übernehmen?

Die Ontologie liefert Domänenwissen.

Der Orchestrator kontrolliert den Loop.

Die State Machine kontrolliert erlaubte Übergänge.

Der Circuit Breaker zieht den Stecker.

Das sind vier verschiedene Jobs.

Ein Post-Check macht keinen Side Effect ungeschehen#

Geminis End-to-End-Beispiel schreibt zuerst den Verkäufer in ein Dictionary und setzt den Wert bei einer verletzten Post-Condition wieder auf None.

In einem Demo funktioniert das.

In Production wird daraus schnell:

  • ein Refund bei Stripe,
  • eine versendete E-Mail,
  • eine gebuchte Reise,
  • ein übermittelter Schriftsatz,
  • ein Write in einem fremden System.

Diese Aktionen kannst du nicht mit einer Python-Zuweisung zurückrollen.

Deshalb sollte der Agent zuerst eine Zustandsänderung vorschlagen:

json
{
  "transition": "REFUND_REQUESTED",
  "order_id": "order_123",
  "recipient_id": "customer_9",
  "amount_cents": 4900
}

Dann prüft dein System:

  1. Schema und Typen
  2. Authentifizierung und Autorisierung
  3. aktuelle Order- und Payment-Daten
  4. Domain-Invarianten
  5. Idempotency Key
  6. Approval Policy

Erst danach entsteht der Side Effect.

Und selbst dann brauchst du je nach System Transaktionen, Outbox-Patterns oder eine Saga mit kompensierenden Aktionen. „Rollback“ ist keine Eigenschaft des Agenten. Es ist eine Eigenschaft deiner Integrationsarchitektur.

Die Production-Architektur#

Der robuste Loop sieht eher so aus:

text
User-Ziel

[Task Envelope]
Ziel · Actor · erlaubte Tools · Step-Budget · Approval Policy

LLM schlägt Tool Call vor

[1] Schema-Check
Pydantic / Structured Output

[2] Policy- und Precondition-Check
Rollen · Berechtigungen · aktueller State

[3] Semantik- und Daten-Check
Ontology Reasoner · SHACL · Business Invariants

[4] Approval Boundary
automatisch · Human-in-the-Loop · abgelehnt

[5] Side Effect
idempotent · transactional · auditiert

[6] Postcondition
Ist der erwartete State wirklich eingetreten?

Observation zurück an den Agenten

Wenn ein Check fehlschlägt, bekommt der Agent kein diffuses „Action rejected“. Er bekommt strukturiertes Feedback:

json
{
  "code": "RECIPIENT_ROLE_MISMATCH",
  "message": "Refund recipient must be the buyer of the order.",
  "current_recipient_role": "SupportRepresentative",
  "allowed_recipient_id": "customer_9",
  "retryable": true
}

Jetzt muss das LLM nicht blind raten.

Es kann seinen Vorschlag korrigieren. Nach zwei oder drei identischen Fehlern stoppt der Orchestrator den Loop und eskaliert.

Nicht die Ontologie stoppt die Schleife.

Dein Code stoppt sie — mit Informationen aus der Ontologie.

Legal AI ist ein guter Test für dieses Pattern, weil „klingt plausibel“ dort nicht reicht.

Angenommen, ein Anwalt lädt eine Akte hoch und fragt:

Können wir einen Verzugsschaden geltend machen?

Die naive Architektur wirft alle PDFs in eine Vector DB, sucht ähnliche Chunks und lässt das LLM eine juristische Antwort schreiben.

Das Problem: Ein ähnlicher Chunk ist noch kein bewiesener Fakt.

Ein Schreiben mit dem Satz „Zahlbar bis 26.04.“ beweist nicht automatisch, dass die Mahnung zugegangen ist. Ein LLM kann beides sprachlich sehr überzeugend zusammenziehen.

Ich würde die Daten deshalb in drei Ebenen trennen:

text
Quelldokumente
  → unveränderbare PDFs, Seiten und Chunks
 
Extraktionsvorschläge
  → "könnte Mahnung sein", mit Quelle und Confidence
 
Bestätigter Case Graph
  → behauptet · bestritten · belegt · bestätigt

Der Extraction Agent darf neue Nodes und Edges vorschlagen. Er darf sie nicht still als Wahrheit in den Case Graph schreiben.

Ein sinnvoller Tool-Satz wäre:

text
search_case_evidence(query)
get_claim_requirements(claim_type, jurisdiction, valid_at)
propose_case_fact(fact_type, source_chunk_id)
link_evidence_proposal(fact_id, source_chunk_id)
get_missing_requirements(claim_id)
draft_argument(claim_id, approved_evidence_ids)

Beachte das Verb propose.

Der Agent kann eine Fundstelle entdecken. Der Graph hält fest, woher sie kommt. Ein Anwalt oder ein ausreichend klarer Review-Workflow bestätigt, welchen Status sie bekommt.

Die Ontologie kann dann ausdrücken:

text
Verzugsschaden
  erfordert → Fälligkeit
  erfordert → Mahnung ODER Entbehrlichkeit
  erfordert → Vertretenmüssen
  erfordert → Schaden

Das System kann zuverlässig zeigen:

  • Fälligkeit: Quelle vorhanden und bestätigt
  • Mahnung: Schreiben gefunden, Zugang nicht belegt
  • Schaden: Betrag extrahiert, Berechnung nicht geprüft

Was es nicht sagen sollte:

Der Anspruch ist zu 66 % begründet.

Das ist falsche Präzision. Tatbestandsmerkmale sind keine gleich gewichteten Progress Bars.

Der bessere Output lautet:

Für die Mahnung wurde ein Schreiben gefunden. In der Akte fehlt bisher ein bestätigter Nachweis des Zugangs. Ohne diesen Nachweis bleibt dieses Tatbestandsmerkmal offen.

Das ist weniger spektakulär.

Und deutlich nützlicher.

Top-down oder bottom-up?#

Auch hier ist „beides“ allein noch keine Strategie.

Top-down modellierst du den stabilen Kern:

  • Rollen und Berechtigungen
  • erlaubte Zustände und Übergänge
  • fachliche Invarianten
  • Provenance
  • Jurisdiktion und zeitliche Gültigkeit

Bottom-up lässt du aus echten Akten, Queries und Failure-Cases neue Kandidaten entstehen:

  • unbekannte Dokumenttypen
  • neue Bezeichnungen für bekannte Konzepte
  • fehlende Beziehungen
  • wiederkehrende Extraktionsmuster

Diese Kandidaten landen in einer Review Queue. Ein Domain-Experte entscheidet, ob sie ein Synonym, eine neue Subklasse oder einfach Rauschen sind.

So wächst die Ontologie mit der Realität, ohne dass ein probabilistischer Agent sein eigenes Regelwerk umschreibt.

Und noch ein Detail aus dem Talk: Wikipedia basiert nicht auf DBpedia. DBpedia extrahiert strukturierte Informationen aus Wikipedia. Das klingt klein, ist aber genau die Art semantischer Richtung, die in Knowledge-Graph-Systemen zählt.

Der eigentliche Shift#

Ich würde Coyles Satz „Pydantic at the door, ontology at the ledger“ erweitern:

Pydantic prüft die Form. OWL leitet Bedeutung ab. SHACL prüft den Graphen. Policy-Code erlaubt die Aktion. Die Transaction Boundary kontrolliert, was real wird.

Dann darf das LLM das tun, worin es gut ist:

Hypothesen bilden. Evidenz finden. Wege vorschlagen. Mit unvollständiger Sprache arbeiten.

Aber zwischen seinem Vorschlag und der echten Welt liegt Code, den du erklären, testen und auditieren kannst.

Das ist nicht weniger agentic.

Das ist der Punkt, an dem aus einem Agenten ein System wird.


Quellen: Frank Coyle, „Why Agentic Systems Need Ontologies“, AI Engineer World’s Fair 2026; W3C, OWL 2 Structural Specification und Shapes Constraint Language (SHACL); DBpedia Association.

AI Engineering, wöchentlich.

Praktisches AI Engineering direkt in dein Postfach.