Criando uma interface de banco de dados em linguagem natural
Crie um sistema no qual os usuários façam perguntas em linguagem simples, o modelo gere SQL por meio de chamadas de funções, sua aplicação execute a consulta com segurança e o modelo descreva os resultados.
Criando uma interface de banco de dados em linguagem natural é uma aula grátis de AI Engineering Academy no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de AI Engineering Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de AI Engineering Academy inclui 4 aulas no total.
Da linguagem natural para SQL: a visão
Imagine perguntar ao seu banco de dados “Quais clientes gastaram mais de US$ 1.000 no mês passado?” e receber uma resposta — sem escrever uma única consulta SQL. Uma interface de banco de dados em linguagem natural usa chamadas de função para permitir que o LLM gere SQL; sua aplicação o executa com segurança, e o modelo descreve os resultados em linguagem comum. Esse padrão democratiza o acesso aos dados para usuários não técnicos.
Visão geral da arquitetura do sistema
O fluxo de trabalho de NL para SQL tem quatro componentes que atuam em conjunto:
- Contexto do esquema: o LLM recebe o esquema do seu banco de dados para saber quais tabelas e colunas existem.
- Geração de SQL: o modelo gera uma consulta SQL como argumento de uma chamada de função.
- Execução segura: sua aplicação valida e executa a consulta e, em seguida, retorna os resultados.
- Descrição dos resultados: o modelo recebe os resultados da consulta e os explica em linguagem natural.
Definindo a ferramenta de consulta ao banco de dados
Defina uma função query_database que aceite uma instrução SQL SELECT. A descrição do esquema na definição da função ensina ao modelo quais tabelas e colunas estão disponíveis, para que ele gere consultas precisas sem fazer suposições.
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']
}
}
}Execução segura de SQL
Nunca execute SQL bruto gerado pelo modelo sem validação. Implemente uma camada de segurança que: permita apenas instruções SELECT, rejeite palavras-chave perigosas, limite o número de linhas dos resultados para evitar problemas de memória e execute a consulta em uma transação de banco de dados somente leitura. A defesa em profundidade é essencial ao executar código gerado por 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]Injetando o contexto do esquema no prompt do sistema
O modelo gera SQL melhor quando consegue ver o esquema completo do banco de dados. Crie um prompt do sistema que inclua as definições das tabelas, os nomes e tipos das colunas e valores de exemplo para colunas categóricas. Assim, o modelo sabe se deve usar country = 'US' ou country_code = 'US' sem fazer suposições.
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.
'''Formatando os resultados da consulta para o modelo
Os resultados brutos do banco de dados (listas de dicionários) precisam ser formatados como texto legível antes de serem enviados de volta ao modelo. Converta o conjunto de resultados em uma representação compacta — uma tabela ou um resumo em JSON — que o modelo possa consultar ao descrever a resposta. Evite enviar milhares de linhas; resuma conjuntos de resultados grandes.
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 resultImplementação completa do fluxo de trabalho
Juntando tudo: a função que processa uma pergunta do usuário, chama o modelo para gerar SQL, executa a consulta com segurança e envia os resultados de volta para serem descritos. O modelo recebe tanto a pergunta original quanto os resultados da consulta e, então, produz uma resposta em linguagem comum.
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.contentLidando com perguntas de dados em várias etapas
Perguntas complexas podem exigir várias consultas. “Quem são nossos 5 principais clientes por receita e quais são os pedidos mais recentes deles?” precisa de duas consultas: uma para encontrar os principais clientes e outra para obter os pedidos deles. Permita que o modelo faça várias chamadas sequenciais de ferramentas executando o ciclo de encaminhamento várias vezes, até 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.'Evitando riscos de injeção de SQL
Mesmo com a proteção que permite apenas SELECT, um modelo ardiloso (ou um usuário mal-intencionado) pode tentar extrair dados por meio de subconsultas ou truques baseados em comentários. As proteções adicionais incluem: usar um usuário de banco de dados somente leitura que tenha apenas permissão SELECT, executar em um conjunto separado de conexões e validar se os nomes das tabelas na consulta correspondem à sua lista de permissões do esquema.
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 TrueArmazenando consultas comuns em cache
Muitas perguntas de negócios são feitas repetidamente e têm a mesma resposta: “Quantos clientes temos?” “Qual foi a receita do mês passado?” Armazene esses resultados em cache no Redis com um TTL curto. Verifique o cache antes de executar a consulta — isso reduz a carga no banco de dados e acelera as respostas para perguntas analíticas comuns.
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 rowsExplicando as consultas aos usuários
Crie confiança mostrando aos usuários a consulta SQL gerada junto com a resposta em linguagem natural. Quando os usuários conseguem ver “Executei esta consulta: SELECT COUNT(*) FROM customers WHERE country = ?UK?”, eles podem verificar se a resposta está correta e aprender padrões de SQL. O campo explanation no esquema da nossa ferramenta é perfeito para isso.
Verificação rápida
Teste sua compreensão sobre a criação de uma interface de banco de dados em linguagem natural.
Recapitulação da lição
Nesta lição, você aprendeu: o esquema da ferramenta query_database injeta o contexto do esquema para que o modelo gere SQL preciso, a validação de segurança deve bloquear instruções que não sejam SELECT e palavras-chave perigosas antes da execução e um ciclo de chamadas ao modelo permite análises de dados em várias etapas que exigem consultas sequenciais. A seguir, vamos explorar o Model Context Protocol (MCP), o padrão aberto para conectar a IA a ferramentas externas.
Perguntas Frequentes
A aula “Criando uma interface de banco de dados em linguagem natural” é grátis?
Sim — o texto completo de “Criando uma interface de banco de dados em linguagem natural” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de AI Engineering Academy, atualize para CoddyKit PRO. O curso de AI Engineering Academy inclui 4 aulas no total.
O que vou aprender em “Criando uma interface de banco de dados em linguagem natural”?
Crie um sistema no qual os usuários façam perguntas em linguagem simples, o modelo gere SQL por meio de chamadas de funções, sua aplicação execute a consulta com segurança e o modelo descreva os resu… Você pratica AI Engineering Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar AI Engineering Academy?
Nenhuma experiência prévia é necessária. AI Engineering Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.
Quanto tempo leva a aula “Criando uma interface de banco de dados em linguagem natural”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de AI Engineering Academy?
Sim. Cada aula de AI Engineering Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Definindo esquemas de funções para a API
- Processando chamadas de ferramentas na sua aplicação
- Chamadas de funções em paralelo
- Criando uma interface de banco de dados em linguagem natural