LangChain4j 1.21 : DecisionModel, les modèles de décision en Java
LangChain4j 1.21 ajoute DecisionModel : c'est quoi, et quand l'utiliser ?
Un modèle de décision répond à des questions typées sur une entrée, avec une probabilité pour chaque réponse, sans générer de texte. LangChain4j 1.21 l'expose avec une API DecisionModel, des Decision Services et des composants prêts à l'emploi. Le tout est marqué expérimental.
La version est sortie le 2 octobre 2026. Elle apporte en Java la famille de modèles que j'ai comparée dans mon article sur Jev et GLiDE.
La documentation retient trois critères pour préférer un modèle de décision à un chat model :
- vous voulez des probabilités, par exemple pour envoyer les cas incertains à un humain ;
- vous posez plusieurs questions sur la même entrée, en un seul appel ;
- la décision se prend à chaque requête, sur un chemin où la latence et le coût comptent.
Pour rédiger, raisonner en plusieurs étapes ou appeler des outils, elle renvoie vers un chat model.
Appeler un DecisionModel : questions, réponses et probabilités
On construit une DecisionRequest : une entrée (un texte ou une Map) et des questions nommées. Il existe trois types de questions : YesNoQuestion, ChoiceQuestion (une option parmi au moins deux) et ScaleQuestion (un niveau sur une échelle ordonnée). La documentation de TypeSafe appelle les premières « noul » et les dernières « score ».
import dev.langchain4j.model.decision.DecisionModel;
import dev.langchain4j.model.decision.request.*;
import dev.langchain4j.model.decision.response.*;
import dev.langchain4j.model.typesafe.TypeSafeDecisionModel;
DecisionModel decisionModel = TypeSafeDecisionModel.builder()
.apiKey(System.getenv("TYPESAFE_API_KEY"))
.modelName("jev-1.13.0")
.build();
DecisionRequest request = DecisionRequest.builder()
.input("Help! My payouts have been failing for 3 days and nobody answers my emails.")
.question("team", ChoiceQuestion.builder()
.text("Which team should handle this ticket?")
.option("billing", "Payments, payouts, invoices, refunds")
.option("support", "Problems using the product")
.build())
.question("urgent", YesNoQuestion.of("Does this need attention today?"))
.build();
DecisionResponse response = decisionModel.decide(request);
ChoiceAnswer team = response.choice("team");
YesNoAnswer urgent = response.yesNo("urgent");
Les deux questions partent dans un seul appel. team.value() donne l'option retenue et team.probabilities() la probabilité de chacune. urgent.probability() donne celle du « oui ».
La documentation montre deux usages de ces probabilités. urgent.isYes(0.8) franchit un seuil, et team.margin() < 0.2 confie le ticket à un humain quand le modèle hésite. La marge est l'écart entre les deux options les plus probables.
Un seuil réglé sur un modèle ne vaut pas pour un autre, ni pour une autre version du même modèle. Réglez-le sur vos données, et recommencez à chaque changement. Le détail est dans le tutoriel Decision Models.
Decision Services : les décisions comme interface Java
Quand les questions sont connues à la compilation, les Decision Services évitent ce code. On déclare des méthodes annotées @Decide, et LangChain4j fournit l'implémentation, comme pour les AI Services. Ils sont dans dev.langchain4j:langchain4j:1.21.0.
import dev.langchain4j.model.decision.response.YesNoAnswer;
import dev.langchain4j.model.output.structured.Description;
import dev.langchain4j.service.decision.*;
enum Team {
@Description("Payments, payouts, invoices, refunds") BILLING,
@Description("Problems using the product") SUPPORT,
@Description("Pricing, upgrades, new accounts") SALES
}
record Triage(
@Decide("Which team should handle this ticket?") Team team,
@Decide("Does this need attention today?") boolean urgent,
@Decide("Does the customer ask for money back?") YesNoAnswer refund) {}
interface SupportDesk {
@Decide("Is this message spam?")
boolean isSpam(String message);
@Decide("Which team should handle this ticket?")
Choice<Team> route(String ticket);
Triage triage(String ticket);
}
SupportDesk desk = DecisionServices.create(SupportDesk.class, decisionModel);
Choice<Team> choice = desk.route("I was charged twice this month");
System.out.println(choice.margin() < 0.2 ? "à confier à un humain" : choice.value().name());
Le type de retour fixe la question :
boolean: oui ou non, selon un seuil ;- une
enum: un choix ; Choice<E>: un choix avec ses probabilités ;Scale<E>: une échelle ;- un record : chaque champ devient une question, et toutes partent en un seul appel.
Le modèle reçoit les noms des paramètres : compilez avec l'option -parameters. Il lit aussi les @Description des constantes d'enum pour comprendre chaque option. Passez un petit record plutôt qu'une entité JPA : tous les champs d'un objet partent chez le fournisseur.
Les composants prêts à l'emploi : garde-fous, re-ranking, routage, outils
LangChain4j branche un modèle de décision là où une réponse rapide par oui, non ou choix suffit :
- garde-fous (module
langchain4j-guardrails) :DecisionModelInputGuardrailetDecisionModelOutputGuardrailposent des questions oui/non, où « oui » rejette le message ; - re-ranking RAG :
DecisionScoringModelnote chaque passage selon la probabilité du « oui » à « Ce document aide-t-il à répondre ? » ; - routage :
DecisionModelQueryRouterchoisit les sources à interroger,DecisionModelChatModelRouterle chat model ; - outils :
DecisionModelFilteringToolProviderne passe au LLM que les outils pertinents d'un fournisseur d'outils, un serveur MCP par exemple ; - agents :
DecisionRouterPlannerchoisit les sous-agents qui traitent une demande.
Chaque composant fait un appel de plus au modèle de décision. Ses tokens ne sont pas comptés dans l'usage de la réponse du chat model : ils vont aux listeners du modèle de décision.
En cas de panne, les garde-fous font échouer la requête. Par défaut, le routeur de modèle prend sa route par défaut. Le routeur de requêtes ne récupère rien et le filtre d'outils passe tous les outils.
Quel serveur derrière : TypeSafe, OpenRouter ou Ollama ?
TypeSafeDecisionModel vit dans dev.langchain4j:langchain4j-typesafe:1.21.0-beta31 et vise https://api.typesafe.ai par défaut. Tout serveur qui implémente POST /v1/systemone fonctionne en changeant l'URL de base. La documentation cite OpenRouter, SGLang pour des modèles ouverts, et Ollama.
DecisionModel decisionModel = TypeSafeDecisionModel.builder()
.baseUrl("https://openrouter.ai/api")
.apiKey(System.getenv("OPENROUTER_API_KEY"))
.modelName("typesafe/jev-1.13")
.build();
En local, Ollama (à partir de la v0.35.0) propose les modèles nimble et tev1. Leurs limites, selon la documentation : du texte seulement et 2 048 tokens de prompt au maximum.
Une question accepte 2 à 26 options, et une requête 64 questions. Sans GPU, une requête peut prendre plusieurs secondes.
L'intégration est testée avec l'API TypeSafe, OpenRouter et Ollama. Selon la documentation, les petits modèles et les modèles généralistes sont en général moins précis et moins décisifs que les modèles dédiés dans ce rôle.
Avant la production : ce que la documentation recommande de vérifier
Mon avis : l'API est prometteuse pour le tri et le routage, mais elle est expérimentale et la documentation est franche sur ses limites. Les exemples ci-dessus compilent contre la 1.21.0. Je ne les ai pas exécutés, car ils appellent un service payant, et je n'ai mesuré aucun modèle.
Quatre points à régler avant de la mettre en production :
- Figer la version du modèle. Un alias comme
jev-latestpeut changer de version à tout moment, donc les réponses et la calibration des probabilités. Utilisez une version fixe et enregistrezresponse.modelName()avec chaque décision. - Décider de la panne. Un filtre contre l'abus ou la fraude doit en général échouer fermé, c'est-à-dire rejeter ou retenir l'entrée. Un routage peut se replier sur une valeur par défaut.
- Régler les délais. Sauf réglage contraire, l'intégration TypeSafe attend 15 secondes pour se connecter et 60 secondes pour lire, puis refait 2 tentatives. Un appel peut donc durer plusieurs fois le délai.
- Ne pas en faire un contrôle d'accès. Ces composants optimisent la pertinence, le coût et la latence. Un document récupéré peut contenir une consigne qui tente d'influencer la décision : combinez-les avec d'autres contrôles dès que la sécurité est en jeu.
Le texte envoyé part chez le fournisseur du modèle. N'envoyez que ce qu'il faut, et n'activez pas la journalisation des requêtes en production s'il contient des données personnelles.
Vous voulez brancher un modèle de décision ou un agent IA sur une application Java / Spring Boot existante ? Je développe sur cette stack depuis 2015. Réserver un appel découverte de 30 minutes.