Introducción
Escribir SQL a mano es una de esas habilidades que, sin querer, deja fuera a mucha gente del mundo de los datos. Product managers, agentes de soporte e incluso desarrolladores junior muchas veces saben exactamente qué quieren preguntarle a una base de datos, pero no siempre saben cómo expresarlo sintácticamente en SQL.
Los sistemas Text-to-SQL intentan resolver justamente ese problema: convertir una pregunta en lenguaje natural en una consulta real, devolviendo los datos sin fricción.
Sin embargo, generar SQL automáticamente no es el final del problema, es apenas el principio. Un modelo de lenguaje (LLM) puede producir una consulta sintácticamente correcta y aun así completamente equivocada a nivel semántico: un join mal hecho, un filtro incorrecto, o —en el peor de los casos— un intento de modificar datos que solo debían leerse.
En este artículo construyo un asistente de IA para SQL que no solo genera consultas, sino que las valida de forma determinista antes de ejecutarlas. Para ello utilizo smolagents de Hugging Face como capa de razonamiento, y una capa de seguridad propia (un SQL Guard) como filtro final antes de que cualquier consulta toque la base de datos.
El código fuente completo de este proyecto está disponible en el repositorio público enlazado al final del artículo.
¿Por qué no basta con pedirle SQL a un LLM?
El pipeline más simple de Text-to-SQL se ve así:
- Enviar el esquema de la base de datos y la pregunta del usuario a un LLM.
- Pedirle al LLM que devuelva una consulta SQL.
- Ejecutar esa consulta a ciegas y mostrar el resultado.
Esto funciona… hasta que deja de funcionar. El problema radica en que una sola llamada al LLM es frágil: el modelo puede generar una consulta válida en sintaxis pero incorrecta en significado, y el sistema la ejecutará igual, sin ningún mecanismo para detectar el error.
Peor aún, en un entorno real nada impide que el modelo intente inyectar un DELETE o un UPDATE si no se le prohíbe explícitamente a nivel de sistema. Decírselo únicamente en el prompt no es una garantía de seguridad, es una sugerencia que el modelo puede ignorar. Ese es exactamente el vacío arquitectónico que este proyecto busca cerrar.
¿Qué es smolagents y por qué es relevante aquí?
smolagents es un framework de agentes ligero de Hugging Face, descrito por sus propios creadores como "una librería minimalista para agentes que piensan en código". En lugar de forzar al modelo a producir llamadas a herramientas en un JSON rígido, un CodeAgent escribe y ejecuta código Python real como parte de su ciclo iterativo de razonamiento:
- Pensar: el modelo razona sobre qué hacer a continuación.
- Actuar: escribe código Python que invoca una herramienta (en nuestro caso, un motor SQL).
- Observar: el resultado de esa herramienta vuelve al contexto del modelo.
- Repetir o responder: el agente decide si necesita ajustar la consulta (por ejemplo, si hubo un error de sintaxis) o si ya puede entregar la respuesta final.
Ese ciclo es extremadamente útil, pero no es suficiente por sí solo. Un agente puede autocorregirse ante resultados sospechosos, pero no sabe, por diseño, qué acciones son destructivas para tu lógica de negocio. Esa responsabilidad la debe asumir una capa adicional y determinista: el validador.
Arquitectura de la solución
En lugar de ejecutar directamente cualquier SQL que el modelo proponga, diseñamos un sistema con el siguiente flujo:
- El usuario hace una pregunta en lenguaje natural.
- El agente de IA propone una consulta SQL tras razonar sobre el esquema.
- Un validador estricto intercepta y revisa esa consulta.
- Solo se ejecutan consultas
SELECTcatalogadas como seguras. - El resultado se procesa y se devuelve al usuario.
Ese paso adicional —la validación— es lo que eleva una simple demostración técnica a un asistente que realmente podrías conectar a datos reales en producción.
Usuario ──▶ Agente (smolagents) ──▶ SQL Guard ──▶ Base de datos
│ │
razona y rechaza si
propone SQL no es seguro
Construyendo el proyecto: base de datos de ventas
Para mantener el ejemplo reproducible, utilizo una base de datos SQLite con dos tablas sencillas: customers y sales.
1. Definiendo el esquema
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL
);
CREATE TABLE sales (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
order_date TEXT,
total_amount REAL,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
2. Cargando datos de ejemplo (semilla)
INSERT INTO customers (id, name, city) VALUES
(1, 'Alan Payne', 'Lima'),
(2, 'Marta Lopez', 'Arequipa');
INSERT INTO sales (id, customer_id, order_date, total_amount) VALUES
(1, 1, '2026-07-01', 120.50),
(2, 2, '2026-07-02', 89.99);
3. Conexión en Python
import sqlite3
def get_connection():
conn = sqlite3.connect("safe_sql_demo.db")
return conn
La pieza clave: el validador de SQL (SQL Guard)
Esta es la parte fundamental del proyecto y lo que marca la diferencia frente a un Text-to-SQL básico. Antes de ejecutar cualquier consulta generada por el agente, la instrucción debe sortear este filtro en Python:
BLOCKED = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER"]
def is_safe_sql(query: str) -> bool:
q = query.strip().upper()
# Prevenir ataques de inyección encadenada
if ";" in q[:-1]:
return False
# Exigir explícitamente operaciones de solo lectura
if not q.startswith("SELECT"):
return False
# Bloquear palabras clave destructivas, incluso en subconsultas
return not any(word in q for word in BLOCKED)
Este validador aplica tres reglas deterministas innegociables:
- Solo se permite
SELECT. - No se permiten múltiples sentencias encadenadas.
- Se bloquean palabras destructivas en cualquier parte de la cadena, incluso dentro de subconsultas.
Integrando el agente con el validador
El flujo de ejecución intercepta la petición del agente de esta forma:
from db import get_connection
from validator import is_safe_sql
def ask_database(question: str, generated_sql: str) -> str:
# Capa de seguridad
if not is_safe_sql(generated_sql):
return "Consulta bloqueada por el validador: operación no permitida."
# Ejecución aprobada
try:
with get_connection() as conn:
rows = conn.execute(generated_sql).fetchall()
return str(rows)
except Exception as e:
return f"Error SQL: {e}"
Aquí radica la diferencia arquitectónica: el modelo LLM puede alucinar, o el usuario puede intentar inyectar código malicioso a través del prompt, pero la aplicación retiene la autoridad final de decidir si esa sugerencia llega a ejecutarse. El agente asiste; el sistema controla.
Ejemplo de uso
Si un usuario pregunta de buena fe:
¿Cuál es el total de ventas por mes?
El agente generará algo como:
SELECT strftime('%Y-%m', order_date) AS month,
SUM(total_amount) AS total
FROM sales
GROUP BY month
ORDER BY month;
El validador revisa que sea un SELECT seguro, la aprueba y el sistema devuelve el resultado real.
¿Qué pasa ante un ataque o una alucinación del modelo?
Si el agente, o el usuario a través del prompt, intentara ejecutar DELETE FROM sales WHERE total_amount > 100;, el validador lo rechazaría en milisegundos, devolviendo la alerta de seguridad sin llegar siquiera a tocar el motor de base de datos.
Prompt directo al LLM vs. agente con validación
| Criterio | LLM directo (zero-shot) | Agente + validador |
|---|---|---|
| Generación | Un solo intento, sin retroalimentación | Iterativa, con capacidad de corrección |
| Manejo de errores | Devuelve respuestas erróneas en silencio | Se detiene y bloquea ante operaciones dudosas |
| Seguridad | Depende de la "buena voluntad" del prompt | Aplicada estrictamente a nivel de código |
| Auditabilidad | Consulta oculta en la respuesta | Consulta explícita y lista para hacer log |
| Confiabilidad en producción | Baja | Considerablemente más alta |
La conclusión es clara: un validador no elimina el riesgo inherente de la IA, pero lo mitiga, transformando una decisión implícita del modelo en una regla explícita del sistema.
Seguridad: consideraciones adicionales para producción
Además del SQL Guard propuesto, cualquier solución real a nivel corporativo debe contemplar:
-
Mínimo privilegio (DB roles): conectar a la base de datos usando un rol restringido que físicamente solo tenga permisos de
SELECT, jamás como administrador. - Vistas en lugar de tablas crudas: exponer a la IA únicamente views curadas, ocultando columnas con contraseñas o datos personales sensibles (PII).
-
Límites inyectados (
LIMIT): forzar programáticamente unLIMIT 50al final de cada consulta válida para evitar que un modelo escanee accidentalmente millones de filas. - Registro (logging): guardar cada pregunta, la consulta generada y su latencia, para poder auditar el uso de la herramienta con el tiempo.
Casos de uso reales
- Dashboards dinámicos — dejar que los usuarios de negocio generen agregaciones seguras sobre la marcha, sin depender del equipo de ingeniería.
- Plataformas de soporte — permitir a los agentes buscar datos operativos de clientes mediante lenguaje natural, bajo políticas de lectura estricta.
- Proyectos académicos y de investigación — fomentar el uso de bases de datos relacionales eliminando la fricción de la sintaxis SQL, sin comprometer la integridad de los datos.
Conclusión
El uso de arquitecturas Text-to-SQL demuestra que delegar tareas a modelos fundacionales requiere de ingeniería de software defensiva. La diferencia entre un prompt llamativo y un sistema validado es, en última instancia, la diferencia entre una demo y un sistema productivo.
El enfoque basado en código de smolagents vuelve el razonamiento del LLM transparente y auditable. Si le sumamos una capa de validación SQL, logramos trasladar la seguridad del prompt volátil a una regla de infraestructura confiable, que no depende de que el modelo se comporte bien.
Repositorio público:
El código funcional completo de este proyecto (base de datos, validador, orquestación y demostración) está disponible en mi cuenta de GitHub:
🔗 https://github.com/KiaaraZM/safe-sql-ai
Top comments (0)