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
Usersalors que votre base utiliseUtilisateursouAccounts. 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
ClientsetCommandes, et peut-ĂȘtreLignesDeCommandeselon 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 unDELETE FROMdestructeur. đ± Ă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 :
- 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.
- 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 … ».
- 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.
- 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.
- 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,UPDATEouDROPdans 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 unWHEREquand 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 lesSELECT *sans limite, etc., et qui renvoie un avertissement ou optimise la requĂȘte en ajoutant unTOP 100par 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 :
- 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
Clientset une tableCommandesliĂ©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. - 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âappellentClientsetCommandesavecTotalĂ©tant le montant de la commande, et queClientIdest 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. - 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.
- 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 âŹ
- 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 đ”