Un agent IA en 3 fichiers en Java
- Java
- LangChain4J
- Llama.cpp
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
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.



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.
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.
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.
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.
Vraiment 3 fichiers
Pas de Maven. On utilise jBang pour gérer les dépendances et exécuter notre fichier .java.
ItemMerchantAgent.java
Agent.java
ItemSystemTools.java
llm.properties (config LLM local)
- Terminal interactif avec JLine
- Prompts typés : texte libre + entier (budget)
- Affiche la fiche item retournée par 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)
@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()etfinalizeItem()exposés au LLM en tant qu'outils
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.
previewItem(SWORD, atk+8, spd+0, def+0) previewItem(SWORD, atk+5, spd+0, def+2) previewItem(SWORD, atk+6, spd+1, def+2) finalizeItem(SWORD, atk+6, spd+1, def+2, "Lame...") Pour que ça fonctionne, il faut que le LLM ait bien compris comment utiliser les outils. D'où l'importance du système prompt.
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.
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).
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
} MRES : résistance magique
CRIT : chance de coup critique
RNG : portée d'attaque
LIFE : bonus de points de vie
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 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.
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é
$ 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
github.com/ggml-org/llama.cpp
huggingface.co/unsloth/Qwen3.5-2B-GGUF
Données privées · Pas de latence réseau · Pas de coût à l'inférence
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é.
Assure que toutes les images partagent le même style.
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.

github.com/leejet/stable-diffusion.cpp
huggingface.co/leejet/FLUX.2-klein-9B-GGUF
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