Lección 05
Proyecto final: extractor de datos estructurados
Convierte texto libre (emails, facturas, mensajes) en JSON fiable con tests. El portfolio piece de este curso.
Lo que vamos a construir
Una función extract(text) que recibe texto libre y devuelve un objeto JavaScript con datos estructurados. Por ejemplo:
"Hola, soy Ana García, mi DNI es 12345678A y vivo en Calle Mayor 12, Madrid.
Mi teléfono es +34 600 123 456. Quiero reservar para el 15 de mayo a las 21:00."
…tiene que devolver:
{
"nombre": "Ana García",
"dni": "12345678A",
"direccion": "Calle Mayor 12, Madrid",
"telefono": "+34 600 123 456",
"fechaReserva": "2026-05-15",
"horaReserva": "21:00"
}
Paso 1 — Define el esquema
Antes del prompt, define el esquema en código. Esto es lo que va a hacer que tu sistema sea fiable.
import { z } from "zod";
const ReservaSchema = z.object({
nombre: z.string(),
dni: z.string().regex(/^\d{8}[A-Z]$/),
direccion: z.string(),
telefono: z.string(),
fechaReserva: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
horaReserva: z.string().regex(/^\d{2}:\d{2}$/),
});
type Reserva = z.infer<typeof ReservaSchema>;
Si el modelo se sale del esquema, lo cazamos en parse() y reintentamos.
Paso 2 — El prompt
Aplica todo lo aprendido:
const SYSTEM = `Eres un extractor de datos. Recibes un mensaje y devuelves SIEMPRE JSON válido con esta forma:
{
"nombre": string,
"dni": string (8 dígitos + 1 letra mayúscula),
"direccion": string,
"telefono": string (con prefijo internacional),
"fechaReserva": "YYYY-MM-DD",
"horaReserva": "HH:MM"
}
Reglas:
- Si un campo no está en el mensaje, devuélvelo como null.
- NO incluyas texto fuera del JSON.
- NO uses bloques markdown (sin \`\`\`).`;
const EJEMPLOS = `
Mensaje: "Soy Luis Pérez (DNI 87654321B). Reserva para el 1 de junio a las 14h."
{ "nombre": "Luis Pérez", "dni": "87654321B", "direccion": null, "telefono": null, "fechaReserva": "2026-06-01", "horaReserva": "14:00" }
`;
Paso 3 — La función
async function extract(texto: string): Promise<Reserva> {
const res = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 500,
system: SYSTEM,
messages: [
{ role: "user", content: `${EJEMPLOS}\n\nMensaje: "${texto}"` },
{ role: "assistant", content: "{" },
],
});
const raw = "{" + res.content[0].text;
const json = JSON.parse(raw);
return ReservaSchema.parse(json);
}
Fíjate en dos detalles clave:
- Assistant prefill con
{— fuerza al modelo a empezar por JSON, sin texto introductorio. ReservaSchema.parse()— si el modelo se sale del esquema, lanza una excepción que puedes capturar y reintentar.
Paso 4 — Tests
Sin tests no es producción. Sin tests es magia.
import { describe, it, expect } from "vitest";
describe("extract", () => {
it("extrae todos los campos cuando están", async () => {
const r = await extract(
"Soy Ana García (12345678A), Calle Mayor 12 Madrid, +34 600 123 456. Reserva 15/05 21:00."
);
expect(r.nombre).toBe("Ana García");
expect(r.dni).toBe("12345678A");
expect(r.fechaReserva).toBe("2026-05-15");
});
it("devuelve null para campos ausentes", async () => {
const r = await extract("Soy Luis. Quiero reservar el 1 de junio.");
expect(r.nombre).toBe("Luis");
expect(r.dni).toBeNull();
});
it("normaliza la hora a HH:MM", async () => {
const r = await extract("Reserva a las 9 de la noche.");
expect(r.horaReserva).toBe("21:00");
});
});
Paso 5 — Despliega
Mete tu función en un endpoint serverless (Cloudflare Workers, Vercel, Deno Deploy) y ya tienes una API de extracción que puede gestionar emails de tu negocio.
Coste estimado: menos de 0.001 € por extracción con claude-haiku-4-5.
Lo que llevas aprendido
- Anatomía de un prompt: rol, contexto, instrucción, ejemplos, formato.
- Zero-shot vs few-shot, y cuándo merece la pena cada uno.
- Chain-of-thought para razonamiento.
- JSON prefill para formato fiable.
- Validación con
zodpara producción.
Siguiente paso
¿Quieres más? El siguiente curso es “Construye un chatbot con IA”, donde montamos un asistente con memoria conversacional y herramientas. Te espero allí.
Reto Pro
Reto Pro de esta lección
Misma idea, sin pistas, evaluada por tests automáticos.
Hard Mode
Hard Mode
Variante extrema del reto. Tiempo límite y restricciones adicionales.
Hard Mode
🏆 Boss Challenge
Construye y despliega un clasificador de tickets en producción que tu instructor evaluará en vivo