Accueil / Articles

LangChain4j pour les équipes Java : outils, agents, compétences et communication A2A dans les services Spring

Comment intégrer les appels d’outils, les pipelines d’agents, les compétences sur demande, les workflows de pause et de reprise, ainsi que les agents A2A à distance dans un service Java existant à l’aide de LangChain4j.

Éditeur : REACTAPP.TOP7 min de lecture1718 mots

Sources: Java AI in 2026, LangChain4j Turns Agents into Ordinary Interfaces

Imaginez une entreprise qui fonctionne avec Java. Les commandes, l’inventaire, le contrôle des risques et le service client sont gérés depuis des années avec Spring Boot, et maintenant on cherche un agent de support capable de vérifier une commande, d’émettre un remboursement et de transférer le dossier à une autre personne. Les tutoriels en ligne commencent par FastAPI, passent ensuite par LangChain, puis par l’installation d’un environnement Python, tandis que le service Java reste inactif en arrière-plan.

LangChain4j intègre ces fonctionnalités dans un projet Java. Il ne s’agit pas d’une adaptation du framework Python, mais d’une refonte adaptée au mode de travail des développeurs Java. En bref : créer des agents avec cet outil signifie relier trois couches à un service déjà existant, à savoir les outils, les compétences et les agents, sans avoir besoin de changer de langage au préalable.

Outils : permettre au modèle d’appeler des méthodes métier

De nombreuses équipes se retrouvent bloquées ici, non pas sur le modèle lui-même, mais sur la manière dont il détermine quelle méthode Java appeler. L’annotation @Tool gère ce problème : en marquant une méthode avec elle, le framework la transforme en un JSON Schema que le modèle peut lire.

Commencez par les dépendances. Le module compatible avec OpenAI n’est qu’une des options possibles. Passer à Azure, Ollama ou Qwen signifie principalement remplacer l’implémentation du modèle de conversation, tandis que le code métier reste pratiquement inchangé.

<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-open-ai</artifactId>
  <version>1.19.0</version>
</dependency>
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-agentic</artifactId>
  <version>1.19.0</version>
</dependency>
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-skills</artifactId>
  <version>1.19.0-beta29</version>
</dependency>

Ensuite, il y a la catégorie des outils de type ordre. Rien d’exotique : il s’agit de méthodes Java ordinaires dont les paramètres et les valeurs de retour sont clairement décrits.

Le code nécessite Java 17 ou une version ultérieure. Dans un projet Maven simple, activez le paramètre de compilation -parameters ; sinon, un nom de paramètre tel qu’un marqueur de question pourrait être compilé en arg0 et ne signifier rien pour le modèle. Spring et Quarkus gèrent cela par défaut.

class OrderTools {

    @Tool("Look up the status of an order by ID")
    String queryOrder(@P("Order ID") String orderId) {
        return "Order " + orderId + " has shipped and should arrive tomorrow";
    }
    @Tool("Refund the specified order")
    String refund(@P("Order ID") String orderId, @P("Refund reason") String reason) {
        return "Order " + orderId + " refund initiated. Reason: " + reason;
    }
}

Ensuite, attachez les outils à une interface. Le client demande simplement, et le modèle décide s’il faut rechercher une commande ou lancer un remboursement.

interface CustomerServiceAssistant {

@UserMessage("You are an e-commerce customer service assistant. Answer questions about orders and refunds. The customer's question is {{question}}")
    String answer(String question);
}
CustomerServiceAssistant assistant = AiServices.builder(CustomerServiceAssistant.class)
        .chatModel(model)
        .tools(new OrderTools())
        .chatMemory(MessageWindowChatMemory.withMaxMessages(20))
        .build();
String reply = assistant.answer("Where is order 20260907001?");

C’est ici que les développeurs Java se sentent à l’aise, car écrire un point d’entrée de requête et exposer une méthode @Tool à un modèle s’avèrent être des pratiques très proches.

Agents : une interface et un champ d’application partagé

L’orchestration repose sur deux idées principales. L’annotation @Agent est appliquée sur une méthode plutôt que sur une classe, et un AgenticScope permet de transmettre les résultats d’un agent à l’autre.

Prenons trois petits agents. Ils ne s’appellent jamais directement les uns les autres. Chacun enregistre son résultat dans une variable partagée que le suivant récupère.

public interface DraftWriter {

@UserMessage("""
            You are a technical editor. Write an opening of no more than 200 words on the topic {{topic}}.
            Return only the opening, with no explanation.
            """)
    @Agent(outputKey = "draft", description = "Writes an opening based on a topic")
    String write(@V("topic") String topic);
}

public interface Sharpener {
    @UserMessage("""
            You are a reviewer. Make the following opening more specific and remove filler.
            The opening is {{draft}}.
            Return only the revised text.
            """)
    @Agent(outputKey = "draft", description = "Makes the opening more specific")
    String polish(@V("draft") String draft);
}

public interface TitlePicker {
    @UserMessage("""
            Write 3 headlines suitable for a technical blog for the following content, one per line.
            The content is {{draft}}.
            """)
    @Agent(outputKey = "titles", description = "Writes headlines for the content")
    String pickTitle(@V("draft") String draft);
}

Chaque agent stocke sa sortie sous la variable nommée outputKey. L’agent suivant lit cette variable via @V, et la chaîne devient un pipeline.

DraftWriter writer = AgenticServices
        .agentBuilder(DraftWriter.class)
        .chatModel(model)
        .build();


Sharpener sharpener = AgenticServices
        .agentBuilder(Sharpener.class)
        .chatModel(model)
        .build();
TitlePicker titlePicker = AgenticServices
        .agentBuilder(TitlePicker.class)
        .chatModel(model)
        .build();
UntypedAgent editor = AgenticServices
        .sequenceBuilder()
        .subAgents(writer, sharpener, titlePicker)
        .outputKey("titles")
        .build();
Object result = editor.invoke(Map.of("topic", "Building agents with LangChain4j"));

L’avantage est que l’orchestration se transforme en composants réutilisables. La rédaction, le polissage et l’attribution de titres peuvent être testés séparément et réordonnés. Les modèles de boucle, parallèles et conditionnels sont également fournis avec le framework ; afin d’améliorer continuellement le résultat jusqu’à ce qu’il atteigne un certain niveau de qualité, utilisez loopBuilder avec un agent d’évaluation et une condition de sortie.

Compétences : instructions chargées sur demande

Les compétences LangChain4j ressemblent davantage aux paquets de compétences de Claude Code qu’à toute autre forme d’annotation. Une compétence est un répertoire contenant un fichier SKILL.md. Par défaut, le modèle ne voit pas le texte complet. Ce n’est que lorsqu’il juge qu’une tâche correspond qu’il appelle activate_skill pour charger le contenu, ce qui permet de maintenir un contexte de départ réduit et de rendre chaque appel moins coûteux.

Commencez par créer un fichier de compétence :

skills/refund/SKILL.md

---
name: refund
description: Handle refund requests. Look up the order, check the conditions, then either refund or escalate to a human.
---
When a customer requests a refund:
1. First call queryOrder to check the order status.
2. Only call refund when the order has shipped or is complete.
3. If the order is still awaiting payment, tell the customer to cancel it.
4. If the amount exceeds 500, escalate to a human instead of refunding directly.

Ensuite, associez les compétences au service. Le mode outil est la méthode officiellement recommandée, et le modèle ne peut accéder qu’aux outils que vous avez enregistrés, sans accès au système de fichiers ni à la shell.

List<FileSystemSkill> loadedSkills = FileSystemSkillLoader.loadSkills(Path.of("skills"));
Skills skillSet = Skills.from(loadedSkills);

CustomerServiceAssistant assistant = AiServices.builder(CustomerServiceAssistant.class)
        .chatModel(model)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(20))
        .tools(new OrderTools())
        .toolProvider(skillSet.toolProvider())
        .systemMessage("You have access to the following skills. When a request relates to one of them, activate it first.\n"
                + skillSet.formatAvailableSkills())
        .build();

L’alternative est le mode shell, qui donne au modèle un outil run_shell_command. Il est pratique pour les prototypes, mais en production il nécessite prudence : les commandes s’exécutent directement sur l’hôte sans aucun environnement isolé. À moins que les entrées ne soient entièrement fiables, il vaut mieux l’éviter.

Pause et reprise de workflows longs

Dans un environnement d’entreprise, la situation difficile ne se résout pas par une réponse simple, mais par un workflow interrompu au milieu de dizaines d’étapes. La persistance et l’approbation humaine dans LangChain4j visent précisément cela. Pour que un workflow puisse être récupéré, son interface doit étendre AgenticScopeAccess et ajouter @MemoryId sur la méthode, ce qui active un espace de travail persistant.

public interface OrderWorkflow extends AgenticScopeAccess {

    @Agent
    String processOrder(@MemoryId String orderId, @V("order") String orderDetails);
}

Ensuite, on enchaîne trois étapes : la validation, l’approbation par un humain et l’expédition. L’étape d’approbation renvoie une SuspendedResponse ; par conséquent, le système enregistre l’état actuel et libère le thread au lieu de le conserver.

AgenticScopeAction validateOrder = AgenticServices.agentAction(scope -> {
    String order = scope.readState("order", "");
    scope.writeState("validated_order", "VALIDATED: " + order);
});


HumanInTheLoop approvalGate = AgenticServices.humanInTheLoopBuilder()
        .description("Large orders require manual approval")
        .outputKey("approval")
        .responseProvider(scope -> new SuspendedResponse<>("manager-approval"))
        .build();
AgenticScopeAction shipOrder = AgenticServices.agentAction(scope -> {
    String validated = scope.readState("validated_order", "");
    String approval = scope.readState("approval", "");
    scope.writeState("result", "Order processed " + approval);
});
OrderWorkflow workflow = AgenticServices.sequenceBuilder(OrderWorkflow.class)
        .subAgents(validateOrder, approvalGate, shipOrder)
        .outputKey("result")
        .build();

L’appelant gère la suspension à l’aide de ResultWithAgenticScope. Une fois que le gestionnaire a donné son accord, le flux de travail reprend à son point de contrôle précédent.

ResultWithAgenticScope<String> result =
        workflow.processOrder("order-10001", "1000 units");

if (result.suspended()) {
    result = result.completePendingResponse("Manager approved");
}

Un état en mémoire seul ne survivra pas à une redémarrage, il faut donc mettre en œuvre un AgenticScopeStore soutenu par Redis, une base de données ou le système de fichiers. Le framework effectue des points de contrôle après que chaque agent a terminé son travail et enregistre la position du planificateur, de sorte que les étapes déjà effectuées ne sont pas répétées lors de la récupération.

AgenticScopePersister.setStore(new MyAgenticScopeStore());

Il semble ordinaire, mais c’est précisément cela qui distingue un agent de démonstration d’un agent en production. Les pannes, les redémarrages et les retards d’approbation sont des événements courants, et non des cas particuliers.

A2A : des agents distants traités comme des éléments locaux

Dans différents équipes, l’agent de gestion des risques peut fonctionner au sein d’un service tandis que le service client appartient à un autre groupe. A2A sert de lien entre ces agents distants : en intégrant le module langchain4j-agentic-a2a, le code d’orchestration traite un agent éloigné exactement comme s’il se trouvait dans le même processus.

<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-agentic-a2a</artifactId>
  <version>1.19.0</version>
</dependency>

Déclarez une interface locale qui fait référence au service A2A distant.

public interface RemoteRiskAgent {


@Agent
    String check(@V("transaction") String transaction);
}
UntypedAgent riskAgent = AgenticServices
        .a2aBuilder("https://risk.internal/a2a", RemoteRiskAgent.class)
        .outputKey("risk_result")
        .build();

Pour les conversations à plusieurs tours, écrivez contextId et taskId à nouveau dans le scope partagé : le serveur les génère au premier tour et le client les renvoie au deuxième.

public interface ChatAgent {


@A2AClientAgent(a2aServerUrl = "http://localhost:8080", outputKey = "response")
    ResultWithAgenticScope<String> chat(
            @V("question") String question,
            @A2AContextId @V("contextId") String contextId,
            @A2ATaskId @V("taskId") String taskId);
}
ResultWithAgenticScope<String> first = chatAgent.chat("Hello", null, null);
String contextId = (String) first.agenticScope().readState("contextId");
String taskId = (String) first.agenticScope().readState("taskId");
ResultWithAgenticScope<String> second = chatAgent.chat("Continue", contextId, taskId);

La coopération entre plusieurs agents n’exige donc plus que tout soit regroupé dans un seul répertoire. Les équipes peuvent gérer et exécuter le service A2A pour leurs agents respectifs, tandis qu’une couche supérieure assemble les différentes composantes.

Conseils pour les équipes Java

Commencez par quelque chose de simple. Évitez l’agent d’opérations entièrement autonome et créez d’abord un agent de soutien capable de vérifier les commandes et d’émettre des remboursements, avant d’ajouter progressivement des compétences et une validation humaine.

Séparez les outils des compétences. Les outils représentent ce que le code est capable de faire. Les compétences, quant à elles, correspondent aux procédures étape par étape présentées au modèle. Gardez les outils petits et stables, et permettez aux compétences de changer fréquemment au gré des règles commerciales, sans que l’un ne perturbe l’autre.

Intégrez dès le début une capacité de persistance pour les workflows critiques. Si un flux implique des attentes de validation, des appels à distance ou des étapes durentant plus de quelques dizaines de secondes, configurez un AgenticScopeStore à l’avance, car ajouter des mécanismes de récupération ultérieurement coûte généralement plus cher.

LangChain4j offre l’avantage de permettre aux agents de reprendre le travail typique en Java. Aucune fonctionnalité particulièrement spectaculaire ne le distingue. Les interfaces, l’injection de dépendances, les tests, l’emballage et la surveillance restent tous en place, et l’IA devient simplement une capacité supplémentaire plutôt qu’une raison de tout reconstruire.