5 Pattern für KI-Agenten - detaillierte Erklärung mit Java-Beispielen

Zu diesem Artikel gibt es ein kurzes Video:YouTubeTikTok

1. Überblick

Wenn du ein LLM mit ein paar Zeilen Code kombinierst, damit es sich „selbst ausführt”, hast du einen rudimentären KI-Agenten — aber solche Agenten antworten meist einfach blind auf jede Frage, können sich selbst nicht korrigieren, wissen nicht, wie sie Tools aufrufen, und können Arbeit nicht in Schritte aufteilen. Dieser Artikel erklärt 5 Design-Pattern, die Agenten „intelligenter” machen: Reflection, Tool Use, Planning, Multi-Agent und ReAct. Jedes Pattern hat ein kurzes, funktionierendes Java-Beispiel (Spring AI) mit Erklärungen, wann man es verwenden sollte und wann nicht. Nach der Lektüre kannst du das richtige Pattern für dein Problem auswählen, anstatt alles in einen riesigen Prompt zu packen.

Vor den Details brauchst du diese Spring-AI-Abhängigkeit für die folgenden Beispiele:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini

Hinweis: Der Code in diesem Artikel veranschaulicht die Pattern-Struktur, ist aber nicht produktionsreif — du musst beim Deployment noch Retry, Timeout und Logging hinzufügen.

2. Pattern Reflection: Antworten selbst kritisieren und korrigieren

Reflection ist das einfachste Pattern: Der Agent erzeugt eine Antwort, nutzt dann dasselbe LLM (oder ein anderes) um diese zu bewerten und zu kritisieren, und korrigiert sie danach. Diese Schleife gleicht dem Schreiben eines Entwurfs und dann selbst nochmal alles durchzulesen, bevor man es versendet.

2.1. Arbeitsablauf

  1. Generate: Agent erzeugt die erste Antwort.
  2. Critique: Agent (oder ein anderer) spielt den „Reviewer”, zeigt Fehler auf.
  3. Revise: Agent korrigiert basierend auf der Kritik, kann 2–3 Schritte wiederholen, bis die Qualität stimmt.
@Service
public class ReflectionAgent {

    private final ChatClient chatClient;

    public ReflectionAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String answerWithReflection(String question, int maxRounds) {
        String draft = chatClient.prompt()
                .user("Beantworte kurz und bündig die Frage: " + question)
                .call()
                .content();

        for (int round = 0; round < maxRounds; round++) {
            String critique = chatClient.prompt()
                    .user("""
                        Du bist ein kritischer Reviewer. Zeige Fehler und Lücken
                        in der folgenden Antwort auf. Wenn sie gut ist, antworte mit "OK".
                        Frage: %s
                        Antwort: %s
                        """.formatted(question, draft))
                    .call()
                    .content();

            if (critique.strip().equalsIgnoreCase("OK")) {
                break;
            }

            draft = chatClient.prompt()
                    .user("Verbessere die Antwort basierend auf diesem Feedback: " + critique
                            + "\nAlte Antwort: " + draft)
                    .call()
                    .content();
        }
        return draft;
    }
}

answerWithReflection erzeugt einen Entwurf, dann schleift bis maxRounds Mal: ruft das LLM auf um als Reviewer zu fungieren, wenn der Reviewer „OK” antwortet dann stop, sonst eine neue Version erstellen. Das Ergebnis ist eine Antwort, die tendenziell präziser ist als das Aufrufen des LLM nur einmal.

Hinweis: Reflection kostet 2–3 Mal so viele LLM-Aufrufe wie normal. Nutze es nur für Aufgaben, die hohe Genauigkeit brauchen (Code-Review, Berechnungen, Verträge), nicht für normales Chat, da es langsam ist und Kosten verursacht.

3. Pattern Tool Use: Dem Agent erlauben, externe Tools zu nutzen

LLMs kennen die aktuelle Zeit nicht, können nicht auf Datenbanken zugreifen, können große Zahlen nicht korrekt berechnen. Mit dem Tool-Use-Pattern kann der Agent „Funktionen aufrufen”, wenn nötig — das LLM entscheidet, welches Tool es aufruft, der Code führt es aus und gibt das Ergebnis zurück, damit das LLM weiter überlegen kann.

@Component
public class OrderTools {

    private final OrderRepository orderRepository;

    public OrderTools(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Tool(description = "Nachschlagen des Bestellstatus anhand der Bestellnummer")
    public String getOrderStatus(String orderId) {
        return orderRepository.findById(orderId)
                .map(Order::getStatus)
                .orElse("Bestellung " + orderId + " nicht gefunden");
    }

    @Tool(description = "Berechne Gesamtpreis mit 10% Steuern")
    public double calculateTotal(double subtotal) {
        return subtotal * 1.1;
    }
}
@Service
public class CustomerSupportAgent {

    private final ChatClient chatClient;

    public CustomerSupportAgent(ChatClient.Builder builder, OrderTools orderTools) {
        this.chatClient = builder
                .defaultTools(orderTools)
                .build();
    }

    public String handle(String userMessage) {
        return chatClient.prompt()
                .user(userMessage)
                .call()
                .content();
    }
}

@Tool markiert eine Methode als Funktion, die das LLM aufrufen kann, die description hilft dem Modell zu verstehen, wann man sie nutzen sollte. Wenn defaultTools(orderTools) registriert ist, verwaltet Spring AI automatisch: sendet das Schema der Tools an das LLM, empfängt den Aufruf-Request (als JSON), führt die entsprechende Java-Methode aus, und sendet das Ergebnis zurück zum LLM, damit es die Antwort vervollständigt.

Beispiel: Wenn der User fragt „Wo ist meine Bestellung ORD-001?“, entscheidet der Agent selbst, getOrderStatus("ORD-001") aufzurufen — du musst keine Intent-Parsing-Logik selbst schreiben.

User: Wo ist meine Bestellung ORD-001?
Agent: Bestellung ORD-001 ist derzeit im Status "In Lieferung".

Einfacher Test mit JUnit 5 + Mockito um zu prüfen, dass das Tool richtig funktioniert, ohne echte LLM-Aufrufe:

class OrderToolsTest {

    private final OrderRepository repository = mock(OrderRepository.class);
    private final OrderTools tools = new OrderTools(repository);

    @Test
    void shouldReturnStatusWhenOrderExists() {
        when(repository.findById("ORD-001"))
                .thenReturn(Optional.of(new Order("ORD-001", "In Lieferung")));

        String result = tools.getOrderStatus("ORD-001");

        assertThat(result).isEqualTo("In Lieferung");
    }
}

Hinweis: Validiere Input immer bevor das Tool die Datenbank oder externe APIs anfasst — das LLM kann Parameter falsch weitergeben (z.B. leere orderId), wenn der Prompt nicht klar ist.

4. Pattern Planning: Große Aufgaben in kleinere Schritte unterteilen

Wenn die Anforderung komplex ist („Schreibe einen Bericht, der 3 Lieferanten vergleicht, mit Preistabelle und Empfehlung”), das LLM alles auf einmal zu tun ist fehleranfällig. Das Planning-Pattern teilt in zwei Rollen auf: Planner erstellt eine Schrittliste, Executor führt jeden Schritt nacheinander aus.

public record PlanStep(int order, String description) {}

@Service
public class PlannerAgent {

    private final ChatClient chatClient;

    public PlannerAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public List<PlanStep> createPlan(String goal) {
        String json = chatClient.prompt()
                .user("""
                    Teile das folgende Ziel in maximal 5 Schritte auf.
                    Gib ein JSON-Array zurück im Format [{"order":1,"description":"..."}]:
                    %s
                    """.formatted(goal))
                .call()
                .content();

        return parseSteps(json);
    }
}
@Service
public class ExecutorAgent {

    private final ChatClient chatClient;

    public ExecutorAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String runPlan(String goal, List<PlanStep> steps) {
        StringBuilder context = new StringBuilder();
        for (PlanStep step : steps) {
            String result = chatClient.prompt()
                    .user("Ziel: " + goal
                            + "\nBisheriger Kontext: " + context
                            + "\nFühre diesen Schritt durch: " + step.description())
                    .call()
                    .content();
            context.append("Schritt ").append(step.order())
                    .append(": ").append(result).append("\n");
        }
        return context.toString();
    }
}

PlannerAgent ist nur für die Erstellung der Schrittliste als JSON verantwortlich, ExecutorAgent führt jeden Schritt aus und sammelt den context, damit der nächste Schritt weiß, was davor schon getan wurde. Das Trennen von Planner/Executor hilft beim Debugging: Wenn das Ergebnis falsch ist, überprüfe zuerst den Plan (ist er sinnvoll), dann die einzelnen Schritte.

Plan für "Schreibe einen Bericht, der 3 Lieferanten vergleicht":
1. Sammle Preis- und Feature-Informationen von jedem Lieferanten
2. Erstelle eine Vergleichstabelle
3. Analysiere Vor- und Nachteile
4. Gib eine Empfehlung
5. Schreibe eine Executive Summary

Hinweis: Das Planning-Pattern hilft am meisten, wenn die Task mehrere voneinander abhängige Schritte hat. Bei einstufigen Fragen (z.B. „Fasse diesen Absatz zusammen”) macht ein Planner dich nur langsamer und verbraucht Token unnötig.

5. Pattern Multi-Agent: Spezialisierte Agenten arbeiten zusammen

Statt einem „Allround”-Agent teilt das Multi-Agent-Pattern die Arbeit auf spezialisierte Agenten auf: Research-Agent, Writer-Agent, Review-Agent… und ein Orchestrator koordiniert den Ablauf zwischen ihnen. Wie ein kleines Team, wo jeder eine Sache gut beherrscht.

@Service
public class ResearchAgent {
    private final ChatClient chatClient;

    public ResearchAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String research(String topic) {
        return chatClient.prompt()
                .user("Liste die 5 wichtigsten Informationen zu: " + topic)
                .call()
                .content();
    }
}

@Service
public class WriterAgent {
    private final ChatClient chatClient;

    public WriterAgent(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String write(String topic, String researchNotes) {
        return chatClient.prompt()
                .user("Schreibe einen 200-Wort-Artikel über '" + topic
                        + "' basierend auf diesen Daten: " + researchNotes)
                .call()
                .content();
    }
}
@Service
public class OrchestratorAgent {

    private final ResearchAgent researchAgent;
    private final WriterAgent writerAgent;

    public OrchestratorAgent(ResearchAgent researchAgent, WriterAgent writerAgent) {
        this.researchAgent = researchAgent;
        this.writerAgent = writerAgent;
    }

    public String produceArticle(String topic) {
        String notes = researchAgent.research(topic);
        return writerAgent.write(topic, notes);
    }
}

OrchestratorAgent ruft nicht selbst das LLM auf, sondern koordiniert nur: ruft ResearchAgent auf um Rohdaten zu holen, gibt sie an WriterAgent um einen vollständigen Artikel zu schreiben. Jeder Agent hat seinen eigenen Prompt, optimiert für genau eine Aufgabe, deshalb ist die Qualität jedes Teils normalerweise besser als wenn du alles in einen langen Prompt packst.

Test mit Mock um den Koordinierungs-Ablauf zu prüfen, ohne vom LLM abhängig zu sein:

class OrchestratorAgentTest {

    @Test
    void shouldPassResearchNotesToWriter() {
        ResearchAgent research = mock(ResearchAgent.class);
        WriterAgent writer = mock(WriterAgent.class);
        when(research.research("Spring AI")).thenReturn("note-A, note-B");
        when(writer.write("Spring AI", "note-A, note-B")).thenReturn("Vollständiger Artikel");

        OrchestratorAgent orchestrator = new OrchestratorAgent(research, writer);
        String article = orchestrator.produceArticle("Spring AI");

        assertThat(article).isEqualTo("Vollständiger Artikel");
        verify(writer).write("Spring AI", "note-A, note-B");
    }
}

Hinweis: Multi-Agent erhöht Latenz (mehrere aufeinanderfolgende LLM-Aufrufe) und Systemkomplexität. Teile Agents nur auf, wenn jeder wirklich einen unterschiedlichen Prompt/Kontext hat — zu viele kleine Agents (jeder nur eine Zeile) erzeugen nur Overhead ohne Nutzen.

6. Pattern ReAct: Überlegung und Aktion im Wechsel

ReAct (Reasoning + Acting) kombiniert Tool Use mit einer Überlegungsschleife: Agent denkt (Thought) → handelt (Action, z.B. ruft ein Tool auf) → beobachtet das Ergebnis (Observation) → wiederholt, bis genug Information da ist um zu antworten. Anders als reines Tool Use (1 Tool aufrufen, fertig), erlaubt ReAct dem Agent mehrere Tools nacheinander flexibel aufzurufen und den nächsten Schritt basierend auf bisherigen Observations zu entscheiden.

@Service
public class ReActAgent {

    private final ChatClient chatClient;
    private final List<Object> tools;

    public ReActAgent(ChatClient.Builder builder, WeatherTool weatherTool, SearchTool searchTool) {
        this.tools = List.of(weatherTool, searchTool);
        this.chatClient = builder.defaultTools(tools.toArray()).build();
    }

    public String solve(String question) {
        return chatClient.prompt()
                .system("""
                    Wenn du echte Daten brauchst, rufe ein geeignetes Tool auf bevor du antwortest.
                    Denke Schritt für Schritt: Thought -> Action -> Observation -> Thought...
                    Antworte erst am Ende wenn du genug Informationen hast.
                    """)
                .user(question)
                .call()
                .content();
    }
}

Bei der Frage „Sollte ich einen Regenschirm mitnehmen, wenn ich Danang am Wochenende besuche?“, kann der Agent eigenständig überlegen: rufe weatherTool.forecast("Danang") auf um die Vorhersage zu holen, beobachte das Ergebnis „70% Regenwahrscheinlichkeit”, und beantworte dann die Frage — diese ganze Thought/Action/Observation-Kette läuft innerhalb des Frameworks ab, du musst nicht manuell eine Schleife schreiben, weil Spring AI die Tool-Aufrufe-Schleife selbst verwaltet bis das Modell beschließt, zu stoppen.

Thought: Ich muss das Wetter in Danang für das Wochenende wissen.
Action: forecast("Danang")
Observation: 70% Regen, 18-22 Grad
Thought: Ein Regenschirm ist eine gute Idee.
Answer: Ja, du solltest einen Regenschirm mitnehmen, die Vorhersage zeigt 70% Regenwahrscheinlichkeit für Danang am Wochenende.

Hinweis: ReAct kann in eine „unendliche Schleife” geraten, wenn der Agent immer Tools aufruft ohne zu einer Antwort zu konvergieren. Setze immer eine maximale Anzahl Tool-Aufrufe (maxToolCalls oder Timeout) um unkontrollierte API-Kosten zu vermeiden.

7. Schneller Vergleich der 5 Pattern

Pattern Löst welches Problem Kosten (LLM-Aufrufe) Wann verwenden
Reflection Antwortqualität ist niedrig Hoch (2–3x) Task braucht Genauigkeit, kann selbst überprüft werden
Tool Use LLM kann nicht auf echte Daten/Systeme zugreifen Niedrig–mittel Datenbankabfrage, Berechnung, API-Aufrufe
Planning Task mit vielen Schritten, leicht etwas vergessen Mittel Komplexe Task mit klarer Struktur
Multi-Agent Ein Agent ist nicht spezialisiert genug Hoch Klare Pipeline (Research/Write/Review)
ReAct Brauche Tools, aber die Anzahl der Schritte ist unbekannt Variabel Flexible Informationssuche

In der Praxis schließen sich diese Patterns nicht gegenseitig aus — ein Customer-Support-Agent könnte Tool Use für Bestellabfragen nutzen, Planning für komplexe Anfragen und Reflection um empfindliche Antworten zu überprüfen bevor sie versendet werden.

8. Fazit

Die 5 Pattern oben — Reflection, Tool Use, Planning, Multi-Agent, ReAct — sind die Grundbausteine um intelligente KI-Agenten zu bauen, die sich selbst korrigieren, mit echten Systemen interagieren, komplexe Tasks bewältigen und flexibel Informationen sammeln können. Du brauchst nicht alle 5 für jeden usecase; fang mit Tool Use an (fast jeder Agent braucht das), dann füge Planning oder ReAct dazu wenn Tasks komplexer werden, und nutze Reflection/Multi-Agent nur wenn die zusätzlichen Kosten die Qualitätsgewinne wert sind. Das richtige Pattern für dein Problem zu wählen ist wichtiger als zu versuchen, alles in einen einzigen Agenten zu packen.

ai-agentspring-aijavallmdesign-pattern