IA agentique en Java : construire des agents autonomes avec Spring AI
Agents IA autonomes en Java : architecture, tool use, orchestration multi-agents avec Spring AI et LangChain4j. Exemples concrets pour passer du chatbot à l'agent.
En 2024, on parlait de LLMs et de chatbots. En 2026, la tendance est aux agents autonomes : des systèmes qui planifient, agissent, observent et s’adaptent sans intervention humaine à chaque étape. Plus d’un quart du code produit chez Google est généré par des agents IA. Cette technologie sort des labos et entre en production.
Voici comment construire un agent IA robuste en Java.
Ce qui distingue un agent d’un simple LLM
Un LLM répond à un prompt. Un agent utilise un LLM comme moteur de raisonnement, mais il peut :
- Appeler des outils (fonctions Java, APIs, requêtes SQL)
- Observer les résultats de ces appels
- Planifier la prochaine action en fonction de ce qu’il a observé
- Itérer jusqu’à atteindre l’objectif
Le pattern dominant est ReAct (Reasoning + Acting) :
Thought: Je dois trouver le prix du produit X
Action: search_product(name="X")
Observation: {"id": 42, "price": 19.99, "stock": 12}
Thought: J'ai le prix. Je dois maintenant vérifier si le stock est suffisant.
Action: check_availability(product_id=42, quantity=5)
Observation: {"available": true}
Thought: Je peux passer la commande.
Action: create_order(product_id=42, quantity=5)
Final Answer: Commande créée pour 5 unités de X à 19,99 €.
Chaque itération est un appel LLM. L’agent décide seul quand s’arrêter.
Dépendances Maven
<!-- Spring AI avec Claude (Anthropic) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Ou LangChain4j (plus de features agents) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
<version>0.32.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.32.0</version>
</dependency>
Construire un agent avec LangChain4j
1. Définir les outils
Les outils sont de simples méthodes Java annotées. Le LLM décide quand les appeler en lisant leurs descriptions.
@Component
public class ProductTools {
@Autowired
private ProductRepository productRepository;
@Tool("Recherche un produit par nom. Retourne son ID, son prix et son stock.")
public ProductInfo searchProduct(@P("Nom du produit à rechercher") String name) {
return productRepository.findByName(name)
.map(p -> new ProductInfo(p.getId(), p.getName(), p.getPrice(), p.getStock()))
.orElseThrow(() -> new ToolException("Produit '" + name + "' introuvable"));
}
@Tool("Vérifie si une quantité donnée est disponible en stock pour un produit.")
public AvailabilityResult checkAvailability(
@P("ID du produit") Long productId,
@P("Quantité souhaitée") int quantity) {
Product p = productRepository.findById(productId)
.orElseThrow(() -> new ToolException("Produit ID " + productId + " introuvable"));
boolean available = p.getStock() >= quantity;
return new AvailabilityResult(available, p.getStock());
}
@Tool("Crée une commande pour un produit. Requiert la confirmation que le stock est disponible.")
public OrderResult createOrder(
@P("ID du produit") Long productId,
@P("Quantité à commander") int quantity,
@P("Email du client") String customerEmail) {
// Logique métier : vérification, décrémentation stock, envoi confirmation
Order order = orderService.create(productId, quantity, customerEmail);
return new OrderResult(order.getId(), order.getTotalPrice());
}
}
2. Définir l’interface agent
public interface ShoppingAgent {
@SystemMessage("""
Tu es un assistant e-commerce. Tu aides les clients à trouver des produits,
vérifier leur disponibilité et passer des commandes.
Utilise toujours les outils disponibles pour obtenir des informations réelles.
Ne confirme jamais une commande sans avoir vérifié le stock au préalable.
""")
String chat(@MemoryId String sessionId, @UserMessage String userMessage);
}
3. Assembler l’agent
@Configuration
public class AgentConfig {
@Bean
public ShoppingAgent shoppingAgent(ProductTools tools) {
return AiServices.builder(ShoppingAgent.class)
.chatLanguageModel(
AnthropicChatModel.builder()
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
.modelName("claude-sonnet-4-6")
.maxTokens(4096)
.build()
)
.tools(tools)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.withMaxMessages(20)
)
.build();
}
}
4. Exposer via un endpoint REST
@RestController
@RequestMapping("/api/agent")
public class AgentController {
@Autowired
private ShoppingAgent agent;
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(
@RequestHeader("X-Session-Id") String sessionId,
@RequestBody ChatRequest request) {
String response = agent.chat(sessionId, request.message());
return ResponseEntity.ok(new ChatResponse(response));
}
}
Test :
curl -X POST http://localhost:8080/api/agent/chat \
-H "Content-Type: application/json" \
-H "X-Session-Id: user-123" \
-d '{"message": "Je veux commander 3 unités du produit Java Handbook"}'
L’agent va automatiquement :
- Chercher le produit
search_product("Java Handbook") - Vérifier le stock
check_availability(id, 3) - Créer la commande si disponible
create_order(id, 3, email) - Répondre à l’utilisateur avec le récapitulatif
Gestion des erreurs et des cas limites
Un agent en production doit être robuste. Les LLMs peuvent tenter d’appeler des outils avec des paramètres incorrects, ou boucler.
@Tool("Envoie un email de confirmation au client.")
public EmailResult sendConfirmation(@P("Adresse email") String email,
@P("Contenu du message") String message) {
// Validation : jamais faire confiance à ce que le LLM passe
if (!email.matches("^[^@]+@[^@]+\\.[^@]+$")) {
throw new ToolException("Email invalide : " + email);
}
if (message.length() > 2000) {
throw new ToolException("Message trop long (max 2000 caractères)");
}
// Envoi réel
emailService.send(email, message);
return new EmailResult(true, "Email envoyé à " + email);
}
Timeout sur le cycle agent :
@Bean
public ShoppingAgent shoppingAgent(ProductTools tools) {
return AiServices.builder(ShoppingAgent.class)
.chatLanguageModel(
AnthropicChatModel.builder()
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
.modelName("claude-sonnet-4-6")
.maxTokens(4096)
.timeout(Duration.ofSeconds(60)) // timeout global
.build()
)
.tools(tools)
.maxSequentialToolCalls(10) // max 10 appels d'outils par tour
.build();
}
Agents multi-étapes avec Spring AI
Spring AI (depuis 1.0) propose une API ChatClient fluente qui permet de chaîner des appels avec des outils déclarés comme des @Bean :
@Service
public class AnalysisAgentService {
private final ChatClient chatClient;
public AnalysisAgentService(ChatClient.Builder builder,
DataAnalysisTools tools) {
this.chatClient = builder
.defaultSystem("Tu es un analyste de données. Utilise les outils pour répondre.")
.defaultFunctions("fetchSalesData", "computeStats", "generateReport")
.build();
}
public String analyze(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
@Component
public class DataAnalysisTools {
@Bean
@Description("Récupère les données de ventes pour une période donnée")
public Function<SalesRequest, SalesData> fetchSalesData() {
return req -> salesRepository.findByPeriod(req.from(), req.to());
}
@Bean
@Description("Calcule des statistiques (moyenne, max, tendance) sur un jeu de données")
public Function<SalesData, Statistics> computeStats() {
return data -> statisticsService.compute(data);
}
@Bean
@Description("Génère un rapport structuré à partir des statistiques")
public Function<Statistics, Report> generateReport() {
return stats -> reportGenerator.generate(stats);
}
}
Orchestration multi-agents
Pour les tâches complexes, plusieurs agents spécialisés se coordonnent. Un agent orchestrateur délègue aux agents spécialistes.
@Service
public class OrchestratorAgent {
private final ShoppingAgent shoppingAgent;
private final SupportAgent supportAgent;
private final AnalysisAgent analysisAgent;
public String route(String sessionId, String userMessage) {
// L'orchestrateur classe l'intention et délègue
String intent = classifyIntent(userMessage);
return switch (intent) {
case "ORDER" -> shoppingAgent.chat(sessionId, userMessage);
case "SUPPORT" -> supportAgent.chat(sessionId, userMessage);
case "ANALYSIS" -> analysisAgent.analyze(userMessage);
default -> "Je ne comprends pas votre demande.";
};
}
private String classifyIntent(String message) {
// Appel LLM léger (Haiku) pour la classification
return classifierModel.classify(message, List.of("ORDER", "SUPPORT", "ANALYSIS", "OTHER"));
}
}
Mémoire de l’agent
Un agent sans mémoire repart de zéro à chaque conversation. LangChain4j propose plusieurs niveaux :
// Mémoire fenêtre : garde les N derniers échanges (adapté à la plupart des cas)
@Bean
public ShoppingAgent shoppingAgent(ProductTools tools) {
return AiServices.builder(ShoppingAgent.class)
.chatLanguageModel(model())
.tools(tools)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.withMaxMessages(20)
)
.build();
}
// Mémoire persistante : sauvegarde en base de données entre les sessions
@Bean
public ShoppingAgent shoppingAgentWithPersistence(ProductTools tools,
ChatMemoryStore store) {
return AiServices.builder(ShoppingAgent.class)
.chatLanguageModel(model())
.tools(tools)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(50)
.chatMemoryStore(store) // implémentation Redis, PostgreSQL, etc.
.build()
)
.build();
}
Pour les agents à longue durée de vie, il est utile de résumer périodiquement le contexte afin de ne pas dépasser la fenêtre de contexte du LLM :
// Résumé automatique de la mémoire ancienne
.chatMemoryProvider(memoryId ->
SummarizingChatMemory.builder()
.id(memoryId)
.maxMessages(100)
.chatLanguageModel(fastModel()) // modèle léger pour le résumé
.build()
)
RAG : donner à l’agent accès à vos données
Le RAG (Retrieval Augmented Generation) permet à l’agent d’interroger une base de connaissances interne plutôt que de répondre uniquement depuis sa formation.
// 1. Indexation des documents (une fois, ou à chaque mise à jour)
@Service
public class DocumentIndexer {
@Autowired
private EmbeddingStore<TextSegment> embeddingStore;
@Autowired
private EmbeddingModel embeddingModel;
public void index(Path documentPath) {
Document doc = FileSystemDocumentLoader.loadDocument(documentPath);
// Découpage en segments
DocumentSplitter splitter = DocumentSplitters.recursive(500, 50);
List<TextSegment> segments = splitter.split(doc);
// Calcul des embeddings et stockage
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
embeddingStore.addAll(embeddings, segments);
}
}
// 2. Outil RAG exposé à l'agent
@Component
public class KnowledgeBaseTools {
@Autowired
private EmbeddingStore<TextSegment> embeddingStore;
@Autowired
private EmbeddingModel embeddingModel;
@Tool("Recherche dans la base de connaissances interne. Utilise cet outil avant de répondre à toute question sur nos produits, politiques ou procédures.")
public String searchKnowledgeBase(
@P("Question ou sujet à rechercher") String query) {
Embedding queryEmbedding = embeddingModel.embed(query).content();
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(
queryEmbedding, 3, 0.75 // top 3, seuil de similarité 75%
);
if (matches.isEmpty()) {
return "Aucun résultat pertinent trouvé dans la base de connaissances.";
}
return matches.stream()
.map(m -> m.embedded().text())
.collect(Collectors.joining("\n\n---\n\n"));
}
}
En pratique, combiner RAG + outils d’action (API, base de données) donne un agent capable à la fois de répondre sur des connaissances métier et d’agir sur les systèmes.
Observabilité et debugging
Sans traces, un agent est une boîte noire. Chaque itération (appel LLM, appel outil, réponse) doit être loguée.
// Listener sur chaque interaction de l'agent
@Component
public class AgentAuditListener implements AiServiceListener {
private static final Logger log = LoggerFactory.getLogger(AgentAuditListener.class);
@Autowired
private AgentTraceRepository traceRepository;
@Override
public void onRequest(AiServiceRequestContext context) {
log.info("[AGENT] Requête LLM — session={} messages={}",
context.memoryId(), context.messages().size());
}
@Override
public void onToolCall(ToolCallContext context) {
log.info("[AGENT] Outil appelé — tool={} args={}",
context.toolName(), context.arguments());
traceRepository.save(new AgentTrace(
context.memoryId(), "TOOL_CALL",
context.toolName(), context.arguments(), Instant.now()
));
}
@Override
public void onToolResult(ToolResultContext context) {
log.info("[AGENT] Résultat outil — tool={} result={}",
context.toolName(), context.result());
}
@Override
public void onResponse(AiServiceResponseContext context) {
log.info("[AGENT] Réponse finale — session={} tokens={}",
context.memoryId(), context.tokenUsage());
}
}
Pour la production, intégrez ces traces dans votre stack observabilité (Datadog, Grafana, OpenTelemetry) et mettez des alertes sur :
- Le nombre d’itérations par session (anomalie si > 15)
- Le coût en tokens par session (anomalie si > 10k tokens)
- Le taux d’erreur des outils
- Le temps de réponse p99 par session
Points de vigilance en production
Actions irréversibles : gardez un humain dans la boucle pour les actions critiques (suppression de données, virements, envois massifs). Implémentez un step de confirmation explicite dans le prompt système : “Pour toute action irréversible, demande une confirmation explicite à l’utilisateur avant d’exécuter.”
Logging complet : chaque appel d’outil, chaque réponse LLM, chaque itération doit être tracé avec son contexte (session, timestamp, durée). Sans logs, déboguer un comportement inattendu est presque impossible.
Coûts : un agent avec 10 itérations consomme 10× les tokens d’un appel simple. Avec claude-sonnet-4-6, 10 itérations de 2k tokens input + 500 tokens output = ~25k tokens ≈ 0,08 $ par session. Surveillez votre usage via l’API Anthropic et mettez des alertes budgétaires.
Tests : testez chaque outil indépendamment avec des mocks (test unitaire), puis testez le flux agent complet avec un LLM réel sur les scénarios critiques (test d’intégration). Anticipez les cas limites : outil qui retourne une erreur, produit introuvable, stock insuffisant.
Prompt injection : si votre agent traite du contenu externe (emails, documents, messages utilisateur), protégez-vous contre les injections de prompt qui pourraient détourner le comportement de l’agent. Validez et sanitisez les entrées avant de les passer au LLM.
Les agents IA en Java sont maintenant accessibles et productifs. Spring AI et LangChain4j fournissent les briques pour passer d’un PoC à la production en quelques semaines. La clé : commencer par des outils simples, bien délimités, avec des validations strictes des entrées/sorties — et ajouter la complexité (mémoire persistante, RAG, multi-agents) uniquement quand le cas d’usage le justifie.
Sur le même sujet
- MCP (Model Context Protocol) : connecter vos LLMs à vos outils — Le protocole standard pour exposer vos APIs et bases de données à un agent sans réécrire l’intégration dans chaque application.
- Platform Engineering : construire une plateforme développeur interne — Comment industrialiser le déploiement des agents et microservices IA via un IDP avec ArgoCD et Backstage.
- DevSecOps : intégrer la sécurité dans votre pipeline CI/CD Java — Les security gates indispensables avant de déployer un agent IA en production (scan des dépendances, SAST, secrets).
Réalisation liée
Applications métier & CMS - SIFAST
Conception et évolution d'applications web, CMS et interfaces métier avec une logique de structuration technique et d'intégration.
Voir la réalisation
Amine MEGDICHE
Développeur AEM & Java Full Stack — Freelance depuis 2013