Spring AI : amener Claude dans un backend Kotlin/Spring Boot
Spring Boot est le framework que j'utilise pour tous mes backends Kotlin qui illustrent une architecture hexagonale, MissionMatch en tête. Quand un client demande d'ajouter une capacité IA à ce genre de backend, la question n'est jamais « est-ce que c'est possible », c'est « est-ce que ça s'intègre proprement dans une architecture qui sépare déjà strictement domaine, application et infrastructure ». Spring AI répond à cette question mieux que je ne m'y attendais, à condition de ne pas le laisser s'infiltrer là où il n'a rien à faire.
Un client Claude comme n'importe quel bean#
Le principe de Spring AI est de traiter un modèle de langage comme n'importe
quelle autre dépendance externe injectée par le conteneur Spring : un
ChatClient se configure et s'injecte exactement comme un JdbcTemplate ou
un RestClient.
@Configuration
class ClaudeConfig {
@Bean
fun chatClient(builder: ChatClient.Builder): ChatClient =
builder
.defaultSystem("Tu réponds en JSON valide, sans texte hors du JSON.")
.build()
}Rien dans ce code ne trahit qu'un LLM est impliqué : c'est exactement le niveau d'abstraction attendu d'un client Spring. C'est la première chose qui m'a convaincu que Spring AI n'est pas un gadget marketing greffé sur l'écosystème Spring, mais une intégration pensée pour respecter ses conventions.
Où ça doit vivre dans une architecture hexagonale#
Un ChatClient Spring AI est une dépendance d'infrastructure, au même titre
qu'un accès base de données. Il n'a rien à faire dans la couche domaine,
même quand la tentation d'appeler directement l'API depuis un cas d'usage
est forte pour aller vite.
Le port reste défini dans le domaine, sous forme d'interface Kotlin sans dépendance à Spring AI :
interface MissionScorer {
fun score(mission: MissionOffer, stack: TechStack): ScoreResult
}L'implémentation qui appelle réellement ChatClient vit dans
l'infrastructure, comme n'importe quel autre adapter :
@Component
class ClaudeMissionScorer(
private val chatClient: ChatClient,
) : MissionScorer {
override fun score(mission: MissionOffer, stack: TechStack): ScoreResult =
chatClient.prompt()
.user("Stack : ${stack.summary()}\n\nOffre : ${mission.description}")
.call()
.entity(ScoreResult::class.java)
}Cette séparation n'est pas un exercice de pureté architecturale. Elle veut
dire concrètement que je peux tester la logique métier qui consomme
MissionScorer avec un faux scorer déterministe, sans jamais appeler un
vrai LLM dans mes tests unitaires, et sans que le domaine sache seulement
que Claude existe.
entity(), ou sortir enfin du texte libre#
Le point qui a le plus changé ma perception de la maturité de Spring AI :
entity(ScoreResult::class.java) désérialise directement la réponse du
modèle vers une data class Kotlin, en s'appuyant sur un schéma généré
automatiquement. Plus besoin de parser un JSON à la main ni de gérer les cas
où le modèle décide d'ajouter une phrase d'explication avant l'objet attendu.
data class ScoreResult(
val score: Int,
val justification: String,
val redFlags: List<String>,
)Un schéma bien typé réduit le risque de sortie mal formée, il ne l'élimine pas. Valider les bornes métier (un score entre 0 et 100, une liste non vide quand le contexte l'exige) reste une responsabilité du code appelant, pas quelque chose que la désérialisation garantit.
Le vrai coût : les tests, pas l'intégration#
L'intégration elle-même se fait vite, en quelques dizaines de lignes. Ce qui
prend du temps, c'est de décider comment tester une couche qui appelle un
service non déterministe. La réponse que j'applique est la même que pour
n'importe quel adapter d'infrastructure : un stub de ChatClient renvoyant
des réponses fixées pour les tests unitaires du domaine, et une poignée de
tests d'intégration réels, isolés, exécutés à part du reste de la suite,
qui vérifient que l'appel réel produit bien une sortie conforme au schéma.
Ce que ça généralise#
Spring AI ne change rien à la discipline d'architecture qu'impose un backend Spring Boot sérieux, il l'étend simplement à un nouveau type de dépendance externe. La question à se poser reste celle de toujours : est-ce que le domaine dépend de Spring AI, ou seulement d'un port que l'infrastructure implémente avec Spring AI. Répondre correctement à cette question, et pas la qualité du prompt envoyé au modèle, est ce qui détermine si l'ajout d'une capacité IA renforce l'architecture existante ou commence à l'éroder.