CrĂ©er un agent NL2SQL avec Semantic Kernel đŸš€

Passer du langage naturel au SQL (on parle d’agent NL2SQL) n’est pas une mince affaire. Pourtant, l’idĂ©e de pouvoir questionner une base de donnĂ©es en language naturel est terriblement sĂ©duisante, que ce soit pour aider des analystes non techniciens ou pour gagner du temps en dĂ©veloppement. En tant que dĂ©veloppeur et passionnĂ© d’IA, je me suis donc lancĂ© un dĂ©fi : concevoir moi-mĂȘme un agent NL2SQL fiable en m’appuyant sur Semantic Kernel, tout en y intĂ©grant des garde-fous pour Ă©viter les dĂ©rapages.

Dans cet article, je vais vous raconter cette aventure, du constat des difficultĂ©s aux solutions mises en place (notamment un composant de Quality Assurance fait maison), avec du code et des exemples Ă  l’appui. Attachez vos ceintures, on plonge dans le SQL assistĂ© par IA ! 😉

Les dĂ©fis de la gĂ©nĂ©ration de requĂȘtes SQL en langage naturel

Traduire automatiquement une question en français en une requĂȘte SQL correcte est un dĂ©fi technique de taille. Voici quelques difficultĂ©s majeures que j’ai rencontrĂ©es :

  • Hallucinations de l’IA : Un LLM (modĂšle de langage) peut “inventer” des noms de tables ou de colonnes qui n’existent pas. Si le schĂ©ma de base de donnĂ©es n’est pas fourni explicitement dans le contexte, le modĂšle va deviner en s’appuyant sur sa connaissance gĂ©nĂ©rale
 avec le risque de tomber Ă  cĂŽtĂ©. Par exemple, demander « Donne-moi la liste des utilisateurs actifs » pourrait le pousser Ă  gĂ©nĂ©rer une requĂȘte sur une table Users alors que votre base utilise Utilisateurs ou Accounts. Ces hallucinations donnent des requĂȘtes SQL invalides ou inadaptĂ©es.
  • ComplexitĂ© des schĂ©mas : Les bases de donnĂ©es du monde rĂ©el ne sont pas de simples tables isolĂ©es. On a souvent des dizaines de tables reliĂ©es par des clĂ©s Ă©trangĂšres, avec des noms pas toujours Ă©vidents. Le modĂšle doit comprendre que « combien de commandes chaque client a passĂ©es » implique de joindre les tables Clients et Commandes, et peut-ĂȘtre LignesDeCommande selon la structure. Plus le schĂ©ma est large et technique, plus il est difficile pour l’IA de s’y retrouver. Fournir tout le schĂ©ma dans chaque requĂȘte n’est pas idĂ©al (beaucoup de tokens, et confusion possible), il faut donc une stratĂ©gie pour que le modĂšle ait le contexte pertinent sans ĂȘtre noyĂ© sous les dĂ©tails.
  • RequĂȘtes SQL incorrectes ou incomplĂštes : MĂȘme si le LLM connaĂźt bien le schĂ©ma, rien ne garantit qu’il va gĂ©nĂ©rer du premier coup une requĂȘte 100% correcte. Il peut oublier une condition (WHERE), mal utiliser une agrĂ©gation (GROUP BY), ou se tromper dans la syntaxe. Par exemple, j’ai vu des cas oĂč l’IA propose une sous-requĂȘte lĂ  oĂč un simple JOIN suffirait, ou tente un SELECT * inefficace sur une Ă©norme table lĂ  oĂč on attendait une sĂ©lection prĂ©cise. En somme, le SQL produit est probable (selon le modĂšle) mais pas toujours exact. Il faut prĂ©voir la possibilitĂ© que la requĂȘte nĂ©cessite des ajustements ou des vĂ©rifications.

En rĂ©sumĂ©, sans aide supplĂ©mentaire, l’IA gĂ©nĂ©rative seul n’est pas un expert SQL fiable. Lui donner la capacitĂ© de formuler des requĂȘtes exploitables demande de lui fournir du contexte (schĂ©ma, exemples) et d’encadrer sa gĂ©nĂ©ration pour Ă©viter les Ă©carts. C’est exactement lĂ  oĂč Semantic Kernel va entrer en jeu dans ma solution.

Risques Ă  laisser une IA rĂ©diger (et exĂ©cuter) du SQL 😹

Au-delĂ  de la difficultĂ© Ă  obtenir une requĂȘte correcte, il y a une question cruciale : mĂȘme si l’IA propose une requĂȘte SQL, peut-on l’exĂ©cuter en toute confiance sur la base de donnĂ©es ? Quelles sont les consĂ©quences potentielles ? J’ai identifiĂ© plusieurs risques sĂ©rieux Ă  mitiger avant de laisser mon agent NL2SQL branchĂ© sur une base de prod :

  • Des requĂȘtes dangereuses ou imprĂ©vues : Un LLM n’a pas de conscience du bien ou mal technique. Si on lui demande « Supprime tous les utilisateurs », il pourrait trĂšs bien sortir un DROP TABLE Users; ou un DELETE FROM destructeur. đŸ˜± Évidemment, on ne veut jamais qu’une IA exĂ©cute ce genre de commande sans contrĂŽle humain ! MĂȘme sans aller jusque-lĂ , le modĂšle pourrait, par maladresse, gĂ©nĂ©rer une modification non voulue (par ex., confondre « liste des ventes » et « efface les ventes »). Interdire les opĂ©rations d’écriture (INSERT/UPDATE/DELETE
) est un prĂ©requis de sĂ©curitĂ© de base pour notre agent NL2SQL. On le limitera aux requĂȘtes de lecture (SELECT) sur un ensemble autorisĂ© de tables/vues.
  • Performances et charge : Une requĂȘte mal fichue peut faire trĂšs mal Ă  votre SGBD. Imaginons que l’agent gĂ©nĂšre un SELECT * sur une table de millions de lignes, ou une jointure cartĂ©sienne gĂ©ante par oubli d’une condition de jointure
 On risque de saturer le serveur, de lock des tables, bref d’affecter les performances pour tous les utilisateurs. Il faut donc contrĂŽler la taille et la complexitĂ© des requĂȘtes. Par exemple, Ă©viter les requĂȘtes sans clause WHERE sur des tables volumineuses, ou les agrĂ©gations sans filtre sur des milliards de donnĂ©es. IdĂ©alement, l’agent devrait ĂȘtre capable de refuser ou d’ajuster une requĂȘte potentiellement trop lourde.
  • ConfidentialitĂ© des donnĂ©es : L’accĂšs aux donnĂ©es doit ĂȘtre restreint Ă  ce qui est lĂ©gitime. Si l’agent NL2SQL est mis Ă  disposition d’utilisateurs, il ne doit pas leur permettre d’extraire des donnĂ©es sensibles non autorisĂ©es. Par exemple, un employĂ© lambda ne devrait pas pouvoir demander « Montre-moi les salaires de tous les employĂ©s » si ce n’est pas dans ses droits. Dans mon cas, j’ai conçu l’agent pour un usage en interne, mais il faut garder Ă  l’esprit la nĂ©cessitĂ© d’intĂ©grer l’agent dans le systĂšme d’authentification et de permissions existant. Aussi, sur un plan plus technique, envoyer le schĂ©ma complet ou des donnĂ©es brutes au modĂšle (surtout s’il est hĂ©bergĂ© dans le cloud) peut poser des problĂšmes de conformitĂ© ou de fuite de donnĂ©es. Il convient de ne partager avec l’IA que le minimum requis (par ex., descriptions de schĂ©ma, et seulement pour les Ă©lĂ©ments nĂ©cessaires Ă  la requĂȘte).
  • RĂ©sultats incorrects ou inappropriĂ©s : Si l’agent comprend mal la question, il peut trĂšs bien fournir une rĂ©ponse parfaitement formulĂ©e
 mais fausse ou hors sujet 😕. Le danger est qu’un utilisateur non averti prenne la rĂ©ponse pour argent comptant. Imaginons que l’agent, suite Ă  une confusion, rĂ©ponde avec des chiffres qui ne correspondent pas vraiment Ă  la question posĂ©e. Cela peut induire de mauvaises dĂ©cisions. On touche ici Ă  la confiance dans le systĂšme : il faut idĂ©alement que l’agent exprime aussi son niveau de certitude, ou s’abstienne de rĂ©pondre quand il n’est pas sĂ»r de lui. Mieux vaut aucune rĂ©ponse qu’une rĂ©ponse erronĂ©e en business intelligence !

En somme, brancher un agent LLM sur une base de donnĂ©es sans filet de sĂ©curitĂ©, c’est s’exposer Ă  des soucis de sĂ©curitĂ©, de performance et de fiabilitĂ©. Conscient de ces risques, j’ai orientĂ© le dĂ©veloppement de mon agent NL2SQL autour d’un principe fondamental : jamais de requĂȘte non maĂźtrisĂ©e. Chaque requĂȘte candidate doit passer par des filtres de qualitĂ© avant d’ĂȘtre exĂ©cutĂ©e pour de vrai. Heureusement, j’ai pu m’appuyer sur Semantic Kernel pour mettre en place ces garde-fous.

Conception de l’agent NL2SQL avec Semantic Kernel

La bonne nouvelle, c’est qu’il existe maintenant des outils pour orchestrer des LLMs et du code natif de façon fluide. Comme je l’avais Ă©voquĂ© dans un prĂ©cĂ©dent billet, des orchestrateurs comme Semantic Kernel de Microsoft permettent de combiner le meilleur des deux mondes : la comprĂ©hension du langage par l’IA, et l’exĂ©cution fiable de logique mĂ©tier (ici du SQL) par du code classique. C’est exactement l’approche que j’ai suivie pour construire mon agent NL2SQL.

Architecture gĂ©nĂ©rale de l’agent

Mon agent “DatabaseAgent” se compose de plusieurs briques qui collaborent pour transformer une question en langage naturel en une rĂ©ponse basĂ©e sur les donnĂ©es :

  1. MĂ©moire du schĂ©ma : DĂšs l’initialisation de l’agent, j’utilise le Kernel Memory de Semantic Kernel pour stocker une reprĂ©sentation du schĂ©ma de la base de donnĂ©es. ConcrĂštement, je parcours les tables, colonnes et relations (par exemple les clĂ©s Ă©trangĂšres) et j’enregistre ces informations dans une mĂ©moire sĂ©mantique. Ainsi, l’agent a en quelque sorte une connaissance de la structure de la base, qu’il peut retrouver au besoin via des similaritĂ©s sĂ©mantiques. PlutĂŽt que de fournir tout le schĂ©ma Ă  chaque question, l’agent peut rĂ©cupĂ©rer juste les Ă©lĂ©ments pertinents (tables, champs concernĂ©s) en fonction de la question posĂ©e, un peu Ă  la maniĂšre d’une recherche documentaire (Retrieval Augmented Generation). Cette Ă©tape rĂ©duit les risques d’hallucination (puisque l’IA a accĂšs aux vrais noms) et allĂšge la charge cognitive du modĂšle.
  2. InterprĂ©tation de la question & gĂ©nĂ©ration du SQL : Lorsqu’une question utilisateur est posĂ©e, l’agent construit un prompt qui combine la question en langage naturel et les informations de schĂ©ma pertinentes retrouvĂ©es en mĂ©moire (noms de tables, descriptions, etc. liĂ©s aux termes de la question). Il envoie ensuite ce contexte au modĂšle de langage (par exemple GPT-4) en lui demandant de formuler la requĂȘte SQL correspondante. C’est une fonction sĂ©mantique typique de Semantic Kernel : on fournit un template de prompt et le kernel le remplit avec les donnĂ©es (question + schĂ©ma) avant d’appeler l’IA. Le rĂ©sultat attendu de cette fonction est du texte SQL. À ce stade, on obtient une proposition de requĂȘte, par ex. « SELECT … FROM … JOIN … WHERE … ».
  3. VĂ©rifications de la requĂȘte (Quality Assurance) : Avant exĂ©cution, la requĂȘte SQL gĂ©nĂ©rĂ©e passe par une sĂ©rie de filtres de qualitĂ© (j’y reviens dans la section suivante). C’est une Ă©tape cruciale oĂč l’agent va Ă©valuer si la requĂȘte est sĂ»re et pertinente. S’il y a le moindre doute (requĂȘte non pertinente, potentiellement dangereuse ou trop coĂ»teuse), l’agent peut soit ajuster sa requĂȘte, soit refuser de l’exĂ©cuter tel quel. Cette logique de vĂ©rification est implĂ©mentĂ©e sous forme de composants injectables (grĂące Ă  la configuration de Semantic Kernel en C#) pour garder la solution modulaire.
  4. ExĂ©cution en base de donnĂ©es : Si la requĂȘte passe les filtres de QA, on la considĂšre suffisamment fiable pour ĂȘtre exĂ©cutĂ©e. L’agent utilise alors un plugin natif (en l’occurrence, un appel Ă  la base de donnĂ©es via une connexion .NET classique) pour exĂ©cuter le SQL et rĂ©cupĂ©rer les rĂ©sultats. C’est ici que la sĂ©paration des responsabilitĂ©s est prĂ©cieuse : l’IA a produit la requĂȘte, mais c’est le moteur SQL rĂ©el qui fait le calcul sur les donnĂ©es, garantissant un rĂ©sultat exact et Ă  jour. Semantic Kernel permet d’enchaĂźner cela proprement : aprĂšs la fonction sĂ©mantique de gĂ©nĂ©ration SQL, on appelle une fonction native (C#) d’exĂ©cution SQL. Tout ça s’orchestre de façon transparente via l’agent.
  5. RĂ©ponse Ă  l’utilisateur : Enfin, l’agent formule une rĂ©ponse Ă  l’intention de l’utilisateur. Selon le cas, ça peut ĂȘtre brute (par ex. afficher un tableau de rĂ©sultats ou un nombre), ou rĂ©intĂ©grĂ© dans une phrase en français. Par exemple « 42 commandes ont Ă©tĂ© passĂ©es en 2021. ». Cette derniĂšre Ă©tape peut ĂȘtre rĂ©alisĂ©e soit par du code (formatage des donnĂ©es) soit par le LLM lui-mĂȘme (en lui demandant de prĂ©senter le rĂ©sultat dans une phrase), ou un mĂ©lange des deux. Dans la premiĂšre version de mon agent, je me suis concentrĂ© sur le bon fonctionnement du triptyque question->SQL->rĂ©sultat, la mise en jolis mots de la rĂ©ponse pourra ĂȘtre affinĂ©e plus tard si besoin.

L’IA apporte la souplesse du langage naturel, et le code apporte la fiabilitĂ© de l’exĂ©cution. 🍒

Aperçu du code de l’agent

Pour les plus curieux, voici Ă  quoi ressemble l’initialisation de l’agent NL2SQL en C# avec Semantic Kernel :

// 1. Configuration du Kernel et de la mémoire
var memory = new KernelMemoryBuilder()
    .Build(); // on configure ici le stockage de la mémoire sémantique (ex: Azure Cognitive Search, VolatileMemory, etc.)

var kernelBuilder = Kernel.Builder;
kernelBuilder.WithCompletionService(myCompletionConfig);  // config du modĂšle LLM (OpenAI/Azure OpenAI...)
kernelBuilder.WithEmbeddingGenerationService(myEmbeddingsConfig);  // service d'embeddings pour la mémoire
// On injecte la connexion DB dans la dépendance du kernel, pour que l'agent l'utilise
kernelBuilder.Services.AddSingleton<DbConnection>(sp => new SqliteConnection(connectionString));

IKernel kernel = kernelBuilder.Build();

// 2. Création de l'agent base de données
var agent = await DBMSAgentFactory.CreateAgentAsync(kernel, memory);

// 3. Préparation de la question utilisateur
var question = "Quelle est la croissance du nombre de clients par année ?";
var chatHistory = new ChatHistory(question, AuthorRole.User);

// 4. Invocation de l'agent (génération du SQL, exécution et récupération de la réponse)
var responses = await agent.InvokeAsync(chatHistory);

// 5. Récupération de la réponse formulée par l'agent
Console.WriteLine(responses[0].Message);

Quelques explications sur ce bout de code : on construit d’abord un Kernel Semantic Kernel avec un service de complĂ©tion (le LLM, par ex. GPT-4) et un service d’embeddings (pour la similaritĂ© sĂ©mantique en mĂ©moire). On ajoute Ă©galement un DbConnection (ici une base SQLite pour l’exemple) dans les services, ce qui va permettre Ă  l’agent d’exĂ©cuter physiquement les requĂȘtes. Ensuite on crĂ©e l’agent avec DBMSAgentFactory.CreateAgentAsync(kernel, memory) – cette mĂ©thode instancie toute la machinerie NL2SQL en utilisant le kernel et la mĂ©moire de schĂ©ma fournie.

Puis on envoie une question sous forme de ChatHistory (c’est l’objet que l’agent utilise pour garder le contexte de conversation, ici on a juste un utilisateur qui pose une question). L’appel agent.InvokeAsync(chatHistory) dĂ©clenche toute la sĂ©quence : le prompt est Ă©laborĂ©, le LLM gĂ©nĂšre le SQL, le filtre de qualitĂ© vĂ©rifie, la requĂȘte est exĂ©cutĂ©e en base, et la rĂ©ponse finale est produite. On obtient une liste de messages de rĂ©ponse (dans responses), le premier Ă©tant typiquement la rĂ©ponse de l’assistant. Un petit Console.WriteLine et hop, on voit ce que l’agent rĂ©pond.

À ce stade, si tout se passe bien, on a un agent capable de comprendre une question en français et de retourner une rĂ©ponse issue des donnĂ©es, le tout sans intervention manuelle. Mais attendons – ai-je vraiment envie qu’il exĂ©cute n’importe quelle requĂȘte que le LLM propose ? Certainement pas ! C’est ici qu’intervient la partie Quality Assurance que j’ai intĂ©grĂ©e.

Quality Assurance : le garde-fou intĂ©grĂ© Ă  l’agent SQL đŸ€–đŸ›Ąïž

Pour rendre l’agent utilisable en conditions rĂ©elles, j’ai dĂ©veloppĂ© un composant de Quality Assurance (QA) spĂ©cifique Ă  la gĂ©nĂ©ration de SQL. Son but : assurer la sĂ»retĂ© et la pertinence des requĂȘtes avant exĂ©cution. Ce composant s’insĂšre naturellement dans l’architecture Semantic Kernel grĂące Ă  la configuration des services (DI) du kernel.

ConcrĂštement, qu’a fait mon agent lors de l’étape de vĂ©rification de la requĂȘte SQL ? Voici les principaux contrĂŽles mis en place dans cette premiĂšre version :

  • Filtrage de pertinence de la requĂȘte (Query Relevancy Filter) : C’est le premier filtre QA que j’ai activĂ©, et probablement le plus original. L’idĂ©e est de vĂ©rifier que la requĂȘte SQL gĂ©nĂ©rĂ©e correspond bien Ă  l’intention de la question utilisateur, pour dĂ©tecter les cas oĂč le modĂšle partirait hors sujet. Comment vĂ©rifier cela automatiquement ? J’utilise en fait le LLM une seconde fois : l’agent demande au modĂšle de gĂ©nĂ©rer une description textuelle de la requĂȘte SQL qu’il s’apprĂȘte Ă  exĂ©cuter, puis il compare cette description Ă  la question initiale de l’utilisateur via une similaritĂ© cosinus sur des embeddings. Si la description de la requĂȘte s’écarte trop de la question (en dessous d’un certain seuil de similaritĂ©, par exemple 0.8), l’agent considĂšre que la requĂȘte n’est pas pertinente et la rejette. En clair, si le LLM hallucine une requĂȘte qui fait autre chose que rĂ©pondre Ă  la question posĂ©e, on le dĂ©tecte et on Ă©vite de l’exĂ©cuter. Cette approche est trĂšs utile pour attraper les dĂ©lires du modĂšle du genre : Question: « Combien de clients actifs ? » -> RequĂȘte hallucinĂ©: « SELECT * FROM LogsServeur  » (rien Ă  voir avec les clients actifs). Avec le filtre de pertinence, ce genre de dĂ©rive serait bloquĂ© net. Bien entendu, on peut ajuster le seuil de similaritĂ© pour ĂȘtre plus ou moins strict.
  • Restrictions sur le SQL : En plus de la pertinence, le composant QA peut appliquer des rĂšgles mĂ©tiers sur la requĂȘte. Par exemple, dans mon implĂ©mentation j’ai prĂ©vu de filtrer tout ce qui n’est pas une requĂȘte de sĂ©lection simple. L’agent n’a aucune raison d’exĂ©cuter un DELETE, UPDATE ou DROP dans le cadre des questions utilisateurs classiques, donc autant le bannir explicitement. De mĂȘme, je peux limiter l’accĂšs Ă  certaines tables sensibles, ou exiger la prĂ©sence de certaines clauses (par exemple un WHERE quand on s’attend Ă  un filtrage par date). Pour l’instant, ces rĂšgles sont rudimentaires, mais l’infrastructure est en place pour en rajouter facilement.
  • ContrĂŽle de performance (Ă  envisager) : Une amĂ©lioration en cours de rĂ©flexion est d’analyser le plan d’exĂ©cution estimĂ© de la requĂȘte (par exemple via un EXPLAIN) avant de la lancer pour de bon. L’agent pourrait ainsi dĂ©tecter qu’une requĂȘte va probablement scanner 10 millions de lignes et dĂ©cider de ne pas l’exĂ©cuter tel quel. Ce n’est pas encore implĂ©mentĂ© dans la version actuelle, mais l’architecture modulaire du filtre QA le permettrait. On pourrait imaginer un filtre qui compte le nombre de jointures, dĂ©tecte les SELECT * sans limite, etc., et qui renvoie un avertissement ou optimise la requĂȘte en ajoutant un TOP 100 par exemple.

L’ensemble de ces contrĂŽles de Quality Assurance vit dans un sous-module dĂ©diĂ© de l’agent. J’ai publiĂ© le code source complet sur GitHub (voir le repo SemanticKernel.Agents.DatabaseAgent et son package QualityAssurance). L’activation du filtre de pertinence est trĂšs simple via la configuration du Kernel, comme illustrĂ© ci-dessous :

// Activation des filtres de Quality Assurance pour l'agent DB
kernelBuilder.Services.UseDatabaseAgentQualityAssurance(options =>
{
    options.EnableQueryRelevancyFilter = true;
    options.QueryRelevancyThreshold = 0.8f;
    // On pourrait activer/configurer d'autres filtres ici Ă  l'avenir
});

Ici, j’enregistre dans les services du kernel un composant de QA qui va automatiquement intercepter les tentatives d’exĂ©cution de requĂȘte de l’agent. Le fait de dĂ©finir EnableQueryRelevancyFilter = true suffit Ă  brancher le fameux filtre de pertinence dĂ©crit plus haut. On fixe aussi le seuil de similaritĂ© Ă  0.8 (valeur empirique qui pourra ĂȘtre ajustĂ©e selon les retours). À chaque requĂȘte, juste avant l’exĂ©cution sur la base, ce filtre va faire son boulot et Ă©ventuellement bloquer l’exĂ©cution en levant une exception ou en renvoyant une rĂ©ponse d’erreur contrĂŽlĂ©e de l’agent.

L’architecture est prĂ©vue pour ĂȘtre extensible. On peut crĂ©er ses propres filtres QA en implĂ©mentant l’interface IQueryExecutionFilter. Par exemple, si je veux empĂȘcher les requĂȘtes retournant plus de 1000 lignes, je pourrais coder un filtre personnalisĂ© qui, une fois la requĂȘte gĂ©nĂ©rĂ©e, ajoute LIMIT 1000 Ă  la fin ou refuse la requĂȘte si pas de LIMIT. Il suffirait ensuite de l’enregistrer ainsi :

kernelBuilder.Services.AddTransient<IQueryExecutionFilter, CustomQueryExecutionFilter>();

(oĂč CustomQueryExecutionFilter est ma classe maison qui vĂ©rifie/altĂšre le SQL dans sa mĂ©thode OnQueryExecutionAsync).

GrĂące Ă  ce systĂšme de filtres chaĂźnĂ©s, je peux donc renforcer progressivement la robustesse de l’agent sans toucher au cƓur de la gĂ©nĂ©ration. Chaque filtre agit comme une couche de validation indĂ©pendante. Pour le moment, le filtre de pertinence a dĂ©jĂ  grandement sĂ©curisĂ© les choses dans mes tests, en Ă©vitant les exĂ©cutions hors-sujet. À l’avenir, j’en ajouterai d’autres pour couvrir les aspects performance et sĂ©curitĂ© de façon encore plus pointue.

Illustration : l’agent NL2SQL en action 🎬

Il est temps de voir concrĂštement ce que donne notre agent NL2SQL. Prenons un scĂ©nario simple : j’ai une base de donnĂ©es de commerce avec des clients et des commandes, et je veux savoir quels sont les 3 clients qui ont dĂ©pensĂ© le plus chez nous. Autrement dit, pour chaque client on fait la somme du montant de ses commandes, puis on classe par montant dĂ©croissant et on prend le top 3.

En langage naturel, je pose la question : « Quels sont les 3 clients qui ont dĂ©pensĂ© le plus ? ». Voici comment l’agent va traiter cela :

  1. ComprĂ©hension de la question : L’agent dĂ©tecte qu’on parle de « clients » et de « dĂ©penses ». Il interroge sa mĂ©moire de schĂ©ma et trouve que la base contient une table Clients et une table Commandes liĂ©es (chaque commande a un client). Il remarque aussi la notion de top 3, donc qu’on attend un tri dĂ©croissant sur un montant.
  2. GĂ©nĂ©ration de la requĂȘte SQL : Fort de ces infos, le LLM propose une requĂȘte SQL du genre : SELECT c.Nom, SUM(o.Total) AS MontantTotal FROM Clients c JOIN Commandes o ON o.ClientId = c.Id GROUP BY c.Nom ORDER BY MontantTotal DESC LIMIT 3; Ici, on suppose que les tables s’appellent Clients et Commandes avec Total Ă©tant le montant de la commande, et que ClientId est la clĂ© Ă©trangĂšre. La requĂȘte SQL agrĂšge les montants par client, trie par le total dĂ©croissant et limite aux 3 premiers – cela correspond bien Ă  la question.
  3. VĂ©rification QA : Avant d’exĂ©cuter, l’agent QA prend la requĂȘte ci-dessus et demande au LLM une description : « sĂ©lectionne le nom des clients avec la somme de leurs commandes, triĂ© par le montant total dĂ©croissant, et limite aux 3 plus gros ». Il compare avec la question initiale « 3 clients qui ont dĂ©pensĂ© le plus » et obtient une forte similaritĂ© – logique, ça correspond. Le filtre de pertinence donne son feu vert 👍. La requĂȘte n’a pas l’air dangereuse (c’est un SELECT agrĂ©gĂ© sur un volume raisonnable), donc aucun autre filtre ne la bloque non plus.
  4. ExĂ©cution SQL : L’agent envoie alors la requĂȘte Ă  la base de donnĂ©es. Le SGBD exĂ©cute le calcul et renvoie un petit tableau de rĂ©sultats, par exemple : Nom du client MontantTotal Alice 12 340 € Bob 9 870 € Eve 8 150 €
  5. RĂ©ponse formulĂ©e : L’agent compile la rĂ©ponse Ă  retourner Ă  l’utilisateur. Ça peut ĂȘtre sous forme de phrase : « Les 3 clients ayant le plus dĂ©pensĂ© sont Alice (12 340 €), Bob (9 870 €) et Eve (8 150 €). » Parfait, exactement ce qu’on voulait savoir !

Dans cet exemple, tout s’est bien passĂ© du premier coup. Imaginons maintenant un cas oĂč le LLM se trompe de colonne, par exemple en essayant de sommer une colonne qui n’existe pas. Le filtre de pertinence pourrait ne pas le voir (puisque la description resterait dans le sujet de la question), mais l’exĂ©cution SQL elle-mĂȘme lĂšverait une erreur (colonne inconnue). Mon agent est capable d’attraper cette exception et pourrait alors soit la remonter telle quelle, soit tenter une stratĂ©gie de secours (par exemple, reformuler la question diffĂ©remment ou demander plus de contexte). Pour l’instant, j’ai optĂ© pour la solution simple : en cas d’erreur SQL, l’agent renvoie un message d’échec au user du genre « DĂ©solĂ©, je n’ai pas rĂ©ussi Ă  interprĂ©ter la question. » afin de ne pas laisser l’utilisateur avec une stacktrace technique. Libre Ă  nous ensuite d’analyser l’erreur cĂŽtĂ© dev pour amĂ©liorer l’agent.

Autre situation intĂ©ressante : si l’utilisateur posait une question trĂšs floue ou ambiguĂ«, le LLM pourrait gĂ©nĂ©rer une requĂȘte Ă  cĂŽtĂ© de la plaque. Dans ce cas, le filtre de pertinence dĂ©tecterait une faible correspondance entre la question et la requĂȘte. Par exemple « clients les plus fidĂšles » pourrait ĂȘtre mal compris et donner une requĂȘte sur la mauvaise mĂ©trique – le filtre pourrait la bloquer. L’agent pourrait alors rĂ©pondre « Je ne suis pas certain de comprendre, pouvez-vous prĂ©ciser votre question ? ». Ce genre de rebond n’est pas encore totalement implĂ©mentĂ©, mais c’est envisagĂ© : faire de l’affinement de requĂȘte en redemandant des prĂ©cisions Ă  l’utilisateur quand on n’est pas sĂ»r de nous.

Conclusion et perspectives d’évolution

DĂ©velopper cet agent NL2SQL m’a permis de mesurer Ă  quel point comprendre une question humaine et la traduire en requĂȘte SQL est un dĂ©fi passionnant, mĂȘlant NLP et ingĂ©nierie logicielle. GrĂące Ă  Semantic Kernel, j’ai pu assembler une solution oĂč ChatGPT et la base de donnĂ©es travaillent main dans la main : l’un pour le langage naturel, l’autre pour les donnĂ©es. Le tout agrĂ©mentĂ© d’une couche de Quality Assurance indispensable pour Ă©viter les mauvaises surprises.

Les premiers rĂ©sultats sont encourageants : sur des cas simples, l’agent fournit la bonne rĂ©ponse avec un SQL propre, et sur des cas tordus, les garde-fous Ă©vitent de faire n’importe quoi. Bien sĂ»r, tout n’est pas parfait et il reste pas mal de boulot pour rendre l’agent infaillible et gĂ©nĂ©raliste. Parmi les perspectives d’évolution :

  • Élargir la palette des filtres QA : comme Ă©voquĂ©, ajouter des filtres de performance (dĂ©tection de requĂȘtes trop lourdes) et de sĂ©curitĂ© (blocage de certains patterns ou termes sensibles) rendra l’agent encore plus robuste. On pourrait aussi intĂ©grer un systĂšme de quota ou de monitoring pour Ă©viter les abus.
  • Gestion des permissions/utilisateurs : Ă  terme, imaginer que l’agent s’intĂšgre Ă  un environnement avec des utilisateurs authentifiĂ©s, et qu’il sache limiter les donnĂ©es qu’il retourne en fonction des droits de chacun. Cela nĂ©cessiterait de croiser l’agent NL2SQL avec la couche sĂ©curitĂ© de la base (par exemple, ne permettre que des vues prĂ©-autorisĂ©es).
  • AmĂ©lioration du prompt et du modĂšle : peaufiner les instructions donnĂ©es au LLM pour orienter la gĂ©nĂ©ration SQL (par ex., l’encourager Ă  utiliser tel indice ou telle convention sĂ©mantique). Peut-ĂȘtre tester d’autres modĂšles spĂ©cialisĂ©s en gĂ©nĂ©ration de code SQL, ou fine-tuner un modĂšle sur des exemples de notre schĂ©ma pour gagner en fiabilitĂ©.
  • ExpĂ©rience utilisateur : travailler la façon dont l’agent rĂ©pond. Actuellement, il renvoie le rĂ©sultat brut ou une phrase sommaire. On pourrait le rendre plus conversationnel, qu’il explique comment il a trouvĂ© la rĂ©ponse, ou mĂȘme qu’il affiche un joli tableau formatĂ© dans une interface web. L’intĂ©gration dans un chatbot avec mĂ©moire contextuelle ouvrirait aussi la porte Ă  des requĂȘtes de suivi (enchaĂźner des questions : « Montre-moi le top 3 clients. Maintenant pour chacun, dĂ©taille leurs 5 derniĂšres commandes. » etc.).

En partageant cet outil en open-source, j’espĂšre aussi recueillir des feedbacks et contributions de la communautĂ©. 👉 Le code complet de l’agent NL2SQL avec sa Quality Assurance est disponible sur GitHub (kbeaugrand/SemanticKernel.Agents.DatabaseAgent). N’hĂ©sitez pas Ă  l’essayer, Ă  l’adapter Ă  vos bases de donnĂ©es, et Ă  proposer des amĂ©liorations !

En conclusion, doter ChatGPT (ou un autre LLM) de solides compĂ©tences en SQL est Ă  notre portĂ©e, Ă  condition de l’entourer de garde-fous et de l’inscrire dans une architecture hybride IA + code. Les hallucinations et les erreurs de l’IA ne sont pas une fatalitĂ© dĂšs lors qu’on les canalise avec des approches comme le RAG, des filtres de pertinence, et une exĂ©cution contrĂŽlĂ©e. Mon agent NL2SQL n’en est qu’à ses dĂ©buts, mais il montre dĂ©jĂ  qu’un assistant IA peut collaborer avec une base de donnĂ©es de maniĂšre sĂ»re et productive. J’ai commencĂ© cette aventure avec l’envie de rendre ChatGPT fort en SQL ; je la poursuis avec la conviction que ce genre d’outil a un Ă©norme potentiel pour faciliter l’accĂšs Ă  l’information dans nos systĂšmes. 🚀

Et vous, prĂȘt Ă  poser vos prochaines questions SQL en toute sĂ©rĂ©nitĂ© Ă  votre nouvel assistant base de donnĂ©es ? 😉

Un commentaire sur “CrĂ©er un agent NL2SQL avec Semantic Kernel đŸš€

Laisser un commentaire