← SCRAM AI Lab
Single-pass NL→SQL alucina tablas y joins. El patrón 2-pass separa intent (qué quiere) de traducción (cómo). Sonnet 4.6 para pass 1, gpt-4o-mini para pass 2.
May 21, 2026
5 lecturas
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.
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.
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.
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.
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.
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
});
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.
¿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.
Artículos relacionados
Traefik v2.10 con auto-renewal certs para 94 containers
Wildcard *.scram2k.com cubre la mayoría, certs individuales para el resto. acme.json shared, DNS-01 para wildcards, HTTP-01 para subdomains. Anti-patrón: cert por container.
OpenTelemetry tracing para pipelines LLM
Instrumentar pipelines LLM multi-tier con spans nested: router → tier-selection → provider-call → parsing → side-effects. Atributos gen_ai semantic conventions y flame chart en Tempo.
Loki + Grafana para logs de chatbot: query patterns
Labels útiles (org_id, session_id, tier, provider) sin caer en high-cardinality. LogQL patterns para errores por tier, p99 latency y costos por hora. Retention 30d hot.