DEV Community

Cover image for SQL seguro con IA: cómo construir un asistente que valida las consultas antes de ejecutarlas
Kiara Holly Zapana Murillo
Kiara Holly Zapana Murillo

Posted on

SQL seguro con IA: cómo construir un asistente que valida las consultas antes de ejecutarlas

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í:

  1. Enviar el esquema de la base de datos y la pregunta del usuario a un LLM.
  2. Pedirle al LLM que devuelva una consulta SQL.
  3. 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:

  1. El usuario hace una pregunta en lenguaje natural.
  2. El agente de IA propone una consulta SQL tras razonar sobre el esquema.
  3. Un validador estricto intercepta y revisa esa consulta.
  4. Solo se ejecutan consultas SELECT catalogadas como seguras.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

3. Conexión en Python

import sqlite3

def get_connection():
    conn = sqlite3.connect("safe_sql_demo.db")
    return conn
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Este validador aplica tres reglas deterministas innegociables:

  1. Solo se permite SELECT.
  2. No se permiten múltiples sentencias encadenadas.
  3. 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}"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 un LIMIT 50 al 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


Referencias

Top comments (0)