Módulo 3: Promptear para SQL correcto

Introducción al Módulo 3: Promptear para SQL correcto

Descripción

En el Módulo 2 le diste al modelo el esquema de Reservo como contexto: los CREATE TABLE, las descripciones de columnas, las filas de muestra y las relaciones. Ese fue un salto enorme —el modelo pasó de adivinar tablas a nombrarlas correctamente—. Pero saber qué tablas hay no basta para que el SQL salga bien. Un modelo con el esquema perfecto todavía puede sumar las reservas canceladas al calcular el ingreso, escribir DATE_TRUNC('month', start_at) (una función que en SQLite no existe), devolver un párrafo de prosa alrededor del SQL que luego cuesta parsear, o —peor— adivinar en silencio qué quisiste decir con "los mejores socios" y darte una respuesta que responde una pregunta distinta a la tuya.

Este módulo cierra esa brecha. Vas a aprender a redactar el prompt para que el SQL generado sea correcto, seguro y fácil de procesar. La herramienta central es el system prompt de un asistente SQL: un mensaje de sistema que le fija al modelo su rol (analista de solo lectura), su dialecto objetivo (SQLite), su esquema (el contexto de M2) y sus reglas. Encima de eso, aprenderás few-shot —darle 2-3 ejemplos de preguntas ya resueltas—, a fijar el dialecto para que no mezcle motores, a escribir instrucciones-restricción que acotan la forma de la salida, y a manejar la ambigüedad para que el asistente pida aclaración o declare su supuesto en vez de adivinar.

Como en toda la guía, la llamada al modelo es concepto: te muestro el system prompt que enviarías y una respuesta plausible de claude-sonnet-5, rotulada como ejemplo. Pero el SQL que "genera" se ejecuta de verdad contra Reservo. Así verás, con salida real, la diferencia entre un prompt pobre y uno rico: el pobre da un número equivocado o falla con un error de dialecto; el rico acierta.


¿Dónde estamos en la guía?

Vas por el tercer escalón. El Módulo 1 planteó el problema text-to-SQL y sus peligros; el Módulo 2 resolvió el primer eslabón —el esquema como contexto—; este resuelve el segundo —el prompt que produce SQL correcto—.

   M1  Por qué SQL y LLMs           el problema, los peligros, el primer round trip
        │
        ▼
   M2  Darle el esquema al LLM      el esquema como contexto (CREATE TABLE, muestras, FKs)
        │   el modelo ya sabe QUÉ tablas hay
        ▼
   ESTÁS AQUÍ
   M3  Promptear para SQL correcto  rol, dialecto, reglas, few-shot, ambigüedad
        │   el modelo genera SQL que respeta tu motor, tu negocio, tu formato
        ▼
   M4  Validar y ejecutar           ¿parsea? ¿es SELECT? ¿tablas reales? loop de corrección
        │
        ▼
   M5  Guardrails y seguridad       solo-lectura, allowlist, límites, anti-inyección
        │
        ▼
   M6  El loop del agente SQL       el tool run_sql, tool-calling, multi-paso

La división de trabajo con los módulos vecinos es fina, y conviene tenerla clara desde ya porque en este tema es fácil invadir el terreno de al lado:

  • El esquema como contexto fue M2. Aquí lo reutilizamos —va dentro del system prompt—, pero no volvemos a explicar cómo introspeccionar sqlite_master o PRAGMA table_info. Si necesitas refrescar cómo se genera ese bloque, es el Módulo 2.
  • Validar el SQL generado es M4. Aquí un prompt pobre generará SQL malo, y lo veremos fallar o mentir —pero como motivación para prompear mejor, no como algo que atrapamos con un validador. El chequeo "¿empieza por SELECT?" que aparecerá es deliberadamente mínimo; el validador de verdad es M4.
  • Los guardrails duros son M5. Cuando en este módulo escribamos la regla "genera solo SELECT, nunca DELETE", eso es una instrucción del prompt: una petición al modelo. Forzar solo-lectura a nivel de conexión (PRAGMA query_only), la allowlist ejecutada que bloquea de verdad lo destructivo, los límites y el anti-inyección son M5. La diferencia es la columna vertebral de la guía: el prompt sube la probabilidad; el guardrail la garantiza.

La idea rectora: el modelo responde con lo que le pides y le muestras

Un modelo de lenguaje no tiene intenciones sobre tu base de datos. Genera la continuación más probable del texto que le diste. Si le das poco —solo la pregunta y el esquema—, la continuación más probable arrastra todo lo que vio en su entrenamiento: una mezcla de dialectos SQL (Postgres, MySQL, SQL Server, SQLite, todos revueltos), convenciones de negocio genéricas ("ingreso = suma de todo", sin saber que en Reservo las canceladas no cuentan), y un formato de respuesta conversacional (prosa explicativa alrededor del SQL).

Prompear para SQL correcto es estrechar esa distribución: darle al modelo suficiente contexto y suficientes reglas para que la continuación más probable sea justo el SQL que tú consideras correcto. Y hay dos palancas para hacerlo:

  1. Decirle (instrucciones): el rol, el dialecto, las reglas, el formato. "Eres un analista de SQLite de solo lectura. Genera una sola sentencia SELECT. El ingreso cuenta solo las reservas confirmadas."
  2. Mostrarle (ejemplos, few-shot): 2-3 pares pregunta→SQL correctos. A veces un ejemplo comunica en tres líneas lo que un párrafo de instrucciones no logra —sobre todo una convención sutil, como qué columna de fecha filtrar o cómo se escribe un JOIN idiomático—.

Este módulo te enseña a usar ambas palancas. Y como el resultado es SQL, podemos ejecutarlo y comprobar si la palanca funcionó.


El artefacto que construyes en este módulo: el system prompt

Todo lo que aprendas aquí converge en un solo artefacto: el system prompt del asistente de Reservo. Es el mensaje de sistema que acompaña cada pregunta del usuario, y tiene cuatro piezas. Míralo entero una vez —lo desarmaremos pieza por pieza a lo largo del módulo, y lo ensamblarás tú en el mini-proyecto—:

┌─ SYSTEM PROMPT DEL ASISTENTE DE RESERVO ─────────────────────────┐
│                                                                  │
│  [1] ROL          Eres un analista de datos de SOLO LECTURA      │
│                    para Reservo. Traduces la pregunta del        │
│                    usuario a una consulta SQL.                   │
│                                                                  │
│  [2] DIALECTO     El motor es SQLite 3.50. Usa funciones de      │
│                    SQLite (strftime, date). NUNCA DATE_TRUNC,    │
│                    EXTRACT, NOW ni sintaxis de otros motores.    │
│                                                                  │
│  [3] ESQUEMA      (el contexto de M2: CREATE TABLE, notas de     │
│                    columnas, relaciones. El dinero es centavos.  │
│                    status: 'confirmed'|'cancelled'.)             │
│                                                                  │
│  [4] REGLAS       - UNA sola sentencia SELECT (o WITH...SELECT). │
│                    - Solo estas tablas y columnas.               │
│                    - El ingreso cuenta solo confirmadas.         │
│                    - Si la pregunta es ambigua, declara tu       │
│                      supuesto o pide aclaración.                 │
│                    - Responde solo el SQL (o JSON {sql,          │
│                      explanation}).                              │
│                                                                  │
│  + FEW-SHOT       2-3 ejemplos pregunta→SQL correctos.           │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Cada cápsula de este módulo construye y justifica una de estas piezas, ejecutando el SQL que produce para probar que la pieza sirve. Al final tendrás el prompt completo, cableado, listo para el asistente de M8.


La regla dura, aplicada a este módulo

Repasemos la convención que gobierna la guía, porque en este módulo es fácil olvidarla —parece que estamos "hablando con el modelo" todo el tiempo—:

  • La llamada al modelo NO se ejecuta. No hay API de Claude ni red en el entorno donde corres los ejemplos. Cada vez que veas un system prompt, unos ejemplos few-shot y "supón que claude-sonnet-5 devolvió este SQL", es un ejemplo realista rotulado como tal. Usamos modelos actuales (claude-sonnet-5); nunca claude-3 ni nada retirado.
  • El SQL resultante SÍ se ejecuta, con Python y sqlite3, contra la Reservo que poblaste en M1. Cuando un prompt pobre produce un número equivocado o un error de dialecto, ese número y ese error salen de correr el SQL de verdad.
  • Además, hay dos piezas de procesamiento que también se ejecutan: parsear la respuesta del modelo (por ejemplo, json.loads de un {sql, explanation}) y el chequeo mínimo "¿empieza por SELECT?". Son Python puro y los corremos.

Verificación rápida del entorno

Los ejemplos suponen que ya poblaste Reservo en el Módulo 1 (el archivo reservo.db, con 5 salas, 8 socios, 23 reservas, 26 pagos). Si necesitas recrearla, vuelve a correr el seed de M1. Confirmemos que está y que responde:

import sqlite3

con = sqlite3.connect("reservo.db")
print("sqlite engine", sqlite3.sqlite_version)
for t in ("rooms", "members", "bookings", "payments"):
    n = con.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
    print(f"{t:10} {n}")
con.close()

Qué esperar:

sqlite engine 3.50.4
rooms      5
members    8
bookings   23
payments   26

Si ves esos cuatro números, tienes el campo de juego listo. Todo el módulo se ejecutó con Python 3.14 y SQLite 3.50.4.


Un anticipo: el mismo pregunta, dos prompts

Para que sientas de inmediato de qué va este módulo, veamos el patrón que repetiremos capsula tras capsula: la misma pregunta, un prompt pobre y uno rico, y el SQL de cada uno ejecutado.

La pregunta del usuario:

"¿Cuánto ingresó la sala Focus en total?"

Prompt pobre — solo la pregunta y el esquema, sin reglas de negocio. Supón que claude-sonnet-5 devolvió:

-- SQL generado con un prompt pobre (ejemplo realista, NO ejecutado por el modelo)
SELECT SUM(b.price_cents) AS revenue_cents
FROM bookings b
JOIN rooms r ON r.id = b.room_id
WHERE r.name = 'Focus';

Prompt rico — con la regla "el ingreso cuenta solo las reservas confirmadas". Supón que devolvió:

-- SQL generado con un prompt rico (ejemplo realista)
SELECT SUM(b.price_cents) AS revenue_cents
FROM bookings b
JOIN rooms r ON r.id = b.room_id
WHERE r.name = 'Focus' AND b.status = 'confirmed';

La única diferencia es AND b.status = 'confirmed'. Ejecutemos los dos contra Reservo:

import sqlite3

con = sqlite3.connect("reservo.db")

def scalar(sql):
    return con.execute(sql).fetchone()[0]

poor = """
SELECT SUM(b.price_cents)
FROM bookings b JOIN rooms r ON r.id = b.room_id
WHERE r.name = 'Focus'
"""
rich = """
SELECT SUM(b.price_cents)
FROM bookings b JOIN rooms r ON r.id = b.room_id
WHERE r.name = 'Focus' AND b.status = 'confirmed'
"""
print("prompt pobre:", scalar(poor))
print("prompt rico :", scalar(rich))
con.close()

Qué esperar:

prompt pobre: 47500
prompt rico : 34000

Los dos SQL ejecutan sin error. Los dos devuelven un número que se ve razonable. Pero solo uno responde la pregunta de negocio: Focus tiene dos reservas canceladas (7500 + 6000 = 13500 centavos) que el prompt pobre contó como ingreso. La diferencia entre 47500 y 34000 es exactamente esas canceladas. Nadie lo nota si solo mira el número —por eso es peligroso—, y el prompt rico lo previene porque le enseñó al modelo la convención de tu negocio.

Ese es el módulo en una frase: el prompt no cambia lo que la base de datos contiene; cambia qué SQL pide el modelo, y con eso, qué número obtienes.


Objetivo del módulo

Al completar este módulo serás capaz de:

  • ✅ Escribir el system prompt de un asistente SQL con sus cuatro piezas: rol, dialecto, esquema (de M2) y reglas.
  • ✅ Usar few-shot —2-3 ejemplos pregunta→SQL— y explicar, ejecutando, el error que corrigen.
  • Fijar el dialecto SQLite y evitar funciones de otro motor (DATE_TRUNC, EXTRACT, NOW), viendo el error real que producen.
  • ✅ Escribir instrucciones-restricción ("solo SELECT", "solo estas tablas", "responde solo el SQL") y entender por qué facilitan parsear y validar.
  • ✅ Diseñar el prompt para manejar la ambigüedad: pedir aclaración o declarar el supuesto, viendo que dos interpretaciones dan respuestas distintas.
  • ✅ Pedir SQL + explicación estructurada (JSON {sql, explanation} o tool-calling) para mostrarle al usuario qué se va a correr.
  • Ensamblar y probar el system prompt completo del asistente de Reservo con tres preguntas.

Prerrequisitos

  • Módulos 1 y 2 de esta guía. Necesitas Reservo poblada (M1) y entender el esquema como contexto (M2), que aquí reutilizamos dentro del prompt.
  • Python 3.10 o superior con sqlite3 (librería estándar). No necesitas servidor de base de datos ni clave de API: la llamada al modelo es concepto.
  • Leer SQL con soltura. Para juzgar si el SQL "generado" está bien —el corazón de este módulo—, tienes que poder leerlo. Escribirlo a mano es la guía de consulta; leerlo con criterio es indispensable aquí.

Roadmap del módulo

CápsulaTemaQué aprenderás
01Introducción (esta cápsula)La idea rectora, el system prompt como artefacto, pobre vs. rico
02El system prompt de un asistente SQLLas cuatro piezas: rol, dialecto, esquema, reglas; ensamblarlo
03Few-shot: ejemplos pregunta→SQLDar 2-3 ejemplos y el error que corrigen, ejecutado
04Fijar el dialecto SQLitestrftime vs DATE_TRUNC/EXTRACT/NOW; el error de otro motor
05Instrucciones-restricción y formatoSolo SELECT, solo estas tablas, responder solo el SQL; parsearlo
06Manejar la ambigüedadPedir aclaración o declarar el supuesto; dos interpretaciones, dos respuestas
07SQL + explicación estructuradaJSON {sql, explanation} y el adelanto del tool-calling (M6)
08Mini-proyecto: el prompt del asistente de ReservoEnsamblas el prompt completo y lo pruebas con 3 preguntas

¿Qué NO se cubre en este módulo?

  • Cómo introspeccionar y serializar el esquema — Módulo 2. Aquí el bloque de contexto ya existe; lo reutilizamos dentro del prompt.
  • Validar el SQL generado (parsear, EXPLAIN QUERY PLAN, referencias reales, loop de auto-corrección) — Módulo 4. El chequeo "¿empieza por SELECT?" que veremos es mínimo y provisional.
  • Guardrails duros (PRAGMA query_only, allowlist ejecutada, límites, timeout, anti-inyección) — Módulo 5. "Genera solo SELECT" aquí es una petición del prompt, no una defensa forzada.
  • El loop del agente y el tool-calling completo — Módulo 6. En la cápsula 07 damos un adelanto de la forma del tool, no el loop.
  • Evaluar el asistente (exactitud de ejecución, set de prueba) — Módulo 7.
  • Prompting general de LLMs (cadenas de razonamiento, RAG, memoria) — ecosistema de AI Engineering. Aquí es la rebanada de SQL: el prompt que produce buen SQL.

Resumen

  • Darle el esquema al modelo (M2) fue necesario pero no suficiente: con el esquema perfecto todavía puede sumar canceladas, usar funciones de otro motor o adivinar en silencio. Este módulo cierra esa brecha con el prompt.
  • La idea rectora: el modelo responde con lo que le pides (instrucciones) y le muestras (ejemplos). Prompear para SQL correcto es estrechar la distribución hacia el SQL que tú consideras correcto.
  • El artefacto del módulo es el system prompt del asistente de Reservo: rol + dialecto + esquema (M2) + reglas + few-shot. Se construye pieza por pieza.
  • Regla dura: el prompt y la respuesta del modelo son concepto (claude-sonnet-5); el SQL resultante y el parseo se ejecutan con Python + sqlite3.
  • Anticipo ejecutado: la pregunta de Focus dio 47500 con un prompt pobre (contó canceladas) y 34000 con uno rico (solo confirmadas). El prompt cambia el número.

Recursos adicionales

  1. Claude — Giving Claude a role with a system prompt — El mensaje de sistema que fija rol y reglas; el corazón de este módulo.
  2. Claude — Use examples (multishot prompting) — La técnica few-shot que verás en la cápsula 03.
  3. SQLite — Date and time functions — La referencia de strftime/date que fijamos como dialecto en la cápsula 04.
  4. Spider: Yale Semantic Parsing and Text-to-SQL — El benchmark académico donde el prompting y el few-shot mueven la aguja de forma medible.
  5. BIRD: Big Bench for Large-Scale Database Grounded Text-to-SQLBenchmark con foco en exactitud de ejecución sobre bases realistas.

Siguiente cápsula: El system prompt de un asistente SQL — Desarmaremos las cuatro piezas (rol, dialecto, esquema, reglas), ensamblaremos el prompt de Reservo, y ejecutaremos el SQL que produce para ver por qué un system prompt preciso vence a uno vago.