5 Patterns d'AI Agent — explications détaillées et exemples Java

Cet article a une courte vidéo :YouTubeTikTok

1. Aperçu

Quand vous combinez un LLM avec quelques lignes de code pour qu’il « s’exécute tout seul », vous disposez d’un AI Agent basique — mais un agent de ce type répond généralement à chaque question sans pouvoir se corriger, appeler des outils, ou diviser le travail. Cet article explique 5 patterns de conception qui rendent un agent « plus intelligent » : Reflection, Tool Use, Planning, Multi-Agent et ReAct. Chaque pattern dispose d’un exemple de code Java court et fonctionnel (Spring AI), avec des explications sur quand l’utiliser ou non. À la fin, vous pourrez choisir le bon pattern pour votre cas d’usage, au lieu de tout fourrer dans une énorme prompt.

Avant d’entrer dans les détails, vous aurez besoin de la dépendance Spring AI pour les exemples ci-dessous :

<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

Remarque : Le code de cet article illustre la structure du pattern, pas une solution prête pour la production — vous devez ajouter retry, timeout, logging lors du déploiement réel.

2. Pattern Reflection : auto-critique et correction de réponse

Reflection est le pattern le plus simple : l’agent génère une réponse, puis utilise le même LLM (ou un autre) pour évaluer/critiquer cette réponse, et la corrige ensuite. Cette boucle ressemble à vous qui relisez votre brouillon avant d’envoyer.

2.1. Flux d’exécution

  1. Generate : l’agent génère la première réponse.
  2. Critique : l’agent (ou un autre agent) joue le rôle de « revieweur » et signale les erreurs.
  3. Revise : l’agent corrige la réponse en se basant sur la critique, on peut répéter les étapes 2–3 jusqu’à atteindre la qualité désirée.
@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("Répondez brièvement à la question : " + question)
                .call()
                .content();

        for (int round = 0; round < maxRounds; round++) {
            String critique = chatClient.prompt()
                    .user("""
                        Vous êtes un reviewer exigeant. Pointez les erreurs,
                        les lacunes dans la réponse ci-dessous. Si elle est
                        bonne, retournez "OK".
                        Question : %s
                        Réponse : %s
                        """.formatted(question, draft))
                    .call()
                    .content();

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

            draft = chatClient.prompt()
                    .user("Corrigez la réponse en vous basant sur le retour : " + critique
                            + "\nAncienne réponse : " + draft)
                    .call()
                    .content();
        }
        return draft;
    }
}

answerWithReflection génère un brouillon d’abord, puis boucle jusqu’à maxRounds fois : l’agent appelle le LLM qui joue revieweur pour évaluer, si le reviewer retourne “OK” on arrête, sinon on génère une version corrigée. Le résultat est une réponse qui tend à être plus exacte qu’un simple appel au LLM une seule fois.

Remarque : Reflection consomme 2–3 fois plus d’appels au LLM que l’approche normale. À n’utiliser que pour les tâches qui demandent une grande précision (revue de code, calculs, rédaction de contrats), pas pour la discussion ordinaire car c’est lent et coûteux.

3. Pattern Tool Use : permettre à l’agent d’appeler des outils externes

Un LLM ne connaît pas l’heure actuelle, ne peut pas accéder à votre base de données, ne calcule pas les grands nombres avec précision. Le pattern Tool Use donne à l’agent la capacité « d’appeler des fonctions » quand il en a besoin — le LLM décide quel outil appeler, votre code exécute cet outil, puis retourne le résultat au LLM pour continuer son raisonnement.

@Component
public class OrderTools {

    private final OrderRepository orderRepository;

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

    @Tool(description = "Consulte le statut d'une commande par son ID")
    public String getOrderStatus(String orderId) {
        return orderRepository.findById(orderId)
                .map(Order::getStatus)
                .orElse("Commande non trouvée " + orderId);
    }

    @Tool(description = "Calcule le montant total avec la TVA de 10%")
    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 marque une méthode comme fonction que le LLM peut appeler, la description aide le modèle à comprendre quand l’utiliser. Quand defaultTools(orderTools) est enregistré, Spring AI fait automatiquement : envoie le schéma des outils au LLM, reçoit la demande d’appel d’outil (en JSON), exécute la méthode Java correspondante, puis envoie le résultat au LLM pour finaliser la réponse.

Par exemple, si l’utilisateur demande « Où en est ma commande ORD-001 ? », l’agent décide automatiquement d’appeler getOrderStatus("ORD-001") sans que vous ayez à coder la logique de parsing d’intention manuellement.

Utilisateur : Où en est ma commande ORD-001 ?
Agent : Votre commande ORD-001 est actuellement en cours de livraison.

Test simple avec JUnit 5 + Mockito pour vérifier que l’outil fonctionne correctement, sans appeler le LLM réel :

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", "En cours de livraison")));

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

        assertThat(result).isEqualTo("En cours de livraison");
    }
}

Remarque : Validez toujours l’input avant que l’outil ne touche à la base de données ou n’appelle une API réelle — le LLM peut passer des paramètres mal formés (par exemple orderId vide) si la prompt n’est pas explicite.

4. Pattern Planning : diviser une grosse tâche en petites étapes

Quand la demande est complexe (« écrivez un rapport comparatif de 3 fournisseurs, avec tableau de prix et recommandations »), demander au LLM de tout faire d’un coup risque d’oublier des étapes. Le pattern Planning sépare en deux rôles : un Planner qui établit la liste des étapes, un Executor qui les exécute séquentiellement.

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("""
                    Divisez l'objectif ci-dessous en maximum 5 étapes à exécuter,
                    retournez un tableau JSON de la forme [{"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("Objectif : " + goal
                            + "\nContexte du travail fait : " + context
                            + "\nExécutez l'étape : " + step.description())
                    .call()
                    .content();
            context.append("Étape ").append(step.order())
                    .append(" : ").append(result).append("\n");
        }
        return context.toString();
    }
}

PlannerAgent génère seulement la liste des étapes en JSON, ExecutorAgent exécute chaque étape et accumule un context pour que l’étape suivante sache ce qui a déjà été fait. Séparer planner/executor facilite le debug : si le résultat est mauvais, on vérifie d’abord le plan (a-t-il du sens ?) avant d’examiner chaque étape.

Plan pour « écrire un rapport comparatif de 3 fournisseurs » :
1. Collecter les infos de prix et caractéristiques de chaque fournisseur
2. Construire un tableau de comparaison
3. Analyser les avantages et inconvénients
4. Formuler une recommandation
5. Rédiger le résumé exécutif

Remarque : Le pattern Planning est efficace quand la tâche a plusieurs étapes qui dépendent l’une de l’autre. Pour une question simple (« résumez ce paragraphe »), ajouter un planner ne fait que ralentir et gaspiller des tokens inutilement.

5. Pattern Multi-Agent : plusieurs agents spécialisés en collaboration

Au lieu d’un seul agent « touche-à-tout », le pattern Multi-Agent distribue le travail à des agents spécialisés : agent de recherche, agent de rédaction, agent de révision… tandis qu’un Orchestrator coordonne le flux entre eux. Comme une petite équipe où chacun maîtrise son domaine.

@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("Listez les 5 informations les plus importantes sur : " + 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("Écrivez un article de 200 mots sur '" + topic
                        + "' en vous basant sur ces données : " + 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 n’appelle pas directement le LLM, il coordonne : appelle ResearchAgent pour obtenir les données brutes, puis les passe à WriterAgent pour rédiger l’article complet. Chaque agent dispose de sa propre prompt, optimisé pour une seule tâche, donc la qualité de chaque partie tend à être meilleure que si on tout mettait dans une longue prompt unique.

Test avec mock pour vérifier que l’orchestration fonctionne, sans dépendre du LLM :

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("Article complet");

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

        assertThat(article).isEqualTo("Article complet");
        verify(writer).write("Spring AI", "note-A, note-B");
    }
}

Remarque : Multi-Agent augmente la latence (plusieurs appels au LLM en série) et la complexité du système. À n’utiliser que si chaque agent a vraiment une prompt/contexte différent et clairement distinct — découper trop finement (un agent par phrase) crée juste de l’overhead inutile.

6. Pattern ReAct : alternance entre réflexion et action

ReAct (Reasoning + Acting) combine Tool Use et une boucle de raisonnement : l’agent pense (Thought) → agit (Action, par exemple appel d’outil) → observe le résultat (Observation) → recommence jusqu’à avoir assez d’infos pour répondre. Différent de Tool Use simple (appeler 1 outil puis répondre), ReAct permet à l’agent d’appeler plusieurs outils dans un ordre flexible, en décidant chaque étape suivante selon l’observation précédente.

@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("""
                    Quand vous avez besoin de données réelles, appelez d'abord l'outil adéquat avant de répondre.
                    Raisonnez étape par étape : Thought -> Action -> Observation -> Thought...
                    Ne répondez finalement que quand vous avez assez d'informations.
                    """)
                .user(question)
                .call()
                .content();
    }
}

Avec la question « Faut-il que j’apporte un imperméable en allant à Dalat ce weekend ? », l’agent peut se demander : appelez weatherTool.forecast("Dalat") pour obtenir les prévisions, observez « pluie 70% », puis répondez — toute la chaîne Thought/Action/Observation se déroule à l’intérieur du framework sans que vous ayez à écrire manuellement la boucle, car Spring AI gère automatiquement les appels d’outil répétés jusqu’à ce que le modèle décide d’arrêter.

Thought : J'ai besoin de connaître la météo à Dalat ce weekend.
Action : forecast("Dalat")
Observation : Pluie 70%, 18-22°C
Thought : Je dois recommander un imperméable.
Answer : Oui, apportez un imperméable, la prévision indique 70% de pluie à Dalat ce weekend.

Remarque : ReAct peut facilement boucler indéfiniment si l’agent appelle des outils sans converger vers une réponse. Fixez toujours une limite du nombre d’appels d’outil maximum (maxToolCalls ou timeout) pour éviter de gaspiller des crédits API de façon incontrôlée.

7. Comparaison rapide des 5 patterns

Pattern Résout quel problème Coût (nombre d’appels LLM) Quand l’utiliser
Reflection Faible qualité de réponse Élevé (2–3x) Tâche nécessitant une grande précision, peut se vérifier seul
Tool Use Le LLM n’a pas accès aux données/systèmes réels Bas–moyen Besoin de consultation, calcul, appel API
Planning Tâche complexe, risque d’oublier des étapes Moyen Tâche complexe avec structure claire
Multi-Agent Un agent seul ne suffit pas pour tous les rôles Élevé Pipeline aux rôles bien définis (recherche/rédaction/révision)
ReAct Besoin d’outils mais nombre d’étapes inconnu à l’avance Variable Tâche nécessitant une recherche flexible d’information

En pratique, ces patterns ne s’excluent pas — un agent de support client peut utiliser Tool Use pour consulter les commandes, Planning pour gérer les demandes complexes, et Reflection pour vérifier ses réponses sensibles avant envoi.

8. Conclusion

Les 5 patterns ci-dessus — Reflection, Tool Use, Planning, Multi-Agent, ReAct — sont les briques fondamentales pour construire un AI Agent capable de se corriger, d’interagir avec des systèmes réels, de gérer des tâches complexes et de chercher flexiblement des informations. Vous n’avez pas besoin de tous les 5 pour chaque cas — commencez par Tool Use (presque tout agent en a besoin), puis ajoutez Planning ou ReAct quand la tâche se complexifie, et ne recourez à Reflection ou Multi-Agent que si le coût supplémentaire vaut la peine. Choisir le bon pattern selon votre problème importe plus que d’essayer d’en fourrer tout plein dans un seul agent monstrueux.

ai-agentspring-aijavallmdesign-pattern