De. Mi
Retour

AskPostgreSQL : Posez une question à votre base de données, en anglais

StackNext.js 15, PostgreSQL, GPT-5 Mini, Recharts, NextAuth.js
FocusLangage naturel vers SQL, pipelines LLM, visualisation de données

Ce que ça fait

AskPostgreSQL transforme une question posée en anglais courant en une requête SQL fonctionnelle, l'exécute, explique ce qu'elle a fait, et dessine le bon graphique, le tout en une seule requête. Demandez « quelle industrie a la valorisation moyenne la plus élevée en Europe » et obtenez en retour un graphique en barres, le SQL exact derrière, et une explication en langage simple de ce que fait chaque clause.

L'interface d'AskPostgreSQL : une question en anglais courant à côté du SQL généré, un tableau de résultats, et un graphique
Les lignes de description et de conclusion affichées ici sont le texte de repli codé en dur de l'application, visible chaque fois que la réponse du modèle omet ces champs optionnels. Plus de détails sur le pourquoi ci-dessous.

Le problème des outils « chat avec vos données »

La plupart des gens qui veulent une réponse d'une base de données ne veulent pas apprendre le SQL pour l'obtenir. La plupart des outils « chat avec vos données » cachent entièrement la requête, ce qui rend la réponse invérifiable, ou déversent du SQL brut sur quelqu'un qui ne sait pas le lire, ce qui rend l'outil inutile pour la personne qui en avait besoin au départ.

AskPostgreSQL montre les deux : la réponse, et la requête exacte qui l'a produite, expliquée dans l'ordre.

Quatre appels, de la question au graphique

Quatre étapes portent une question du texte au graphique, trois d'entre elles étant des appels à GPT-5 Mini, une étant une vérification locale qui n'en est délibérément pas un :

  1. 01

    generateQuery

    Transforme la question en une seule instruction SELECT, avec le vrai schéma de la table dans le prompt système. Il connaît les particularités spécifiques de ce jeu de données : les valorisations sont stockées en milliards, les noms de pays comme « UK » sont développés en « United Kingdom » avant d'atteindre la clause WHERE, et les noms d'industries doivent correspondre à une liste fixe qui a son propre désordre intégré, « healthcare & life sciences » et « health » sont deux catégories valides parce que les données sous-jacentes ont les deux, donc le prompt contourne le problème plutôt que de le nettoyer.

  2. 02

    Le garde-fou

    La requête générée passe par une vérification qui n'autorise que les instructions SELECT. Tout ce qui contient DROP, DELETE, INSERT, UPDATE, ALTER, TRUNCATE, CREATE, GRANT, ou REVOKE est rejeté avant d'atteindre Postgres.

  3. 03

    explainQuery

    Découpe le même SQL en sections étiquetées, SELECT, FROM, WHERE, GROUP BY, et écrit une ligne en langage simple pour chacune, en ne présumant aucune connaissance du SQL.

  4. 04

    generateChartConfig

    Examine la forme réelle de l'ensemble de résultats et choisit un type de graphique, des clés d'axes, et des couleurs par série, en passant à une comparaison multi-lignes dès qu'il voit plus d'une catégorie à tracer sur le même axe.

Le repli de parsing, et son coût visible

Chacun des trois appels au modèle demande du JSON structuré en retour, et le parser ne fait pas simplement confiance à ce qu'il a reçu. Il essaie d'abord un parsing JSON direct, puis un bloc de code markdown, puis une correspondance regex sur un littéral d'objet JS brut, puis une extraction regex pour une instruction SQL brute, puis un parsing en liste à puces pour les explications, et alors seulement se replie sur le traitement de toute la réponse comme du texte brut. Cinq stratégies de repli pour un seul appel au modèle.

Le texte de description et de conclusion du graphique sous l'image principale ci-dessus, c'est à quoi ce repli ressemble en pratique : quand le JSON du modèle omet un champ optionnel, l'application le remplit avec une valeur par défaut codée en dur plutôt que de faire échouer la requête. C'est le chemin de dégradation en douceur qui fonctionne comme prévu, et de l'extérieur, un repli qui fonctionne et un texte de substitution visible se ressemblent exactement.

Chaîne de repli de parsing : cinq stratégies de parsing tentées en séquence, avec repli sur du texte brut en dernier recours si les cinq échouent

Le garde-fou SQL, et ses limites

Le garde-fou SQL ici est une liste noire de mots-clés, pas un générateur de requêtes paramétrées. C'est un compromis délibéré pour une démo tournant sur un jeu de données synthétique préchargé, sans aucun chemin d'écriture exposé ailleurs : il bloque les catégories de requêtes qui comptent, les mutations et les changements de schéma, sans avoir besoin d'un générateur de requêtes complet devant un modèle qui doit déjà, par conception, écrire des instructions SELECT arbitraires.

La liste noire n'est pas non plus consciente des tokens, c'est une simple vérification de sous-chaîne sur le texte de la requête en minuscules. Demandez les entreprises avec « updated » dans le nom et la requête est rejetée avant d'atteindre Postgres, non pas parce que le SQL fait quoi que ce soit de dangereux, mais parce que le mot « update » apparaît à l'intérieur d'un autre mot. Une vraie faille, et sur ce jeu de données une rare : aucune licorne ne porte un nom qui s'en approche.

Ce ne serait pas le bon choix devant une base de données de production contenant de vraies données utilisateur, la surface de requête et la frontière de confiance ont toutes deux une allure complètement différente.

vont réellement les données

Deux des trois appels au modèle ne voient jamais que le schéma et votre question. generateChartConfig voit plus : les résultats réels de la requête, sérialisés en JSON, envoyés au modèle pour qu'il puisse déduire la bonne forme de graphique à partir des données réelles plutôt que de la seule question. Sur un jeu de données préchargé de valorisations publiques de licornes, ce n'est pas un problème.

Devant une base de données de production, c'est le même problème de frontière de confiance que le garde-fou SQL, un appel plus profond. Le garde-fou SQL contrôle ce qui sort du modèle. Rien ici ne contrôle ce qui y entre.

Un diagramme des trois appels au modèle : generateQuery et explainQuery ne transportant que la question, generateChartConfig transportant aussi les résultats de la requête
Deux des trois étapes atteignent OpenAI en ne transportant que la question. La troisième transporte aussi les données.

Choisir le bon graphique

Côté frontend, un composant de graphique dynamique lit cette configuration et l'affiche directement dans Recharts : barres, lignes, camembert, ou comparaison multi-lignes selon ce que le modèle a choisi. Les couleurs reviennent sous forme de références à des variables CSS plutôt que de valeurs hexadécimales codées en dur, si bien que chaque graphique reste cohérent avec le thème, en mode clair comme en mode sombre, sans que le modèle ait jamais besoin de connaître la palette de l'application.

Requêtes sauvegardées

Chaque requête exécutée par un utilisateur connecté est sauvegardée avec son SQL, ses résultats, et sa configuration de graphique attachée, si bien que rouvrir une ancienne question ne veut pas dire la reposer depuis zéro. Un panneau de requêtes suggérées donne aux nouveaux utilisateurs un point de départ avec des questions pré-écrites sur le jeu de données préchargé, 1 277 vraies entreprises licornes, plutôt qu'une zone de texte vide et un curseur qui clignote.

Ce que je referais

Un dessin illustrant l'étape generateChartConfig, l'appel le moins déterministe du pipeline

generateChartConfig est l'appel que je referais en premier. Confier à un modèle l'ensemble de résultats brut et lui demander d'inventer des clés d'axes et des couleurs à partir de rien fonctionne, mais c'est l'étape la moins déterministe du pipeline, et celle la plus susceptible de choisir un graphique bizarre pour une question qui avait une réponse évidente.

Une petite couche à base de règles devant lui, n'escaladant vers le modèle que pour les formes de résultats ambiguës, serait plus fiable qu'un appel LLM à chaque requête. Cela réduirait aussi la fréquence à laquelle l'ensemble complet de résultats doit ne serait-ce que quitter le serveur, tout ce qui a une forme évidente est classé localement, et seuls les cas véritablement ambigus envoient une copie des données à OpenAI.