Grab utilise les AI Agent pour augmenter la productivité
1. Aperçu
De nombreuses grandes entreprises technologiques comme Grab passent de « utiliser un chatbot pour répondre à des questions » à « utiliser des AI Agent pour agir de manière autonome » afin d’augmenter la productivité technique : revue automatique du code, résumé automatique des pull requests, triage automatique des incidents. Cet article explique en quoi un AI Agent diffère fondamentalement d’un simple appel LLM, l’architecture courante Planner – Tool – Executor, et comment vous pouvez construire un agent similaire en utilisant Java 21 + Spring AI. Après la lecture, vous saurez écrire un agent simple appelant de vrais outils (lecture de fichiers, appel aux API internes) et comprendre quand ne pas déployer un agent en production.
2. En quoi un AI Agent diffère d’un appel LLM ordinaire
Un appel LLM simple reçoit un prompt et retourne du texte — pas d’état, pas d’actions. Un AI Agent possède trois caractéristiques :
- Planification autonome : diviser une grande demande en petites étapes.
- Appel d’outils (tool calling / function calling) : l’agent peut appeler des API, lire des fichiers, exécuter des requêtes — pas seulement générer du texte.
- Boucle observation-action : l’agent observe le résultat retourné par l’outil, décide de l’étape suivante, et répète jusqu’à la fin.
Exemple concret dans les grandes équipes d’ingénierie : l’agent lit le diff d’une pull request, appelle l’outil « obtenir la convention de codage de l’équipe », appelle l’outil « exécuter l’analyse statique », puis synthétise tout cela en commentaire de review — tout cela sans qu’une personne ait besoin d’attendre chaque étape.
Important : si votre tâche consiste simplement à « résumer un texte » ou « traduire une langue », un simple appel LLM suffira. N’ajoutez pas le label « agent » à chaque appel à l’API OpenAI — une complexité inutile rend le système difficile à déboguer.
3. Architecture d’un agent basique : Planner – Tool – Executor
Le modèle le plus courant comprend trois parties :
Demande utilisateur
│
▼
[Planner] -- génère la liste des étapes à effectuer, ou choisit le prochain outil
│
▼
[Tool Executor] -- appelle le vrai outil : API, DB, système de fichiers
│
▼
[Observation] -- le résultat de l'outil est renvoyé au Planner
│
└── répète jusqu'à ce que Planner signale « terminé »
Le Planner est généralement un LLM auquel on fournit un « menu » d’outils (nom, description, schéma des paramètres). Le LLM ne peut pas exécuter du code lui-même — il décide d’appeler quel outil avec quels paramètres ; c’est votre code qui exécute réellement.
3.1. Dépendances nécessaires
Spring AI fournit une abstraction assez propre pour ce pattern :
<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
4. Exemple pratique : Code Review Agent
Supposons que vous vouliez un agent qui reçoit le diff d’un service Order, vérifie automatiquement la convention et propose des améliorations. D’abord, déclarez les outils que l’agent est autorisé à appeler — ici, un outil pour lire la convention de codage depuis un fichier interne.
@Component
public class ConventionTool {
@Tool(description = "Obtenir la convention de codage actuelle de l'équipe pour un langage")
public String getConvention(String language) {
return switch (language.toLowerCase()) {
case "java" -> "Utiliser les records pour les DTO ; pas de @Autowired sur les champs ; logging via SLF4J.";
default -> "Aucune convention spécifique pour ce langage.";
};
}
}
@Toolest une annotation Spring AI qui marque cette méthode comme une fonction que le LLM peut appeler.descriptionest ce que le LLM lit pour décider « dois-je appeler cet outil ? » — écrivez clairement, sinon le LLM fera un mauvais choix.
Ensuite vient le service d’orchestration — le rôle du Planner + Executor :
@Service
public class CodeReviewAgent {
private final ChatClient chatClient;
public CodeReviewAgent(ChatClient.Builder builder, ConventionTool conventionTool) {
this.chatClient = builder
.defaultTools(conventionTool)
.build();
}
public String review(String diff) {
return chatClient.prompt()
.system("Vous êtes un reviewer. Utilisez l'outil si vous avez besoin de la convention avant de faire vos remarques.")
.user("Vérifiez ce diff :\n" + diff)
.call()
.content();
}
}
defaultTools(conventionTool)enregistre l’outil pour toutes les requêtes duchatClient.- Quand vous appelez
review(diff), Spring AI envoie automatiquement le « menu des outils » avec la description du prompt au modèle. Si le modèle décide d’appelergetConvention("java"), le framework exécute automatiquement la méthode Java correspondante, récupère le résultat, et le renvoie au modèle afin qu’il génère la réponse finale — toute cette boucle se produit dans un seul appelcall().
Essayez :
@RestController
@RequestMapping("/api/review")
public class ReviewController {
private final CodeReviewAgent agent;
public ReviewController(CodeReviewAgent agent) {
this.agent = agent;
}
@PostMapping
public String review(@RequestBody String diff) {
return agent.review(diff);
}
}
Résultat typique :
La convention de l'équipe exige l'utilisation de records pour les DTO et SLF4J pour le logging.
Dans ce diff, la classe OrderDto est actuellement une classe classique avec getters/setters —
elle devrait être convertie en record. À la ligne 42, System.out.println est utilisé au lieu d'un logger,
ce qui viole la convention.
Point clé : vous n’écrivez pas la logique « si je vois une classe classique, suggérer un record » — le modèle le déduit lui-même en se basant sur la convention obtenue via l’outil. C’est pourquoi l’agent « augmente la productivité » : la revue ne dépend plus de la capacité d’une personne à se souvenir manuellement de la convention.
5. Agent appelant plusieurs outils : PR Summary Agent
En réalité, les grandes équipes ont besoin d’agents appelant plusieurs outils successivement — par exemple : obtenir la liste des fichiers modifiés, obtenir la couverture de test, puis résumer la PR pour le reviewer.
@Component
public class PullRequestTools {
private final GitService gitService;
private final CoverageService coverageService;
public PullRequestTools(GitService gitService, CoverageService coverageService) {
this.gitService = gitService;
this.coverageService = coverageService;
}
@Tool(description = "Obtenir la liste des fichiers modifiés dans une PR par son id")
public List<String> listChangedFiles(long prId) {
return gitService.changedFiles(prId);
}
@Tool(description = "Obtenir le pourcentage de couverture de test d'un fichier")
public double getCoverage(String filePath) {
return coverageService.coverageOf(filePath);
}
}
- Deux outils indépendants, chacun faisant une seule petite tâche, faciles à tester séparément.
- Le modèle décide lui-même de l’ordre : généralement
listChangedFilesd’abord, puisgetCoveragepour chaque fichier retourné — c’est exactement la boucle « observation-action » décrite à la section 3.
@Service
public class PullRequestSummaryAgent {
private final ChatClient chatClient;
public PullRequestSummaryAgent(ChatClient.Builder builder, PullRequestTools tools) {
this.chatClient = builder.defaultTools(tools).build();
}
public String summarize(long prId) {
return chatClient.prompt()
.system("Résumez la PR en incluant : nombre de fichiers modifiés, quels fichiers ont une couverture inférieure à 70 % à vérifier.")
.user("Résumez la PR id " + prId)
.call()
.content();
}
}
Un agent comme celui-ci résout le vrai problème de productivité : le reviewer n’a pas besoin d’ouvrir chaque fichier pour vérifier la couverture — l’agent a déjà synthétisé le tout en un seul résumé.
6. Test d’un agent
La partie la plus difficile en intégrant les agents en production n’est pas d’appeler le LLM, c’est de s’assurer que les outils sont appelés correctement quand les entrées changent. C’est pourquoi il faut séparer les tests unitaires des outils (déterministes) et les tests d’intégration de l’agent (on peut mocker ChatClient).
class ConventionToolTest {
private final ConventionTool tool = new ConventionTool();
@Test
void shouldReturnJavaConvention() {
String result = tool.getConvention("Java");
assertThat(result)
.contains("record")
.contains("SLF4J");
}
@Test
void shouldReturnDefaultMessageForUnknownLanguage() {
String result = tool.getConvention("cobol");
assertThat(result).isEqualTo("Aucune convention spécifique pour ce langage.");
}
}
- Tester l’outil comme vous testeriez une méthode ordinaire, pas besoin de mocker le LLM — c’est la partie que vous contrôlez à 100 %.
- Pour la partie agent appelant le LLM réel, écrivez des tests au niveau « contrat » : vérifiez que la réponse contient les mots clés obligatoires (par exemple « record », « coverage »), évitez d’affirmer une chaîne exacte car la sortie du LLM n’est pas 100 % stable entre les appels.
@Test
void reviewShouldMentionConventionKeyword() {
String diff = "class OrderDto { private String id; }";
String result = codeReviewAgent.review(diff);
assertThat(result.toLowerCase()).containsAnyOf("record", "convention");
}
Important : tester avec un vrai appel LLM coûte cher et est lent — séparez une suite de tests (par exemple tag
@Tag("llm-integration")) et ne l’exécutez que dans un pipeline dédié, pas à chaque commit.
7. Erreurs courantes lors du déploiement
- Description d’outil vague : si la description n’est pas claire, le modèle choisit le mauvais outil ou ne l’appelle pas quand il faudrait. Écrivez la description comme vous écririez une Javadoc pour quelqu’un qui lit pour la première fois.
- Pas de limite sur le nombre d’itérations : un agent peut entrer dans une boucle infinie en appelant les outils en continu si la logique du point final n’est pas claire. Fixez toujours
maxIterationsou un timeout au niveau orchestration. - Donner à l’agent le droit d’écrire des données sensibles : ne donnez pas à l’agent l’outil « déployer en production » ou « supprimer des données » sans une étape d’approbation humaine. L’agent devrait avoir des droits lecture larges, des droits écriture restreints.
- Ne pas consigner l’historique des appels d’outils : quand l’agent donne une mauvaise réponse, vous devez savoir quel outil il a appelé, avec quels paramètres, pour déboguer. Sans ce logging, tracer l’erreur devient quasi impossible.
- Utiliser un agent pour des problèmes solubles par une simple règle : si la logique « si X alors Y » est fixe, écrire du code ordinaire sera plus rapide, moins cher et plus facile à tester qu’un agent.
8. Quand ne pas utiliser un AI Agent
Un agent convient aux tâches aux nombreuses étapes non-déterministes, nécessitant de synthétiser des informations provenant de multiples sources, et tolérant de petites erreurs (suggestions de revue, résumés). Un agent ne convient pas pour :
- Les tâches exigeant une précision absolue (facturation, traitement des transactions financières).
- Les tâches ayant une procédure fixe et peu susceptible de changer — un moteur de règles ou un moteur de workflow (Camunda, Temporal) reste plus stable et plus facile à auditer.
- Les systèmes ayant besoin d’une très faible latence, car chaque boucle tool-calling consomme un aller-retour supplémentaire vers le LLM.
9. Conclusion
Les AI Agent apportent une valeur de productivité évidente quand ils sont appliqués au bon endroit : tâches d’ingénierie répétitives, multi-étapes, nécessitant une synthèse d’informations dispersées — revue de code, résumé de PR, triage d’incidents. Au lieu de construire un agent « sachant faire tout », commencez avec quelques outils simples et bien définis, avec leurs propres tests, puis laissez le Planner (le LLM) décider comment les orchestrer. Ce qui importe le plus : séparez les droits — donnez à l’agent une large capacité de lecture, une capacité de écriture restreinte, et conservez toujours une étape de revue humaine pour les actions importantes.