Un agent IA en 3 fichiers en Java

7 mai 2026

Un projet personnel pour explorer LangChain4j et les LLMs locaux : un marchand de jeu vidéo qui génère des armes et armures en respectant les règles d’un moteur de jeu, à partir de la demande en langage naturel du joueur.

Le plus intéressant : trouver la bonne séparation entre ce que fait le LLM (comprendre la demande, inventer le lore) et ce que fait Java (contraindre la génération, calculer les prix).

Le slideshow ci-dessous entre dans les détails de conception.

Code sous licence GPL3 sur mon gitlab

Java · LangChain4j · LLM local

Un agent IA
en 3 fichiers Java

Un LLM local joue le rôle d'un marchand de jeu vidéo. Il invente une arme ou une armure compatible avec le moteur de jeu, pas juste du texte.

JBang · zéro build
LangChain4j · agent + tools
Llama.cpp · LLM offline
Illustration de la hache décrite
Rare
Hache de l'Aube Brisée
Axe
Forgée à l'aube d'une éclipse, elle porte la fureur des anciens forgerons nains.
ATK 22 (+6) SPD 4 (+2)
168 pièces d'or
Illustration de la dague décrite
Épique
Lame du Crépuscule Ardent
Dagger
Sa lame arbore des motifs de braise incandescente qui pulsent comme des braises mourantes au crépuscule.
ATK 9 (+3) SPD 16 (+6) DEF 1 (+1)
198 pièces d'or
Illustration de la hache décrite
Uncommon
Cuirasse des Terres Grises
Chestplate
Acier grisé par les tempêtes du nord, elle absorbe les coups sans broncher.
DEF 18 (+4) SPD −1
196 pièces d'or
Démo

Parler au marchand

Pour ce projet l'interface est textuelle (sur terminal) et très simple : l'utilisateur décrit l'objet voulu en langage naturel puis précise son budget. L'objet généré par l'agent est également affiché au format texte, mais il est en réalité créé dans un format structuré pouvant être utilisé par le moteur de jeu.

$ jbang ItemMerchantAgent.java
The merchant stands before you, awaiting your request.
Your request › Je veux une dague rapide pour combattre les goblins ninjas.
Budget (gold) › 200

🗡 "Ah, un aventurier avec du goût, et du budget.
Laissez-moi vous montrer quelque chose de spécial."

⚔ Lame du Crépuscule Ardent (DAGGER)
Une lame forgée dans l'obscurité des mines naines, arborant des motifs rougeoyants.

✦ EPIC   ATK 9 (+2)   SPD 12 (+6)   DEF 1 (+1)
Prix : 198 pièces d'or

Pas de génération libre : l'item respecte les contraintes du moteur de jeu.

Architecture

LLM + Java, responsabilités distinctes

La clé de l'intégration d'un agent à un système existant : laisser suffisamment de liberté au LLM mais lui mettre des contraintes avec des outils bien pensés.

Responsabilités LLM
Comprendre la demande de l'utilisateur
Calibrer les bonus pour maximiser la puissance
Inventer le nom et le lore de l'item
Jouer le personnage du marchand
Responsabilités Java
Contraindre la génération d'item aux règles du jeu
Calculer le prix et la rareté
Orienter le LLM pour le respect du budget
Formater et retourner la fiche item

Règle d'or : le LLM apporte la créativité  ·  Java apporte la rigueur

Pourquoi utiliser un LLM ?

La compréhension du langage naturel est le vrai apport du LLM ici : sans lui, il faudrait coder des règles pour mapper "une dague rapide pour les goblins" vers DAGGER + SPD bonus.

Le système actuel est volontairement simplifié, mais en enrichissant le system prompt avec la sémantique du jeu (types de dégâts, faiblesses d'ennemis) le LLM pourrait adapter ses recommandations au contexte de l'adversaire et proposer des bonus vraiment pertinents pour la situation du joueur.

Architecture

Vraiment 3 fichiers

Pas de Maven. On utilise jBang pour gérer les dépendances et exécuter notre fichier .java.

📁 item_merchant/
ItemMerchantAgent.java
Agent.java
ItemSystemTools.java
llm.properties (config LLM local)
ItemMerchantAgent.java
Point d'entrée · CLI
  • Terminal interactif avec JLine
  • Prompts typés : texte libre + entier (budget)
  • Affiche la fiche item retournée par l'agent
Agent.java
Cerveau de l'agent
  • Construit avec Langchain4J pour réduire le boilerplate
  • System prompt : rôle du marchand + instructions
  • Mémoire glissante sur 30 messages
  • Plafond à 10 appels d'outils par requête (évite les boucles infinis)
ItemSystemTools.java
Outils @Tool · moteur de jeu
  • 12 prototypes d'items définis dans une enum Java
  • Calcul de prix déterministe : ATK 12g/pt · DEF 9g/pt · SPD 6g/pt
  • Applique les contraintes du moteur de jeu
  • previewItem() et finalizeItem() exposés au LLM en tant qu'outils
Le concept

Ce que fait l'agent

  • Choisit un prototype parmi le catalogue (épée, dague, plastron...)
  • Optimise les bonus de stats pour coller au budget sans le dépasser
  • Génère un nom unique et un texte de lore
  • Répond en personnage pour plus d'immersion

Pattern ReAct

Le pattern ReAct (Reason + Act) en action : chaque résultat d'outil est réinjecté dans le contexte du LLM, qui raisonne dessus pour décider de la prochaine action.

LangChain4j gère la boucle outil ↔ LLM automatiquement. Le modèle itère jusqu'à trouver le bon prix.
2 outils : previewItem pour simuler un item sans le valider, finalizeItem pour le retourner à l'utilisateur.

USER → "une arme puissante"  ·  budget 200g
LLM → previewItem(SWORD, atk+8, spd+0, def+0)
TOOL → Prix 228g : au-dessus du budget ❌
LLM → previewItem(SWORD, atk+5, spd+0, def+2)
TOOL → Prix 192g : EPIC, sous budget peut-être amélioré ⚠
LLM → previewItem(SWORD, atk+6, spd+1, def+2)
TOOL → Prix 198g : EPIC, très proche du budget ✓
LLM → finalizeItem(SWORD, atk+6, spd+1, def+2, "Lame...")

USER ← Fiche item complète 🗡

Pour que ça fonctionne, il faut que le LLM ait bien compris comment utiliser les outils. D'où l'importance du système prompt.

Fichier 1 / 3

ItemMerchantAgent.java (CLI)

Interface en ligne de commande avec JLine. Lit la requête et le budget, puis appelle l'agent.

private int run() throws IOException {

    Terminal terminal = TerminalBuilder.builder()
        .provider("FFM").system(true).build();
    Prompter prompter = PrompterFactory.create(terminal);

    // Prompts typés : texte libre + entier
    PromptBuilder builder = prompter.newBuilder();
    builder.createInputPrompt()
           .message("Your request").name("request").addPrompt()
           .createNumberPrompt()
           .message("Your budget in gold pieces")
           .allowDecimals(false).name("budget").addPrompt();

    /* ellipse : utiliser le prompter Jline pour récupérer request et budget */

    // Appel de l'agent avec le budget dans le prompt
    // et comme paramètre d'invocation
    String prompt = request + "
Budget : " + budget;
    Result<String> resp = agent.createItem(prompt,
        new InvocationParameters(Map.of("budget", Integer.valueOf(budget))));

    /* ellipse : affichage du résultat */
}

InvocationParameters : le budget est stocké comme métadonnées et pas uniquement dans le prompt. Les outils peuvent l'utiliser directement et ne se reposent pas sur l'interprétation du LLM.

Fichier 2 / 3

Agent.java (le service IA)

LangChain4j construit l'agent en quelques lignes.

// Langchain4j va construire une implémentation de cette interface
interface InnerAgent {
    Result createItem(
            @UserMessage String prompt, InvocationParameters parameters);
}

// constructor
public Agent(
        String modelUrl, String modelName, String apiKey) {
    this.model = OpenAiChatModel.builder()
            .baseUrl(modelUrl)     // http://localhost:8080/v1
            .modelName(modelName)  // "qwen"
            .apiKey(apiKey)      // "dummy"
            .build();

    this.agent = AiServices.builder(InnerAgent.class)
            .chatModel(model)
            .systemMessage(systemPrompt)     // rôle + workflow
            .tools(new ItemSystemTools())    // @Tool auto-découverts
            .maxSequentialToolsInvocations(10)   // cap la boucle
            .afterToolExecution(this::logTools)  // logging
            .chatMemory(
                    MessageWindowChatMemory.builder()
                            .maxMessages(30).build())
            .build();
}

chatMemory(30)
Historique glissant pour que le marchand se souvienne de la conversation.

maxSequentialTools(10)
Plafond sur la boucle preview pour éviter les boucles infinies (ça arrive avec les modèles pas assez intelligents).

Fichier 3 / 3

ItemSystemTools.java (le moteur d'items)

Implémente un système d'objets (armes et armures) simple que l'on pourrait retrouver dans un jeu vidéo. La formule de prix et les seuils de rareté sont déterministes. Le LLM ne peut donc inventer que des objets qui sont compatibles avec le moteur de jeu. Et les prix sont toujours cohérents.

// types d'objets et stats de base
public enum Prototype {
    //          weapon   atk  spd  def  basePrice
    DAGGER(true, 6, 10, 0, 60),
    SWORD(true, 12, 6, 2, 120),
    AXE(true, 16, 2, 0, 140),
    CHESTPLATE(false, 0, -2, 14, 160),
    // ...12 prototypes au total
}

// Rareté = ratio bonus-gold / basePrice
// UNCOMMON ≥ 20 %  ·  RARE ≥ 60 %  ·  EPIC ≥ 120 %
record RarityThresholds(double uncommon, double rare, double epic) {
    static RarityThresholds defaults() {
        return new RarityThresholds(0.20, 0.60, 1.20);
    }
}

private ComputedItem computeItem(
        Prototype proto, int atkBonus, int spdBonus, int defBonus) {
    //calcule l'item final, permet d'appliquer les contraintes du moteurs de jeu
    //exemple : pas d'attaque sur une armure
}
Stats disponibles
ATK Attaque12 g / pt ajoutés
DEF Défense9 g / pt ajoutés
SPD Vitesse6 g / pt ajoutés
Rareté = bonus dépensés / prix de base  ·  U ≥20% R ≥60% E ≥120%
On pourrait aller plus loin...
D'autres stats pourraient enrichir le système :
MRES : résistance magique
CRIT : chance de coup critique
RNG : portée d'attaque
LIFE : bonus de points de vie
Le LLM saurait les utiliser dès lors qu'elles sont décrites dans le system prompt.
Fichier 3 / 3

ItemSystemTools.java suite (les outils de l'agent)

Le pont LLM ↔ Java. Ces méthodes peuvent être appelées par le modèle, le code est exécuté et le retour est transmis au LLM. LangChain4j lit la signature des méthodes annotées @Tool pour générer les schémas JSON qui seront utilisés dans l'API du LLM.

@Tool("""
    Preview an item without finalizing it. Returns price,     rarity, and stat breakdown. Call repeatedly with adjusted     bonuses until the buy price fits the user's budget""")
public String previewItem(
        Prototype prototype,
        @P("Greater than 0") int atkBonus,
        @P("Greater than 0") int spdBonus,
        @P("Greater than 0") int defBonus,
        InvocationParameters parameters) {

  int budget = parameters.get("budget");   // budget depuis les métadonnées
  ComputedItem item = computeItem(prototype, atkBonus, spdBonus, defBonus);
  Rarity rarity = deriveRarity(item.totalDelta(), prototype.basePrice);
  int price = prototype.basePrice + item.totalDelta();

  // Retourne une prévisualisation au format texte pour le LLM.
  // On utilise la variable budget pour indiquer dans la réponse
  // au LLM si le budget est respecté.
  return formatPreview(prototype, rarity, price, budget);
}

Prototype est une enum Java → LangChain4j génère un paramètre "enum" dans le schéma JSON. Le LLM ne peut envoyer que des valeurs valides.

Le rôle

Le system prompt : donner un rôle au LLM

La personnalité et le workflow sont dictés dans un text block Java assemblé à l'initialisation.

Nous utilisons des règles suffisamment simples avec des statistiques communes à beaucoup de jeux vidéo, donc inutiles de les expliquer. Des règles plus spécifiques à notre moteur de jeu devraient être expliquées dans le prompt.

String systemPrompt = """
    You are a Dungeon Master playing the role of the
    Weapon and Armor Merchant. The user will ask for
    a weapon or armor to buy with a specific budget.
    You must invent an item for them.

    Workflow to follow:
      1. Call previewItem() with a Prototype enum value
         and chosen bonuses : get price + rarity feedback
         WITHOUT committing.
      2. Adjust bonuses and call previewItem() again until
         the result fits the budget / power target.
      3. Call finalizeItem() with the same configuration.

    Try to get as close as possible to the budget to
    propose the most powerful item possible.
    You must call finalizeItem() with your proposed item.

    Base item prototypes to choose from :
    """ + ItemSystemTools.Prototype.summarize();
    // → "DAGGER : ATK 6, SPD 10 SWORD : ATK 12, SPD 6, DEF 2"

Le catalogue de prototypes est généré dynamiquement depuis l'enum Java pour rester synchronisée avec le code.

Infrastructure

LLM local : exemple avec Llama.cpp

Le service LangChain4j utilisé est compatible avec l'API OpenAI. La plupart des moteurs d'inférence locaux (Llama.cpp, LM Studio, Ollama) implémente cette API.

# llm.properties
baseUrl=http://localhost:8080/v1
modelName=qwen
apiKey=dummy

Qwen 3.5 2B

  • ~3 Go de VRAM
  • Génération rapide
  • Réponses correctes
  • Garde-fou parfois nécessaire

Qwen 3.5 9B

  • ~10 Go de VRAM
  • Génération plus lente
  • Tool use très fiable
  • RP bien plus élaboré
# Télécharger le modèle (Hugging Face)
$ huggingface-cli download unsloth/Qwen3.5-2B-GGUF \
Qwen3.5-2B-Q4_K_M.gguf --local-dir ./models

# Lancer le serveur llama.cpp (endpoint OpenAI)
$ llama-server -m ./models/Qwen3.5-2B-Q4_K_M.gguf \
--port 8080 --jinja -fa
llama.cpp
github.com/ggml-org/llama.cpp
Qwen3.5-2B-GGUF
huggingface.co/unsloth/Qwen3.5-2B-GGUF

Données privées  ·  Pas de latence réseau  ·  Pas de coût à l'inférence

Bonus

Illustrer les items : génération d'image

Une fois l'item finalisé, on peut générer son illustration. La cohérence visuelle entre les items repose sur un prompt de base fixe qui définit le style graphique, on y concatène simplement le nom et la description de l'item généré.

Prompt de style
Définit la perspective, rendu, palette, épaisseur de traits, ombrage...
Assure que toutes les images partagent le même style.
Partie dynamique par item
Nom de l'item + description de lore générés par le LLM.
Aucun prompt image à écrire manuellement, le pipeline text→image est entièrement automatique.

Il s'agit d'un exemple de prototypage rapide. Ce type d'art généré ne peut pas remplacer un artiste pour un vrai jeu vidéo. Une direction artistique cohérente nécessite un vrai travail humain.

Exemple de prompt

Cuirasse of the Grey Lands, Chestplate. Made of steel grayed by northern storms, it absorbs blows without flinching.
Line art sprite for a medieval fantasy RPG video game, clean and precise black ink outlines with no anti-aliasing, consistent stroke weight of 2px for outer contours and 1px for inner details, rendered on a transparent background, top-down 3/4 oblique perspective slightly facing the viewer, single directional light source from the upper-left at 45° producing hard cel-shaded shadows with flat color fills using a limited palette of rich jewel tones, deep emerald greens, royal purples, warm amber, aged parchment beige, avoiding pure white in favor of warm cream, with 2 shading levels per color zone (base tone and one shadow pass 30% darker with a slightly cooler hue), medieval fantasy aesthetic with hand-forged iron, worn leather and arcane ornamental details, style consistent with classic JRPG illustrated sprites, isolated subject centered in frame with no background.
Cuirasse des Terres Grises
stable-diffusion.cpp
github.com/leejet/stable-diffusion.cpp
FLUX.2-klein-9B-GGUF
huggingface.co/leejet/FLUX.2-klein-9B-GGUF
Conclusion

Ce qu'on retient

3 fichiers Java peuvent suffire pour un agent IA conversationnel avec outils.

JBang est parfait pour prototyper rapidement.

LangChain4j permet de mettre en place un agent très rapidement sans boilerplate.

Un LLM local n'est pas si complexe à utiliser, mais peut nécessite du matériel puissant.

L'équilibre LLM créatif/Java déterministe pour contrôler la génération du LLM et avoir des résultats fiables.

🗡 Code disponible sur Gitlab
Testez avec votre LLM local préféré (Qwen, Gemma) ou en ligne avec ChatGPT

À propos de l'auteur

Florian Susini

Je suis un passionné de développement web, de logiciels open-source et de tout ce qui touche au numérique. Je partage de temps en temps des projets web ou des idées sur ce blog.

Retour à l'accueil