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)