ImageToDiagram – 2è partie – REX

Comme promis une 2e partie avec un peu plus de LLM dedans.

Depuis la sortie de chatGPT, malgré les versions successives avec leurs lots d’améliorations, a chaque use case que j’expérimente mon sentiment reste le même : « C’est presque bon… mais il manque un rien pour que le résultat soit vraiment exploitable. ».

L’objectif ici : me concentrer sur un seul use case, pousser l’expérimentation plus loin et chercher à obtenir un résultat fiable et consistant.

Première tentative : l’approche naïve

Je commence simple : à partir d’une image, je demande au LLM de me générer un diagramme dans un format comme mermaid, mxGraphModel, SVG ou D2.

Résultat ? Dans le meilleur des cas (avec Claude Sonnet), j’ai un résultat utilisable 1 fois sur 5. Les autres modèles sont très loin derrière, même si Claude 3.7 et Artifacts ont amélioré la donne depuis mes premiers tests.

Approche itérative : Chain of Thought après erreur

Ma première idée d’amélioration est donc de prendre la sortie obtenue, la passer à la lib de rendu, récupérer l’erreur (si erreur il y a) et la renvoyer au modèle pour qu’il s’auto-corrige. Un genre de Chain of Thought appliqué à la correction syntaxique.

Mais plusieurs points sont bloquant :

  • Les messages d’erreur ne sont pas assez explicites ou consistants
  • Beaucoup de problèmes de rendu ne génèrent aucune erreur explicite
  • Le coût en tokens explose en itérant sur des textes/images longs
  • Et les temps de traitement deviennent trop longs avec les images

Autre point : je laisse tomber l’idée de générer directement du SVG. Trop bancal. Je n’ai trouvé aucun éditeur JS de SVG qui soit convaincant + le format SVG est très (trop) complexe : gestion de la viewbox, placement des éléments, css, …

Option fine-tuning ? Trop cher, trop risqué

Je pense un moment à fine-tuner un modèle… mais je tombe vite sur plusieurs gros murs :

  • Difficile de constituer un corpus de données propre, libre de droits
  • Coût énorme : chez AWS Bedrock, charger un modèle fine-tuné coûte ~5000€/mois
  • OpenAI est bien moins cher de ce côté, mais pas question pour moi d’envoyer par exemple des schémas d’architecture réseau chez OpenAI ou autre plateforme grand public, suite à mon analyse des fournisseurs que vous trouverez ici. J’ai donc choisi de passer par des services destinés à des professionnels plutôt qu’au grand public ( -> AWS Bedrock, Azure OpenAiService, …)

Solution : miser sur le format JSON

L’étape d’après c’est ce point très simple : la seule grammaire que tous les LLMs respectent, c’est le JSON.

Les systèmes de tool/function calling s’appuient tous dessus. Et même les modèles sans « JSON mode » s’en sortent bien… à condition que le prompt soit bien construit. (reposez vous sur une lib pour cette partie, en Java SpringAi et Langchain4J le proposent)

J’utilise donc une structure intermédiaire en JSON pour décrire le diagramme. Ensuite, j’applique un pattern visiteur qui transforme ce JSON vers du D2Lang ou Mermaid.

Résultat : les sorties sont fiables, structurées, testables.

Deuxième étape : mieux comprendre les diagrammes

Prenons un exemple concret : un diagramme de Gantt. CI-dessus en exemple GPT 4o me sort un JSON avec une tâche « Réparer la charpente » avec une durée de 8 jours, planifiée du jeudi 8 au samedi 17.

Problème : sur le diagramme, les dates affichées vont du 9 au 21. Pourquoi ? Parce que seuls les jours ouvrés sont comptabilisés dans les 8 jours, mais les week-ends et jours fériés allongent la planification. Ce genre de subtilité, un LLM ne le comprend pas spontanément.

La clé : le bon prompting

Chaque type de diagramme a ses spécificités. Et c’est là que le prompting fait toute la différence. J’en viens donc à faire systématiquement les étapes suivantes :

  1. Identification du type de schéma présent sur l’image (diagramme de séquence, de classe, …)
  2. utilisation d’un prompt spécifique à ce type de schéma pour obtenir une description textuelle exhaustive des éléments métiers pour ce type de schéma
  3. A partir de la description textuelle, extraction de données structurées
  4. génération du code mermaid / D2Lang / …

Et pour générer le prompt du point 2, pour chaque type de schéma que je veux supporter dans mon application je passe par le workflow suivant :

  1. Je définis le format JSON attendu (ex. pour un Gantt : Section, Milestone, Task, etc.)
  2. Je précise que je veux extraire depuis une image une description textuelle complète
  3. J’ajoute des indications sur les points « métier » du type de diagramme qui peuvent être source d’ambiguïté / de complexité (par ex pour les tâches d’un diagramme de Gantt : « relève les dates de début et de fin des tâches », plutôt que la durée qui comme on l’a vue est plus complexe à gérer du fait de la problématique des « jours ouvrés »)
  4. Je demande à un LLM de me générer un prompt couvrant ce besoin

Validation : tests visuels et itérations

Pour valider les résultats, je conseil la mise en place de tests de niveaux test d’intégration :

  • Pas d’assertions classiques
  • Un rendu Markdown des résultats pour un contrôle visuel
  • Les points spécifiques comme la cohérence des dates, la structure des objets, etc peuvent être contrôlés automatiquement pour éviter les régressions sur des points particuliers et sensibles

Ces tests ne sont pas dans la CI, je les joue lorsque je modifie des prompts ou quand j’essaye de nouveaux modèles, de nouveaux paramètres d’inférence, …

Et toujours, je n’écris aucun prompt moi-même. J’applique cette boucle :

« Voici ce que je veux améliorer / les cas qui posent problème »
+
« Voici mon prompt actuel »
→ « Proposes-moi une meilleure version »
→ « Je teste, puis j’itère »


Conclusion

Oui, le prompt engineering permet d’améliorer sensiblement les résultats.

Mais non, les résultats ne sont jamais parfaits, jamais exactement ce que j’espérais obtenir. Il y a toujours ce petit « truc en plus » qui manque, peut-être impossible à obtenir automatiquement. Bref à l’instant t, pour moi, une IA ne fait pas un boulot seule, l’UX doit toujours prévoir un moyen pour l’utilisateur de compléter les 5% restants

Et pour la route, ma recommandation : n’écrivez pas vos prompts, fournissez des exemples de résultats, décrivez vos données d’entré puis faites générer vos prompts par des LLMs. La console d’Anthropic, le « prompt management » du studio Vertex Ai de Google sont des alliés précieux pour créer les bons prompts à partir d’exemples des tâches à réaliser

ImageToDiagram – 1ère partie

Cet article pour lancer une série consacrée à mon modeste voyage pour dev une appli utilisant assez intensivement les LLMs, sait-on jamais quelques leçons pourront servir à d’autres que moi.

C’est un collègue qui, lors d’un échange, me partage un magnifique schéma de son déploiement réseau (je me garderai bien de critiquer ses talents de dessinateur puisque personnellement je suis toujours bloqué à l’étape des bonhommes bâtons). Bouche bée comme lorsque mon médecin traitant m’envoi à la pharmacie avec quelques traits relevant plus de l’art minimaliste que d’une tentative de communication je me dis que ça serait rigolo de voir ce que ChatGPT peut en faire. Puis de voir si il est capable de reproduire le diagramme en SVG directement (puisque le SVG est un format texte – mmmm possible que ça ait bougé depuis Décembre l’année dernière mais le résultat tenait plus d’une boucherie que d’un diagramme) ou bien en mxGraphModel (format XML permettant la serialization des diagrammes pour draw.io/diagrams.net). Et c’est en l’envoyant à Claude que je découvre une lib que je trouve vraiment génial : mermaid.js

L’idée de Mermaid.js c’est de permettre de décrire un diagramme dans des formats simples de type markdown. Et cette librairie est assez complète puisqu’elle permet de générer une bonne vingtaine de diagrammes très différents allant des classiques FlowChart ou diagramme de classe jusqu’à des graphiques de Sankey ou des historiques de commit Git. Et les intégrations sont nombreuses : github, gitlab, confluence, vsStudio, … (voir plus).

ça y est c’est certain, mon SAAS va atteindre les 10k de MRR en 3 jours!. J’ai suffisamment séché devant Powerpoint/Ms Project/… en me demandant comment diable je vais faire pour générer le Gantt de la roadmap attendue par le copil pour savoir que ce genre de soft sont une vraie galère et que pouvoir faire simplement ceci (voir plus) :

gantt
    title ma roadmap
    dateFormat YYYY-MM-DD
    section Gestion des utilisateurs
        Social login avec AOL :a1, 2014-01-01, 3d
        Social login avec FaceBook :after a1, 2d
    section TODO list
        Ajouter un TODO :2014-01-03, 2d
        Passer le TODO à Done    :4d

c’est un véritable GAME CHANGER !!!! (oui oui je m’emballe dès fois)

En vrai les documentations produits / repos git sont truffées de diagramme de classes / schéma réseaux pas à jours et dont on a perdu le fichier source modifiable, ou de photos illisibles de tableaux blancs qu’un jour on a voulu re-créer sous forme numérique mais entretemps un mail plus important est arrivé et c’est la photo flou qui est restée dans le wiki d’entreprise. Je pense donc toujours que l’idée de ImageToDiagram est bonne :

à partir d’une image, recréer un diagramme dans un format simple clair et lisible qui sera simple à faire évoluer

Image d’origine

sortie SVG à partir de Mermaid

Mermaid est une lib puissante mais elle atteint vite ses limites lorsqu’on sort du cadre des types de diagrammes gérés (le diagramme d’architecture ne permet pour l’instant de réaliser que des architectures très simples). J’ai donc cherché une autre librairie (un peu moderne si possible) pour gérer des rendus plus complexes. Je n’ai malheureusement pas trouvé mon bonheur qui utiliserait du markdown et qui propose autant d’intégrations que mermaid. Mais D2lang m’a semblé un bon compromis en terme d’expressivité de la syntaxe et de richesse du contenu. Avec D2Lang il n’y a qu’une grammaire et les représentations de type diagramme de séquence ou de classe peuvent être intégrées dans un même rendu avec des éléments de flow et autre. Un autre point intéressant par rapport à mermaid c’est que le positionnement des éléments peut être indiqué grâce à l’opérateur near (voir ici), le résultat est l’obtention de diagramme beaucoup plus proches de ce qu’un utilisateur peut attendre (et potentiellement beaucoup plus fidèles à l’image d’origine en considérant notre use case).

D2Lang est une lib en Go et j’avais donc besoin d’un service permettant le rendu des diagrammes en un format image, si vous utilisez divers diagrammes dans votre quotidien, allez voir kroki! le principe du service est de permettre le rendu dans des formats images de diagrammes provenant de (très) nombreuses librairies. En regardant les librairies pour lesquelles ils peuvent fournir un rendu vous trouverez forcément une lib qui fait votre bonheur.

La référence pour moi pour la réalisation de schéma d’architecture ou autre reste draw.io (devenu diagrams.net). J’ai donc voulu le proposer aussi en format de sortie (et l’éditeur est facilement intégrable du côté front), mais je ne suis pas certain de l’intérêt et le format de donnée basé sur XML est beaucoup trop verbeux (ça coute cher en token), les résultats sont très complexes à influencer avec du prompting. Si les utilisateurs demandent plus de contrôle sur le positionnement des éléments que ne peuvent proposer mermaid.js ou d2Lang (et si il y a des utilisateurs) je pense que cela passera soit par excalidraw soit par un outil a développer.

Les types de diagrammes gérés pour l’instant dans l’application sont les suivants : diagrammes de séquence, de classes, d’entités et relations bdd, les flowchart, les mindmap, les gantt, les timeline sortent au format mermaid.js, et des diagrammes plus complexes du type architecture par D2lang.

Autre angle d’attaque : Le RAG.

Le format de sortie préféré des LLMs c’est le markdown puisqu’on a la capacité sur les interfaces web de traiter ce markdown pour avoir des rendus sympas (tableaux, mise en forme) et des fonctionnalités comme Claude Artefacts (L’aperçu sur un panneau latéral du code créé).

On a vu notamment avec Mistral OCR que l’état de l’art actuel c’est d’être capable de récupérer des PDF scannés en faisant la part des choses entre le texte, les tableaux et les images. Donc en proposant ImageToDiagram en API on peut envisager que les diagrammes présents dans une source de données soient traités comme du texte dans le process d’indexation (calcul des embeddings) et que le RAG permette dans la discussion d’afficher des graphiques, puis de poursuivre la conversation en demandant par exemple d’ajouter des données à jour dans le graphique.

Bon pour l’instant on n’a pas trop parlé IA générative mais c’était juste une introduction !

Quel framework AI utiliser en Java?

Comme j’interviens sur des projets développés en Java s’est posé rapidement la question de comment intégrer des fonctionnalités utilisant de l’IA générative au sein de ces projets. Les outils python les plus communs sont bien entendus LangChain et LlamaIndex.

Un collègue m’a montré OpenAi-Java, un client non officiel pour utiliser les APIs OpenAI. A savoir que Azur dans son SDK propose aussi un client supportant la connexion à Azure OpenAi Service et aux API OpenAi.

Ces librairies ne sont que des clients supportant la connexion à un service (enfin deux dans le cas de la lib Azure). Dans le monde en évolution rapide des LLMs il semble dommage de se coupler ainsi à un fournisseur de service. A ma connaissance en Java nous avons actuellement deux choix possibles : LangChain4J ou Spring AI.

Langchain4J

LangChain4j est la première librairie que j’ai utilisé. La lib est simple d’utilisation et s’inspire (sans être liée si ce n’est par le nom) des libs plus connues (et plus complètes) LangChain, Haystack, LlamaIndex.

La lib abstraie les ChatLanguageModel (l’accès à un service d’IA générative textuelle d’un fournisseur), les EmbeddingModel (l’accès à un modèle permettant la représentation sémantique sous forme vectorielle d’un texte/d’une image) les EmbeddingsStore (base de données vectorielles), la lecture de documents.

A cela s’ajoute différents éléments bien utile : des mémoires (permettent de maintenir une conversation comme le font les Threads dans l’API OpenAI), des QueryTransformer (très utiles pour le RAG -> par exemple on va réécrire le prompt de l’utilisateur à partir de la conversation pour récupérer le contexte des demandes précédentes de l’utilisateur et c’est ce prompt reformulé que l’on va utiliser pour la recherche de documentation).

On peut donc assez simplement combiner ces éléments pour l’ingestion de documents dans un EmbeddingStores ou, grâce aux AI Services, pour implémenter les cas d’usage les plus courants pour ces outils : RAG, Tools, extraction de données structurées, classification

Mon avis actuel est que la librairie est très efficace et permet de réaliser rapidement des POCs. Cependant elle manque pour l’instant beaucoup de flexibilité et d’extensibilité pour répondre aux besoins d’une application complexe et complète. (il n’est par exemple pas possible de citer les références des documents utilisés lors de la génération d’une réponse ce qui est bien dommage puisque cela renforce considérablement le niveau de confiance pour l’utilisateur)

Un tuto Baeldung pour la route.

Spring AI

Depuis 2 ans je regarde souvent https://app.dvf.etalab.gouv.fr du coup je me suis dit que ça pourrait être cool de l’avoir en chat et du coup j’ai lancé un petit projet de test avec spring AI : chatDVF. Bon rien de fou, le projet est initialisé avec JHipster et c’est un work in progress mais j’ai appris quelques trucs au passage.

Donc dans SpringAI, comme pour LangChain4J on retrouve tout ce qui permettra de découpler notre application d’un fournisseur, que ce soit au niveau du modèle de langage ou du modèle d’embeddings ou au niveau des embeddingStores.

Par contre je n’ai pas trouvé d’équivalent au AI Services de LangChain4j ce qui est bien dommage puisque le niveau d’abstraction qu’ils fournissent permettent vraiment d’avoir un code concis et efficace.

Dans les faits voici les points sur lesquels j’ai souffert :

  • le manque de ce que Langchain4J appelle ChatMemory : je me suis retrouvé à m’appuyer sur le front pour me renvoyer la conversation et avoir les messages précédents
  • avec la conversation, le but est de ne pas envoyer juste le dernier message de l’utilisateur au LLM mais une compression des x derniers messages. Le but étant d’être capable de gérer une conversation sur laquelle l’tuilisateur ne répète pas dans chaque message la totalité de sa demande mais la complète au fur est à mesure des réponse qu’il reçoit pour préciser sa demande. A ce niveau Langchain fournit un QueryCompresser qu’on ajoute à la configuration de son agent. Dans le cas de Spring AI j’ai du implémenter un service réalisant cette opération (rien de bien compliqué mais c’est clairement du boilerplate qui sera recopié dans chaque projet…)
  • la persistence de mes embeddings n’a pas été aisée non plus, je n’ai pas ajouté de metadata dans mes embeddings et ça causait des soucis (si vous testez le projet en local vous verrez que vous aurez des soucis à ce niveau avec le code en l’état)
  • Je n’ai pas encore réussi a avoir la bonne configuration pour les appels de fonction local. Sur cet aspect je n’ai pas trop pris le temps, ça ne doit pas être grand chose et pour le coup l’approche en s’appuyant sur java.util.function.Function est plutôt intéressante.

Un tuto Baeldung pour la route.

Edit: après avoir du gérer un ticket sur langchain4j par rapport a une PR que j’avais envoyé j’ai compris un truc : en fait SpringAI évite soigneusement de proposer des prompts (et donc d’avoir a les maintenir et gérer l’instabilité, les compatibilités, …). En effet l’extraction de POJO depuis un texte libre sur langchain4j se fait très simplement en définissant une interface et il prend ensuite en charge le prompt pour l’extraction du POJO. Simple et efficace mais un changement du code générant ce prompt au niveau de la librairie peut bien sûr avoir des répercussions sur le fonctionnement de votre application. Je n’ai pas trouvé de doc permettant d’affirmer que c’est la politique de Spring AI de ne jamais descendre au niveau du prompt mais c’est ce qu’on constate. On a donc deux librairies avec des approches bien différentes.

Ma conclusion a l’instant T :

Pour l’instant la balance fonctionnalité / prise en main penche clairement en faveur de Langchain4J car plus complète et simple d’appréhension (le repo d’exemples est aussi un point positif) mais les deux librairies sont en développement actif (une dizaine de PR ouverte chacune pendant la dernière semaine) donc ça reste une affaire à suivre…

2023 année des LLMs

Si une chose est à retenir de l’année passée d’un point de vue techno c’est l’avènement des LLMs auprès du grand public. OpenAI a bouleversé l’univers de l’IA en affichant au vu de tous les capacités de ces grands modèles de langages.

Nous avons tous joué avec la version gratuite de ChatGPT, composé des odes à l’odeur des patates avant leur découpage et passage à la friteuse, demandé de l’aide pour rédiger un mail lorsqu’on avait le syndrome de la feuille blanche ou rédigé un discours de départ suite à une rupture conventionnelle et se retrouver a déclamer d’un ton enflammé un discours de départ à la retraite (et oui a des moments on touche du doigt les limites de ces outils statistiques).

Si Microsoft investit si fortement sur ces technos (on a vu récemment encore un accord avec Mistral pour l’exploitation de leurs modèles sur le cloud Azure) c’est bien qu’ils ne s’y trompent pas et tous les professionnels de l’informatique s’en doutent, la manière d’interagir avec les machines est en train de changer radicalement.

Si 2023 a été l’année de la découverte pour le grand public, 2024 est le moment ou toutes les entreprises cherchent à voir comment mettre ces outils au service de leurs employés pour gagner en productivité.

Beaucoup de choses vont se passer cette année, le paysage sera peut être complètement différent en 2025, on en reparlera d’ici là 😉