Come funzionano gli agenti NL-to-SQL
Iniezione dello schema, generazione ed esecuzione delle query e formattazione dei risultati
Come funzionano gli agenti NL-to-SQL è una lezione AI Agents gratuita su CoddyKit. Questa è la lezione 1 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 Agents, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Agents include 4 lezioni in totale.
Che cos'è un agente NL-to-SQL
Un agente da linguaggio naturale a SQL traduce le domande in linguaggio naturale in query SQL, le esegue su un database e restituisce risposte comprensibili.
Invece di scrivere SELECT COUNT(*) FROM orders WHERE status='pending', gli utenti possono semplicemente chiedere: "Quanti ordini in sospeso abbiamo?"
L'architettura di base
Ogni agente NL-to-SQL segue la stessa pipeline:
- Iniezione dello schema — inserire la struttura del DB nel prompt
- L'LLM genera SQL — il modello produce una query
- Esecuzione — eseguire la query sul database
- Formattazione dei risultati — trasformare le righe in testo leggibile
- Restituzione della risposta — rispondere all'utente
# High-level pipeline
def nl_to_sql_agent(user_question, db_connection):
schema = get_schema(db_connection)
sql = llm_generate_sql(user_question, schema)
rows = execute_query(db_connection, sql)
answer = format_results(rows, user_question)
return answerSpiegare l'iniezione dello schema
L'LLM non conosce la struttura del database. È necessario inserire lo schema in ogni prompt, affinché il modello sappia quali tabelle e colonne esistono.
Una descrizione compatta dello schema comunica al modello: "La tabella orders ha le colonne: id, user_id, status, total, created_at."
def build_schema_prompt(schema_info):
lines = []
for table in schema_info:
cols = ', '.join(
f"{c['name']} ({c['type']})"
for c in table['columns']
)
lines.append(f"Table {table['name']}: {cols}")
return '\n'.join(lines)
# Output:
# Table users: id (INT), email (VARCHAR), created_at (TIMESTAMP)
# Table orders: id (INT), user_id (INT), status (VARCHAR), total (FLOAT)
if __name__ == '__main__':
demo_schema = [
{'name': 'users', 'columns': [{'name': 'id', 'type': 'INT'}, {'name': 'email', 'type': 'VARCHAR'}]},
{'name': 'orders', 'columns': [{'name': 'id', 'type': 'INT'}, {'name': 'user_id', 'type': 'INT'}]},
]
print(build_schema_prompt(demo_schema))
Prompt per la generazione SQL da parte dell'LLM
Il prompt deve fornire all'LLM tre elementi: lo schema, la domanda e istruzioni esplicite per restituire esclusivamente SQL valido.
È fondamentale specificare chiaramente l'uso di SELECT-only e il dialetto SQL di destinazione (PostgreSQL, MySQL, SQLite) per garantire sicurezza e correttezza.
SYSTEM_PROMPT = '''You are a SQL expert. Given a database schema and a question,
generate a valid {dialect} SELECT query. Return ONLY the SQL query, no explanation.
Do not use INSERT, UPDATE, DELETE, or DROP.
Schema:
{schema}
'''
def llm_generate_sql(question, schema, dialect='PostgreSQL'):
prompt = SYSTEM_PROMPT.format(schema=schema, dialect=dialect)
response = client.chat.completions.create(
model='gpt-4o',
messages=[
{'role': 'system', 'content': prompt},
{'role': 'user', 'content': question}
]
)
return response.choices[0].message.content.strip()Eseguire l'SQL generato
Dopo che l'LLM ha restituito l'SQL, lo esegua sul database reale. Utilizzi query parametrizzate quando possibile e intercetti sempre le eccezioni: l'LLM può produrre SQL non valido.
Racchiudere l'esecuzione in un blocco try/except consente di riprovare inviando all'LLM un suggerimento basato sull'errore.
import psycopg2
def execute_query(conn, sql):
try:
with conn.cursor() as cur:
cur.execute(sql)
columns = [desc[0] for desc in cur.description]
rows = cur.fetchmany(100) # limit rows
return {'columns': columns, 'rows': rows}
except psycopg2.Error as e:
return {'error': str(e), 'sql': sql}Formattare i risultati per l'utente
Le righe grezze del database non sono facili da usare per gli utenti. L'agente deve trasformarle in una risposta in linguaggio naturale.
Per insiemi di risultati piccoli, passi le righe all'LLM affinché le interpreti. Per insiemi più grandi, calcoli prima le statistiche riepilogative.
def format_results(result, original_question):
if 'error' in result:
return f'Query failed: {result["error"]}'
rows = result['rows']
columns = result['columns']
if not rows:
return 'No results found.'
# For simple counts/aggregates — just return the value
if len(columns) == 1 and len(rows) == 1:
return f'Result: {rows[0][0]}'
# For multi-row results — summarize
summary = f'Found {len(rows)} rows.\n'
for row in rows[:5]: # show first 5
summary += ', '.join(f'{columns[i]}: {row[i]}' for i in range(len(columns))) + '\n'
return summary
if __name__ == '__main__':
demo_result = {'rows': [[42]], 'columns': ['count']}
print(format_results(demo_result, 'How many users signed up?'))
demo_result2 = {'rows': [], 'columns': ['id']}
print(format_results(demo_result2, 'Any orders today?'))
Perché NL-to-SQL è difficile: ambiguità
L'ambiguità è la sfida principale. Consideri la richiesta: "Mostrami i clienti principali."
- Principali per fatturato? Per numero di ordini? Per recenza?
- Nell'ultimo mese? Di sempre?
- I primi 10? I primi 100?
Le persone comprendono il contesto; gli LLM fanno supposizioni. Gli agenti hanno bisogno di strategie per gestire o chiarire le domande ambigue.
AMBIGUITY_PROMPT = '''If the question is ambiguous, respond with JSON:
{"needs_clarification": true, "question": "your clarifying question"}
If clear, respond with the SQL query directly.
User question: {question}
'''
def generate_or_clarify(question, schema):
response = llm_call(AMBIGUITY_PROMPT.format(
question=question, schema=schema
))
if '"needs_clarification"' in response:
import json
return json.loads(response)
return {'sql': response}Perché NL-to-SQL è difficile: dimensioni dello schema
I database aziendali possono contenere centinaia di tabelle e migliaia di colonne. Inserire lo schema completo supererebbe la finestra di contesto dell'LLM.
Tra le soluzioni ci sono: la ricerca dello schema (incorporare le descrizioni delle tabelle e recuperare quelle pertinenti), il filtraggio delle tabelle (chiedere prima all'LLM quali tabelle sono necessarie) e la compressione dello schema (omettere le colonne degli indici e degli audit).
# Two-phase approach for large schemas
def get_relevant_tables(question, all_tables):
prompt = f'''Given these tables: {all_tables}
Which 3-5 tables are most relevant to answer: "{question}"?
Return a JSON list of table names only.'''
response = llm_call(prompt)
import json
return json.loads(response)
def nl_to_sql_large_db(question, conn):
all_tables = list_all_tables(conn) # just names
relevant = get_relevant_tables(question, all_tables)
schema = get_schema_for_tables(conn, relevant)
return llm_generate_sql(question, schema)Perché NL-to-SQL è difficile: differenze tra i dialetti SQL
SQL non è universale. LIMIT in PostgreSQL/MySQL diventa TOP in SQL Server. Le funzioni per le date variano tra i database. L'LLM deve sapere quale dialetto utilizzare.
Includa sempre il dialetto di destinazione nel system prompt e valuti l'aggiunta di esempi specifici del dialetto nel prompting few-shot.
DIALECT_EXAMPLES = {
'postgresql': 'Use LIMIT for row limits. Use NOW() for current time.',
'mysql': 'Use LIMIT for row limits. Use NOW() for current time.',
'sqlite': 'Use LIMIT. Use datetime("now") for current time.',
'mssql': 'Use TOP N for row limits. Use GETDATE() for current time.',
'bigquery': 'Use LIMIT. Use CURRENT_TIMESTAMP() for current time. Use backtick for table names.'
}
def get_dialect_hint(dialect):
return DIALECT_EXAMPLES.get(dialect.lower(), '')
if __name__ == '__main__':
for dialect in ['postgresql', 'sqlite', 'mssql']:
print(f'{dialect}: {get_dialect_hint(dialect)}')
Ciclo di recupero dagli errori
L'SQL generato spesso non funziona al primo tentativo. Un agente robusto implementa un ciclo di recupero dagli errori: invia all'LLM l'SQL non riuscito e il messaggio di errore, chiedendogli di correggere la query.
Limiti i tentativi a 2-3 per evitare cicli infiniti con query impossibili da correggere.
def nl_to_sql_with_retry(question, schema, conn, max_retries=3):
sql = llm_generate_sql(question, schema)
for attempt in range(max_retries):
result = execute_query(conn, sql)
if 'error' not in result:
return format_results(result, question)
# Ask LLM to fix the error
fix_prompt = f'The SQL query failed with error: {result["error"]}\n'\
f'Original SQL: {sql}\n'\
f'Please fix the SQL query.'
sql = llm_call(fix_prompt)
print(f'Retry {attempt + 1} with fixed SQL')
return 'Could not generate a valid query after retries.'Unire tutti gli elementi
Un agente NL-to-SQL per la produzione combina tutti gli elementi: recupero dello schema, costruzione del prompt, generazione dell'SQL, convalida, esecuzione, recupero dagli errori e formattazione dei risultati.
L'aggiunta della memorizzazione nella cache delle query (stessa domanda → stesso SQL) riduce drasticamente la latenza e i costi dell'LLM per le query ripetute.
import hashlib
query_cache = {}
def cached_nl_to_sql(question, schema_hash, conn):
cache_key = hashlib.md5((question + schema_hash).encode()).hexdigest()
if cache_key in query_cache:
print('Cache hit!')
sql = query_cache[cache_key]
else:
schema = get_schema(conn)
sql = llm_generate_sql(question, schema)
query_cache[cache_key] = sql
result = execute_query(conn, sql)
return format_results(result, question)Verifica delle conoscenze
Qual è l'ordine corretto dei passaggi nella pipeline di un agente NL-to-SQL?
Riepilogo: architettura NL-to-SQL
Gli agenti NL-to-SQL trasformano le domande in linguaggio naturale in query SQL eseguibili tramite una pipeline strutturata: iniettare lo schema → generare SQL → eseguire → formattare → restituire.
Le sfide principali sono l'ambiguità delle domande degli utenti, le grandi dimensioni degli schemi che superano le finestre di contesto e le differenze tra i dialetti SQL dei vari database. I cicli di recupero dagli errori gestiscono l'SQL generato dall'LLM che non riesce alla prima esecuzione.
Domande Frequenti
La lezione «Come funzionano gli agenti NL-to-SQL» è gratuita?
Sì — il testo completo di «Come funzionano gli agenti NL-to-SQL» è 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 Agents, passa a CoddyKit PRO. Il corso AI Agents include 4 lezioni in totale.
Cosa imparerò in «Come funzionano gli agenti NL-to-SQL»?
Iniezione dello schema, generazione ed esecuzione delle query e formattazione dei risultati Eserciti AI Agents 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 Agents?
Non è richiesta alcuna esperienza precedente. AI Agents su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.
Quanto tempo richiede la lezione «Come funzionano gli agenti NL-to-SQL»?
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 Agents?
Sì. Ogni lezione AI Agents 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
- Come funzionano gli agenti NL-to-SQL
- Comprensione e iniezione dello schema
- Generazione e convalida delle query SQL
- Gestione delle domande ambigue sui database