Grounding in KI-Agenten: Leere, irrelevante und fehlerhafte Tool-Ergebnisse behandeln

Aktualisiert:
KI zu diesem Artikel befragen
Grounding in KI-Agenten: Leere, irrelevante und fehlerhafte Tool-Ergebnisse behandeln

Stellen Sie sich vor: Ihr KI-Agent erhält die Anfrage „Wie hoch ist der Preis für den Enterprise-Plan?“. Er ruft ein Tool auf. Das Tool antwortet. Der Agent formuliert eine Antwort – überzeugend, kohärent, mit einer konkreten Zahl. Der Kunde erhält die Antwort und geht zufrieden davon.

Das Problem ist, dass das Tool ein leeres Ergebnis zurückgegeben hat – das Dokument wurde nicht gefunden. Und der Agent hat, anstatt zu sagen „Ich habe es nicht gefunden“, einen Preis aus seinen eigenen Trainingsdaten erfunden. Überzeugend. Ohne Vorwarnung.

Das ist kein Bug. Das ist ein Mangel an Grounding – und genau darum geht es in diesem Artikel.

Dieser Artikel ist Teil einer Serie über KI-Agenten mit Spring Boot. Wenn Sie noch nicht gelesen haben, wie ein Modell entscheidet, wann es ein Tool aufrufen soll – beginnen Sie mit Wie LLM entscheidet, wann ein Tool aufgerufen werden soll.

Inhalt

Was ist Grounding und warum fehlt es in Tutorials

Die meisten Tutorials über KI-Agenten sehen so aus:

1. Erhalte eine Anfrage vom Benutzer
2. Rufe ein Tool auf
3. Gib das Ergebnis an das Modell weiter
4. Erhalte eine Antwort
5. ???
6. Profit

Schritt 5 ist Grounding. Und Tutorials lassen ihn aus, weil er „nicht interessant“ ist. Es gibt keine beeindruckende Demo. Kein Wow-Effekt. Nur eine langweilige Qualitätsprüfung, bevor geantwortet wird.

Grounding ist der Prozess, bei dem der Agent prüft, ob das Ergebnis, das das Tool zurückgegeben hat, die Anfrage tatsächlich beantwortet, ob es ausreichend qualitativ ist, um darauf eine Antwort aufzubauen, und ob er sich auf eine bestimmte Quelle beziehen kann, anstatt eine Antwort zu erfinden.

Warum das entscheidend ist – eine Analogie aus dem echten Leben

Stellen Sie sich vor, Sie haben einen Assistenten eingestellt und ihn gebeten: „Finde mir die Bedingungen unseres Vertrags mit dem Kunden Alpha“.

Der Assistent ging ins Archiv. Kam nach einer Minute zurück. Und erzählte Ihnen die Vertragsbedingungen – überzeugend und detailliert.

Aber tatsächlich fand er nichts im Archiv. Er erinnerte sich einfach an einen ähnlichen Vertrag mit einem anderen Kunden und dachte, es sei „ungefähr dasselbe“.

Sie haben auf der Grundlage dieser Bedingungen eine neue Vereinbarung unterzeichnet. Und erst eine Woche später stellten Sie fest, dass die Bedingungen ganz anders waren.

Genau so verhält sich ein KI-Agent ohne Grounding. Und genau deshalb ist das keine akademische Konzept – es ist eine Frage des Vertrauens in Ihr Produkt.

Ein realer Fall mit AskYourDocs: zu Beginn der Entwicklung antwortete der Agent manchmal auf Anfragen zu bestimmten Dokumenten, auch wenn das Dokument nicht in die Wissensdatenbank hochgeladen worden war. Das Modell „erinnerte“ sich an ähnliche Inhalte aus den Trainingsdaten und präsentierte sie als Ergebnis der Suche in den Kundendokumenten. Nach der Einführung von Grounding-Prüfungen – verschwand dieses Problem.

Drei Szenarien für schlechte Tool-Ergebnisse – mit realen Beispielen

Nicht alle schlechten Ergebnisse sind gleich. Betrachten wir drei grundlegend unterschiedliche Szenarien – jedes erfordert eine eigene Verarbeitungsstrategie. Und jedes von ihnen kann zu einem anderen Fehlertyp führen, wenn es nicht explizit verarbeitet wird.

Szenario 1: Leeres Ergebnis

Das Tool wurde erfolgreich ausgeführt, hat aber nichts gefunden. Es scheint – der einfachste Fall. Tatsächlich – der häufigste Auslöser für Halluzinationen.

Warum das gefährlich ist: das Modell sieht eine leere Antwort und weiß nicht, was es damit tun soll. Anstatt zu sagen „nicht gefunden“ – füllt es die Lücke mit seinem eigenen Trainingswissen. Überzeugend. Ohne Vorwarnung.

// Das Tool gab eine leere Liste zurück
{
  "results": [],
  "total": 0
}

// Was das Modell OHNE Grounding tut:
// Benutzer: „Wie hoch ist der Preis für den Enterprise-Plan?“
// Agent: „Gemäß den Unternehmensstandards 
//         beträgt der Preis für den Enterprise-Plan 500 $/Monat…“
// (aus Trainingsdaten erfunden – der tatsächliche Preis beträgt 850 $)

// Was das Modell MIT Grounding tut:
// Agent: „Ich habe keine Informationen über Preise in der Wissensdatenbank gefunden. 
//         Ich empfehle, sich an die Verkaufsabteilung zu wenden.“

Wie man es in Java verarbeitet:

@Tool(description = "Sucht nach Informationen in der Wissensdatenbank")
public String searchKnowledgeBase(String query) {
    List<SearchResult> results = vectorStore.search(query);

    // Leeres Ergebnis explizit verarbeiten
    if (results == null || results.isEmpty()) {
        log.warn("Leeres Ergebnis für Anfrage: '{}'", query);
        // Kein leerer String zurückgeben – eine klare Anweisung zurückgeben
        return String.format("""
            ERGEBNIS: Nichts gefunden für die Anfrage "%s"
            ANWEISUNG FÜR DAS MODELL: Antworte nicht aus eigenem Wissen.
            Informiere den Benutzer, dass die Informationen in der Wissensdatenbank fehlen.
            """, query);
    }

    return formatResults(results);
}

Reales Beispiel aus Agent Chat: Das Wikipedia-Tool gibt manchmal ein leeres Ergebnis für sehr spezifische Anfragen zurück – zum Beispiel „vibe coding productivity statistics 2025“. Ohne Grounding begann der Agent sofort, Statistiken zu liefern, die er selbst erfunden hatte – und sie wurden als „reale Fakten“ in den Dialog aufgenommen. Nach Hinzufügen einer klaren Anweisung bei einem leeren Ergebnis – wechselt der Agent korrekt zu Tavily oder gibt zu, dass er nichts gefunden hat.

Szenario 2: Irrelevantes Ergebnis

Das Tool hat etwas gefunden – aber nicht das, was benötigt wurde. Dies ist das gefährlichste Szenario von dreien. Denn das Modell sieht „Ergebnis vorhanden“ und baut darauf überzeugend eine Antwort auf – ohne zu prüfen, ob es tatsächlich das ist, wonach der Benutzer gefragt hat.

Warum das gefährlich ist: die semantische Suche gibt immer etwas zurück, das der Anfrage am nächsten kommt – auch wenn das eigentliche Dokument nicht in der Datenbank vorhanden ist. Ein Score von 0,61 bedeutet „das Beste von dem, was da ist“ – bedeutet aber nicht „beantwortet die Frage“.

// Anfrage: „Bedingungen für die Kündigung des Vertrags mit dem Kunden Alpha“
// Das Tool gab das nächstgelegene Dokument zurück:
{
  "results": [
    {
      "title": "Allgemeine Bedingungen für die Vertragsauflösung",
      "content": "Die Vertragsauflösung ist mit einer Frist von 30 Tagen möglich...",
      "score": 0.61  // geringe Relevanz – allgemeines Muster, nicht der Vertrag von Alpha
    }
  ]
}

// OHNE Grounding wird das Modell antworten:
// „Der Vertrag mit dem Kunden Alpha kann mit einer Frist von 30 Tagen gekündigt werden.“
// (tatsächlich im Vertrag – 60 Tage und Strafzahlungen)

// MIT Grounding – wir prüfen den Score, bevor wir ihn an das Modell weitergeben

Wie man es in Java verarbeitet – Prüfung eines Relevanzschwellenwerts:

@Tool(description = "Sucht nach Informationen in der Wissensdatenbank")
public String searchKnowledgeBase(String query) {
    List<SearchResult> results = vectorStore.search(query);

    if (results == null || results.isEmpty()) {
        return buildNotFoundMessage(query);
    }

    SearchResult best = results.get(0);
    double score = best.getScore();

    if (score >= 0.75) {
        // Hohe Relevanz – wird überzeugend weitergegeben
        return String.format("""
            ERGEBNIS (Relevanz: hoch):
            %s
            QUELLE: %s
            """, best.getContent(), best.getDocumentTitle());

    } else if (score >= 0.55) {
        // Mittlere Relevanz – Modell warnen
        return String.format("""
            ERGEBNIS (Relevanz: mittel, Score: %.2f):
            %s
            ACHTUNG FÜR DAS MODELL: Das Ergebnis entspricht möglicherweise nicht genau der Anfrage.
            Informiere den Benutzer, dass die Antwort möglicherweise ungenau ist.
            QUELLE: %s
            """, score, best.getContent(), best.getDocumentTitle());

    } else {
        // Geringe Relevanz – besser zugeben, dass nichts gefunden wurde
        log.warn("Geringer Relevanz-Score {:.2f} für Anfrage: '{}'", score, query);
        return buildNotFoundMessage(query);
    }
}

private String buildNotFoundMessage(String query) {
    return String.format("""
        ERGEBNIS: Informationen für die Anfrage "%s" nicht gefunden
        ANWEISUNG: Antworte nicht aus eigenem Wissen.
        Informiere, dass die Informationen fehlen oder schlage vor, die Anfrage zu präzisieren.
        """, query);
}

Reales Beispiel von AskYourDocs: Ein Kunde fragte nach den Bedingungen eines bestimmten Vertrags. Das Dokument war nicht in die Datenbank hochgeladen – aber die Vektorsuche fand ein allgemeines Vertragsmuster mit einem Score von 0,63. Ohne Grounding antwortete der Agent mit den Bedingungen des Musters, als wären es die Bedingungen eines bestimmten Kunden. Nach Hinzufügen eines Schwellenwerts von 0,75 – antwortet der Agent korrekt, dass der spezifische Vertrag nicht gefunden wurde.

Szenario 3: Ausführungsfehler

Das Tool ist mit einer Exception abgestürzt. Externes API nicht erreichbar. Timeout. Rate-Limit erschöpft. Dies ist ein technischer Fehler – und er ist am einfachsten zu verarbeiten, wenn er richtig gemacht wird.

Warum das ohne Verarbeitung gefährlich ist: das Modell erhält eine Fehlermeldung und tut eines von zwei Dingen – entweder ignoriert es sie und antwortet aus eigenem Wissen, oder es gibt die technischen Details des Fehlers direkt an den Benutzer weiter. Beide Optionen sind schlecht.

// Das Tool gab einen Fehler zurück
{
  "error": "Connection timeout after 5000ms to https://api.tavily.com",
  "results": null
}

// OHNE Grounding – Option 1: Das Modell ignoriert den Fehler
// „Laut den neuesten Studien erhöht Vibe Coding die Produktivität um 40 %…“
// (erfunden, weil es nicht wusste, was es mit dem Fehler tun sollte)

// OHNE Grounding – Option 2: Das Modell gibt den Fehler wieder
// „Es gab einen Verbindungsfehler zu https://api.tavily.com nach einem Timeout von 5000 ms…“
// (technische Details, die Benutzer nicht sehen sollten)

Wie man es in Java verarbeitet – try-catch mit expliziter Grounding-Anweisung:

@Tool(description = """
    Sucht nach aktuellen Informationen im Internet über Tavily.
    Verwenden Sie es für aktuelle Nachrichten und Statistiken.
    """)
public String searchWeb(String query) {
    log.info("Tavily-Suche: '{}'", query);

    if (apiKey == null || apiKey.isBlank()) {
        // API-Schlüssel nicht konfiguriert – explizite Grounding-Anweisung
        return """
            KONFIGURATIONSFEHLER: Suche nicht verfügbar.
            ANWEISUNG: Informiere den Benutzer, dass die Online-Suche 
            derzeit nicht konfiguriert ist. Antworte nicht aus eigenem Wissen.
            """;
    }

    try {
        TavilyResponse response = callTavilyApi(query);

        if (response == null || response.results().isEmpty()) {
            return buildNotFoundMessage(query);
        }

        return formatResults(response.results());

    } catch (ResourceAccessException e) {
        // Timeout oder Dienst nicht verfügbar
        log.warn("Tavily-Timeout für '{}': {}", query, e.getMessage());
        return """
            TEMPORÄRER FEHLER: Suchdienst nicht verfügbar.
            ANWEISUNG: Informiere den Benutzer, dass die Suche vorübergehend 
            nicht verfügbar ist und schlage vor, es später erneut zu versuchen.
            ANTWORTE NICHT aus eigenem Wissen anstelle der Suche.
            """;

    } catch (HttpClientErrorException e) {
        // Rate-Limit oder Autorisierungsfehler
        log.error("Tavily API-Fehler für '{}': {} {}", 
            query, e.getStatusCode(), e.getMessage());
        return """
            API-FEHLER: Anfrage-Limit erreicht.
            ANWEISUNG: Informiere den Benutzer, dass die Suche 
            derzeit eingeschränkt ist. Versuche zu antworten, 
            nur wenn du genaues Wissen zu dem Thema hast.
            """;

    } catch (Exception e) {
        log.error("Unerwarteter Tavily-Fehler für '{}': {}", query, e.getMessage());
        return """
            UNBEKANNTER SUCHFEHLER.
            ANWEISUNG: Informiere den Benutzer, dass ein 
            technisches Problem aufgetreten ist. Antworte nicht aus eigenem Wissen.
            """;
    }
}
Beachten Sie das Muster: jeder catch-Block gibt eine andere Grounding-Anweisung zurück, abhängig vom Fehlertyp. Rate-Limit – man kann versuchen zu antworten, wenn genaues Wissen vorhanden ist. Timeout – besser gar nicht antworten. Dies ist ein feiner, aber wichtiger Unterschied, der die Qualität der Agentenantworten beeinflusst.
Szenario Was gibt das Tool zurück Risiko ohne Grounding Verarbeitungsstrategie Priorität
Leeres Ergebnis results: [] Halluzination aus Trainingsdaten Klare Anweisung im Inhalt 🔴 Kritisch
Irrelevantes Ergebnis results mit Score < 0,55 Antwort auf die falsche Frage Schwellenwertprüfung + Warnung 🔴 Kritisch
Ausführungsfehler exception / timeout Technische Details in der Antwort try-catch mit separaten Anweisungen 🟠 Hoch
Fallstrick: Verwenden Sie keinen leeren String als Rückgabe bei einem Fehler. Das Modell interpretiert einen leeren String je nach Kontext unterschiedlich – manchmal als „nichts gefunden“, manchmal als „kann selbst antworten“. Geben Sie immer eine explizite Textanweisung zurück, was genau passiert ist und was das Modell als Nächstes tun soll.

Was das Modell mit einem schlechten Ergebnis macht

Wenn das Modell ein schlechtes Tool-Ergebnis erhält – es stoppt nicht und fragt nicht „was nun tun?“. Es generiert weiterhin eine Antwort. Immer. Automatisch. Und tut dies auf eine von drei Arten – jede davon hat ein unterschiedliches Gefahrenniveau und eine unterschiedliche Diagnosemethode.

Variante A: Ignoriert und antwortet aus dem Gedächtnis

Die häufigste und gefährlichste Variante. Das Modell sieht ein leeres oder irrelevantes Ergebnis – und „entscheidet“, dass es besser ist, aus seinem eigenen Trainingswissen zu antworten, als Unwissenheit zuzugeben. Die Antwort klingt überzeugend und kohärent. Keine Warnung.

Warum das die gefährlichste Variante ist: der Fehler ist aus der Antwort nicht erkennbar. Es gibt kein „Ich bin mir nicht sicher“. Kein „vielleicht“. Nur eine überzeugende Antwort, die vollständig erfunden sein kann.

// Situation: Der Kunde fragt nach dem Preis, das Dokument wurde nicht gefunden
// Das Tool gab zurück: results: []

// Interner Prozess des Modells (vereinfacht):
// „Das Suchergebnis ist leer…
//  Aber ich wurde auf Tausenden von SaaS-Preislisten trainiert.
//  Ich weiß, dass Enterprise-Pläne normalerweise 300-800 $/Monat kosten.
//  Ich werde auf der Grundlage von Allgemeinwissen antworten. Das klingt überzeugend.“

// Was der Benutzer erhält:
// „Der Enterprise-Plan beinhaltet eine unbegrenzte Anzahl von Benutzern
//  und kostet 500 $ pro Monat bei jährlicher Zahlung.“
// stop_reason: „end_turn“ – kein tool_use in den Logs nach einem leeren Ergebnis

// Tatsächlicher Preis: 850 $/Monat nach einer Preiserhöhung vor 2 Monaten

Wie man es in den Logs erkennt: dies ist die einzige Variante, bei der die Diagnose aktive Anstrengungen erfordert. Die Antwort sieht normal aus – das Problem ist nur erkennbar, wenn der gesamte Tool-Calling-Zyklus protokolliert wird.

// Was man in den Logs suchen muss, um Variante A zu erkennen:
// 1. Tool-Aufruf ist vorhanden
// 2. Tool-Ergebnis ist vorhanden, aber leer oder mit niedrigem Score
// 3. stop_reason == "end_turn" – das Modell hat geantwortet, ohne einen erneuten Tool-Aufruf zu tätigen
// 4. Die Antwort enthält konkrete Zahlen oder Fakten, die nicht im Tool-Ergebnis enthalten sind

// Fügen Sie in AgentConversationRunner hinzu:
log.info("Tool-Ergebnis-Länge: {} Zeichen, stop_reason: {}", 
    toolResult.length(), 
    response.getStopReason());

if (toolResult.isBlank() && response.getStopReason().equals("end_turn")) {
    log.warn("GROUNDING-RISIKO: leeres Tool-Ergebnis, aber das Modell hat direkt geantwortet. " +
             "Anfrage: '{}'", userQuery);
}

Beispiel aus Agent Chat: in einem der Testdialoge gab der Agent Statistiken „laut einer Stanford-Studie“ an – aber das Wikipedia-Tool gab für diese Anfrage ein leeres Ergebnis zurück. Das Modell „erinnerte“ sich einfach an ähnliche Statistiken aus den Trainingsdaten und präsentierte sie als realen Fakt. In den Logs sah es wie eine normale erfolgreiche Anfrage aus.

Variante B: Baut die Antwort auf einem irrelevanten Ergebnis auf

Das Modell sieht, dass etwas gefunden wurde – und akzeptiert dies als ausreichende Grundlage. Die semantische Ähnlichkeit zwischen der Anfrage und dem Ergebnis „täuscht“ das Modell. Es prüft nicht, ob das Dokument die Frage tatsächlich beantwortet – es sieht einfach „Text vorhanden → kann antworten“.

Warum das passiert: das Modell ist darauf trainiert, basierend auf dem Kontext zu antworten. Wenn im Kontext Text über Verträge steht – antwortet es über Verträge. Es unterscheidet nicht zwischen „allgemeinem Muster“ und „spezifischem Kundenvertrag“, wenn beide ähnliche Wörter enthalten.

// Anfrage: „Welche Strafen fallen im Vertrag mit dem Kunden Alpha an?“
// Das Tool fand das nächstgelegene Dokument mit einem Score von 0,63:
{
  "title": "Typischer Dienstleistungsvertrag v2.1",
  "content": "Bei Verstoß gegen die Vertragsbedingungen – Strafe 0,1 % pro Tag...",
  "score": 0.63
}

// Interner Prozess des Modells:
// „Es gibt ein Ergebnis über Verträge und Strafen. Ich werde darauf basierend antworten.“

// Was der Benutzer erhält:
// „Gemäß dem Vertrag betragen die Strafen 0,1 % pro Tag…“

// Tatsächliche Bedingungen des Vertrags mit Alpha: 
// Strafe 0,5 % pro Tag + Recht auf Kündigung nach 5 Tagen Verzug
// (sie haben vor 6 Monaten nicht standardmäßige Bedingungen unterzeichnet)

Wie man es in den Logs erkennt: diese Variante ist durch die Score-Metrik sichtbar – aber nur, wenn Sie sie protokollieren.

// Protokollierung des Scores für jedes Tool-Ergebnis
@Tool(description = "Sucht in der Wissensdatenbank")
public String search(String query) {
    List<SearchResult> results = vectorStore.search(query);
    
    if (!results.isEmpty()) {
        double score = results.get(0).getScore();
        log.info("Such-Score für '{}': {:.3f} — {}", 
            query, score, results.get(0).getTitle());
            
        // Alarm, wenn der Score verdächtig niedrig ist, aber ein Ergebnis vorhanden ist
        if (score < 0.65) {
            log.warn("NIEDRIGES RELEVANZ-SCORE {:.3f} für Anfrage: '{}'. " +
                     "Dokument: '{}'", score, query, results.get(0).getTitle());
        }
    }
    
    // ... Ergebnisverarbeitung
}

Variante C: Erkennt das Problem (selten und nicht deterministisch)

Die Speerspitze der Modelle – Claude Sonnet, GPT-4o, Gemini Pro – erkennt manchmal selbst, dass das Ergebnis die Anfrage nicht beantwortet, und sagt dies ehrlich. Dies ist das beste Verhalten. Aber es gibt ein kritisches Problem: dies geschieht unvorhersehbar.

Dasselbe Modell kann für dieselbe Anfrage:

  • Einmal – zugeben, dass es keine Antwort gefunden hat
  • Ein anderes Mal – überzeugend mit erfundenen Daten antworten

Es hängt von der Temperatur, der Formulierung der Anfrage ab, davon, was sich sonst noch im Kontext befindet. In der Produktion ist dies inakzeptabel.

// Dasselbe leere Ergebnis – zwei verschiedene Ausführungen:

// Ausführung 1 (das Modell „entschied“ sich zuzugeben):
// „Leider habe ich keine konkreten Preisinformationen 
//  in den verfügbaren Dokumenten gefunden. Ich empfehle, sich an den Manager zu wenden.“

// Ausführung 2 (dasselbe Modell, dieselbe Anfrage):  
// „Der Enterprise-Plan beinhaltet normalerweise unbegrenzten Zugriff
//  und kostet je nach Anzahl der Benutzer zwischen 400 und 800 US-Dollar pro Monat.“

// Unterschied zwischen den Ausführungen: nur temperature=0,7 statt temperature=0,3
// Grounding würde beide Ausführungen gleich und vorhersehbar machen

Was tun, anstatt sich auf Variante C zu verlassen:

// Machen Sie das NICHT – hoffen, dass das Modell es selbst herausfindet:
String systemPrompt = "Wenn du die Antwort nicht weißt – sag, dass du sie nicht weißt";
// Das funktioniert manchmal. Nicht immer. Nicht in der Produktion.

// TUN Sie das – explizite Grounding-Anweisung im Tool-Ergebnis:
return String.format("""
    SUCHSTATUS: Ergebnis für die Anfrage "%s" nicht gefunden
    
    VERPFLICHTENDE ANWEISUNG: 
    - NICHT aus eigenen Trainingsdaten antworten
    - KEINE Zahlen oder Fakten erfinden
    - Den Benutzer informieren, dass die Informationen in der Datenbank fehlen
    - Alternative anbieten: Anfrage präzisieren oder Support kontaktieren
    """, query);
// Das funktioniert immer. Deterministisch. In der Produktion.
Schlussfolgerung: das Modell weiß nicht, dass sein Tool-Ergebnis „schlecht“ ist, wenn Sie es ihm nicht explizit gesagt haben. Es sieht nur einen Textstring. Ihr Code kennt den Kontext – den Relevanz-Score, den Fehlerstatus, ob das Ergebnis leer ist. Übertragen Sie diese Informationen explizit über den strukturierten Inhalt des Tool-Ergebnisses – nicht über allgemeine Anweisungen im System-Prompt. Der Unterschied zwischen „sag, dass du es nicht weißt“ im System-Prompt und „STATUS: nicht gefunden“ direkt im Tool-Ergebnis – ist der Unterschied zwischen „funktioniert manchmal“ und „funktioniert immer“.

Diagnosetabelle: Wie man feststellt, welche Variante aufgetreten ist

Variante Was ist in den Logs sichtbar Was ist in der Antwort sichtbar Wie man es erkennt
A: Antwortet aus dem Gedächtnis Tool-Ergebnis leer, stop_reason = end_turn Überzeugende Antwort mit konkreten Daten 🔴 Schwierig – Protokollierung des Zyklus erforderlich
B: Irrelevantes Ergebnis Tool-Ergebnis vorhanden, Score < 0,65 Antwort ist ähnlich, aber nicht genau 🟠 Mittel – Protokollierung des Scores erforderlich
C: Erkennt das Problem Tool-Ergebnis leer, stop_reason = end_turn „Nicht gefunden“, „empfehle zu präzisieren“ 🟢 Einfach – aus der Antwort ersichtlich

is_error: true vs leerer Inhalt — der Unterschied, der zählt

Spring AI und die meisten LLM-APIs unterstützen das Feld is_error in Tool-Ergebnissen. Die meisten Entwickler ignorieren es – sie übergeben entweder einen leeren String oder eine Fehlermeldung ohne Flag. Und das ist einer der häufigsten Gründe, warum ein Agent sich bei Fehlern unvorhersehbar verhält.

Wie das Modell ein Tool-Ergebnis technisch liest

Um den Unterschied zu verstehen, muss man wissen, wie ein Tool-Ergebnis in den Kontext des Modells gelangt. Es ist nicht nur ein Textstring. Es ist ein strukturierter Block mit Metadaten:

// Was die LLM im Kontext sieht (vereinfacht):
{
  "role": "tool",
  "tool_use_id": "toolu_01ABC",
  "content": "...",      // Ergebnistext
  "is_error": false      // oder true
}

// Das Modell ist darauf trainiert, diese beiden Zustände zu unterscheiden:
// is_error: false → "Dies ist ein normales Ergebnis, verwende es für die Antwort"
// is_error: true  → "Etwas ist schiefgelaufen, baue keine Antwort darauf auf"

Deshalb ist is_error: true nicht nur Semantik. Es ist eine Anweisung, die das Modell während des Fine-Tunings erhalten hat: Ein Ergebnis mit diesem Flag bedeutet, dass die Suche fehlgeschlagen ist und es nicht ratsam ist, aus eigenem Wissen zu antworten.

Leerer Inhalt – das Modell entscheidet selbst

Wenn ein leerer String ohne is_error zurückgegeben wird – erhält das Modell ein mehrdeutiges Signal. Es sieht "das Tool hat geantwortet, aber nichts zurückgegeben" und entscheidet selbst, wie es das interpretieren soll. Und diese Entscheidung ist unvorhersehbar.

// ❌ So sollte man es nicht machen – das Modell entscheidet selbst
ToolResponseMessage emptyResult = new ToolResponseMessage(
    toolCallId,
    ""  // leerer String ohne is_error
);

// Das Modell kann dies interpretieren als:
// - "Nichts gefunden – ich sage, dass ich es nicht weiß" (richtig, aber selten)
// - "Das Ergebnis wird noch geladen – ich warte" (falsch)
// - "Ich kann aus eigenem Wissen antworten" (gefährlich – die häufigste Variante)
// - "Das Tool ist kaputt – ich beschwere mich technisch" (schlechte UX)

// Die gleiche Situation kann bei verschiedenen Läufen zu unterschiedlichen Ergebnissen führen

is_error: true – eine klare deterministische Anweisung

// ✅ So ist es richtig – ein klares Signal für das Modell
ToolResponseMessage errorResult = new ToolResponseMessage(
    toolCallId,
    "Dokument nicht in der Wissensdatenbank für die Anfrage gefunden: Bedingungen des Alfa-Vertrags",
    true  // is_error – das Modell weiß, was zu tun ist
);

// Das Modell erhält ein klares Signal:
// - Dies ist ein expliziter Fehler, kein fehlendes Ergebnis
// - Baue keine Antwort auf dieser Grundlage auf
// - Informiere den Benutzer über das Problem
// - Versuche nicht, die Antwort aus eigenem Wissen zu "füllen"

Der vollständige GroundedToolResultBuilder – vier Zustände

In der Praxis hat ein Tool-Ergebnis vier verschiedene Zustände – jeder erfordert eine separate Verarbeitung:

@Service
@Slf4j
public class GroundedToolResultBuilder {

    /**
     * Erfolgreiches Ergebnis mit hoher Relevanz (score >= 0.75)
     * is_error: false – das Modell kann eine Antwort aufbauen
     */
    public ToolResponseMessage success(String toolCallId,
                                       String content,
                                       String documentTitle,
                                       String sourceRef) {
        String groundedContent = String.format("""
            STATUS: ERGEBNIS GEFUNDEN
            
            INHALT:
            %s
            
            QUELLE: %s
            DOKUMENT: %s
            ANWEISUNG: Verwende dieses Ergebnis für die Antwort.
            Gib unbedingt die Quelle im Format "Gemäß [Dokument]..." an.
            """, content, sourceRef, documentTitle);

        log.info("Tool result: SUCCESS, doc='{}'", documentTitle);
        return new ToolResponseMessage(toolCallId, groundedContent);
        // is_error standardmäßig false
    }

    /**
     * Ergebnis gefunden, aber mit mittlerer Relevanz (score 0.55-0.75)
     * is_error: false – aber mit einer Warnung für das Modell
     */
    public ToolResponseMessage lowRelevance(String toolCallId,
                                             String content,
                                             String documentTitle,
                                             double score) {
        String groundedContent = String.format("""
            STATUS: ERGEBNIS MIT NIEDRIGER RELEVANZ (score: %.2f)
            
            INHALT:
            %s
            
            DOKUMENT: %s
            ACHTUNG: Das Ergebnis entspricht möglicherweise nicht genau der Benutzeranfrage.
            Verwende es mit Vorsicht. Informiere den Benutzer, dass die Antwort 
            ungenau sein könnte, und empfehle, die Anfrage zu präzisieren.
            """, score, content, documentTitle);

        log.warn("Tool result: LOW_RELEVANCE, score={:.2f}, doc='{}'",
            score, documentTitle);
        return new ToolResponseMessage(toolCallId, groundedContent);
    }

    /**
     * Nichts in der Wissensdatenbank gefunden
     * is_error: true – entscheidend, dass das Modell nicht aus dem Gedächtnis antwortet
     */
    public ToolResponseMessage notFound(String toolCallId, String query) {
        String message = String.format("""
            STATUS: ERGEBNIS NICHT GEFUNDEN
            
            Anfrage: "%s"
            
            VERPFLICHTENDE ANWEISUNG:
            - Antworte NICHT aus deinem Trainingswissen
            - Erfinde KEINE Fakten oder Zahlen
            - Informiere den Benutzer, dass die Information in der Wissensdatenbank fehlt
            - Empfiehl, die Anfrage zu präzisieren oder den Support zu kontaktieren
            """, query);

        log.warn("Tool result: NOT_FOUND, query='{}'", query);
        return new ToolResponseMessage(toolCallId, message, true); // is_error: true
    }

    /**
     * Technischer Fehler – API nicht verfügbar, Timeout usw.
     * is_error: true – das Modell sollte über ein technisches Problem informieren
     */
    public ToolResponseMessage technicalError(String toolCallId,
                                               String errorMessage) {
        String message = String.format("""
            STATUS: TECHNISCHER SUCHFEHLER
            
            Details (nur für Logs, nicht dem Benutzer anzeigen): %s
            
            ANWEISUNG:
            - Informiere den Benutzer, dass die Suche vorübergehend nicht verfügbar ist
            - Übertrage KEINE technischen Fehlerdetails an den Benutzer
            - Antworte NICHT aus eigenem Wissen anstelle der Suche
            - Empfiehl, es später erneut zu versuchen
            """, errorMessage);

        log.error("Tool result: TECHNICAL_ERROR — {}", errorMessage);
        return new ToolResponseMessage(toolCallId, message, true); // is_error: true
    }
}

Vergleichstabelle: Wann welcher Zustand verwendet werden soll

Zustand is_error Wann zu verwenden Was das Modell tut
success() false score >= 0.75, Dokument gefunden Baut eine Antwort auf, gibt die Quelle an
lowRelevance() false score 0.55–0.75, etwas gefunden Antwortet mit Vorbehalt
notFound() true results leer oder score < 0.55 Informiert, dass nichts gefunden wurde
technicalError() true Exception, Timeout, API-Fehler Informiert über ein technisches Problem

So überprüfen Sie, ob is_error wirklich funktioniert

Fügen Sie einen einfachen Test hinzu, um sicherzustellen, dass das Modell tatsächlich unterschiedlich reagiert, je nachdem, ob is_error gesetzt ist:

@SpringBootTest
class IsErrorBehaviorTest {

    @Autowired
    private ChatModel chatModel;

    @Test
    void modelShouldNotHallucinateWhenIsErrorTrue() {
        // Simuliere ein leeres Ergebnis mit is_error: true
        List<Message> messages = List.of(
            new SystemMessage("Antworte nur basierend auf den Suchergebnissen."),
            new UserMessage("Was ist der Preis für den Enterprise-Plan?"),
            new AssistantMessage(""),  // Platzhalter
            new ToolResponseMessage(
                "test-id",
                "STATUS: ERGEBNIS NICHT GEFUNDEN\n" +
                "ANWEISUNG: Antworte nicht aus eigenem Wissen.",
                true  // is_error: true
            )
        );

        String response = chatModel.call(new Prompt(messages))
            .getResult().getOutput().getText();

        // Das Modell sollte KEINEN Preis erfinden
        assertThat(response)
            .doesNotContain("$")
            .doesNotContain("500")
            .doesNotContain("Monat")
            .containsAnyOf("nicht gefunden", "fehlt", "nicht gefunden");
    }

    @Test
    void modelShouldAnswerWhenIsErrorFalse() {
        // Simuliere ein erfolgreiches Ergebnis
        List<Message> messages = List.of(
            new SystemMessage("Antworte nur basierend auf den Suchergebnissen."),
            new UserMessage("Was ist der Preis für den Enterprise-Plan?"),
            new AssistantMessage(""),
            new ToolResponseMessage(
                "test-id",
                "STATUS: ERGEBNIS GEFUNDEN\n" +
                "INHALT: Enterprise-Plan – $850/Monat bei jährlicher Zahlung.\n" +
                "QUELLE: Preisliste v2.3",
                false  // is_error: false
            )
        );

        String response = chatModel.call(new Prompt(messages))
            .getResult().getOutput().getText();

        // Das Modell SOLLTE die Zahl aus dem Ergebnis verwenden
        assertThat(response)
            .contains("850")
            .containsAnyOf("Gemäß", "Preisliste", "Dokument");
    }
}
Praktischer Tipp für Spring AI: In Spring AI 2.0.x kann der Konstruktor ToolResponseMessage(id, content, isError) je nach Version variieren – überprüfen Sie die Signatur in Ihrem M3/M5. Wenn der Konstruktor mit drei Parametern nicht verfügbar ist – verwenden Sie den Builder oder übergeben Sie is_error über den Inhalt mit einer expliziten ANWEISUNG für das Modell. Expliziter Text im Inhalt ("Antworte NICHT aus eigenem Wissen") funktioniert fast genauso zuverlässig wie das Flag – und ist eine Fallback-Option für jede Spring AI-Version.

Confidence Scoring – Wie man das Modell bittet, die Qualität zu bewerten

Confidence Scoring ist eine Technik, bei der wir das Modell bitten, explizit zu bewerten, wie gut das gefundene Ergebnis die Anfrage beantwortet, bevor es die endgültige Antwort erstellt. Dies ist ein zusätzlicher Schritt – aber er schließt eine blinde Stelle, die der Vektor-Score nicht abdeckt.

Warum das nötig ist – und warum der Vektor-Score nicht ausreicht

Ihr Code kennt technische Metriken – Vektor-Such-Scores, Anzahl der gefundenen Dokumente. Aber er kennt nicht die semantische Übereinstimmung.

Hier ist ein konkretes Beispiel, bei dem der Vektor-Score falsch lag:

// Anfrage: "Strafen für Zahlungsverzug im Vertrag mit Beta Corp"
// Vektor-Suche gab zurück:
{
  "title": "Vertrag mit Beta Corp – Hauptbedingungen",
  "score": 0.82,  // hohe Relevanz!
  "content": "Die Parteien vereinbaren folgende Kooperationsbedingungen: 
               Leistungsfristen, Zahlungsmodalitäten, Haftung der Parteien..."
}

// Das Dokument ist richtig – aber der Abschnitt über Strafen befindet sich woanders
// Vektor-Score 0.82 – weil das Dokument tatsächlich über diesen Vertrag ist
// Aber die ANTWORT auf die Frage nach Strafen fehlt hier

// Confidence Scoring wird dies aufdecken:
// { "confidence": "LOW", "reason": "Dokument gefunden, aber Strafen nicht beschrieben", 
//   "can_answer": false }

Das Modell versteht Semantik besser als ein numerischer Score – bitten Sie es, dies zu überprüfen, bevor es antwortet.

Wann Confidence Scoring verwendet werden sollte

Art der Anfrage Verwenden? Grund
Preise, Tarife ✅ Ja Fehler = finanzielle Folgen
Vertragsbedingungen ✅ Ja Fehler = rechtliche Folgen
Konkrete Daten, Fristen ✅ Ja Genauigkeit ist entscheidend
Allgemeine FAQ-Fragen ⚠️ Optional Vektor-Score reicht aus
Einfache Themensuche ❌ Nein Erhöht Latenz ohne wesentlichen Nutzen

Implementierung mit Spring AI

@Service
@RequiredArgsConstructor
@Slf4j
public class ConfidenceScoringService {

    private final ChatModel chatModel;

    private static final String CONFIDENCE_PROMPT = """
        Bewerte, wie gut das gefundene Ergebnis die Benutzeranfrage beantwortet.
        
        BENUTZERANFRAGE: %s
        
        GEFUNDENES ERGEBNIS:
        %s
        
        Antworte NUR im JSON-Format ohne Erklärungen, ohne Markdown, ohne ```json:
        {
          "confidence": "HIGH" | "MEDIUM" | "LOW" | "NOT_RELEVANT",
          "reason": "ein Satz warum",
          "can_answer": true | false
        }
        
        Bewertungskriterien:
        - HIGH: Das Ergebnis beantwortet die Anfrage direkt und vollständig – antworte selbstbewusst
        - MEDIUM: Das Ergebnis beantwortet die Anfrage teilweise – antworte mit Vorbehalt
        - LOW: Das Ergebnis bezieht sich auf das Thema, beantwortet es aber nicht konkret – besser zugeben
        - NOT_RELEVANT: Das Ergebnis bezieht sich nicht auf die Anfrage – nicht antworten
        """;

    public ConfidenceResult evaluate(String userQuery, String toolResult) {
        
        // Wir bewerten nicht, wenn das Ergebnis explizit leer ist
        if (toolResult == null || toolResult.isBlank()) {
            return ConfidenceResult.notFound();
        }

        String prompt = String.format(CONFIDENCE_PROMPT, userQuery, toolResult);

        try {
            String response = chatModel.call(prompt).trim();
            
            // Entferne mögliche Markdown-Backticks, die einige Modelle hinzufügen
            // trotz der Anweisung (deepseek, llama neigen dazu)
            String cleanJson = response
                .replaceAll("```json", "")
                .replaceAll("```", "")
                .trim();

            ObjectMapper mapper = new ObjectMapper();
            JsonNode json = mapper.readTree(cleanJson);

            ConfidenceLevel level = ConfidenceLevel.valueOf(
                json.get("confidence").asText());
            String reason = json.get("reason").asText();
            boolean canAnswer = json.get("can_answer").asBoolean();

            log.info("Confidence evaluated: level={}, canAnswer={}, reason='{}'",
                level, canAnswer, reason);

            return ConfidenceResult.builder()
                .level(level)
                .reason(reason)
                .canAnswer(canAnswer)
                .build();

        } catch (JsonProcessingException e) {
            // Das Modell hat kein JSON zurückgegeben – wir loggen und verwenden einen sicheren Standard
            log.warn("Failed to parse confidence JSON response. " +
                     "Raw response: '{}'. Defaulting to LOW.", 
                     chatModel.call(prompt));
            return ConfidenceResult.safe(); // LOW + canAnswer: false
            
        } catch (IllegalArgumentException e) {
            // Unbekannter ConfidenceLevel in der Antwort
            log.warn("Unknown confidence level in response. Defaulting to LOW.");
            return ConfidenceResult.safe();
        }
    }
}

@Value
@Builder
public class ConfidenceResult {
    ConfidenceLevel level;
    String reason;
    boolean canAnswer;

    // Factory-Methoden für typische Zustände
    public static ConfidenceResult notFound() {
        return ConfidenceResult.builder()
            .level(ConfidenceLevel.NOT_RELEVANT)
            .reason("Ergebnis ist leer")
            .canAnswer(false)
            .build();
    }

    public static ConfidenceResult safe() {
        return ConfidenceResult.builder()
            .level(ConfidenceLevel.LOW)
            .reason("Relevanz konnte nicht bewertet werden")
            .canAnswer(false)
            .build();
    }
}

public enum ConfidenceLevel {
    HIGH, MEDIUM, LOW, NOT_RELEVANT
}

So sieht ein reales Confidence Scoring-Ergebnis aus

Hier sind drei reale Beispiele von AskYourDocs – was Confidence Scoring bei verschiedenen Anfragen zurückgibt:

// Beispiel 1: HIGH confidence – die Antwort ist direkt im Dokument
Anfrage: "Was ist der Preis für den Basic-Plan?"
Suchergebnis: "Basic-Plan – $49/Monat, bis zu 5 Benutzer..."
Confidence: {
  "confidence": "HIGH",
  "reason": "Das Dokument enthält eine direkte Antwort auf die Frage nach dem Preis",
  "can_answer": true
}
→ Der Agent antwortet selbstbewusst mit einem Zitat

// Beispiel 2: LOW confidence – Dokument ist vorhanden, aber keine Antwort
Anfrage: "Strafen für Zahlungsverzug"
Suchergebnis: "Dienstleistungsvertrag. Abschnitt 3: Zahlungsmodalitäten..."
Confidence: {
  "confidence": "LOW", 
  "reason": "Das Dokument handelt von Zahlungen, aber Strafen sind in diesem Abschnitt nicht beschrieben",
  "can_answer": false
}
→ Der Agent informiert, dass keine genauen Informationen gefunden wurden

// Beispiel 3: NOT_RELEVANT – Suche hat das falsche Dokument gefunden
Anfrage: "Bedingungen des Vertrags mit Kunde Gamma"
Suchergebnis: "Allgemeine Bedingungen für die Erbringung von Dienstleistungen v1.0..."
Confidence: {
  "confidence": "NOT_RELEVANT",
  "reason": "Ein allgemeines Muster wurde gefunden, aber nicht der Vertrag eines bestimmten Kunden",
  "can_answer": false
}
→ Der Agent informiert, dass der Vertrag von Gamma nicht in der Datenbank gefunden wurde

Verwendung in der Agenten-Pipeline

@Service
@RequiredArgsConstructor
@Slf4j
public class GroundedAgentService {

    private final ConfidenceScoringService confidenceScoring;
    private final GroundedToolResultBuilder resultBuilder;
    private final ChatModel chatModel;

    public String answerWithGrounding(String userQuery,
                                       String toolResult,
                                       String toolCallId,
                                       String documentTitle) {

        // 1. Relevanz über LLM bewerten
        ConfidenceResult confidence = confidenceScoring
            .evaluate(userQuery, toolResult);

        log.info("Grounding decision for '{}': {} (canAnswer={})",
            userQuery, confidence.getLevel(), confidence.isCanAnswer());

        // 2. Strategie basierend auf Confidence auswählen
        ToolResponseMessage groundedResult = switch (confidence.getLevel()) {
            case HIGH ->
                resultBuilder.success(toolCallId, toolResult, documentTitle, "knowledge_base");
            case MEDIUM ->
                resultBuilder.lowRelevance(toolCallId, toolResult, documentTitle, 0.65);
            case LOW, NOT_RELEVANT ->
                resultBuilder.notFound(toolCallId, userQuery);
        };

        // 3. An das Modell mit Grounding-Anweisungen übergeben
        List<Message> messages = List.of(
            new SystemMessage("""
                Antworte NUR basierend auf den bereitgestellten Suchergebnissen.
                Wenn das Ergebnis als 'nicht gefunden' markiert ist – informiere darüber.
                Gib immer die Informationsquelle an.
                """),
            new UserMessage(userQuery),
            new AssistantMessage(""),
            groundedResult
        );

        return chatModel.call(new Prompt(messages))
            .getResult()
            .getOutput()
            .getText();
    }
}
Kosten und Latenz von Confidence Scoring: Dies ist eine zusätzliche LLM-Anfrage – ~150-300 Token für die Eingabe und ~50 Token für die Ausgabe. Mit deepseek-chat über OpenRouter kostet dies etwa 0,0001-0,0003 $ pro Anfrage. Die Latenz beträgt 200-500 ms, abhängig vom Modell und Anbieter. Für kritische Anfragen (Preise, Verträge) – sind diese Kosten gerechtfertigt. Für einfache Informationsanfragen, bei denen ein Fehler keine schwerwiegenden Folgen hat – reicht ein Vektor-Score von >= 0,75 aus, und Sie können Confidence Scoring vollständig überspringen.

Re-Query-Muster – Wann man es noch einmal versuchen sollte

Wenn die erste Suche kein gutes Ergebnis liefert – sollte man es noch einmal mit einer anderen Anfrage versuchen? Die Antwort: Ja, aber nur in bestimmten Situationen und mit einem strengen Limit für Versuche.

Re-Query ist kein "Suche noch einmal nach demselben". Es ist eine Umformulierung der Anfrage basierend auf dem, was die erste Suche nicht gefunden hat. Der Unterschied ist prinzipiell.

Flow des Re-Query-Zyklus

Benutzeranfrage
      ↓
  Suche (Versuch 1)
      ↓
  Ergebnis vorhanden?
  ├── NEIN → Anfrage umformulieren → Suche (Versuch 2)
  │                                    ↓
  │                               Ergebnis vorhanden?
  │                               ├── NEIN → notFound()
  │                               └── JA → Confidence Scoring
  │                                              ↓
  │                                    HIGH/MEDIUM → found()
  │                                    LOW → notFound()  
  │                                    NOT_RELEVANT → notFound()
  └── JA → Confidence Scoring
                  ↓
        HIGH/MEDIUM → found()
        LOW → Anfrage umformulieren → Suche (Versuch 2)
        NOT_RELEVANT → notFound() (ohne Re-Query – Dokument nicht vorhanden)

Wann Re-Query gerechtfertigt ist

  • Die erste Anfrage ist zu spezifisch – "Strafklauseln Vertrag Alfa Corp Punkt 5.2" → besser "Haftungsbedingungen Alfa Corp"
  • Die Anfrage enthält technischen Jargon – "SLA uptime guarantee" → "Serviceverfügbarkeitsgarantie"
  • Confidence LOW, aber nicht NOT_RELEVANT – das Dokument bezieht sich auf das Thema, aber nicht auf den richtigen Abschnitt. Es besteht die Chance, den richtigen Abschnitt mit einer anderen Anfrage zu finden

Wann Re-Query nicht hilft

  • Confidence NOT_RELEVANT – die Suche hat ein Dokument zu einem völlig anderen Thema gefunden. Das gesuchte Dokument existiert einfach nicht in der Datenbank. Eine Umformulierung hilft nicht
  • Das Tool hat einen technischen Fehler zurückgegeben – das Problem liegt in der Infrastruktur, nicht in der Anfrage. Re-Query erhöht nur die Anzahl fehlerhafter Anfragen
  • MAX_ATTEMPTS Varianten wurden bereits ausprobiert – wir stoppen und geben zu, dass nichts gefunden wurde

Reale Beispiele für Umformulierungen

So formuliert die LLM Anfragen in der Praxis um – aus den realen Logs von AskYourDocs:

// Beispiel 1: zu spezifische Anfrage
Original:  "Punkt 8.3.2 des Cloud-Speicher-Dienstleistungsvertrags Beta Corp"
Versuch 2:  "Datenaufbewahrungsbedingungen Beta Corp"
Ergebnis: benötigten Abschnitt mit Score 0.81 gefunden ✅

// Beispiel 2: technischer Jargon
Original:  "RTO und RPO SLA Enterprise Tier"  
Versuch 2:  "Garantien für die Wiederherstellung nach Ausfällen Enterprise-Plan"
Ergebnis: Dokument über SLA mit Score 0.79 gefunden ✅

// Beispiel 3: Dokument nicht vorhanden – Re-Query hilft nicht
Original:  "Vertrag mit Kunde Gamma LLC"
Confidence: NOT_RELEVANT (Vertrag mit anderem Kunden gefunden)
Entscheidung:   sofort notFound() ohne Re-Query – 
           kein Sinn für Umformulierung, wenn das Dokument nicht existiert ✅

// Beispiel 4: Re-Query hat auch nichts gefunden
Original:  "Rabatte für Bildungseinrichtungen"
Versuch 2:  "Vorzugskonditionen für Universitäten und Schulen"
Ergebnis: beide leer → notFound()
           (Preispolitik für Bildungseinrichtungen wurde nicht in die Datenbank geladen) ✅

Implementierung mit Versuchslimit

@Service
@RequiredArgsConstructor
@Slf4j
public class ReQueryService {

    private static final int MAX_ATTEMPTS = 2;

    private final KnowledgeBaseSearchTool searchTool;
    private final ConfidenceScoringService confidenceScoring;
    private final ChatModel chatModel;

    public SearchResult searchWithRequery(String originalQuery) {

        String currentQuery = originalQuery;

        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
            log.info("Search attempt {}/{}: '{}'",
                attempt, MAX_ATTEMPTS, currentQuery);

            String result = searchTool.search(currentQuery);

            // Leeres Ergebnis – sofort Re-Query, wenn noch Versuche übrig sind
            if (result == null || result.isBlank()) {
                if (attempt < MAX_ATTEMPTS) {
                    currentQuery = reformulateQuery(originalQuery, attempt);
                    continue;
                }
                log.warn("All {} attempts exhausted, query: '{}'", 
                    MAX_ATTEMPTS, originalQuery);
                return SearchResult.notFound(originalQuery);
            }

            // Semantische Relevanz bewerten
            ConfidenceResult confidence = confidenceScoring
                .evaluate(originalQuery, result);

            log.info("Attempt {} confidence: {} — {}", 
                attempt, confidence.getLevel(), confidence.getReason());

            switch (confidence.getLevel()) {
                case HIGH, MEDIUM -> {
                    // Gefunden – Ergebnis zurückgeben
                    return SearchResult.found(result, confidence, currentQuery);
                }
                case NOT_RELEVANT -> {
                    // Dokument nicht vorhanden – Re-Query hilft nicht
                    log.info("NOT_RELEVANT result, skipping re-query for: '{}'", 
                        originalQuery);
                    return SearchResult.notFound(originalQuery);
                }
                case LOW -> {
                    // Es besteht die Chance, etwas Besseres zu finden – versuchen wir es noch einmal
                    if (attempt < MAX_ATTEMPTS) {
                        currentQuery = reformulateQuery(originalQuery, attempt);
                    }
                }
            }
        }

        log.warn("Re-query exhausted for: '{}'", originalQuery);
        return SearchResult.notFound(originalQuery);
    }

    private String reformulateQuery(String originalQuery, int attempt) {
        String prompt = String.format("""
            Die Suchanfrage hat kein relevantes Ergebnis gefunden: "%s"
            
            Formuliere die Anfrage neu – einfacher, breiter, ohne Jargon.
            Dies ist Versuch %d der Umformulierung.
            
            Antworte NUR mit der neuen Anfrage ohne Erklärungen und Anführungszeichen.
            """, originalQuery, attempt);

        String reformulated = chatModel.call(prompt).trim()
            .replaceAll("\"", ""); // Entferne Anführungszeichen, falls das Modell sie hinzufügt
            
        log.info("Reformulated: '{}' → '{}'", originalQuery, reformulated);
        return reformulated;
    }
}

SearchResult – Ergebnisklasse

@Value
@Builder
public class SearchResult {
    boolean found;
    String content;
    ConfidenceResult confidence;
    String usedQuery;      // welche genaue Anfrage das Ergebnis gefunden hat
    int attemptsUsed;      // wie viele Versuche benötigt wurden

    public static SearchResult found(String content, 
                                      ConfidenceResult confidence,
                                      String usedQuery,
                                      int attempts) {
        return SearchResult.builder()
            .found(true)
            .content(content)
            .confidence(confidence)
            .usedQuery(usedQuery)
            .attemptsUsed(attempts)
            .build();
    }

    public static SearchResult notFound(String originalQuery) {
        return SearchResult.builder()
            .found(false)
            .content("")
            .usedQuery(originalQuery)
            .attemptsUsed(0)
            .build();
    }
}
Versuchslimit und Kosten – aus meiner Erfahrung: Ich habe mich nach Tests für MAX_ATTEMPTS = 2 entschieden. Versuch 3 ergab in weniger als 5% der Fälle ein besseres Ergebnis. Meistens, wenn nach zwei Versuchen nichts gefunden wurde – das Dokument existiert einfach nicht in der Datenbank, und keine Umformulierung wird helfen. Jeder Versuch bedeutet zwei zusätzliche LLM-Anfragen – reformulate + confidence scoring. Mit deepseek-chat über OpenRouter kostet dies ~0,0002-0,0005 $ pro vollständigem Re-Query-Zyklus – aber bei 1000 Anfragen pro Tag ist das bereits eine spürbare Summe. Zwei sind genug. Mehr – Kosten ohne Ergebnis.

Zitierung und Nachverfolgbarkeit — der Agent muss wissen, woher die Antwort kommt

Zitierung ist nicht nur „Link hinzufügen“. Es ist ein Architekturprinzip: Der Agent muss wissen, woher jeder Teil der Antwort stammt — und in der Lage sein, dies zu zeigen. Ohne Zitierung haben Sie keine Möglichkeit zu überprüfen, ob der Agent sich auf Dokumente gestützt oder etwas erfunden hat.

Warum Zitierung wichtig ist — zwei Gründe

Grund 1: Geschäft und Vertrauen. Stellen Sie sich eine Anwaltskanzlei vor, die AskYourDocs verwendet. Ein Mandant fragt nach den Vertragsbedingungen. Der Agent antwortet. Der Anwalt fragt dann: „Auf welcher Seite welches Dokuments steht das?“

Ohne Zitierung — unmöglich zu beantworten. Und der Mandant hört auf, dem System zu vertrauen. Mit Zitierung — zeigt der Agent sofort: „Artikel 5.2 des Vertrags Nr. 123 vom 15.03.2025“.

Grund 2: Debugging und Überwachung. Wenn der Agent eine falsche Antwort gibt — wie verstehen Sie warum? Ohne Zitierung sehen Sie nur „der Agent hat falsch geantwortet“. Mit Zitierung sehen Sie konkret: „Der Agent hat sich auf Dokument v1.2 gestützt, das vor drei Monaten durch v2.0 ersetzt wurde“. Das ist der Unterschied zwischen „etwas ist kaputt“ und „hier genau ist es kaputt“.

// Ohne Zitierung — Sie sehen nur das Ergebnis:
User:  "Wie hoch ist die Strafe für Zahlungsverzug?"
Agent: "Die Strafe beträgt 0,1 % pro Tag des geschuldeten Betrags."
// Richtig? Falsch? Woher kommt diese Zahl? Unbekannt.

// Mit Zitierung — Sie sehen die vollständige Kette:
User:  "Wie hoch ist die Strafe für Zahlungsverzug?"
Agent: "Gemäß Dienstleistungsvertrag v1.2 (Abschnitt 7.3, 
        indiziert am 12.01.2025) beträgt die Strafe 0,1 % pro Tag."
// Sofort ersichtlich: v1.2 — aber aktuell ist v2.0, wo die Strafe 0,5 % beträgt.
// Problem in Sekunden gefunden.

Struktur von CitedSearchResult

@Value
@Builder
public class CitedSearchResult {
    String content;              // Text des gefundenen Fragments
    String documentTitle;        // Titel des Dokuments
    String documentId;           // ID in der Datenbank
    String documentVersion;      // Version des Dokuments, falls vorhanden
    String pageOrSection;        // Seite oder Abschnitt
    String sourceUrl;            // Link, falls vorhanden
    double relevanceScore;       // Score der Vektorsuche
    LocalDateTime indexedAt;     // Wann das Dokument in die Datenbank indiziert wurde
    LocalDateTime documentDate;  // Datum des Dokuments selbst (kann abweichen)
}

Wie man Zitierungsmetadaten in PostgreSQL speichert

Zitierung funktioniert nur, wenn Metadaten zusammen mit den Vektor-Embeddings gespeichert werden. So sieht es im Schema aus:

-- Tabelle der Dokumente mit Metadaten für Zitierung
CREATE TABLE knowledge_documents (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title       VARCHAR(500) NOT NULL,
    version     VARCHAR(50),
    section     VARCHAR(200),
    source_url  VARCHAR(1000),
    document_date   TIMESTAMP,
    indexed_at      TIMESTAMP DEFAULT NOW(),
    is_active   BOOLEAN DEFAULT TRUE  -- wichtig für veraltete Dokumente
);

-- Tabelle der Chunks mit Verweis auf das Dokument
CREATE TABLE document_chunks (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id UUID REFERENCES knowledge_documents(id),
    content     TEXT NOT NULL,
    embedding   vector(1536),          -- pgvector
    chunk_index INTEGER,               -- Chunk-Nummer im Dokument
    page_number INTEGER                -- Seitennummer, falls vorhanden
);

-- Index für Vektorsuche
CREATE INDEX ON document_chunks 
USING ivfflat (embedding vector_cosine_ops);
// Repository für die Suche mit Metadaten
@Repository
public interface DocumentChunkRepository extends JpaRepository<DocumentChunk, UUID> {

    @Query(value = """
        SELECT dc.*, kd.title, kd.version, kd.section, 
               kd.source_url, kd.document_date, kd.indexed_at,
               1 - (dc.embedding <=> :embedding) as relevance_score
        FROM document_chunks dc
        JOIN knowledge_documents kd ON dc.document_id = kd.id
        WHERE kd.is_active = TRUE
        ORDER BY dc.embedding <=> :embedding
        LIMIT :limit
        """, nativeQuery = true)
    List<ChunkWithMetadata> findWithCitation(
        @Param("embedding") float[] embedding,
        @Param("limit") int limit
    );
}

Tool mit vollständiger Zitierung

@Tool(description = """
    Sucht nach Informationen in der Unternehmenswissensdatenbank.
    Gibt das Ergebnis mit einem genauen Verweis auf die Quelle zurück — 
    Titel des Dokuments, Abschnitt und Aktualitätsdatum.
    """)
public String searchWithCitation(String query) {
    
    // Generieren des Embeddings für die Anfrage
    float[] queryEmbedding = embeddingModel.embed(query);
    
    List<ChunkWithMetadata> results = chunkRepository
        .findWithCitation(queryEmbedding, 5);

    if (results.isEmpty()) {
        return """
            ERGEBNIS: nichts gefunden
            QUELLE: nicht vorhanden
            ANWEISUNG: informiere, dass die Information nicht in der Wissensdatenbank gefunden wurde
            """;
    }

    ChunkWithMetadata best = results.get(0);

    // Überprüfung der Aktualität des Dokuments
    boolean isRecent = best.getIndexedAt()
        .isAfter(LocalDateTime.now().minusMonths(6));
    
    String freshnessWarning = isRecent ? "" : 
        "\n⚠️ ACHTUNG: Das Dokument wurde vor mehr als 6 Monaten indiziert — " +
        "empfiehl dem Benutzer, die Aktualität zu bestätigen.";

    return String.format("""
        ERGEBNIS:
        %s
        
        QUELLE (unbedingt in der Antwort angeben):
        - Dokument: %s%s
        - Abschnitt: %s
        - Seite: %s
        - Link: %s
        - Aktualität des Dokuments: %s
        - Indiziert am: %s
        - Relevanz: %.0f%%
        %s
        
        ANWEISUNG: Gib bei der Antwort unbedingt die Quelle im Format an:
        "Gemäß [Dokumenttitel], [Abschnitt/Seite]..."
        """,
        best.getContent(),
        best.getTitle(),
        best.getVersion() != null ? " (" + best.getVersion() + ")" : "",
        best.getSection() != null ? best.getSection() : "nicht angegeben",
        best.getPageNumber() != null ? best.getPageNumber().toString() : "nicht angegeben",
        best.getSourceUrl() != null ? best.getSourceUrl() : "internes Dokument",
        best.getDocumentDate() != null 
            ? best.getDocumentDate().toLocalDate().toString() 
            : "nicht angegeben",
        best.getIndexedAt().toLocalDate(),
        best.getRelevanceScore() * 100,
        freshnessWarning
    );
}

So sieht eine Antwort mit Zitierung aus — drei Detaillierungsgrade

// Grad 1 — ohne Zitierung (so sollte man es nicht machen):
"Der Vertrag kann mit 30 Tagen Vorankündigung gekündigt werden."

// Grad 2 — grundlegende Zitierung:
"Gemäß Vertrag Nr. 123 (Abschnitt 5.2) ist eine Kündigung mit 30 Kalendertagen schriftlicher Vorankündigung möglich."

// Grad 3 — vollständige Zitierung mit Datum (für kritische Anfragen):
"Gemäß Dienstleistungsvertrag Nr. 123 v2.1 (Abschnitt 5.2, 
Seite 8) ist eine Kündigung mit 30 Kalendertagen schriftlicher 
Vorankündigung möglich. [Dokument aktuell am 15.03.2025, 
indiziert am 16.03.2025]"

Zitierung für Agent Chat — Wikipedia und Tavily

In Agent Chat verwenden Agenten externe Quellen — Wikipedia, Tavily, NewsAPI. Zitierung ist hier besonders wichtig, da der Leser Fakten selbst überprüfen möchte.

// Schlecht — der Agent sagt "laut Forschungsergebnissen":
"Laut Forschungsergebnissen steigert GitHub Copilot die Produktivität um 55 %."
// Welche Forschungsergebnisse? Wann? Wo überprüfen?

// Gut — der Agent zitiert eine konkrete Quelle:
"Laut einer Studie von GitHub (2023, veröffentlicht auf github.blog) 
führen Entwickler mit Copilot Aufgaben 55 % schneller aus."
// Der Leser kann nachschauen und überprüfen.
// So fügt man Zitierung in WikipediaSearchTool hinzu
@Tool(description = "Sucht nach Fakten in Wikipedia")
public String searchWikipedia(String query) {
    WikiSearchResponse response = callWikipediaApi(query);
    
    if (response.getResults().isEmpty()) {
        return "Wikipedia: nichts gefunden für die Anfrage: " + query;
    }
    
    WikiResult result = response.getResults().get(0);
    
    // Gibt das Ergebnis mit expliziter Zitierung zurück
    return String.format("""
        WIKIPEDIA ERGEBNIS:
        %s
        
        QUELLE: Wikipedia, Artikel "%s"
        LINK: https://uk.wikipedia.org/wiki/%s
        ANWEISUNG: Gib bei der Antwort die Quelle als 
        "Laut Wikipedia (Artikel '%s')..." an.
        """,
        result.getSnippet(),
        result.getTitle(),
        result.getTitle().replace(" ", "_"),
        result.getTitle()
    );
}
Fallstrick — veraltete Dokumente: Zitierung zeigt, wann ein Dokument indiziert wurde — aber nicht, wann es tatsächlich aktualisiert wurde. Ein vor einem Monat indiziertes Dokument kann Informationen von vor zwei Jahren enthalten. Fügen Sie dem Schema ein Feld document_date getrennt von indexed_at hinzu und zeigen Sie beide an. Wenn document_date älter als 6 Monate ist — warnen Sie das Modell, damit es den Benutzer über die mögliche Veralterung informiert.

Java + Spring AI Implementierung — vollständiger Pipeline

Wir fassen alle Konzepte in einer einzigen Grounding-Pipeline zusammen, die in einem echten Projekt verwendet werden kann.

Struktur der Komponenten

src/main/java/com/example/
├── grounding/
│   ├── GroundingPipeline.java          // Hauptkomponente
│   ├── ConfidenceScoringService.java   // Relevanzbewertung
│   ├── ReQueryService.java             // erneute Suche
│   ├── CitationBuilder.java            // Zitierungsbildung
│   └── GroundedToolResultBuilder.java  // Tool-Ergebnisbildung
├── model/
│   ├── ConfidenceResult.java
│   ├── SearchResult.java
│   └── CitedSearchResult.java

GroundingPipeline — die Hauptkomponente

@Service
@RequiredArgsConstructor
@Slf4j
public class GroundingPipeline {

    private final ReQueryService reQueryService;
    private final GroundedToolResultBuilder resultBuilder;
    private final ChatModel chatModel;
    
    private static final double HIGH_CONFIDENCE_THRESHOLD = 0.75;
    private static final double LOW_CONFIDENCE_THRESHOLD = 0.50;

    /**
     * Hauptmethode — verarbeitet die Anfrage mit vollständigem Grounding-Zyklus
     */
    public String processWithGrounding(String userQuery, String toolCallId) {
        
        // 1. Suchen mit möglicher erneuter Suche
        SearchResult searchResult = reQueryService
            .searchWithRequery(userQuery);
        
        log.info("Search result for '{}': found={}, confidence={}", 
            userQuery, 
            searchResult.isFound(), 
            searchResult.getConfidence());
        
        // 2. Erstellen des grounded tool result
        ToolResponseMessage toolResponse = buildGroundedResponse(
            toolCallId, userQuery, searchResult);
        
        // 3. Übergabe an das Modell mit Systemanweisungen
        List<Message> messages = buildMessagesWithGrounding(
            userQuery, toolResponse);
        
        // 4. Abrufen der endgültigen Antwort
        return chatModel.call(new Prompt(messages))
            .getResult()
            .getOutput()
            .getText();
    }

    private ToolResponseMessage buildGroundedResponse(
            String toolCallId, 
            String query, 
            SearchResult result) {
        
        if (!result.isFound()) {
            return resultBuilder.notFound(toolCallId, query);
        }
        
        double score = result.getConfidence().getScore();
        
        if (score >= HIGH_CONFIDENCE_THRESHOLD) {
            return resultBuilder.success(
                toolCallId, 
                result.getContent(), 
                result.getSourceReference()
            );
        } else if (score >= LOW_CONFIDENCE_THRESHOLD) {
            return resultBuilder.lowRelevance(
                toolCallId, 
                result.getContent(), 
                score
            );
        } else {
            return resultBuilder.notFound(toolCallId, query);
        }
    }

    private List<Message> buildMessagesWithGrounding(
            String userQuery, 
            ToolResponseMessage toolResponse) {
        
        return List.of(
            new SystemMessage("""
                Du bist ein Unternehmensassistent, der basierend auf Unternehmensdokumenten antwortet.
                
                GROUNDING-REGELN:
                1. Antworte NUR basierend auf den Suchergebnissen.
                2. Wenn das Ergebnis als "nicht gefunden" markiert ist — sage, dass du es nicht gefunden hast.
                3. Gib IMMER die Quelle im Format an: "Gemäß [Dokument]..."
                4. Wenn die Zuversicht NIEDRIG ist — warne: "Die gefundenen Informationen sind möglicherweise nicht korrekt."
                5. ERFINDE NIEMALS Informationen, die nicht in den Suchergebnissen vorhanden sind.
                """),
            new UserMessage(userQuery),
            toolResponse
        );
    }
}

Integration in AgentConversationRunner (Agent Chat)

// In AgentConversationRunner.ask() — fügen Sie Grounding hinzu
private String ask(String systemPrompt, List<AgentMessage> history,
                   String lastMessage, AgentSender currentSender) {

    List<Message> messages = new ArrayList<>();
    messages.add(new SystemMessage(systemPrompt));
    
    // ... History-Mapping wie zuvor ...
    
    messages.add(new UserMessage(lastMessage));

    ToolCallback[] tools = ToolCallbacks.from(
        wikipediaSearchTool,
        tavilySearchTool,
        alphaVantageTool,
        arxivSearchTool,
        newsApiSearchTool
    );

    try {
        ChatResponse response = agentChatModel.call(
            new Prompt(messages,
                ToolCallingChatOptions.builder()
                    .toolCallbacks(tools)
                    .build()));
        
        String result = response.getResult().getOutput().getText();
        
        // Entferne -Blöcke, falls vorhanden (für qwen3)
        return removeThinkingBlock(result);
        
    } catch (IllegalStateException e) {
        // Grounding-Fallback: Tool-Aufruf fehlgeschlagen — antworte ohne Tools
        log.warn("Tool call failed for agent {}: {}", currentSender, e.getMessage());
        return agentChatModel.call(new Prompt(messages))
            .getResult().getOutput().getText();
    }
}

private String removeThinkingBlock(String text) {
    if (text == null) return "";
    // Entferne ...-Blöcke, die qwen3 hinzufügt
    return text.replaceAll("(?s).*?", "").trim();
}

Test des Grounding-Pipelines

@SpringBootTest
class GroundingPipelineTest {

    @Autowired
    private GroundingPipeline pipeline;

    @Test
    void shouldReturnNotFoundWhenDocumentMissing() {
        // Anfrage zu einem nicht vorhandenen Dokument
        String result = pipeline.processWithGrounding(
            "Vertragsbedingungen mit Kunde XYZ-99999",
            "test-tool-call-id"
        );
        
        // Die Antwort sollte anerkennen, dass nichts gefunden wurde — nichts erfinden
        assertThat(result)
            .containsAnyOf("nicht gefunden", "nicht gefunden", "fehlende Information")
            .doesNotContain("$")          // keine Preise erfunden
            .doesNotContain("30 Tage");   // keine Bedingungen erfunden
    }

    @Test  
    void shouldIncludeCitationInResponse() {
        String result = pipeline.processWithGrounding(
            "was ist der Preis für den Basisplan",
            "test-tool-call-id"
        );
        
        // Die Antwort sollte einen Verweis auf die Quelle enthalten
        assertThat(result)
            .containsAnyOf("Gemäß", "Dokument", "Abschnitt");
    }
}

Schlussfolgerungen

Grounding ist der Unterschied zwischen einem vertrauenswürdigen Agenten und einem Agenten, der intelligent aussieht, aber alles erfinden kann. Die meisten Tutorials zeigen, wie man ein Tool aufruft. Niemand zeigt, was nach der Antwort des Tools zu tun ist. Genau hier verstecken sich 90 % der Probleme von Produktionsagenten.

Besonders kritisch für Systeme, bei denen Antworten reale Konsequenzen haben — juristische Dokumente, Preise, Vertragsbedingungen, medizinische Daten. Dort kostet eine einzige falsche Antwort mehr als die gesamte Zeit, die für die Implementierung von Grounding aufgewendet wurde.

Fünf Regeln für Grounding — und wie man überprüft, ob sie funktionieren

1. Leeres Ergebnis ≠ „kann aus dem Gedächtnis antworten“

Übergeben Sie is_error: true und eine explizite Textanweisung in den Inhalt des Tool-Ergebnisses. Wie überprüfen: Entfernen Sie alle Dokumente aus der Datenbank — der Agent sollte auf jede Anfrage mit „nicht gefunden“ antworten, anstatt etwas zu erfinden.

2. Vektor-Score ist nicht ausreichend

Ein Score von 0,85 bedeutet „nächstgelegenes Dokument“ — nicht „Antwort auf die Frage“. Confidence Scoring bietet eine semantische Überprüfung, die der Score nicht bietet. Wie überprüfen: Stellen Sie eine Anfrage zu einem bestimmten Kunden, der nicht in der Datenbank ist — der Agent sollte nicht mit den Bedingungen eines anderen Kunden antworten.

3. Re-Query mit hartem Limit

Maximal 2 Versuche. NOT_RELEVANT — wir stoppen sofort ohne Re-Query. Wie überprüfen: In den Logs sollte nach jeder Suche attempt X/2 und der Grund für den Stopp stehen.

4. Zitierung ist obligatorisch

Der Agent muss wissen, woher jede Antwort stammt und dies zeigen können. Wie überprüfen: Fragen Sie den Agenten „Woher stammen diese Informationen?“ — er sollte ein spezifisches Dokument und einen Abschnitt nennen, nicht „aus der Wissensdatenbank“.

5. Explizite Anweisungen im Tool-Ergebnis — nicht nur im System-Prompt

„Antworte nicht aus eigenem Wissen“ im System-Prompt — das ist eine Empfehlung. Derselbe Satz im Inhalt des Tool-Ergebnisses — das ist eine Anweisung, die das Modell direkt vor der Antwort sieht. Wie überprüfen: Test mit is_error: true — die Antwort sollte keine konkreten Zahlen oder Fakten enthalten.

Checkliste vor dem Deployment eines Agenten in Produktion

// Grounding-Checkliste — überprüfen Sie jeden Punkt vor dem Deployment

□ Das Tool gibt eine explizite Anweisung bei leerem Ergebnis zurück
  (nicht leerer String, sondern "ERGEBNIS NICHT GEFUNDEN + ANWEISUNG")

□ Das Tool verwendet is_error: true für notFound und technicalError

□ Es gibt eine Score-Überprüfung mit Schwellenwerten:
  score >= 0.75 → success()
  score 0.55-0.75 → lowRelevance()
  score < 0.55 → notFound()

□ Confidence Scoring ist für kritische Anfragearten aktiviert
  (Preise, Verträge, Fristen)

□ Re-Query hat MAX_ATTEMPTS = 2 und stoppt bei NOT_RELEVANT

□ Das Tool-Ergebnis enthält Zitierung: Dokumenttitel, Abschnitt, Datum

□ stop_reason wird für jede Antwort des Agenten protokolliert

□ Es gibt einen Test: leere Datenbank → Agent antwortet "nicht gefunden"
  (erfindet keine Antwort)

□ Es gibt einen Test: is_error: true → Antwort ohne konkrete Zahlen und Fakten

Drei typische Fehler, die Grounding zerstören

// ❌ Fehler 1: leerer String statt expliziter Anweisung
return "";  
// Das Modell interpretiert es unterschiedlich — manchmal antwortet es aus dem Gedächtnis.

// ✅ Richtig:
return "ERGEBNIS NICHT GEFUNDEN. Antworte NICHT aus eigenem Wissen.";


// ❌ Fehler 2: sich auf den System-Prompt für Grounding verlassen
new SystemMessage("Wenn du es nicht weißt — sage, dass du es nicht weißt");
// Funktioniert manchmal. Nicht deterministisch. Nicht in Produktion.

// ✅ Richtig:
// Explizite Anweisung in jedem Tool-Ergebnis, wo erforderlich.


// ❌ Fehler 3: Re-Query ohne Limit
while (!found) {
    currentQuery = reformulate(currentQuery);
    result = search(currentQuery);
}
// Endlosschleife, wenn das Dokument nicht in der Datenbank ist.

// ✅ Richtig:
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { ... }
Nächster Schritt in der Serie: Grounding löst das Problem der Ergebnisqualität. Aber es gibt ein weiteres Problem — wenn ein Agent 10-15+ Tools hat, wird die Übergabe aller Tools bei jeder Anfrage ineffizient und die Qualität der Tool-Auswahl sinkt. Wie man ein dynamisches Tool-Register aufbaut und nur die benötigten Tools für eine bestimmte Anfrage verbindet — Tool RAG — Was tun, wenn ein Agent 100 Tools hat.

Lesen Sie auch in der Serie über KI-Agenten:

Tool Use vs Function Calling — grundlegende Mechanik, bevor Sie Grounding aufbauen.

Wie ein LLM entscheidet, wann es ein Tool aufrufen soll — wie das Modell Entscheidungen trifft, bevor es ein Ergebnis erhält.

Agent Chat — ein Live-Beispiel, bei dem Grounding für Wikipedia- und Tavily-Tools entscheidend ist.

Quellen: Spring AI Documentation, Anthropic Tool Use Docs, WildToolBench (2026)