Tool RAG: Lösung für das „Too Many Tools“ Problem bei AI Agents

Aktualisiert:
KI zu diesem Artikel befragen
Tool RAG: Lösung für das „Too Many Tools“ Problem bei AI Agents

Sie haben 5 Tools – alles ist großartig. Sie haben 15 Tools – Probleme beginnen. Sie haben 50 Tools – der Agent degradiert. Aber es gibt eine Lösung, die das Problem der Skalierung elegant löst – und Sie wissen bereits, wie sie funktioniert, weil Sie sie für Dokumente verwenden.

Dieser Artikel ist Teil einer Serie über KI-Agenten mit Spring Boot. Wenn Sie noch nichts über die Mechanik der Tool-Auswahl gelesen haben – beginnen Sie mit Wie LLMs entscheiden, wann sie ein Tool aufrufen sollen. Was zu tun ist, nachdem ein Tool geantwortet hat – Grounding und Vertrauen in Quellen.

Inhalt

Wann Tool RAG benötigt wird – Entscheidungsbaum

Bevor Sie weiterlesen – entscheiden Sie, ob Sie diesen Artikel überhaupt benötigen. 90% der Projekte benötigen kein Tool RAG. Aber es ist gut, davon zu wissen, denn das Skalierungsproblem kommt unbemerkt.

Wie viele Tools hat Ihr Agent?
          ↓
       bis 10
          ↓
Gute Beschreibung + System-Prompt
→ Genug. Tool RAG wird nicht benötigt.
→ Lesen Sie über das Schreiben von Beschreibungen.

      von 10 bis 20
          ↓
Sind die Tools thematisch unterschiedlich?
├── JA → Tool-Kategorien und Routing (Abschnitt 7)
│         Einfacher als Tool RAG, löst das Problem
└── NEIN → Verbessern Sie die Beschreibungen, grenzen Sie Verantwortlichkeiten ab

      von 20 bis 50
          ↓
Sehen Sie eine Verschlechterung der Auswahl in den Logs?
├── JA → Tool RAG (dieser Artikel)
└── NEIN → Kategorien + Routing, Monitoring (Abschnitt 9)

      50+
          ↓
Tool RAG ist obligatorisch.
Ohne ihn degradiert der Agent auf ~13% Auswahlgenauigkeit.
Wichtig: Tool RAG ist keine Silberkugel und kein Muss-Have für jedes Projekt. Es ist ein Werkzeug für ein spezifisches Problem. Wenn Sie weniger als 20 Tools haben – bleiben Sie bei Abschnitt 7 (Kategorien und Routing) und kehren Sie zu diesem Artikel zurück, wenn das Register wächst.

Das Skalierungsproblem: Zahlen, die Sie überraschen werden

Sie fügen Tools schrittweise hinzu. Zuerst 5, dann 10, dann 20. Jedes neue Tool löst eine reale Aufgabe. Aber irgendwann beginnt die Qualität der Agentenarbeit zu sinken – und Sie verstehen nicht warum. Es gibt keine Fehler im Code. Die Beschreibungen sind richtig geschrieben. Aber der Agent wählt immer häufiger das falsche Tool oder ruft gar keines auf.

Das ist kein Problem Ihres Codes. Das ist ein systemisches Problem, das Forscher "choice paralysis" nannten – und es wird durch mehrere unabhängige Studien aus den Jahren 2025-2026 bestätigt.

Was die Studien 2025-2026 zeigen

RAG-MCP (Anthropic, Mai 2025) – eine Studie auf realen MCP-Servern zeigte eine katastrophale nichtlineare Verschlechterung:

Anzahl der Tools Tokens für Beschreibungen Auswahlgenauigkeit (Baseline) Genauigkeit mit Tool RAG
~10 Tools ~2K 78% ~90%
~50 Tools 8K 84-95% ~95%
~200 Tools 32K 41-83% ~85%
100+ Tools ~20K 13.62% 43.13%
~740 Tools 120K 0-20% ~60%

Das Schlüsselergebnis: Tool RAG hat die Genauigkeit mehr als verdreifacht (von 13,62% auf 43,13%) bei einem großen Tool-Register und den Prompt-Umfang um mehr als 50% reduziert.

Beachten Sie die Nichtlinearität der Verschlechterung. Bei 50 Tools ist es noch akzeptabel – 84-95%. Bei 200 Tools ist es bereits kritisch – ein Rückgang auf 41%. Bei 740 Tools wählt der Agent praktisch zufällig – 0-20%. Das ist keine allmähliche Verschlechterung, es ist ein Absturz.

Wie die Verschlechterung in den Logs aussieht

Wenn Sie Tool-Aufrufe in Agent Chat oder AskYourDocs protokollieren – so sieht das Problem in der Realität aus:

// Agent mit 5 Tools – normales Verhalten:
INFO: Round 1 AGENT_A — Tavily search: 'vibe coding productivity'
INFO: Tavily found 3 results
INFO: Round 1 AGENT_A reply: "GitHub Copilot erhöht die Produktivität um 51%..."

// Derselbe Agent, aber mit 30 Tools – Verschlechterung:
INFO: Round 1 AGENT_A — [kein Tool-Aufruf]
INFO: Round 1 AGENT_A reply: "Nach allgemeinen Daten, vibe coding..."
// stop_reason: "end_turn" ohne jegliche Tool-Nutzung
// Der Agent hat "beschlossen", aus dem Gedächtnis zu antworten, weil er sich bei der Auswahl der Tools verloren hat

// Oder eine andere Variante der Verschlechterung:
INFO: Round 1 AGENT_A — Wikipedia search: 'productivity'
// Hat Wikipedia anstelle von Tavily gewählt – obwohl Tavily für aktuelle Statistiken besser geeignet ist
// Bei 30 Tools unterscheidet das Modell feine Unterschiede zwischen den Beschreibungen nicht

Der "Lost in the Middle"-Effekt für Tools

Ein weiteres und weniger bekanntes Problem ist der Positionsbias. Die Studie BiasBusters (2025) zeigte: Tools in der Mitte einer langen Liste werden deutlich seltener ausgewählt als Tools am Anfang oder Ende. Bei 741 Tools:

  • Tools am Anfang und Ende der Liste – 31-32% Genauigkeit
  • Tools in der Mitte (Positionen 40-60%) – nur 22-52% Genauigkeit

Warum das technisch passiert: Transformer-Modelle verwenden Rotary Position Embedding (RoPE), das einen "long-term decay"-Effekt hat – Token am Anfang und Ende des Kontexts erhalten mehr Aufmerksamkeit als Token in der Mitte. Das ist ein Architektur-Bias, der in den meisten modernen LLMs unabhängig vom Anbieter vorhanden ist.

Praktische Konsequenz: Wenn Ihr bestes Tool für eine Anfrage zufällig in der Mitte einer Liste mit 50+ Tools landet – die Wahrscheinlichkeit, dass das Modell es auswählt, ist deutlich geringer, als wenn es das erste wäre. Tool RAG löst dies automatisch – es werden nur 3-5 Tools injiziert, alle sind am Anfang des Kontexts, der Positionsbias ist minimal.

Ein weiterer Grund für die Verschlechterung: Tokens

Jede Tool-Beschreibung verbraucht Tokens. Bei 50 Tools mit detaillierten Beschreibungen – das sind 5.000-15.000 Tokens nur für die Beschreibung der Werkzeuge, noch bevor der Gesprächskontext und die Historie beginnen. Die Studie von Modarressi et al. (2025) zeigte:

  • Eine Erhöhung des Kontexts um 1.000 Tokens → eine Verringerung der Genauigkeit um 16 Prozentpunkte
  • Bei Überschreitung von 8.000 Tokens → ein Rückgang um 50 Prozentpunkte

Details darüber, wie Tokens die Qualität und Kosten von Antworten beeinflussen – im Artikel LLM-Kontextfenster: Warum KI vergisst und was es kostet. Dort finden Sie auch konkrete Kostenangaben für verschiedene Anbieter.

// Token-Mathematik für Agent Chat mit 5 Tools (aktueller Stand):
// 5 Tools × ~200 Tokens = 1.000 Tokens – akzeptabel ✅

// Wenn wir auf 30 Tools skalieren:
// 30 Tools × ~200 Tokens = 6.000 Tokens – Verschlechterung beginnt ⚠️

// 50 Tools:
// 50 Tools × ~200 Tokens = 10.000 Tokens – erhebliche Verschlechterung ❌

// Tool RAG – unabhängig von der Größe des Registers:
// Inject 3 Tools × ~200 Tokens = 600 Tokens – immer akzeptabel ✅
// auch wenn im Register 500 Tools sind
"Prompt Bloat" und der Tod von MCP, der nicht stattfand: Ende 2025 erschienen Artikel mit Schlagzeilen wie "MCP is Dead After Just One Year". InfiniFlow (Dezember 2025) analysierte die Situation genau: Das Problem liegt nicht im MCP-Protokoll – das Problem liegt im Ansatz, "alle Tool-Beschreibungen sofort in den Kontext zu laden". Bei 4.400+ MCP-Servern auf mcp.so (April 2025) und Hunderten von Tools in Unternehmenssystemen – wird "choice paralysis" unvermeidlich. Tool RAG löst genau dieses Problem, ohne das Protokoll zu ändern.

Tool RAG Konzept – dieselbe Idee wie RAG für Dokumente

Wenn Sie ein RAG-System aufgebaut haben – werden Sie Tool RAG in einer Minute verstehen. Wenn nicht – hier ist eine Analogie: Stellen Sie sich eine Bibliothek mit 10.000 Büchern vor. Wenn Sie eine Antwort auf eine Frage benötigen – lesen Sie nicht alle Bücher nacheinander. Sie gehen zum Katalog, finden 3-5 relevante Bücher und lesen nur diese.

Klassisches RAG tut dasselbe mit Dokumenten. Tool RAG tut dasselbe mit den Werkzeugen des Agenten.

Vergleich: Was ändert sich im Prompt

Der anschaulichste Weg, Tool RAG zu verstehen – ist zu sehen, wie der Prompt davor und danach aussieht:

// ❌ OHNE Tool RAG – alle 30 Tools in jeder Anfrage:
{
  "tools": [
    { "name": "searchWikipedia", "description": "Sucht in Wikipedia..." },
    { "name": "searchWeb", "description": "Sucht im Internet..." },
    { "name": "getStockPrice", "description": "Ruft Aktienkurse ab..." },
    { "name": "searchNews", "description": "Sucht nach Nachrichten..." },
    { "name": "searchPapers", "description": "Sucht nach wissenschaftlichen Artikeln..." },
    { "name": "getWeather", "description": "Ruft das Wetter ab..." },
    { "name": "translateText", "description": "Übersetzt Text..." },
    { "name": "summarizeDoc", "description": "Fasst ein Dokument zusammen..." },
    // ... weitere 22 Tools
  ],
  "messages": [{ "role": "user", "content": "wie ist der AAPL Aktienkurs?" }]
}
// Prompt-Größe: ~8.000 Tokens nur für die Tools
// Das Modell sieht 30 Optionen und kann sich verirren

// ✅ MIT Tool RAG – nur 2 relevante Tools:
{
  "tools": [
    { "name": "getStockPrice", "description": "Ruft Aktienkurse ab..." },
    { "name": "searchWeb", "description": "Sucht nach aktuellen Finanzdaten..." }
  ],
  "messages": [{ "role": "user", "content": "wie ist der AAPL Aktienkurs?" }]
}
// Prompt-Größe: ~400 Tokens für die Tools
// Das Modell sieht 2 offensichtliche Optionen – die Auswahl ist präzise

Analogie-Tabelle: RAG für Dokumente vs. Tool RAG

Klassisches RAG Tool RAG
Was wird in der Vektordatenbank gespeichert Dokumenten-Chunks Tool-Beschreibungen
Was wird indiziert (eingebettet) Dokumententext Beschreibung + Trigger-Szenarien des Tools
Was wird gesucht Relevante Textfragmente Relevante Werkzeuge
Was wird in die LLM injiziert Top-K Fragmente als Kontext Top-K Tools als verfügbare Werkzeuge
Technologie pgvector, Qdrant Dasselbe pgvector, Qdrant
Embedding-Modell text-embedding-3-small Dasselbe Modell
Was es löst Halluzinationen aufgrund von Wissensmangel Degradation aufgrund von Überangebot an Auswahlmöglichkeiten

Der Hauptvorteil: Wenn Sie bereits eine RAG-Infrastruktur haben – Tool RAG wird mit minimalem Aufwand hinzugefügt. Dasselbe pgvector, dasselbe Embedding-Modell, derselbe Ansatz. Wenn Sie AskYourDocs oder ein anderes RAG-System auf pgvector verwenden – Tool RAG ist praktisch eine weitere Tabelle in derselben DB.

Wenn Sie noch kein RAG-System gebaut haben: Bevor Sie Tool RAG implementieren – empfehle ich, sich mit dem Grundkonzept vertraut zu machen. Details darüber, wie RAG von innen funktioniert und wie man es auf Spring AI + pgvector aufbaut – RAG mit Ollama: vom Pipeline bis zum Produkt. Tool RAG wird danach sofort verständlich sein.
Tool RAG: Lösung für das „Too Many Tools“ Problem bei AI Agents

Flow Tool RAG: von der Anfrage zur Injektion

Der gesamte Tool RAG-Prozess besteht aus sechs Schritten. Die ersten beiden Schritte erfolgen, bevor die LLM die Anfrage erhält – das ist der Hauptunterschied zum klassischen Ansatz.

Benutzeranfrage: "finde den aktuellen AAPL-Aktienkurs"
          ↓
[1] Einbettung der Anfrage
    embeddingModel.embed("finde den aktuellen AAPL-Aktienkurs")
    → vector[1536]
    // Wir wandeln die Anfrage in einen numerischen Vektor um.
    // Das gleiche Embedding-Modell, das für Dokumente in RAG verwendet wird.
    // Wichtig: Die Embedding-Modelle für Tool-Beschreibungen und für Anfragen
    // müssen identisch sein – andernfalls funktioniert die Vektorsuche nicht korrekt.
          ↓
[2] Vektorsuche in der Tool-Description-Registry
    SELECT tool_name, bean_name, 1 - (embedding <=> query_vector) as score
    FROM tool_registry
    WHERE is_active = TRUE
    ORDER BY embedding <=> query_vector
    LIMIT 5
    → [AlphaVantageTool: 0.91, TavilySearchTool: 0.73,
       NewsApiTool: 0.61, WikipediaSearchTool: 0.44, ArxivTool: 0.31]
    // pgvector gibt alle Tools sortiert nach Relevanz zurück.
    // Sogar WikipediaSearchTool ist im Ergebnis enthalten –
    // aber mit einem niedrigen Score von 0.44. Wir filtern es im nächsten Schritt.
          ↓
[3] Filterung nach Relevanzschwelle
    MIN_RELEVANCE_THRESHOLD = 0.60
    → verbleiben: [AlphaVantageTool: 0.91, TavilySearchTool: 0.73]
    // Tools mit einem Score unterhalb des Schwellenwerts werden abgelehnt.
    // Dies ist ein kritischer Schritt – ohne ihn könnte ein irrelevantes Tool injiziert werden.
    // NewsApiTool (0.61) liegt an der Grenze – abhängig von Ihrem Schwellenwert.
          ↓
[4] Laden der Spring-Beans für die gefundenen Tools
    List<ToolCallback> tools = loadTools(["alphaVantageTool", "tavilySearchTool"])
    // Wir laden die tatsächlichen Spring-Beans anhand des im Registry gespeicherten bean_name.
    // Keine Strings – echte Objekte mit der @Tool-Annotation.
          ↓
[5] LLM-Anfrage mit 2 Tools statt 30+
    agentChatModel.call(prompt, tools)
    // Das Modell sieht nur 2 relevante Tools.
    // ~400 Token für Beschreibungen statt 6.000-15.000.
    // Die Wahl ist offensichtlich – AlphaVantageTool für den Aktienkurs.
          ↓
[6] LLM ruft AlphaVantageTool auf
    → getStockPrice("AAPL")
          ↓
[7] Antwort an den Benutzer
    "AAPL-Aktie: $213.50 | Veränderung: +1.2% | Max: $215.20 | Min: $212.80"

Anstatt 30+ Tool-Beschreibungen (~6.000 Token) zu übergeben – übergeben wir die 2 relevantesten (~400 Token). Token-Einsparung: 93%. Auswahlgenauigkeit: deutlich höher. Latenz: +50-100ms für die Embedding-Anfrage, aber die gesparten Token kompensieren dies durch eine schnellere Verarbeitung des kürzeren Prompts.

Zwei Schritte, die detailliert verstanden werden sollten

Schritt 1 – Einbettung der Anfrage: Umwandlung von Text in einen numerischen Vektor – die Grundlage des gesamten Tool RAG. Von der Qualität des Embedding-Modells hängt ab, wie genau das System ein relevantes Tool findet. Details zur Auswahl eines Embedding-Modells für Ihren Stack – Embedding-Modelle für RAG im Jahr 2026: Wie wählt man aus, Vergleich der Anbieter. Wenn Sie verstehen möchten, wie Embedding intern funktioniert – Embeddings in einfachen Worten: Wie KI Sinn versteht und nicht nur Wörter.

Schritt 3 – Relevanzschwelle: Dies ist der wichtigste Parameter, der für Ihre Registry angepasst werden muss. Eine zu hohe Schwelle (0.85+) – der Agent findet oft kein Tool und antwortet ohne Suche. Eine zu niedrige (0.40-) – es werden irrelevante Tools injiziert und die Degradation kehrt zurück. Empfohlene Startschwelle: 0.60-0.65. Passen Sie sie basierend auf der Überwachung an (Abschnitt 9).

Was tun, wenn kein Tool über der Schwelle gefunden wird? Zwei Optionen: (1) Antworten ohne Tools – sicher, wenn die Anfrage keine aktuellen Daten benötigt; (2) die Schwelle senken und das beste Ergebnis injizieren, auch wenn der Score niedrig ist. In Agent Chat verwenden wir Option 1 als Standard – der Agent antwortet aus eigenem Wissen, wenn Tool RAG nichts gefunden hat.

Implementierung: pgvector für die Tool-Registry auf Spring AI

Datenbankschema für die Tool-Registry

-- Tool-Registry mit Embedding-Beschreibungen
CREATE TABLE tool_registry (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tool_name       VARCHAR(200) NOT NULL UNIQUE,  -- Name der Java-Klasse oder Methode
    display_name    VARCHAR(200) NOT NULL,          -- menschlicher Name
    description     TEXT NOT NULL,                  -- vollständige Beschreibung für Embedding
    category        VARCHAR(100),                   -- Kategorie für Routing
    bean_name       VARCHAR(200) NOT NULL,          -- Spring-Bean-Name für Injektion
    is_active       BOOLEAN DEFAULT TRUE,
    version         INTEGER DEFAULT 1,              -- für Versionierung
    embedding       vector(1536),                   -- pgvector
    created_at      TIMESTAMP DEFAULT NOW(),
    updated_at      TIMESTAMP DEFAULT NOW()
);

-- Index für schnelle Vektorsuche
CREATE INDEX tool_registry_embedding_idx
ON tool_registry
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 10);  -- 10 Listen für eine kleine Registry (bis zu 1000 Tools)

-- Index für Suche nach Kategorie
CREATE INDEX tool_registry_category_idx ON tool_registry(category, is_active);

Tool-Registrierungsdienst

@Service
@RequiredArgsConstructor
@Slf4j
public class ToolRegistryService {

    private final JdbcTemplate jdbcTemplate;
    private final EmbeddingModel embeddingModel;

    /**
     * Registriert ein Tool in der Registry.
     * Wird beim Start oder beim Hinzufügen eines neuen Tools aufgerufen.
     */
    public void registerTool(ToolRegistration registration) {
        // Generiert Embedding aus der Beschreibung
        float[] embedding = embeddingModel.embed(registration.getDescription());

        jdbcTemplate.update("""
            INSERT INTO tool_registry
                (tool_name, display_name, description, category, bean_name, embedding)
            VALUES (?, ?, ?, ?, ?, ?)
            ON CONFLICT (tool_name) DO UPDATE SET
                description = EXCLUDED.description,
                category = EXCLUDED.category,
                embedding = EXCLUDED.embedding,
                version = tool_registry.version + 1,
                updated_at = NOW()
            """,
            registration.getToolName(),
            registration.getDisplayName(),
            registration.getDescription(),
            registration.getCategory(),
            registration.getBeanName(),
            embedding
        );

        log.info("Tool registered: {} (category: {})",
            registration.getToolName(), registration.getCategory());
    }

    /**
     * Semantische Suche nach relevanten Tools für eine Anfrage
     */
    public List<ToolMatch> findRelevantTools(String userQuery, int topK) {
        float[] queryEmbedding = embeddingModel.embed(userQuery);

        return jdbcTemplate.query("""
            SELECT tool_name, display_name, bean_name, category,
                   1 - (embedding <=> ?) as relevance_score
            FROM tool_registry
            WHERE is_active = TRUE
            ORDER BY embedding <=> ?
            LIMIT ?
            """,
            (rs, rowNum) -> ToolMatch.builder()
                .toolName(rs.getString("tool_name"))
                .displayName(rs.getString("display_name"))
                .beanName(rs.getString("bean_name"))
                .category(rs.getString("category"))
                .relevanceScore(rs.getDouble("relevance_score"))
                .build(),
            embedding, embedding, topK
        );
    }
}

@Value
@Builder
public class ToolMatch {
    String toolName;
    String displayName;
    String beanName;
    String category;
    double relevanceScore;
}

@Value
@Builder
public class ToolRegistration {
    String toolName;
    String displayName;
    String description;   // vollständiger Text für Embedding – je detaillierter, desto besser
    String category;
    String beanName;
}

Registrierung aller Tools beim Start

@Component
@RequiredArgsConstructor
@Slf4j
public class ToolRegistryInitializer implements ApplicationRunner {

    private final ToolRegistryService registryService;

    @Override
    public void run(ApplicationArguments args) {
        log.info("Initializing tool registry...");

        List<ToolRegistration> tools = List.of(
            ToolRegistration.builder()
                .toolName("AlphaVantageTool.getStockPrice")
                .displayName("Stock Price Lookup")
                .description("""
                    Ruft den aktuellen Aktienkurs an der Börse ab.
                    Verwenden Sie es für Anfragen zu: Aktienkursen, Marktkapitalisierung von Unternehmen,
                    Finanzkennzahlen, Marktdynamik.
                    Unterstützt Ticker: AAPL, GOOGL, TSLA, AMZN, MSFT und andere.
                    NICHT verwenden für: Nachrichten, Prognosen, allgemeine Unternehmensinformationen.
                    """)
                .category("FINANCE")
                .beanName("alphaVantageTool")
                .build(),

            ToolRegistration.builder()
                .toolName("TavilySearchTool.searchWeb")
                .displayName("Web Search")
                .description("""
                    Sucht nach aktuellen Informationen im Internet über Tavily.
                    Verwenden Sie es für: aktuelle Nachrichten, aktuelle Statistiken,
                    Ereignisse der Jahre 2024-2025, Daten, die nicht in Wikipedia vorhanden sind.
                    NICHT verwenden für: stabile Fakten, Definitionen, Biografien.
                    """)
                .category("SEARCH")
                .beanName("tavilySearchTool")
                .build(),

            ToolRegistration.builder()
                .toolName("WikipediaSearchTool.searchWikipedia")
                .displayName("Wikipedia Search")
                .description("""
                    Sucht nach stabilen Fakteninformationen auf Wikipedia.
                    Verwenden Sie es für: Definitionen von Begriffen, Biografien, wissenschaftliche Fakten,
                    historische Ereignisse, geografische Informationen.
                    NICHT verwenden für: aktuelle Nachrichten, Kurse, aktuelle Ereignisse.
                    """)
                .category("SEARCH")
                .beanName("wikipediaSearchTool")
                .build(),

            ToolRegistration.builder()
                .toolName("ArxivSearchTool.searchPapers")
                .displayName("ArXiv Scientific Papers")
                .description("""
                    Sucht nach wissenschaftlichen Artikeln und Forschungsarbeiten auf ArXiv.
                    Verwenden Sie es für: wissenschaftliche Forschung, akademische Veröffentlichungen,
                    technische Artikel aus AI/ML, Physik, Mathematik, CS.
                    Anfragen müssen auf Englisch sein.
                    """)
                .category("RESEARCH")
                .beanName("arxivSearchTool")
                .build(),

            ToolRegistration.builder()
                .toolName("NewsApiSearchTool.searchNews")
                .displayName("News Search")
                .description("""
                    Sucht über NewsAPI nach aktuellen Nachrichten zu einem Thema.
                    Verwenden Sie es für: die neuesten Nachrichten, aktuelle Ereignisse,
                    Unternehmensnachrichten, Marktnachrichten.
                    Beschränkung: 100 Anfragen pro Tag.
                    """)
                .category("NEWS")
                .beanName("newsApiSearchTool")
                .build()
        );

        tools.forEach(registryService::registerTool);
        log.info("Tool registry initialized: {} tools registered", tools.size());
    }
}

Dynamische Tool-Injektion in Spring AI

Der interessanteste Teil – wie injiziert man nur relevante Tools in die Anfrage an die LLM anstatt aller auf einmal. Die Kernidee: ToolCallingChatOptions in Spring AI akzeptiert ein Array von ToolCallback[] dynamisch – das heißt, verschiedene Anfragen können unterschiedliche Tool-Sets erhalten, ohne den Code zu ändern.

@Service
@RequiredArgsConstructor
@Slf4j
public class ToolRagAgentService {

    private final ToolRegistryService registryService;
    private final ApplicationContext applicationContext;
    private final ChatModel chatModel;

    // Wie viele Tools maximal in eine Anfrage injiziert werden
    private static final int TOP_K_TOOLS = 3;
    // Minimale Relevanzschwelle – passen Sie sie an Ihre Registry an
    private static final double MIN_RELEVANCE = 0.60;

    public String askWithToolRag(String systemPrompt, String userQuery) {
        long startTime = System.currentTimeMillis();

        // 1. Relevante Tools über Vektorsuche finden
        List<ToolMatch> relevantMatches = registryService
            .findRelevantTools(userQuery, TOP_K_TOOLS);

        // 2. Nach minimaler Relevanzschwelle filtern
        List<ToolMatch> filteredTools = relevantMatches.stream()
            .filter(m -> m.getRelevanceScore() >= MIN_RELEVANCE)
            .toList();

        long ragLatency = System.currentTimeMillis() - startTime;

        log.info("Tool RAG: query='{}' found={}/{} tools threshold={} latency={}ms",
            userQuery.length() > 50 ? userQuery.substring(0, 50) + "..." : userQuery,
            filteredTools.size(),
            relevantMatches.size(),
            MIN_RELEVANCE,
            ragLatency);

        filteredTools.forEach(t ->
            log.info("  → {} score={:.3f}", t.getToolName(), t.getRelevanceScore()));

        // 3. Nachricht erstellen
        List<Message> messages = List.of(
            new SystemMessage(systemPrompt),
            new UserMessage(userQuery)
        );

        // 4. Fallback, wenn kein relevantes Tool gefunden wurde
        if (filteredTools.isEmpty()) {
            log.warn("Tool RAG: no tools above threshold={} for query='{}' — answering without tools",
                MIN_RELEVANCE, userQuery);
            return chatModel.call(new Prompt(messages))
                .getResult().getOutput().getText();
        }

        // 5. Spring-Beans laden und Anfrage stellen
        ToolCallback[] tools = loadToolCallbacks(filteredTools);

        return chatModel.call(
            new Prompt(messages,
                ToolCallingChatOptions.builder()
                    .toolCallbacks(tools)
                    .build()))
            .getResult().getOutput().getText();
    }

    /**
     * Lädt ToolCallbacks über den Spring ApplicationContext
     * anhand des im Registry gespeicherten Bean-Namens.
     *
     * Thread-sicher: ApplicationContext.getBean() ist threadsicher –
     * Spring gibt Singleton-Beans ohne Sperren zurück.
     */
    private ToolCallback[] loadToolCallbacks(List<ToolMatch> matches) {
        return matches.stream()
            .map(match -> {
                try {
                    Object bean = applicationContext.getBean(match.getBeanName());
                    return ToolCallbacks.from(bean);
                } catch (NoSuchBeanDefinitionException e) {
                    // Bean nicht gefunden – vielleicht wurde das Tool aus dem Code entfernt
                    // aber ist noch im DB-Registry vorhanden
                    log.error("Tool bean not found: '{}' — " +
                              "deaktivieren Sie das Tool im Registry oder starten Sie die Anwendung neu",
                        match.getBeanName());
                    return new ToolCallback[0];
                } catch (BeansException e) {
                    log.error("Failed to load tool bean '{}': {}",
                        match.getBeanName(), e.getMessage());
                    return new ToolCallback[0];
                }
            })
            .flatMap(Arrays::stream)
            .toArray(ToolCallback[]::new);
    }
}

Caching von Embeddings zur Reduzierung der Latenz

Tool RAG fügt vor jedem LLM-Aufruf eine Embedding-Anfrage hinzu. Wenn gleiche oder ähnliche Anfragen wiederholt werden – Caching ermöglicht es, unnötige Embedding-Anfragen zu vermeiden:

@Service
@RequiredArgsConstructor
@Slf4j
public class CachedToolRegistryService {

    private final ToolRegistryService registryService;

    // Einfacher In-Memory-Cache – für Produktion verwenden Sie Redis
    // ConcurrentHashMap ist thread-sicher
    private final Map<String, CachedResult> cache = new ConcurrentHashMap<>();

    private static final Duration CACHE_TTL = Duration.ofMinutes(5);
    private static final int MAX_CACHE_SIZE = 500;

    public List<ToolMatch> findRelevantToolsCached(String userQuery, int topK) {

        // Normalisieren der Anfrage für eine bessere Cache-Trefferquote
        String cacheKey = userQuery.toLowerCase().trim();

        CachedResult cached = cache.get(cacheKey);
        if (cached != null && !cached.isExpired()) {
            log.debug("Tool RAG cache HIT for query: '{}'", cacheKey);
            return cached.tools();
        }

        // Cache-Miss – tatsächliche Suche durchführen
        log.debug("Tool RAG cache MISS for query: '{}'", cacheKey);
        List<ToolMatch> tools = registryService.findRelevantTools(userQuery, topK);

        // Im Cache speichern, wenn das Limit nicht überschritten wird
        if (cache.size() < MAX_CACHE_SIZE) {
            cache.put(cacheKey, new CachedResult(tools, Instant.now()));
        }

        return tools;
    }

    /**
     * Löscht den Cache bei Aktualisierung der Tool-Registry
     * (nach registerTool oder updateToolDescription aufrufen)
     */
    public void invalidateCache() {
        int size = cache.size();
        cache.clear();
        log.info("Tool RAG cache invalidated: {} entries cleared", size);
    }

    record CachedResult(List<ToolMatch> tools, Instant cachedAt) {
        boolean isExpired() {
            return Instant.now().isAfter(cachedAt.plus(CACHE_TTL));
        }
    }
}

Integration in AgentConversationRunner

So sieht die Migration von Agent Chat von einer statischen Tool-Liste zu dynamischem Tool RAG aus – minimale Änderungen im bestehenden Code:

// In AgentConversationRunner.ask()

// ❌ War – alle 5 Tools in jeder Runde unabhängig vom Thema:
ToolCallback[] tools = ToolCallbacks.from(
    wikipediaSearchTool,   // immer injiziert
    tavilySearchTool,      // immer injiziert
    alphaVantageTool,      // immer injiziert – auch wenn wir über Architektur sprechen
    arxivSearchTool,       // immer injiziert
    newsApiSearchTool      // immer injiziert
);
// ~1.000 Token für Tools in jeder Runde

// ✅ Wurde – nur relevante Tools für die aktuelle Nachricht:
List<ToolMatch> relevantTools = cachedToolRegistryService
    .findRelevantToolsCached(lastMessage, 3);

ToolCallback[] tools = loadToolCallbacks(relevantTools);

log.info("Tool RAG round={} injected={} tools: [{}]",
    round,
    tools.length,
    relevantTools.stream()
        .map(t -> t.getToolName() + ":" + String.format("%.2f", t.getRelevanceScore()))
        .collect(joining(", ")));

// Beispiel-Logs während eines Dialogs über Vibe Coding:
// Tool RAG round=1 injected=2 tools: [TavilySearchTool:0.87, WikipediaSearchTool:0.71]
// Tool RAG round=2 injected=2 tools: [TavilySearchTool:0.83, NewsApiSearchTool:0.68]
// Tool RAG round=3 injected=1 tools: [WikipediaSearchTool:0.79]
// AlphaVantageTool und ArxivSearchTool nicht injiziert – nicht relevant für das Thema
Fallstrick – Desynchronisation von Registry und Code: Wenn Sie einen Spring-Bean gelöscht oder umbenannt haben, aber das DB-Registry nicht aktualisiert haben – loadToolCallbacks() wirft eine NoSuchBeanDefinitionException. Um dies zu vermeiden: Fügen Sie eine Validierung des Registries beim Start der Anwendung hinzu (Methode validateRegistry() aus Abschnitt 8) und deaktivieren Sie veraltete Einträge über deactivateTool() anstatt sie aus der DB zu löschen.

Tool-Kategorien und Routing – eine vereinfachte Alternative

Wenn Sie 10-30 Tools haben und diese thematisch unterschiedlich sind – Kategorien und Keyword-Routing sind einfacher und schneller als ein vollwertiges Tool RAG. Dies ist eine Zwischenlösung, die 80% der Skalierungsprobleme löst ohne Vektor-DB und ohne Embedding-Anfragen.

Der Hauptunterschied zu Tool RAG: Routing bestimmt die Kategorie durch Suche nach Schlüsselwörtern in der Anfrage – das ist eine CPU-Operation in Mikrosekunden, nicht eine Embedding-Anfrage in 50-100ms. Für Systeme, bei denen die Latenz kritisch ist – ist dies ein erheblicher Vorteil.

Ansatz: Keyword-Routing

@Service
@RequiredArgsConstructor
@Slf4j
public class ToolCategoryRouter {

    // Tools werden direkt injiziert – ohne zusätzliche Felder
    private final AlphaVantageTool alphaVantageTool;
    private final TavilySearchTool tavilySearchTool;
    private final WikipediaSearchTool wikipediaSearchTool;
    private final ArxivSearchTool arxivSearchTool;
    private final NewsApiSearchTool newsApiSearchTool;

    // Schlüsselwörter für jede Kategorie
    // Wichtig: Wörter müssen spezifisch genug sein, um keine
    // Fehlalarme auszulösen. "news" kann in jeder Anfrage vorkommen –
    // daher besser "breaking news", "latest news", "Nachrichten heute"
    private static final Map<String, List<String>> CATEGORY_KEYWORDS = Map.of(
        "FINANCE",  List.of("Aktie", "Aktienkurs", "Kapitalisierung",
                            "stock price", "market cap", "AAPL", "TSLA", "GOOGL"),
        "NEWS",     List.of("Nachrichten", "aktuelle Ereignisse", "heute geschehen",
                            "breaking news", "latest news", "aktuelle Ereignisse"),
        "RESEARCH", List.of("Forschung", "wissenschaftlicher Artikel", "arxiv",
                            "research paper", "academic study", "peer-reviewed"),
        "FACTS",    List.of("was ist", "wer ist", "Definition", "wikipedia",
                            "what is", "who is", "definition of", "history of")
    );

    // Mapping von Kategorien zu Tools – einmal definieren
    // LinkedHashSet speichert die Reihenfolge und verhindert Duplikate
    private Map<String, List<Object>> buildCategoryTools() {
        return Map.of(
            "FINANCE",  List.of(alphaVantageTool, tavilySearchTool),
            "NEWS",     List.of(newsApiSearchTool, tavilySearchTool),
            "RESEARCH", List.of(arxivSearchTool, wikipediaSearchTool),
            "FACTS",    List.of(wikipediaSearchTool, tavilySearchTool)
        );
    }

    /**
     * Ermittelt die Kategorie einer Anfrage und gibt die entsprechenden Tools zurück.
     * Dedupliziert Tools, wenn die Anfrage mehreren Kategorien zugeordnet wird.
     */
    public ToolCallback[] routeTools(String userQuery) {
        String queryLower = userQuery.toLowerCase();

        Set<String> matchedCategories = CATEGORY_KEYWORDS.entrySet().stream()
            .filter(entry -> entry.getValue().stream()
                .anyMatch(queryLower::contains))
            .map(Map.Entry::getKey)
            .collect(Collectors.toSet());

        log.info("Tool routing: query='{}' → categories={}",
            userQuery.length() > 60 ? userQuery.substring(0, 60) + "..." : userQuery,
            matchedCategories.isEmpty() ? "[DEFAULT]" : matchedCategories);

        if (matchedCategories.isEmpty()) {
            // Standard: Basis-Suche für jede Anfrage
            log.info("Tool routing: no category matched → using default [Tavily, Wikipedia]");
            return ToolCallbacks.from(tavilySearchTool, wikipediaSearchTool);
        }

        Map<String, List<Object>> categoryTools = buildCategoryTools();

        // LinkedHashSet zur Deduplizierung – tavilySearchTool wird nicht doppelt hinzugefügt
        // wenn die Anfrage FINANCE und NEWS gleichzeitig entspricht
        Set<Object> selectedTools = new LinkedHashSet<>();
        matchedCategories.forEach(category ->
            selectedTools.addAll(categoryTools.getOrDefault(category, List.of()))
        );

        log.info("Tool routing: selected {} tools: {}",
            selectedTools.size(),
            selectedTools.stream()
                .map(t -> t.getClass().getSimpleName())
                .collect(Collectors.joining(", ")));

        return ToolCallbacks.from(selectedTools.toArray());
    }
}

Wann Routing gut funktioniert – und wann es fehlschlägt

// ✅ Routing funktioniert gut:
"wie ist der AAPL-Aktienkurs?"
→ FINANCE → [AlphaVantageTool, TavilySearchTool] ✓

"was ist vibe coding?"
→ FACTS → [WikipediaSearchTool, TavilySearchTool] ✓

"aktuelle Nachrichten über Tesla Aktienkurs"
→ NEWS + FINANCE → [NewsApiSearchTool, TavilySearchTool, AlphaVantageTool] ✓
  (Deduplizierung funktioniert – TavilySearchTool nur einmal)

// ❌ Routing schlägt fehl:
"erzähle mir von einem Unternehmen, das den Markt verändert hat"
→ [] → DEFAULT → [TavilySearchTool, WikipediaSearchTool]
  (kein Schlüsselwort hat funktioniert – aber Tavily passt trotzdem)

"wie ist das Wetter in Kiew und wie ist der Dollarkurs?"
→ [] → DEFAULT
  (keine WEATHER- oder CURRENCY-Kategorien – Routing weiß nicht, was zu tun ist)
  // Mit Tool RAG: Embedding würde WeatherTool und CurrencyTool automatisch finden

"Studien zeigen, dass die Aktienkurse steigen"
→ RESEARCH + FINANCE → zu viele Tools
  (das Wort "Studien" ist vorhanden, aber die Anfrage erfordert kein ArXiv)
  // Dies ist der Moment, in dem Routing brüchig wird

Vergleich: Routing vs. Tool RAG

Keyword-Routing Tool RAG
Overhead pro Anfrage ~0ms (CPU) ~50-100ms (Embedding)
Auswahlgenauigkeit Gut für einfache Anfragen Hoch für alle Anfragen
Code-Wartung Manuelle Aktualisierung von Keywords Aktualisierung der Beschreibung in der DB
Mehrsprachigkeit Separate Keywords für jede Sprache Automatisch über semantische Suche
Anzahl der Tools Bis zu 30 Unbegrenzt
Implementierungskomplexität Niedrig – eine Klasse Mittel – DB + Embedding
Infrastruktur Nichts Zusätzliches pgvector + Embedding-Modell
Drei Signale, dass es Zeit ist, von Routing zu Tool RAG zu wechseln: 1. Die Liste der Keywords wächst – Sie fügen ständig neue Schlüsselwörter hinzu, weil das Routing Anfragen übersieht. Dies ist ein Zeichen dafür, dass die semantische Suche besser funktioniert. 2. Anfragen gehören oft zu 2-3 Kategorien gleichzeitig – die Injektion wird unvorhersehbar, der Agent erhält zu viele Tools. 3. Sie fügen eine neue Schnittstellensprache hinzu – Keywords müssen für jede Sprache dupliziert werden, und Tool RAG unterstützt Mehrsprachigkeit automatisch über semantische Suche.

Tool-Versionierung – wie man die Registry aktualisiert

Ein praktisches Problem, das in keinem Tutorial behandelt wird: Was tun, wenn sich ein Tool ändert? Neue Funktionalität, neue Beschreibung, neue Einschränkungen. Oder noch komplizierter – Sie haben das Embedding-Modell geändert und alle alten Vektoren sind nicht mehr kompatibel.

Grundprinzip: Löschen Sie niemals Einträge aus der Registry – deaktivieren Sie sie. Dies bewahrt die Änderungshistorie und ermöglicht es, bei Problemen zurückzukehren.

Vier Aktualisierungsszenarien

@Service
@RequiredArgsConstructor
@Slf4j
public class ToolVersioningService {

    private final ToolRegistryService registryService;
    private final CachedToolRegistryService cachedRegistryService;
    private final JdbcTemplate jdbcTemplate;
    private final EmbeddingModel embeddingModel;

    /**
     * Szenario 1: Nur die Beschreibung wurde geändert (am häufigsten)
     * Wir generieren nur ein Embedding neu – die restlichen Felder bleiben unverändert
     */
    @Transactional
    public void updateToolDescription(String toolName, String newDescription) {

        // Zuerst prüfen wir, ob das Tool existiert
        int exists = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM tool_registry WHERE tool_name = ? AND is_active = TRUE",
            Integer.class, toolName);

        if (exists == 0) {
            throw new IllegalArgumentException(
                "Tool not found or inactive: " + toolName);
        }

        // Generieren Sie ein neues Embedding nur für die aktualisierte Beschreibung
        float[] newEmbedding = embeddingModel.embed(newDescription);

        jdbcTemplate.update("""
            UPDATE tool_registry
            SET description = ?,
                embedding = ?,
                version = version + 1,
                updated_at = NOW()
            WHERE tool_name = ?
            """,
            newDescription, newEmbedding, toolName
        );

        // Cache unbedingt nach der Aktualisierung invalidieren
        cachedRegistryService.invalidateCache();

        log.info("Tool updated: {} — new embedding generated, cache invalidated",
            toolName);
    }

    /**
     * Szenario 2: Tool deaktiviert (veraltet oder aus dem Code entfernt)
     * NICHT löschen – deaktivieren, Historie beibehalten
     */
    @Transactional
    public void deactivateTool(String toolName) {
        int updated = jdbcTemplate.update("""
            UPDATE tool_registry
            SET is_active = FALSE,
                updated_at = NOW()
            WHERE tool_name = ?
            """,
            toolName
        );

        if (updated == 0) {
            log.warn("Tool not found for deactivation: {}", toolName);
            return;
        }

        cachedRegistryService.invalidateCache();
        log.info("Tool deactivated: {} — removed from active registry", toolName);
    }

    /**
     * Szenario 3: Massenaktualisierung nach Refactoring von Beschreibungen
     * Alle Embeddings neu generieren – kann bei einer großen Registry
     * mehrere Minuten dauern
     */
    @Transactional
    public RebuildResult rebuildAllEmbeddings() {
        log.info("Starting full tool registry rebuild...");
        long startTime = System.currentTimeMillis();

        List<Map<String, Object>> tools = jdbcTemplate.queryForList(
            "SELECT tool_name, description FROM tool_registry WHERE is_active = TRUE"
        );

        int success = 0;
        int failed = 0;

        for (Map<String, Object> tool : tools) {
            String toolName = (String) tool.get("tool_name");
            String description = (String) tool.get("description");

            try {
                float[] newEmbedding = embeddingModel.embed(description);

                jdbcTemplate.update("""
                    UPDATE tool_registry
                    SET embedding = ?,
                        version = version + 1,
                        updated_at = NOW()
                    WHERE tool_name = ?
                    """,
                    newEmbedding, toolName
                );
                success++;

            } catch (Exception e) {
                log.error("Failed to rebuild embedding for tool '{}': {}",
                    toolName, e.getMessage());
                failed++;
            }
        }

        cachedRegistryService.invalidateCache();

        long elapsed = System.currentTimeMillis() - startTime;
        log.info("Tool registry rebuild complete: {}/{} tools updated in {}ms",
            success, tools.size(), elapsed);

        return new RebuildResult(success, failed, elapsed);
    }

    /**
     * Szenario 4: Änderung des Embedding-Modells – der komplizierteste Fall
     * Alle alten Vektoren sind mit dem neuen Modell inkompatibel –
     * die gesamte Registry muss neu aufgebaut und die Dimension aktualisiert werden
     */
    public void migrateEmbeddingModel(int newDimensions) {
        log.warn("EMBEDDING MODEL MIGRATION STARTED — " +
                 "all existing vectors will be invalidated!");

        // 1. Prüfen, ob sich die neue Dimension unterscheidet
        // (wenn sie gleich ist – einfach rebuildAllEmbeddings)
        if (newDimensions != getCurrentDimensions()) {
            log.info("Updating vector dimensions: {} → {}",
                getCurrentDimensions(), newDimensions);

            // 2. Ändern der Spaltendimension in pgvector
            // ACHTUNG: Dies ist DROP und CREATE – alle alten Vektoren werden gelöscht
            jdbcTemplate.execute(
                "ALTER TABLE tool_registry ALTER COLUMN embedding TYPE vector(" +
                newDimensions + ")");
        }

        // 3. Alle Embeddings mit dem neuen Modell neu generieren
        RebuildResult result = rebuildAllEmbeddings();

        log.info("Embedding model migration complete: {}", result);
    }

    private int getCurrentDimensions() {
        // Aktuelle Dimension aus dem ersten Eintrag abrufen
        return jdbcTemplate.queryForObject("""
            SELECT vector_dims(embedding)
            FROM tool_registry
            WHERE embedding IS NOT NULL
            LIMIT 1
            """, Integer.class);
    }

    record RebuildResult(int success, int failed, long elapsedMs) {}
}

Validierung des Registries beim Start – Health Check

Fügen Sie eine Validierung in ApplicationRunner hinzu, um sofort nach dem Start Abweichungen zwischen Code und Registry zu sehen:

@Component
@RequiredArgsConstructor
@Slf4j
public class ToolRegistryValidator implements ApplicationRunner {

    private final ToolVersioningService versioningService;
    private final ApplicationContext applicationContext;
    private final JdbcTemplate jdbcTemplate;

    @Override
    public void run(ApplicationArguments args) {
        log.info("Validating tool registry consistency...");

        // Alle aktiven Bean-Namen aus der Registry abrufen
        List<String> registeredBeans = jdbcTemplate.queryForList(
            "SELECT bean_name FROM tool_registry WHERE is_active = TRUE",
            String.class
        );

        List<String> issues = new ArrayList<>();

        // Prüfen, ob die Beans im Spring-Kontext existieren
        for (String beanName : registeredBeans) {
            if (!applicationContext.containsBean(beanName)) {
                issues.add("ORPHANED: bean '" + beanName +
                           "' in registry but NOT in Spring context");
            }
        }

        // Prüfen, ob Tools mit null-Embedding vorhanden sind
        List<String> noEmbedding = jdbcTemplate.queryForList("""
            SELECT tool_name FROM tool_registry
            WHERE is_active = TRUE AND embedding IS NULL
            """, String.class);

        if (!noEmbedding.isEmpty()) {
            issues.add("NO EMBEDDING: tools without embedding: " + noEmbedding);
        }

        // Ergebnis ausgeben
        if (issues.isEmpty()) {
            log.info("Tool registry OK: {} active tools, all consistent",
                registeredBeans.size());
        } else {
            log.warn("Tool registry has {} issues:", issues.size());
            issues.forEach(issue -> log.warn("  ⚠️ {}", issue));
            log.warn("Run ToolVersioningService.rebuildAllEmbeddings() " +
                     "or deactivateTool() to fix");
        }
    }
}
Fallstrick – Cache nach Aktualisierung: Jede Methode zur Aktualisierung der Registry ruft cachedRegistryService.invalidateCache() auf. Wenn Sie dies vergessen – der Agent wird weiterhin alte Suchergebnisse für weitere 5 Minuten (Cache-TTL) verwenden. Besonders kritisch bei der Deaktivierung eines Tools: Der Agent könnte versuchen, ein deaktiviertes Tool aufzurufen, wenn es noch im Cache ist. Fallstrick – Migration des Embedding-Modells: Dies ist eine nicht umkehrbare Operation, die alle alten Vektoren löscht. Machen Sie immer ein Backup der Tabelle vor der Migration: CREATE TABLE tool_registry_backup AS SELECT * FROM tool_registry;

Überwachung und Metriken des Registers

Das Tool-Register ist eine lebendige Komponente. Ohne Überwachung wissen Sie nicht: welche Tools tatsächlich verwendet werden, welche entfernt werden können, welche Beschreibungen überarbeitet werden sollten. Und vor allem – Sie wissen nicht, wann das Tool RAG anfängt zu versagen.

Analytische Tabellenschema

-- Protokollierung jeder Tool-Auswahl mit Tool RAG
CREATE TABLE tool_usage_log (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tool_name       VARCHAR(200) NOT NULL,
    query_snippet   VARCHAR(500),      -- erste 500 Zeichen der Anfrage
    relevance_score DOUBLE PRECISION,
    was_called      BOOLEAN DEFAULT FALSE, -- wurde das Tool nach dem Inject tatsächlich von der LLM aufgerufen?
    session_id      VARCHAR(100),      -- Sitzungs-ID zur Korrelation
    conversation_id BIGINT,            -- für Agent Chat
    created_at      TIMESTAMP DEFAULT NOW()
);

-- Index für schnelle Abfragen nach Zeit und tool_name
CREATE INDEX tool_usage_log_time_idx ON tool_usage_log(created_at DESC);
CREATE INDEX tool_usage_log_tool_idx ON tool_usage_log(tool_name, created_at DESC);

-- Aggregierte Statistiken der letzten 30 Tage
CREATE VIEW tool_usage_stats AS
SELECT
    tool_name,
    COUNT(*)                                                    as injected_count,
    SUM(CASE WHEN was_called THEN 1 ELSE 0 END)                as called_count,
    ROUND(
        SUM(CASE WHEN was_called THEN 1 ELSE 0 END)::numeric /
        NULLIF(COUNT(*), 0) * 100, 1
    )                                                           as call_rate_pct,
    ROUND(AVG(relevance_score)::numeric, 3)                    as avg_relevance,
    ROUND(MIN(relevance_score)::numeric, 3)                    as min_relevance,
    MAX(created_at)                                            as last_injected,
    MAX(CASE WHEN was_called THEN created_at END)              as last_called
FROM tool_usage_log
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY tool_name
ORDER BY called_count DESC;

ToolUsageMonitor – Überwachungsdienst

@Service
@RequiredArgsConstructor
@Slf4j
public class ToolUsageMonitor {

    private final JdbcTemplate jdbcTemplate;

    /**
     * Batch-Einfügung – eine Anfrage anstelle von N einzelnen
     */
    public void logInjection(List<ToolMatch> injectedTools,
                              String query,
                              String sessionId,
                              Long conversationId) {
        if (injectedTools.isEmpty()) return;

        String querySnippet = query.length() > 500
            ? query.substring(0, 500) : query;

        // Batch-Einfügung für alle Tools gleichzeitig
        jdbcTemplate.batchUpdate("""
            INSERT INTO tool_usage_log
                (tool_name, query_snippet, relevance_score,
                 session_id, conversation_id)
            VALUES (?, ?, ?, ?, ?)
            """,
            injectedTools,
            injectedTools.size(),
            (ps, tool) -> {
                ps.setString(1, tool.getToolName());
                ps.setString(2, querySnippet);
                ps.setDouble(3, tool.getRelevanceScore());
                ps.setString(4, sessionId);
                ps.setObject(5, conversationId);
            }
        );
    }

    /**
     * Aktualisiert was_called=TRUE nach einem tatsächlichen Tool-Aufruf.
     * Verwendet session_id statt Zeit – zuverlässiger.
     */
    public void markToolCalled(String toolName, String sessionId) {
        int updated = jdbcTemplate.update("""
            UPDATE tool_usage_log
            SET was_called = TRUE
            WHERE tool_name = ?
              AND session_id = ?
              AND was_called = FALSE
            """,
            toolName, sessionId
        );

        if (updated == 0) {
            log.warn("markToolCalled: kein Datensatz gefunden für tool='{}' session='{}'",
                toolName, sessionId);
        }
    }

    /**
     * "Tote" Tools – häufig injiziert, aber selten aufgerufen.
     * callRateThreshold: 0.10 = weniger als 10% Aufrufe nach dem Inject
     */
    public List<DeadToolReport> findDeadTools(double callRateThreshold) {
        return jdbcTemplate.query("""
            SELECT tool_name, injected_count, called_count,
                   call_rate_pct, avg_relevance
            FROM tool_usage_stats
            WHERE injected_count >= 10
              AND call_rate_pct < ?
            ORDER BY injected_count DESC
            """,
            (rs, rowNum) -> new DeadToolReport(
                rs.getString("tool_name"),
                rs.getInt("injected_count"),
                rs.getInt("called_count"),
                rs.getDouble("call_rate_pct"),
                rs.getDouble("avg_relevance")
            ),
            callRateThreshold * 100  // Konvertiert 0.10 → 10 für den Vergleich mit call_rate_pct
        );
    }

    /**
     * Tools, die seit 30+ Tagen nicht mehr verwendet wurden
     */
    public List<String> findUnusedTools() {
        return jdbcTemplate.queryForList("""
            SELECT tr.tool_name
            FROM tool_registry tr
            LEFT JOIN tool_usage_log tul
                ON tr.tool_name = tul.tool_name
                AND tul.created_at > NOW() - INTERVAL '30 days'
            WHERE tr.is_active = TRUE
              AND tul.tool_name IS NULL
            ORDER BY tr.tool_name
            """,
            String.class
        );
    }

    /**
     * Tools mit konstant niedrigem Relevanz-Score –
     * ein Signal dafür, dass die Beschreibung nicht gut zu den tatsächlichen Anfragen passt
     */
    public List<String> findLowRelevanceTools(double scoreThreshold) {
        return jdbcTemplate.queryForList("""
            SELECT tool_name
            FROM tool_usage_stats
            WHERE injected_count >= 5
              AND avg_relevance < ?
            ORDER BY avg_relevance ASC
            """,
            String.class,
            scoreThreshold  // z.B. 0.65
        );
    }

    /**
     * Vollständiger Gesundheitsbericht des Registers
     */
    public RegistryHealthReport generateHealthReport() {
        List<DeadToolReport> deadTools = findDeadTools(0.10);
        List<String> unusedTools = findUnusedTools();
        List<String> lowRelevanceTools = findLowRelevanceTools(0.65);

        int totalActive = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM tool_registry WHERE is_active = TRUE",
            Integer.class);

        RegistryHealthReport report = new RegistryHealthReport(
            totalActive, deadTools, unusedTools, lowRelevanceTools);

        if (report.hasIssues()) {
            log.warn("Gesundheitsbericht des Tool-Registers:\n{}", report.summary());
        } else {
            log.info("Tool-Register gesund: {} aktive Tools, keine Probleme", totalActive);
        }

        return report;
    }

    record DeadToolReport(
        String toolName,
        int injectedCount,
        int calledCount,
        double callRatePct,
        double avgRelevance
    ) {}

    record RegistryHealthReport(
        int totalActiveTools,
        List<DeadToolReport> deadTools,
        List<String> unusedTools,
        List<String> lowRelevanceTools
    ) {
        boolean hasIssues() {
            return !deadTools.isEmpty()
                || !unusedTools.isEmpty()
                || !lowRelevanceTools.isEmpty();
        }

        String summary() {
            return String.format("""
                Aktive Tools: %d
                Tote Tools (niedrige Aufrufrate): %s
                Unbenutzte Tools (30+ Tage): %s
                Tools mit niedriger Relevanz: %s
                """,
                totalActiveTools,
                deadTools.stream().map(DeadToolReport::toolName).toList(),
                unusedTools,
                lowRelevanceTools
            );
        }
    }
}

Geplante Überwachung – automatischer Bericht

@Component
@RequiredArgsConstructor
@Slf4j
public class ToolRegistryHealthScheduler {

    private final ToolUsageMonitor monitor;
    private final ToolVersioningService versioningService;

    /**
     * Wöchentliche Gesundheitsprüfung – jeden Montag um 9:00 Uhr
     */
    @Scheduled(cron = "0 0 9 * * MON")
    public void weeklyHealthCheck() {
        log.info("=== Wöchentliche Gesundheitsprüfung des Tool-Registers ===");
        RegistryHealthReport report = monitor.generateHealthReport();

        if (report.hasIssues()) {
            // In Produktion: an Slack/E-Mail/Grafana senden
            // notificationService.sendAlert("Probleme im Tool-Register", report.summary());
            log.warn("Handlungsbedarf – Überprüfung des Tool-Registers");
        }
    }

    /**
     * Tägliche Konsistenzprüfung beim Start
     */
    @Scheduled(cron = "0 0 8 * * *")
    public void dailyConsistencyCheck() {
        // Prüfen, ob Tools ohne Embeddings vorhanden sind
        // (könnten nach einem Fehler während registerTool aufgetreten sein)
        List<String> noEmbedding = versioningService.findToolsWithoutEmbeddings();

        if (!noEmbedding.isEmpty()) {
            log.warn("Tools ohne Embeddings gefunden: {} – werden neu erstellt",
                noEmbedding);
            versioningService.rebuildEmbeddingsForTools(noEmbedding);
        }
    }
}

Was tun mit den Überwachungsergebnissen

Symptom Metrik Ursache Aktion
Tool wird häufig injiziert, selten aufgerufen call_rate_pct < 10% Beschreibung zu breit Anti-Anwendungsfälle in die Beschreibung aufnehmen, Trigger einschränken
Tool erscheint nicht im Inject injected_count = 0 Beschreibung entspricht nicht den Benutzeranfragen Beschreibung in der Sprache der Protokolle neu schreiben
Tool wurde seit 30+ Tagen nicht verwendet last_called IS NULL Tool veraltet oder von einem anderen Tool abgedeckt Über deactivateTool() deaktivieren
avg_relevance konstant niedrig avg_relevance < 0.65 Schwache semantische Übereinstimmung Beschreibung mit Synonymen und Beispielanfragen anreichern
call_rate_pct = 100% called_count = injected_count Tool wird zu selten injiziert – Schwelle zu hoch MIN_RELEVANCE senken oder Beschreibung anreichern
Praktischer Tipp: In der ersten Woche nach der Einführung von Tool RAG – protokollieren Sie alle query_snippet für Injects mit niedrigem Score (<0.70). Diese Anfragen zeigen, wo die semantische Suche das richtige Tool nicht findet. Fügen Sie diese Formulierungen direkt in die Beschreibung als Beispiele ein – und die Suchgenauigkeit wird ohne Codeänderungen steigen.

Vergleichstabelle der Ansätze

Drei Ansätze schließen sich nicht gegenseitig aus – in der Produktion wird oft eine Kombination verwendet: Routing für eine schnelle erste Filterung und Tool RAG für eine präzise Auswahl innerhalb einer Kategorie.

Ansatz Optimal für Latenz-Overhead Auswahlgenauigkeit Infrastruktur Komplexität
Alle Tools in der Anfrage bis zu 10 Tools 0ms 78-95% Nichts Minimal
Kategorien + Keyword-Routing 10-30 Tools, thematisch unterschiedlich ~1ms (CPU) 75-90% Nichts Niedrig
Tool RAG (Vektorsuche) 30+ Tools, semantisch ähnlich ~50-150ms 85-95% pgvector + Embedding-Modell Mittel
Tool RAG + Caching 30+ Tools, wiederkehrende Anfragen ~10-30ms 85-95% pgvector + Redis/ConcurrentHashMap Mittel+
Hybrid: Routing → Tool RAG 50+ Tools, gemischte Kategorien ~20-80ms 90-97% pgvector + Kategorien in der DB Hoch

Hybrid-Ansatz – wie er funktioniert

Für große Register (50+ Tools) ist die effektivste Kombination:

Benutzeranfrage
      ↓
[1] Keyword-Routing → Kategorie bestimmen (FINANCE / NEWS / RESEARCH)
    ~1ms, CPU-Operation
      ↓
[2] Tool RAG nur innerhalb der Kategorie
    Suche nach 10-15 Tools statt 50+
    ~30-50ms statt 100-150ms
      ↓
[3] Inject Top-3 Tools aus der Kategorie
    Genauigkeit ist höher, da die Suche in einem kleineren Raum stattfindet

// Beispiel:
// Anfrage: "wie ist der Aktienkurs von AAPL und was schreiben die Nachrichten über Apple?"
// Routing: FINANCE + NEWS → Suche nur in 15 Finanz- und 10 Nachrichten-Tools
// Tool RAG: AlphaVantageTool (0.91) + NewsApiTool (0.87) + TavilySearchTool (0.74)
// Inject: 3 Tools statt 50

Welche Strategie wählen – schnelle Entscheidung

tools <= 10         → Alle Tools in der Anfrage. Nicht verkomplizieren.
tools 10-30         → Kategorien + Routing. Eine Klasse, keine Infrastruktur.
tools 30-50         → Tool RAG mit pgvector. Wenn RAG vorhanden ist – einfach hinzuzufügen.
tools 50+           → Hybrid: Routing → Tool RAG. Maximale Genauigkeit.
tools 100+          → Hybrid unbedingt. Ohne ihn ist die Genauigkeit < 14%.
Reale Zahlen von RAG-MCP (Anthropic, 2025): bei 100+ Tools liefert der Basisansatz 13,62% Genauigkeit – der Agent wählt praktisch zufällig. Tool RAG liefert 43,13% – mehr als dreimal besser. Die Größe des Prompts wird um mehr als 50% reduziert. Zum Vergleich in Tokens: 100 Tools × ~200 Tokens = 20.000 Tokens für Beschreibungen in jeder Anfrage. Tool RAG injiziert 3 Tools → 600 Tokens. Einsparung: 97% der Tokens für Tools bei höherer Auswahlgenauigkeit. Wenn Sie für Tokens bezahlen – Tool RAG amortisiert sich sehr schnell.

Schlussfolgerungen

Als ich zum ersten Mal mit dem Problem der Skalierung von Tools in Agent Chat konfrontiert wurde – verstand ich nicht, warum der Agent bei Hinzufügen neuer Werkzeuge zu "stumpfen" begann. Der Code ist korrekt, die Beschreibungen sind geschrieben – aber die Auswahl wird schlechter. Es stellte sich heraus, dass dies kein Fehler, sondern ein Architekturproblem ist, das Tool RAG löst.

Gute Nachricht: Wenn Sie bereits pgvector und Spring AI haben – das Hinzufügen von Tool RAG dauert einen Tag Arbeit, nicht eine Woche. Es ist die gleiche Infrastruktur wie für Dokumente, nur angewendet auf die Beschreibungen von Werkzeugen.

Was ich aus der Implementierung mitgenommen habe:

  • Bis zu 10 Tools – eine gute Beschreibung und ein systemischer Prompt. Tool RAG ist nicht nötig. Komplizieren Sie es nicht vorzeitig
  • 10-30 Tools – Kategorien und Keyword-Routing. Eine Klasse, keine Infrastruktur, löst 80% der Skalierungsprobleme
  • 30+ Tools – Tool RAG ist obligatorisch. Ohne ihn verschlechtert sich die Genauigkeit nichtlinear – und Sie werden es nicht sofort bemerken
  • "Prompt Bloat" – ein reales und heimtückisches Problem – der Agent verschlechtert sich schrittweise mit jedem neuen Tool. Bei 100+ Tools sinkt die Genauigkeit auf 13% – der Agent wählt praktisch zufällig
  • Überwachung ist vom ersten Tag an obligatorisch – ohne Protokolle wissen Sie nicht, welche Tools tatsächlich aufgerufen werden, welche injiziert, aber ignoriert werden, und welche sicher deaktiviert werden können
  • Beschreibung – ist keine Dokumentation, sondern ein Prompt – je genauer die Beschreibung den tatsächlichen Benutzeranfragen entspricht, desto höher ist der Relevanz-Score und desto seltener wird eine erneute Abfrage benötigt. In der ersten Woche nach der Implementierung – analysieren Sie Protokolle und reichern Sie Beschreibungen an
Nächster Schritt in der Serie: Tool RAG löst das Problem der Werkzeugauswahl. Aber es gibt noch ein Problem – der Agent "vergisst" den Kontext zwischen den Sitzungen. Er hat eine Frage beantwortet, am nächsten Tag fragt derselbe Benutzer erneut – und der Agent erinnert sich nicht an das vorherige Gespräch. Über vier Arten von Agenten-Gedächtnis und wann welches verwendet werden soll – Gedächtnis des KI-Agenten – in-context, RAG, episodisch und semantisch.

Lesen Sie auch in der Serie:

Tool Use vs Function Calling – grundlegende Mechanik, bevor Sie skalieren.

Wie LLM entscheidet, wann ein Tool aufgerufen werden soll – wie man Beschreibungen schreibt, damit das Modell richtig wählt.

Grounding und Vertrauen in Quellen – was zu tun ist, nachdem ein Tool geantwortet hat.

Quellen: RAG-MCP: Mitigating Prompt Bloat in LLM Tool Selection (2025), vLLM Semantic Tool Selection (2025), Red Hat: Tool RAG – The Next Breakthrough (2026), BiasBusters: Tool Selection Bias in LLMs (2025), RAGFlow: From RAG to Context – 2025 Review, Spring AI Documentation