← SCRAM AI Lab

Tutoriales

NL-to-SQL con Prisma: el patrón 2-pass

Descubre cómo el patrón 2-pass con Prisma elimina alucinaciones en NL-to-SQL mediante validación intermedia de esquema y optimización de costos con LLMs.

May 21, 2026

425 lecturas

NL-to-SQL con Prisma: el patrón 2-pass

Single-pass NL→SQL alucina y vas a pagarlo en producción

El demo es seductor: usuario pregunta "dame los 10 deals más grandes del trimestre" y el LLM escupe SQL. Funciona en el demo. En producción con un schema de 80 tablas, joins de tres niveles y nombres de columnas en español mexicano (estatus, monto_neto, fecha_alta), el modelo inventa tablas que no existen, joins que no se sostienen, y filtros sintácticamente válidos pero semánticamente equivocados. Y como la SQL parsea bien, el sistema corre la query y devuelve resultados que parecen correctos pero no lo son. Eso es peor que un error.

Por qué single-pass falla

Estás pidiendo al modelo que haga tres cosas a la vez: (1) entender el intent del usuario, (2) mapearlo al schema correcto, (3) producir SQL válido. Cada paso multiplica la oportunidad de alucinar. El modelo prioriza "producir SQL que se vea correcto" sobre "asegurarse de que el intent es ese". Y el output es un blob de texto que es difícil validar sin ejecutar.

¿Por qué conviene separar la generación de SQL en dos llamadas de LLM?

Separar el proceso en dos pasos desacopla la comprensión semántica de la sintaxis técnica, reduciendo los errores lógicos en bases de datos complejas. Al generar primero una especificación intermedia en JSON validable contra el esquema, el sistema filtra alucinaciones de tablas y campos antes de compilar a Prisma, garantizando consistencia analítica e integridad relacional.

El patrón 2-pass

  • Pass 1 — Intent y query spec: el modelo recibe la pregunta y un resumen del schema, y devuelve un objeto JSON estructurado describiendo qué quiere el usuario: entidad principal, agregación, filtros, ordenamiento, límite. No SQL todavía.
  • Pass 2 — Traducción a Prisma: con el query spec validado, un segundo modelo (puede ser más barato) traduce el spec a una query de Prisma. Como Prisma es type-safe y el spec ya está acotado, este pass casi no alucina.

Por qué dos modelos distintos

Pass 1 requiere razonamiento sobre intent ambiguo y conocimiento del dominio. Aquí Sonnet 4.6 sobresale. Pass 2 es traducción mecánica de un JSON bien definido a código Prisma. gpt-4o-mini hace esto bien y cuesta una fracción. Ahorras 60-70% del costo total sin perder calidad.

Comparativa de arquitecturas NL-to-Database

Evaluar el mecanismo de consulta implica sopesar costos de inferencia reportados por proveedores como Anthropic y OpenAI, frente a la seguridad operativa en la base de datos empresarial:

Criterio Single-pass Directo (SQL Raw) Single-pass con ORM Patrón 2-Pass con Prisma
Tasa de alucinación semántica Muy alta en esquemas de más de 30 tablas Media a alta; inventa campos del modelo Baja; bloqueada por capa de validación intermedia
Riesgo de inyección o mutación Crítico; requiere sanitización manual y permisos read-only Bajo; el ORM parametriza automáticamente Nulo; el parseador valida métodos de sólo lectura
Costo relativo por consulta Alto (requiere modelos de razonamiento superior como Claude 3.5 Sonnet o GPT-4o para todo) Alto (modelo de frontera generando sintaxis completa) Optimizado (modelo avanzado en Pass 1, modelo ligero como GPT-4o mini en Pass 2)
Mantenibilidad ante cambios Frágil; cualquier renombramiento rompe cadenas sin alertar Media; depende del tipado dinámico Alta; la introspección de Prisma alerta errores en compilación

El prompt de Pass 1

Eres un analista que traduce preguntas a query specs estructurados.
Schema disponible (formato resumido):
- Contact { id, name, email, score, status, ownerId, createdAt }
- Deal { id, amount, stage, probability, contactId, closeDate, createdAt }
- Pipeline { id, name }
- ... (introspección automática del Prisma schema)

Pregunta del usuario:
{userQuestion}

Responde SOLO con JSON válido siguiendo este shape:
{
  "entity": "Contact" | "Deal" | "Pipeline" | ...,
  "operation": "list" | "count" | "sum" | "avg" | "groupBy",
  "filters": [{ "field": string, "op": "eq"|"gt"|"lt"|"contains"|"in", "value": any }],
  "joins": [string],
  "orderBy": { "field": string, "direction": "asc"|"desc" } | null,
  "limit": number | null,
  "groupBy": string | null,
  "confidence": 0..1,
  "clarificationNeeded": string | null
}

Si la pregunta es ambigua, pon clarificationNeeded en texto natural.

Validación del query spec

Antes de pasar a Pass 2, valida el spec contra schema introspection. Esto atrapa hallucinations sin necesidad de ejecutar la query:

function validateSpec(spec, schema) {
  const entity = schema.models[spec.entity];
  if (!entity) return { ok: false, error: `Unknown entity: ${spec.entity}` };

  for (const f of spec.filters) {
    if (!entity.fields[f.field]) {
      return { ok: false, error: `Field ${f.field} not in ${spec.entity}` };
    } 
  }
  if (spec.orderBy && !entity.fields[spec.orderBy.field]) {
    return { ok: false, error: `OrderBy field ${spec.orderBy.field} invalid` };
  }
  for (const j of spec.joins) {
    if (!entity.relations[j]) {
      return { ok: false, error: `Relation ${j} not in ${spec.entity}` };
    }
  }
  return { ok: true };
}

Si falla, retroalimenta al Pass 1 con el error y pídele que corrija. Dos intentos máximo; si no cuadra, devuelve clarification al usuario.

Pass 2: traducción a Prisma

Aquí el modelo recibe el spec validado y produce código Prisma. Como el shape está acotado y Prisma es type-safe, las opciones de error se desploman:

// Spec validado
{
  entity: "Deal",
  operation: "list",
  filters: [
    { field: "stage", op: "in", value: ["negotiation", "proposal"] },
    { field: "amount", op: "gt", value: 50000 }
  ],
  joins: ["contact"],
  orderBy: { field: "amount", direction: "desc" },
  limit: 10
}

// Pass 2 output
prisma.deal.findMany({
  where: { stage: { in: ["negotiation", "proposal"] }, amount: { gt: 50000 } },
  include: { contact: true },
  orderBy: { amount: "desc" },
  take: 10
});

Por qué Prisma y no SQL crudo

Prisma te da type-safety en el output, validación automática contra el schema, y protección contra SQL injection sin que el modelo tenga que pensarlo. Además, el output es código que tu equipo lee y mantiene, no SQL ad-hoc que nadie quiere tocar. Para queries que Prisma no soporta (window functions, CTEs complejos), aíslalas en raw queries explícitas con allowlist.

Consideraciones críticas e impacto para operaciones en América Latina

En el contexto corporativo de México y América Latina, las bases de datos raras veces están normalizadas en un inglés canónico. Es común interactuar con sistemas legados montados sobre SAP, Microsip, Intelisis o desarrollos internos donde conviven campos como rfc_emisor, folio_fiscal, importe_sin_iva o acrónimos contables no documentados. El patrón single-pass colapsa de forma estrepitosa cuando un usuario de finanzas formula una pregunta usando modismos locales como "cuánto cobramos de anticipos esta quincena".

El Pass 1 actúa como una barrera semántica indispensable. Permite incorporar en el prompt reglas de negocio específicas de la región sin alterar la base de datos subyacente. Por ejemplo, definir qué significa un corte quincenal en México (días 15 y último día del mes) o mapear conceptos impositivos locales sin exponer credenciales ni esquemas completos al exterior, cumpliendo de mejor manera con regulaciones de privacidad de datos locales como la Ley Federal de Protección de Datos Personales en Posesión de los Particulares (LFPDPPP).

Errores comunes y cómo mitigarlos en tiempo de ejecución

  • Problemas de rendimiento por N+1 queries: Si el Pass 2 genera un include profundo sin discriminación, la base de datos sufrirá bloqueos de I/O. Limita en el spec los joins a un nivel máximo de profundidad y prioriza select explícitos sobre colecciones completas.
  • Ambigüedad en rangos de fechas fiscales: Las fechas suelen carecer de zona horaria explícita en sistemas legados. Configura el Pass 1 para que normalice cualquier referencia temporal a formato ISO 8601 considerando el huso horario del usuario (como UTC-6 para la zona centro de México).
  • Desbordamiento de memoria por consultas sin paginación: Obliga a que la especificación siempre imponga un valor take por defecto (por ejemplo, 50 registros) si el usuario no especificó un tope.

Lo que vas a aprender en producción

  • El schema cambia y rompe Pass 1: regenera el prompt cada deploy desde introspection, no lo hardcodees.
  • Usuarios usan vocabulario propio: "vendedor" en lugar de "owner", "ventas" en lugar de "deal". Mantén un glosario en el system prompt.
  • Confidence importa: cuando el spec viene con confidence < 0.7, pídele al usuario clarificación antes de ejecutar.

¿Tu NL-to-SQL actual valida contra el schema antes de ejecutar, o confía en que el SQL "se ve correcto"? Si es lo segundo, ya tienes resultados incorrectos en producción; solo no los has detectado.

nl-to-sql
prisma
ai-native-saas
← Volver a SCRAM AI Lab