30.10.26
IA en Kotlin : le retour d’expérience de Cobrainer17 min de lecture3 408 mots

IA en Kotlin : le retour d’expérience de Cobrainer

Découvrez comment Cobrainer intègre l’IA en Kotlin avec Koog : contrats typés, agents, résilience multi-fournisseurs et gains de performance à la clé.

IA en KotlinKoog Kotlindéveloppement IA sur JVMagents IA en productionsorties structurées LLM
IA en Kotlin : le retour d’expérience de Cobrainer

Faire de l'IA en Kotlin : les choix de Cobrainer en production

Un fournisseur de modèles d'IA commence à limiter le débit. Un gros client déclenche un volume inhabituel de tâches. Pour l'équipe qui exploite la plateforme, le problème n'est pas de choisir « le » langage le plus à la mode : il faut encaisser la charge sans interrompre le travail des utilisateurs.

C'est ce qui est arrivé à Cobrainer, une entreprise munichoise de la HR Tech. Leur système a détecté les limites du fournisseur, écarté les modèles concernés et renvoyé les requêtes vers un autre prestataire. Le parcours client n'a pas bougé. L'équipe a compris ce qui s'était passé le lendemain, en regardant leur tableau de suivi des appels aux modèles.

Ce résultat ne vient pas d'un modèle « magique » ni d'une architecture compliquée. Il tient surtout à des décisions assez concrètes : intégrer l'IA dans un environnement Kotlin déjà en place, formaliser ce que le modèle doit renvoyer, prévoir des replis, et observer chaque appel. Et au fond, c'est une question qu'on entend souvent côté technique : faut-il passer par Python pour mettre de l'IA en production, ou la JVM peut faire le job ?

Pourquoi Cobrainer a choisi Kotlin pour l'IA

Cobrainer développe une plateforme de compétences destinée aux grandes entreprises. Elle modélise les métiers, les niveaux, les tâches et les compétences, puis aide les organisations à exploiter ces données pour la mobilité interne, le recrutement, la formation ou la planification des effectifs. La plateforme s'intègre notamment à SAP SuccessFactors et Workday.

L'équipe compte six développeurs qui gèrent tout le périmètre : l'infrastructure cloud, les applications, le backend et les fonctions d'IA. Dans ce contexte, chaque nouveau langage ou service a un coût : le construire, le sécuriser, le surveiller, le maintenir. D'où le choix de garder l'intégration IA dans le monorepo Kotlin.

L'IA de production, c'est surtout de l'intégration

Python reste important pour la recherche, l'entraînement de modèles et une grande partie de l'écosystème scientifique. Mais intégrer un modèle accessible par API dans un produit existant, ce n'est pas la même histoire. Il faut gérer des contrats réseau, des formats de données, des erreurs, des délais, des permissions et des changements fréquents côté fournisseurs.

Chez Cobrainer, les modèles sont appelés par HTTP. Ajouter un environnement Python n'aurait donc rien changé « en soi » sur la qualité des réponses générées. En revanche, cela aurait ajouté une chaîne de déploiement et une frontière de sérialisation entre la couche IA et les objets métier. Et c'est précisément là que l'incertitude est la plus forte : un modèle peut renvoyer une donnée incomplète ou mal formée.

En restant sur Kotlin, l'équipe garde des types de domaine partagés et les outils déjà en place. Les contrats passent par kotlinx.serialization, les accès SQL par Exposed, et les traitements concurrents par les coroutines. Spring Boot reste au centre de la plateforme. Ce n'est donc pas une posture « contre Python » : c'est une décision de cohérence avec ce qu'ils doivent maintenir.

Pourquoi les types comptent quand le modèle peut déraper

Quand les comportements d'un système se retrouvent disséminés dans plein de fichiers, un monolithe peut devenir dur à lire. Et dès qu'il y a des conventions implicites, la revue de code et les changements automatisés deviennent plus risqués.

Kotlin apporte des garde-fous utiles : la nullabilité explicite, des types sérialisables, et des hiérarchies scellées dont les cas peuvent être vérifiés au compilateur.

Ça sert l'équipe, mais ça sert aussi les agents. Un agent peut suivre un changement depuis un type métier jusqu'à ses consommateurs sans changer de langage ni multiplier les dépôts. Ce n'est pas une garantie de correction automatique, mais ça réduit le nombre d'hypothèses à faire pour proposer un changement, puis le vérifier.

Le compromis est assez simple : Kotlin colle bien à une équipe qui est déjà sur la JVM. Si le projet tourne principalement autour de l'entraînement, de l'expérimentation, ou d'un usage massif de bibliothèques scientifiques Python, imposer Kotlin serait probablement une mauvaise idée. Le bon critère, ce n'est pas « qui est populaire », c'est le coût total d'intégration dans le système réel.

À retenir : ajouter un langage n'a de sens que s'il apporte un avantage concret que la pile existante ne peut pas donner à un coût raisonnable. Une API de modèle, seule, ne prouve rien.

Koog face à Spring AI et LangChain4j

Avant de retenir Koog, Cobrainer a construit des implémentations fonctionnelles avec Spring AI, LangChain4j et Koog. Ils ne se sont pas laissés guider par le nombre de briques annoncées. L'équipe a surtout regardé comment chaque option traduisait le comportement côté Kotlin, et à quel point c'était simple de diagnostiquer et faire évoluer ce comportement.

Trois outils, trois usages différents

Spring AI est une approche très « Spring-native ». Pour Cobrainer, ça a du sens pour exposer des points d'accès MCP et s'appuyer sur l'infrastructure REST, ainsi que sur la chaîne de filtres Spring Security. Mais pour le cœur agentique, l'équipe voulait garder un lien plus direct entre son code et les appels aux modèles. La couche intermédiaire aidait moins pour itérer vite.

LangChain4j apporte à la JVM des concepts inspirés de LangChain. Ça démarre rapidement, mais ils ont trouvé certains échecs trop difficiles à rattacher à l'appel LLM d'origine. Les utilitaires qui dépendaient de cette bibliothèque ont fini par être remplacés par quelques dizaines de lignes de Kotlin. Et ça ne veut pas dire que LangChain4j est « inadapté » en général : juste que, dans leur cas, le diagnostic en production a posé problème.

Koog a fait la différence parce que ses abstractions restent proches de ce qu'on sait faire en Kotlin, au lieu de masquer les mécanismes. Les types sérialisables deviennent des contrats de sortie structurée ; les coroutines s'enchaînent naturellement entre tentatives et workflows ; la DSL de graphe s'exprime avec des fonctions d'extension et des lambdas avec récepteur. Résultat : les graphes restent du code lisible, que les développeurs peuvent suivre et vérifier avec leurs outils habituels.

Le bon niveau d'abstraction

Une bibliothèque d'orchestration doit éviter de réécrire les clients des fournisseurs et toute la gestion des agents. Mais une abstraction trop épaisse peut cacher le prompt réel, l'appel qui a échoué, ou la raison d'une nouvelle tentative.

Cobrainer cherchait donc un équilibre : assez de structure pour limiter le code répétitif, assez de proximité pour comprendre ce qui se passe en production.

Il y a aussi une contrainte pratique : les modèles n'offrent pas tous les mêmes options (température, limites de sortie, format structuré natif, etc.). Koog permet d'expliciter ces différences. L'application peut adapter les paramètres au modèle choisi tout en gardant le même contrat métier. Et si un modèle ne sait pas produire nativement le format attendu, la consigne peut être ajustée, puis un parseur de correction peut intervenir.

Après l'évaluation, Cobrainer a nettoyé le prototype et l'a déployé pour tous ses clients en une journée. Ce n'est pas une preuve que « tous les frameworks rendent tout migrationnellement facile ». Ici, ça s'explique surtout par la réutilisation du modèle de domaine, de la sérialisation, des tests et du pipeline de déploiement déjà maîtrisés.

Solution Atout observé par Cobrainer Point de vigilance dans ce contexte
Spring AI Intégration poussée avec Spring et l'infrastructure web Moins direct pour certains besoins du cœur agentique
LangChain4j Bibliothèque riche pour démarrer sur la JVM Traçabilité de certaines erreurs jugée difficile
Koog Abstractions légères, proches de Kotlin Il faut tout de même vérifier les capacités et le comportement de chaque fournisseur

Ce comparatif ne dispense pas de tester. Si vous êtes dans un environnement Spring, un test utile consiste à reproduire le même scénario métier avec chaque option, puis comparer : lisibilité des appels, gestion des erreurs, traçabilité et coûts de maintenance. Les listes de fonctionnalités ne disent pas tout.

Des réponses structurées plutôt que du texte libre

Dans un produit métier, un LLM n'est pas seulement là pour produire une conversation « qui sonne bien ». Il propose aussi des modifications d'une architecture de métiers : renommer un élément, déplacer une tâche, archiver des éléments.

Ces propositions doivent ensuite être validées, présentées à l'utilisateur, et parfois appliquées.

Cobrainer s'appuie donc sur des sorties structurées. Une proposition de changement est modélisée par une hiérarchie Kotlin sérialisable, avec des variantes explicites, par exemple Rename, Archive ou Move. Le schéma peut être transmis au fournisseur, et les descriptions associées aux champs aident le modèle à comprendre le contrat. La « réponse attendue » n'est plus juste un texte à interpréter : c'est une valeur typée que le code peut traiter.

Le compilateur comme garde-fou métier

Une hiérarchie scellée permet d'utiliser un when exhaustif. Si une nouvelle catégorie de changement arrive, le compilateur peut signaler les endroits qui ne la gèrent pas encore. Pareil si un champ obligatoire change : vous voyez immédiatement où les structures sont construites sans tenir compte de la nouvelle forme.

Ça ne rend pas le modèle infaillible. Ça sécurise surtout le passage entre ce que renvoie le modèle et ce que le produit applique. La validation métier reste indispensable : vérifier qu'un identifiant existe, que l'utilisateur a les droits, et que l'action est cohérente avec l'état courant. Une donnée correctement analysée n'est pas forcément une action autorisée ou pertinente.

Et si le modèle renvoie un quasi-JSON qui ne respecte pas exactement le schéma, un parseur de correction peut demander à un modèle léger de réparer la structure, avec un nombre de tentatives limité. La réponse n'est acceptée qu'après analyse et conformité au contrat attendu. Ça améliore la robustesse, mais ça a un coût : appels supplémentaires, latence possible, et consommation de jetons. Donc il faut garder ça observable et borné.

Des appels adaptables aux modèles disponibles

Cobrainer utilise plusieurs fournisseurs et répartit les appels selon les capacités des modèles. Avant chaque tentative, l'exécuteur ajuste la requête : retirer une option non prise en charge, plafonner le nombre de jetons, ou inclure le schéma dans le message quand le format structuré natif n'est pas disponible.

Le code métier n'a pas besoin de savoir quel fournisseur a répondu. Cette indirection reste utile tant qu'elle ne rend pas le comportement opaque. L'équipe garde donc la trace : tentatives, erreurs, modèle utilisé, afin de distinguer un problème côté fournisseur d'un problème de parsing ou d'une régression applicative.

Pour une équipe qui met en place des sorties structurées, une séquence pragmatique ressemble à ça :

  1. Définir un contrat métier minimal, avec des cas explicites.
  2. Générer (ou fournir) le schéma correspondant au fournisseur.
  3. Parser la réponse dans le type attendu, sans accepter de « texte à peu près correct » comme résultat final.
  4. Valider les règles métier et les permissions après le parsing.
  5. Tester le contrat avec chaque fournisseur réellement utilisé.
  6. Mesurer les échecs, les corrections et les appels supplémentaires.

L'objectif n'est pas d'éliminer les erreurs du modèle, mais d'éviter qu'elles se propagent silencieusement dans les données métier.

Un agent, deux services et une même base de types

L'agent qui gère l'architecture des métiers sert à demander des opérations en langage naturel : fusionner deux niveaux, rédiger des tâches manquantes, préparer l'archivage d'éléments inutilisés. Le modèle décide de la suite à donner, mais les opérations qui modifient les données passent par des workflows déterministes contrôlés par le code.

Il y a une validation humaine avant chaque modification. C'est une frontière importante : un agent peut préparer une action et en expliquer les effets, mais le modèle ne doit pas devenir l'autorité qui décide seul d'un changement sensible.

Pourquoi isoler le runtime de l'agent

La plateforme reste organisée autour d'un monolithe : c'est leur point d'entrée principal pour les API REST et MCP. L'agent fait exception : il tourne dans un service Kotlin séparé, conçu pour les conversations longues, le streaming et une concurrence structurée. Les deux services vivent dans le même monorepo et partagent des types. Le compilateur vérifie donc une partie du contrat sans imposer une séparation par langages.

Les données métier restent dans PostgreSQL et sont accessibles par le monolithe via Exposed. Le runtime de l'agent utilise SurrealDB pour la reprise, l'historique et plusieurs formes de mémoire : une fenêtre de conversation récente, un résumé évolutif de la session, et un graphe de faits sur la durée.

Oui, ça ajoute du travail : un service de plus à déployer et à superviser. Mais l'agent a un cycle de vie différent des requêtes classiques. Pour une équipe de six développeurs, l'isolation ne serait pas forcément un bon choix si elle se transformait en collection de microservices. Ici, c'est justifié parce que l'objectif est d'isoler un besoin spécifique, tout en conservant un dépôt et un système de types partagés.

La sécurité ne se délègue pas au prompt

Les outils de l'agent sont exposés via des serveurs MCP dans le monolithe, protégés par Spring Security. Cobrainer sépare les outils selon le périmètre de permission - lecture, architecture, revue - et utilise un jeton délégué de courte durée pour que l'agent agisse avec les droits de l'utilisateur.

Les permissions filtrent les outils disponibles avant même l'appel au modèle. Si un utilisateur n'a pas le droit de modifier une architecture, il ne verra pas les outils de modification proposés à son agent. C'est plus fiable que demander au modèle de « respecter la sécurité » dans un prompt : ici, le contrôle est imposé par l'infrastructure.

L'agent n'envoie pas forcément chaque jeton ou chaque texte généré à l'interface. Dans ce workflow, l'interface affiche surtout des étapes compréhensibles - analyse de l'architecture, préparation des changements - à partir des événements du graphe Koog. L'équipe a jugé ce mode d'indicateur de progression plus utile que le streaming texte, parce que l'utilisateur veut surtout savoir où en est l'opération structurée.

Fiabilité, performance et limites du choix

L'incident lié à la capacité du fournisseur montre l'intérêt d'une chaîne de repli qui traverse plusieurs fournisseurs. Plusieurs modèles d'un même fournisseur peuvent partager un domaine de panne. Si le fournisseur est limité ou dégradé, il faut une autre route réellement indépendante.

L'exécuteur de Cobrainer ajoute à Koog des tentatives avec délai progressif, une chaîne de repli configurée et un disjoncteur par modèle. Le disjoncteur coupe temporairement une route défaillante au lieu de répéter indéfiniment des appels voués à l'échec. Les tentatives et les changements de fournisseur sont tracés dans LLMObs : latence, usage de jetons et erreurs. Le tableau de bord transforme ainsi un événement invisible pour l'utilisateur en quelque chose que l'équipe peut analyser.

Le gain de performance ne venait pas du framework IA

Le problème de performance le plus important rencontré par l'équipe ne venait ni de Koog ni des modèles. Un vieux code bloquant, exécuté dans des chemins de coroutines, saturait les dispatchers sous charge. Les ralentissements étaient variables et difficiles à expliquer. Après suppression de ce code, le débit des chemins concernés a été multiplié par cinq.

La leçon opérationnelle est simple : ne laissez pas runBlocking dans les chemins de requêtes en production sans comprendre précisément ce que ça fait à l'exécution. Même dans une appli Spring Boot qui « semble » aller bien, bloquer un dispatcher sous charge peut empêcher des requêtes d'aboutir, sans forcément déclencher un arrêt clair du service.

Et surtout : ce résultat ne veut pas dire que les coroutines multiplient systématiquement les performances par cinq. Le gain était lié à un goulot d'étranglement précis dans cette application. Il faut mesurer avant et après, regarder l'usage des threads, vérifier les opérations bloquantes, plutôt que d'attribuer automatiquement tout ralentissement à la couche IA.

Tester les contrats et fermer la boucle opérationnelle

Une sortie structurée dépend de plusieurs éléments : le contrat côté application, la génération de schéma côté framework, et la manière dont chaque fournisseur implémente le schéma.

Cobrainer exécute des tests de bout en bout avec de vrais appels et du parsing pour ses fournisseurs, et aussi des tests de contrat dans l'intégration continue.

Les appels réels coûtent du temps et de l'argent. Ils ne remplacent donc pas les tests unitaires, mais complètent ce qui ne peut pas être simulé de façon fiable. Ensuite, les données d'observabilité permettent d'enquêter sur les erreurs en production avec le bon contexte.

Tests et observabilité forment une boucle : observer, diagnostiquer, valider une évolution, puis déployer.

Avant de choisir une pile IA pour votre produit, vérifiez au minimum :

  • Les modèles s'intègrent-ils par API, ou le projet dépend-il de bibliothèques scientifiques d'un autre langage ?
  • Les contrats de données sont-ils partagés avec le domaine métier, ou dupliqués entre services ?
  • Pouvez-vous retracer un appel depuis la requête utilisateur jusqu'au fournisseur et au parsing ?
  • Les replis couvrent-ils plusieurs types de panne, pas seulement plusieurs modèles d'un même prestataire ?
  • Les actions de l'agent sont-elles autorisées par le serveur et les permissions, plutôt que par le seul prompt ?
  • Le coût d'un runtime supplémentaire est-il compensé par une isolation utile ou un vrai besoin de mise à l'échelle ?

Pour Cobrainer, Kotlin, Koog, Exposed et les coroutines forment un ensemble cohérent parce que l'équipe est déjà organisée autour de la JVM. Ce n'est pas une recette universelle. C'est surtout une démonstration utile : le « bon » langage de l'IA en production peut être celui du produit, tant que les abstractions, les contrats et l'exploitation suivent.

Conclusion : choisissez une pile que votre équipe peut exploiter

Le retour de Cobrainer ne dit pas que Kotlin est « meilleur » que Python, ni que Koog convient à tous les projets. Il met surtout l'accent sur des critères moins visibles que la démo : contrats de données, diagnostic des erreurs, capacité de repli, sécurité et coût de maintenance.

Si votre équipe est déjà construite sur la JVM, rester en Kotlin peut éviter une nouvelle frontière technique sans renoncer aux agents ni aux sorties structurées. Si votre équipe est plutôt centrée sur l'entraînement et l'expérimentation de modèles, une autre pile sera probablement plus adaptée. Dans tous les cas, démarrez par un prototype représentatif, testez les fournisseurs avec vos vrais contrats, puis mesurez l'exploitation avant d'étendre l'architecture.

Vous pouvez consulter le retour d'expérience original de Cobrainer, puis comparer ces choix à votre contexte : quel coût aurait un second runtime, et quel risque réduirait-il vraiment ?

Questions frequentes

Non. Python reste particulièrement adapté à la recherche, à l’entraînement et aux bibliothèques scientifiques. Pour intégrer des modèles accessibles par API dans une application existante, le langage déjà utilisé par le produit peut réduire les déploiements et les frontières de données. Le choix dépend du travail à accomplir, pas d’une règle générale.
Koog propose des abstractions pour les appels aux modèles et l’orchestration d’agents, tout en restant proche des types, des coroutines et des DSL Kotlin. Cobrainer l’a retenu parce que son équipe pouvait garder le contrôle du flux et réutiliser ses outils habituels. Il faut néanmoins tester son adéquation au cas d’usage et aux fournisseurs retenus.
Une sortie structurée permet de parser la réponse dans un type connu au lieu de traiter du texte libre. Le compilateur peut aider à repérer les cas oubliés, mais il ne valide pas à lui seul la logique métier. Les données doivent encore être contrôlées et les permissions appliquées avant toute modification.
Prévoyez des tentatives bornées, des disjoncteurs et une chaîne de repli qui couvre plusieurs fournisseurs lorsque c’est possible. Tracez chaque route empruntée, sa latence et son résultat. Des modèles différents chez un même fournisseur ne constituent pas nécessairement des replis indépendants.

Articles similaires