0Pricing
AI Engineering Academy · Lezione

Creare un'interfaccia al database in linguaggio naturale

Crei un sistema in cui gli utenti pongono domande in linguaggio naturale, il modello genera SQL tramite function calling, l'applicazione esegue la query in sicurezza e il modello illustra i risultati.

Creare un'interfaccia al database in linguaggio naturale è una lezione AI Engineering Academy gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Engineering Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Engineering Academy include 4 lezioni in totale.

Dal linguaggio naturale a SQL: la visione

Immagini di chiedere al database «Quali clienti hanno speso più di 1.000 $ il mese scorso?» e di ricevere una risposta, senza scrivere una sola query SQL. Un'interfaccia per database in linguaggio naturale usa il function calling per consentire all'LLM di generare SQL; l'applicazione lo esegue in modo sicuro e il modello illustra i risultati in inglese corrente. Questo approccio rende i dati accessibili anche agli utenti non tecnici.

Panoramica dell'architettura del sistema

La pipeline da linguaggio naturale a SQL comprende quattro componenti che lavorano insieme:

  • Contesto dello schema: l'LLM riceve lo schema del database, così sa quali tabelle e colonne esistono.
  • Generazione di SQL: il modello genera una query SQL come argomento di una chiamata di funzione.
  • Esecuzione sicura: l'applicazione convalida ed esegue la query, quindi restituisce i risultati.
  • Presentazione dei risultati: il modello riceve i risultati della query e li spiega in linguaggio naturale.

Definizione dello strumento per interrogare il database

Definisca una funzione query_database che accetti un'istruzione SQL SELECT. La descrizione dello schema nella definizione della funzione insegna al modello quali tabelle e colonne sono disponibili, permettendogli di generare query accurate senza procedere per tentativi.

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']
        }
    }
}

Esecuzione sicura di SQL

Non esegua mai SQL grezzo generato dal modello senza convalidarlo. Implementi un livello di sicurezza che: consenta solo istruzioni SELECT, rifiuti le parole chiave pericolose, limiti il numero di righe del risultato per evitare problemi di memoria ed esegua l'operazione in una transazione di database in sola lettura. La difesa in profondità è fondamentale quando si esegue codice generato da 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]

Inserimento del contesto dello schema nel prompt di sistema

Il modello genera SQL migliore quando può vedere lo schema completo del database. Costruisca un prompt di sistema che includa le definizioni delle tabelle, i nomi e i tipi delle colonne e valori di esempio per le colonne categoriche. In questo modo il modello sa se deve usare country = 'US' oppure country_code = 'US', senza doverlo dedurre.

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.
'''

Formattazione dei risultati della query per il modello

I risultati grezzi del database (elenchi di dict) devono essere formattati come testo leggibile prima di essere inviati al modello. Converta il set di risultati in una rappresentazione compatta, ad esempio una tabella o un riepilogo JSON, a cui il modello possa fare riferimento per illustrare la risposta. Eviti di inviare migliaia di righe: riepiloghi i set di risultati molto grandi.

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

Implementazione della pipeline completa

Mettiamo insieme tutti i componenti: la funzione che elabora la domanda dell'utente, chiama il modello per generare SQL, esegue la query in modo sicuro e restituisce i risultati al modello affinché li illustri. Il modello riceve sia la domanda originale sia i risultati della query, quindi produce una risposta in inglese corrente.

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

Gestione delle domande sui dati in più passaggi

Le domande complesse possono richiedere più query. «Chi sono i nostri 5 clienti principali per fatturato e quali sono i loro ordini più recenti?» richiede due query: una per trovare i clienti principali e una per recuperare i relativi ordini. Consenta al modello di emettere più chiamate sequenziali agli strumenti eseguendo più volte il ciclo di dispatch, fino a quando 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.'

Prevenzione dei rischi di SQL injection

Anche con il controllo che consente solo SELECT, un modello ingegnoso (o un utente malevolo) potrebbe tentare di esfiltrare dati tramite sottoquery o tecniche basate sui commenti. Tra le protezioni aggiuntive rientrano: l'uso di un utente del database in sola lettura con la sola autorizzazione SELECT, l'esecuzione in un pool di connessioni separato e la convalida del fatto che i nomi delle tabelle nella query corrispondano alla whitelist dello schema.

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

Memorizzazione nella cache delle query comuni

Molte domande aziendali vengono poste ripetutamente e hanno sempre la stessa risposta: «Quanti clienti abbiamo?» oppure «Qual è stato il fatturato del mese scorso?». Memorizzi questi risultati in Redis con un TTL breve. Controlli la cache prima di eseguire la query: in questo modo ridurrà il carico sul database e velocizzerà le risposte alle domande analitiche più comuni.

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

Spiegazione delle query agli utenti

Costruisca fiducia mostrando agli utenti la query SQL generata insieme alla risposta in linguaggio naturale. Quando gli utenti possono vedere «Ho eseguito questa query: SELECT COUNT(*) FROM customers WHERE country = ?UK?» possono verificare che la risposta sia corretta e imparare gli schemi SQL. Il campo explanation nello schema dello strumento è perfetto a questo scopo.

Verifica rapida

Verifichi la sua comprensione della costruzione di un'interfaccia per database in linguaggio naturale.

Riepilogo della lezione

In questa lezione ha imparato che: lo schema dello strumento query_database inserisce il contesto dello schema, consentendo al modello di generare SQL accurato, la convalida di sicurezza deve bloccare le istruzioni diverse da SELECT e le parole chiave pericolose prima dell'esecuzione e un ciclo di chiamate al modello consente analisi dei dati in più passaggi che richiedono query sequenziali. Ora esploreremo il Model Context Protocol (MCP), lo standard aperto per collegare l'IA agli strumenti esterni.

Domande Frequenti

La lezione «Creare un'interfaccia al database in linguaggio naturale» è gratuita?

Sì — il testo completo di «Creare un'interfaccia al database in linguaggio naturale» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Engineering Academy, passa a CoddyKit PRO. Il corso AI Engineering Academy include 4 lezioni in totale.

Cosa imparerò in «Creare un'interfaccia al database in linguaggio naturale»?

Crei un sistema in cui gli utenti pongono domande in linguaggio naturale, il modello genera SQL tramite function calling, l'applicazione esegue la query in sicurezza e il modello illustra i risultati. Eserciti AI Engineering Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare AI Engineering Academy?

Non è richiesta alcuna esperienza precedente. AI Engineering Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.

Quanto tempo richiede la lezione «Creare un'interfaccia al database in linguaggio naturale»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione AI Engineering Academy?

Sì. Ogni lezione AI Engineering Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Definire gli schemi delle funzioni per l'API
  2. Elaborare le chiamate agli strumenti nell'applicazione
  3. Chiamate di funzioni in parallelo
  4. Creare un'interfaccia al database in linguaggio naturale
← Torna a AI Engineering Academy