AI Engineering Academy · Leçon

Créer une interface de base de données en langage naturel

Créez un système dans lequel les utilisateurs posent leurs questions en français courant, le modèle génère du SQL par appel de fonction, votre application exécute la requête en toute sécurité et le modèle présente les résultats.

Leçon 4 sur 413 étapes

Créer une interface de base de données en langage naturel est une leçon AI Engineering Academy gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage AI Engineering Academy, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours AI Engineering Academy comprend 4 leçons au total.

Du langage naturel au SQL : la vision

Imaginez que vous demandiez à votre base de données : « Quels clients ont dépensé plus de 1 000 $ le mois dernier ? » et que vous obteniez une réponse, sans écrire une seule requête SQL. Une interface de base de données en langage naturel utilise les appels de fonctions pour permettre au LLM de générer du SQL ; votre application l’exécute de manière sûre, puis le modèle présente les résultats en anglais courant. Cette approche démocratise l’accès aux données pour les utilisateurs non techniques.

Présentation de l’architecture du système

Le pipeline NL-to-SQL repose sur quatre composants qui fonctionnent ensemble :

  • Contexte du schéma : le LLM reçoit le schéma de votre base de données afin de connaître les tables et les colonnes existantes.
  • Génération SQL : le modèle génère une requête SQL comme argument d’un appel de fonction.
  • Exécution sûre : votre application valide et exécute la requête, puis renvoie les résultats.
  • Présentation des résultats : le modèle reçoit les résultats de la requête et les explique en langage naturel.

Définir l’outil de requête de la base de données

Définissez une fonction query_database qui accepte une instruction SQL SELECT. La description du schéma dans la définition de la fonction indique au modèle quelles tables et colonnes sont disponibles ; il peut ainsi générer des requêtes exactes sans faire de suppositions.

query_db_tool = {
    'type': 'function',
    'function': {
        'name': 'query_database',
        'description': '''Execute a read-only SQL query on the company database.
Use this to answer questions about customers, orders, and products.
Only SELECT statements are allowed. Never use DROP, DELETE, UPDATE, or INSERT.

Available tables:
- customers (id, name, email, created_at, country)
- orders (id, customer_id, total_amount, status, created_at)
- order_items (id, order_id, product_id, quantity, unit_price)
- products (id, name, category, price, stock_quantity)
''',
        'parameters': {
            'type': 'object',
            'properties': {
                'sql': {
                    'type': 'string',
                    'description': 'A valid PostgreSQL SELECT statement.'
                },
                'explanation': {
                    'type': 'string',
                    'description': 'One-sentence explanation of what this query does.'
                }
            },
            'required': ['sql', 'explanation']
        }
    }
}

Exécution SQL sûre

N’exécutez jamais du SQL brut provenant du modèle sans validation. Mettez en place une couche de sécurité qui n’autorise que les instructions SELECT, rejette les mots-clés dangereux, limite le nombre de lignes de résultat pour éviter les problèmes de mémoire et s’exécute dans une transaction de base de données en lecture seule. La défense en profondeur est essentielle lors de l’exécution de code généré par un LLM.

import re
import psycopg2

DANGEROUS_KEYWORDS = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE', 'ALTER', 'CREATE', 'EXEC']

def execute_safe_query(sql: str, max_rows: int = 100) -> list:
    '''Execute a read-only SQL query with safety guards.'''
    sql_upper = sql.upper().strip()

    # Only allow SELECT
    if not sql_upper.startswith('SELECT'):
        raise ValueError('Only SELECT statements are allowed.')

    # Block dangerous keywords
    for keyword in DANGEROUS_KEYWORDS:
        if re.search(r'\b' + keyword + r'\b', sql_upper):
            raise ValueError(f'Forbidden keyword: {keyword}')

    conn = psycopg2.connect('postgresql://readonly_user:pass@localhost/appdb')
    with conn:
        with conn.cursor() as cur:
            # Enforce read-only transaction
            cur.execute('SET TRANSACTION READ ONLY')
            cur.execute(sql)
            columns = [desc[0] for desc in cur.description]
            rows = cur.fetchmany(max_rows)
    return [dict(zip(columns, row)) for row in rows]

Injecter le contexte du schéma dans l’invite système

Le modèle génère du meilleur SQL lorsqu’il peut voir l’intégralité du schéma de la base de données. Construisez une invite système qui inclut les définitions des tables, les noms et les types des colonnes, ainsi que des valeurs d’exemple pour les colonnes catégorielles. Le modèle peut ainsi savoir s’il doit utiliser country = 'US' ou country_code = 'US' sans faire de suppositions.

SYSTEM_PROMPT = '''You are a data analyst assistant with access to the company database.
When users ask data questions, use the query_database tool to look up the answer.
Always explain your query in plain English before executing it.

Database schema:

CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name VARCHAR NOT NULL,
    email VARCHAR UNIQUE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    country VARCHAR(2)  -- ISO 2-letter code: 'US', 'UK', 'DE', etc.
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER REFERENCES customers(id),
    total_amount NUMERIC(10,2),
    status VARCHAR  -- 'pending', 'shipped', 'delivered', 'cancelled'
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Only use columns that exist in the schema above.
'''

Formater les résultats de requête pour le modèle

Les résultats bruts de la base de données (listes de dictionnaires) doivent être mis en forme comme un texte lisible avant d’être renvoyés au modèle. Convertissez l’ensemble de résultats en une représentation compacte — un tableau ou un résumé JSON — à laquelle le modèle pourra se référer pour présenter la réponse. Évitez d’envoyer des milliers de lignes ; résumez les grands ensembles de résultats.

import json

def format_results(rows: list, max_display: int = 20) -> str:
    if not rows:
        return 'The query returned no results.'

    total = len(rows)
    display = rows[:max_display]

    # Format as a simple table
    if display:
        columns = list(display[0].keys())
        lines = [' | '.join(columns)]
        lines.append('-' * len(lines[0]))
        for row in display:
            lines.append(' | '.join(str(row[col]) for col in columns))

    result = '\n'.join(lines)
    if total > max_display:
        result += f'\n... ({total - max_display} more rows not shown)'
    return result

Implémentation complète du pipeline

Voici comment tout assembler : la fonction qui traite la question d’un utilisateur, appelle le modèle pour générer du SQL, exécute la requête en toute sécurité et renvoie les résultats au modèle pour qu’il les présente. Le modèle reçoit à la fois la question originale et les résultats de la requête, puis produit une réponse en anglais courant.

from openai import OpenAI
import json

client = OpenAI()

def answer_data_question(user_question: str) -> str:
    messages = [
        {'role': 'system', 'content': SYSTEM_PROMPT},
        {'role': 'user', 'content': user_question}
    ]

    # First call: get SQL from model
    resp = client.chat.completions.create(
        model='gpt-4o', messages=messages, tools=[query_db_tool]
    )
    assistant_msg = resp.choices[0].message
    messages.append(assistant_msg)

    if resp.choices[0].finish_reason == 'tool_calls':
        tc = assistant_msg.tool_calls[0]
        args = json.loads(tc.function.arguments)
        print(f'Executing: {args["explanation"]}')
        print(f'SQL: {args["sql"]}')

        try:
            rows = execute_safe_query(args['sql'])
            result_text = format_results(rows)
        except ValueError as e:
            result_text = f'Query blocked: {str(e)}'

        messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result_text})

        # Second call: narrate results
        final = client.chat.completions.create(model='gpt-4o', messages=messages)
        return final.choices[0].message.content

    return assistant_msg.content

Gérer les questions de données en plusieurs étapes

Les questions complexes peuvent nécessiter plusieurs requêtes. « Qui sont nos 5 meilleurs clients en termes de chiffre d’affaires et quelles sont leurs commandes les plus récentes ? » nécessite deux requêtes : l’une pour trouver les meilleurs clients, puis l’autre pour obtenir leurs commandes. Autorisez le modèle à effectuer plusieurs appels d’outils séquentiels en exécutant plusieurs fois la boucle de distribution jusqu’à ce que finish_reason='stop'.

def answer_complex_question(user_question: str) -> str:
    messages = [
        {'role': 'system', 'content': SYSTEM_PROMPT},
        {'role': 'user', 'content': user_question}
    ]

    for _ in range(5):  # Max 5 query rounds
        resp = client.chat.completions.create(
            model='gpt-4o', messages=messages, tools=[query_db_tool]
        )
        msg = resp.choices[0].message
        messages.append(msg)

        if resp.choices[0].finish_reason == 'stop':
            return msg.content  # Model is done

        # Process tool call and loop
        tc = msg.tool_calls[0]
        args = json.loads(tc.function.arguments)
        try:
            rows = execute_safe_query(args['sql'])
            result = format_results(rows)
        except Exception as e:
            result = f'Error: {str(e)}'

        messages.append({'role': 'tool', 'tool_call_id': tc.id, 'content': result})

    return 'Could not complete the analysis within the step limit.'

Prévenir les risques d’injection SQL

Même avec la protection qui n’autorise que SELECT, un modèle astucieux (ou un utilisateur malveillant) pourrait tenter d’exfiltrer des données au moyen de sous-requêtes ou d’astuces fondées sur des commentaires. Des protections supplémentaires consistent notamment à utiliser un utilisateur de base de données en lecture seule disposant uniquement de l’autorisation SELECT, à exécuter les requêtes dans un groupe de connexions distinct et à vérifier que les noms de tables de la requête correspondent à la liste blanche de votre schéma.

ALLOWED_TABLES = {'customers', 'orders', 'order_items', 'products'}

def validate_tables_in_sql(sql: str) -> bool:
    '''Check that only whitelisted tables are referenced in the query.'''
    import sqlparse
    parsed = sqlparse.parse(sql)[0]
    table_names = set()
    from_seen = False
    for token in parsed.flatten():
        if token.ttype is sqlparse.tokens.Keyword and token.value.upper() in ('FROM', 'JOIN'):
            from_seen = True
        elif from_seen and token.ttype is sqlparse.tokens.Name:
            table_names.add(token.value.lower())
            from_seen = False
    unknown = table_names - ALLOWED_TABLES
    if unknown:
        raise ValueError(f'References unknown tables: {unknown}')
    return True

Mettre en cache les requêtes courantes

De nombreuses questions métier sont posées à plusieurs reprises et produisent la même réponse : « Combien de clients avons-nous ? » « Quel était le chiffre d’affaires du mois dernier ? » Mettez ces résultats en cache dans Redis avec un TTL court. Consultez le cache avant d’exécuter la requête : cela réduit la charge de la base de données et accélère les réponses aux questions analytiques courantes.

import redis
import hashlib
import json

r = redis.Redis.from_url('redis://localhost:6379')

def cached_query(sql: str, ttl_seconds: int = 300) -> list:
    cache_key = 'nl_query:' + hashlib.sha256(sql.encode()).hexdigest()
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)
    rows = execute_safe_query(sql)
    r.setex(cache_key, ttl_seconds, json.dumps(rows, default=str))
    return rows

Expliquer les requêtes aux utilisateurs

Établissez la confiance en affichant aux utilisateurs la requête SQL générée à côté de la réponse en langage naturel. Lorsqu’ils peuvent voir « J’ai exécuté cette requête : SELECT COUNT(*) FROM customers WHERE country = ?UK? », ils peuvent vérifier que la réponse est correcte et apprendre des structures SQL. Le champ explanation de notre schéma d’outil est parfaitement adapté à cet usage.

Vérification rapide

Vérifiez votre compréhension de la création d’une interface de base de données en langage naturel.

Récapitulatif de la leçon

Dans cette leçon, vous avez appris que le schéma de l’outil query_database injecte le contexte du schéma afin que le modèle génère du SQL exact, que la validation de sécurité doit bloquer les instructions autres que SELECT et les mots-clés dangereux avant l’exécution et qu’une boucle d’appels au modèle permet d’effectuer des analyses de données en plusieurs étapes nécessitant des requêtes séquentielles. Nous allons maintenant découvrir le Model Context Protocol (MCP), le standard ouvert qui permet de connecter l’IA à des outils externes.

Gratuit pour commencer

Apprends Python avec un tuteur IA — gratuit

Écris et exécute du vrai code dans ton navigateur, obtiens de l'aide instantanée d'un tuteur IA disponible 24h/24, et reprends là où tu t'es arrêté sur le web ou dans l'app.

Cours
30
Leçons
120

Questions Fréquemment Posées

La leçon « Créer une interface de base de données en langage naturel » est-elle gratuite ?

Oui — le texte complet de « Créer une interface de base de données en langage naturel » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours AI Engineering Academy, passe à CoddyKit PRO. Le cours AI Engineering Academy comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Créer une interface de base de données en langage naturel » ?

Créez un système dans lequel les utilisateurs posent leurs questions en français courant, le modèle génère du SQL par appel de fonction, votre application exécute la requête en toute sécurité et le m… Tu pratiques AI Engineering Academy avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer AI Engineering Academy ?

Aucune expérience préalable n'est requise. AI Engineering Academy sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Créer une interface de base de données en langage naturel » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon AI Engineering Academy ?

Oui. Chaque leçon AI Engineering Academy inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Définir les schémas de fonctions pour l’API
  2. Traiter les appels d’outils dans votre application
  3. Appels de fonctions en parallèle
  4. Créer une interface de base de données en langage naturel
← Retour à AI Engineering Academy