Texto caótico dentro, forma garantizada fuera.
Un mini-servicio de terminal que recibe una oferta de empleo en prosa y devuelve sus datos como JSON validado, llamando al LLM por SDK directo — sin frameworks de por medio. Primer artefacto de la ruta Data Science → AI Engineering: la llamada cruda, entendida antes de taparla con capas.
De texto libre a datos manejables
Un LLM por defecto devuelve texto libre: distinto en cada llamada e inservible para un
programa. Este artefacto fuerza una salida con forma fija —un molde Pydantic— y la
valida antes de devolverla. Ahora el resultado sí es código:
if oferta.salario_min >= 40000 funciona, porque el salario es un número, no un párrafo.
La extracción, en vivo
La oferta de abajo es el caso real del repo: oferta_ejemplo.txt, y el JSON es
resultado.json tal cual lo produjo la CLI. Pulsa Extraer y mira cómo
cada dato se enciende en el texto y aterriza en su campo del molde — es exactamente lo que el
artefacto hace en cada llamada.
Data Analyst Junior en Barcelona (presencial). Buscamos perfil con SQL, Power BI y Excel avanzado. No se requiere experiencia previa. Salario 24.000 EUR brutos/año. Contrato de prácticas con posibilidad de indefinido.
9 campos validados contra el molde Oferta.
Y los detalles finos: la empresa no aparece en el texto → null explícito, nunca un
invento; una sola cifra de salario → mínimo y máximo iguales; «no se requiere experiencia» →
seniority: "junior", inferido del contexto.
Schema → parse → objeto validado
El molde define qué campos quiero; el SDK obliga al modelo a rellenarlo y lo valida. Lo único específico del dominio son los campos y sus descripciones — el motor es reutilizable para facturas, tickets o contratos.
class Oferta(BaseModel):
puesto: str = Field(description="Título del puesto")
stack: list[str] = Field(description="Tecnologías mencionadas")
salario_min: int | None = Field(description="Salario mínimo EUR, o null")
...
respuesta = client.messages.parse(
model="claude-opus-4-8",
messages=[{"role": "user", "content": texto}],
output_format=Oferta, # ← se pasa el molde
)
oferta = respuesta.parsed_output # ← objeto validado, no texto
Claude y OpenAI, lado a lado
La misma extracción con los dos SDKs: cambian tres nombres de parámetro y el id del modelo; el concepto es idéntico. El SDK por debajo es casi igual — todo lo demás es conveniencia.
client.messages.parse(
messages=[...],
output_format=Oferta,
)
.parsed_output
client.responses.parse(
input=[...],
text_format=Oferta,
)
.output_parsed
Los tres criterios, cumplidos
null explícito cuando el dato no está, nunca un invento.
Antes de tocar frameworks, monté la llamada cruda. Si solo dominas la capa de LangChain, dominas algo que puede desaparecer; si entiendes el SDK por debajo, todo lo demás es conveniencia.