Módulo 1: Modelos y Providers
Structured Output
Descripción de la cápsula
Hasta ahora, cada vez que invocas un modelo obtienes texto libre. El modelo responde con una cadena de caracteres que tú, como desarrollador, necesitas interpretar manualmente. Si quieres extraer un nombre, un rating y un resumen de una reseña, tendrías que parsear el texto con expresiones regulares, dividir por delimitadores, o pedirle al modelo "por favor responde en JSON" — y rezar para que lo haga correctamente.
Structured Output elimina ese problema por completo. En lugar de recibir texto y parsearlo, le dices al modelo exactamente qué estructura quieres — con tipos, campos y descripciones — y recibes un objeto Python tipado. No hay regex, no hay parsing frágil, no hay sorpresas. Defines un schema con Pydantic, TypedDict o JSON Schema, y el método with_structured_output se encarga de que el modelo responda exactamente en ese formato.
Esta capacidad es fundamental para cualquier aplicación seria. APIs que retornan datos, pipelines que procesan información, agentes que toman decisiones — todos necesitan datos estructurados, no texto libre. En el proyecto de este módulo, usarás Structured Output para extraer metadata de cada respuesta del chat: qué proveedor se usó, cuántos tokens consumió, y la latencia de la llamada.
¿Por qué Structured Output?
El problema: parsear texto libre es frágil
Imagina que le pides a un modelo que analice una película:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke("Analiza la película Inception. Dame título, rating del 1 al 10, y un resumen corto.")
print(response.content)
# Output posible 1: "Título: Inception\nRating: 9/10\nResumen: Un ladrón que roba secretos..."
# Output posible 2: "**Inception** - 9 estrellas. Un thriller sobre sueños dentro de sueños..."
# Output posible 3: "La película Inception, dirigida por Christopher Nolan, merece un 9..."
Tres ejecuciones, tres formatos diferentes. Ahora intenta extraer el rating de forma programática. Necesitarías regex, heurísticas, y aun así fallaría con formatos inesperados.
La solución: respuestas tipadas
Con Structured Output, defines la estructura una vez y el modelo la respeta siempre:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class MovieReview(BaseModel):
title: str = Field(description="Título de la película")
rating: int = Field(description="Rating de 1 a 10")
summary: str = Field(description="Resumen en una frase")
model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(MovieReview)
result = structured_model.invoke("Analiza la película Inception")
print(type(result)) # <class 'MovieReview'>
print(result.title) # Inception
print(result.rating) # 9
print(result.summary) # Un ladrón experto en extracción de secretos a través de sueños...
El resultado ya no es texto libre — es un objeto MovieReview con atributos tipados. result.rating siempre es un int, no un string que necesitas parsear.
Beneficios concretos
- ✅ Type safety — Cada campo tiene un tipo definido. No hay conversiones manuales
- ✅ Parse reliability — El modelo respeta la estructura el 100% de las veces
- ✅ No regex — Nunca más escribir expresiones regulares para extraer datos
- ✅ Validación automática — Pydantic valida tipos y constraints por ti
- ✅ Autocompletado en IDE — Tu editor conoce los campos disponibles
- ✅ Composabilidad — Los objetos structured se integran directamente con el resto de tu código
with_structured_output con Pydantic
Pydantic es el approach más recomendado. Te da validación de tipos, descripciones de campos que guían al modelo, y objetos Python nativos como resultado.
Ejemplo básico
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Sentiment(BaseModel):
label: str = Field(description="Sentimiento: positivo, negativo o neutro")
confidence: float = Field(description="Nivel de confianza entre 0.0 y 1.0")
explanation: str = Field(description="Explicación breve del análisis")
model = init_chat_model("openai:gpt-4.1-mini")
analyzer = model.with_structured_output(Sentiment)
result = analyzer.invoke("Me encanta este producto, es increíble")
print(result.label) # positivo
print(result.confidence) # 0.95
print(result.explanation) # El texto expresa entusiasmo y satisfacción clara...
Cómo funciona
- Defines un modelo Pydantic con campos tipados y descripciones
- Llamas
model.with_structured_output(TuModelo)— esto retorna un nuevo modelo que siempre produce objetos del tipo especificado - Invocas el modelo structured normalmente con
.invoke() - El resultado es una instancia de tu modelo Pydantic, no texto
El Field(description=...) es importante: esas descripciones le dicen al modelo qué esperas en cada campo. Piénsalas como mini-instrucciones para el LLM.
Tipos soportados en campos
| Tipo | Ejemplo | Descripción |
|---|---|---|
str | name: str | Texto libre |
int | count: int | Número entero |
float | score: float | Número decimal |
bool | is_valid: bool | Verdadero o falso |
list[str] | tags: list[str] | Lista de strings |
Optional[str] | note: Optional[str] | Puede ser None |
Enum | status: MyEnum | Valor de un conjunto cerrado |
Literal["a", "b"] | choice: Literal["a", "b"] | Valor literal restringido |
Extraer múltiples items
Un patrón muy común es extraer una lista de objetos de un texto:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Person(BaseModel):
name: str = Field(description="Nombre completo de la persona")
role: str = Field(description="Rol o relación mencionada")
class PeopleExtraction(BaseModel):
people: list[Person] = Field(description="Lista de personas mencionadas en el texto")
total_count: int = Field(description="Total de personas encontradas")
model = init_chat_model("openai:gpt-4.1-mini")
extractor = model.with_structured_output(PeopleExtraction)
text = """
En la reunión estuvieron Carlos (CTO), Ana María (product manager)
y el nuevo desarrollador Luis García.
"""
result = extractor.invoke(f"Extrae las personas mencionadas: {text}")
print(result.total_count) # 3
for person in result.people:
print(f" - {person.name}: {person.role}")
# Output:
# - Carlos: CTO
# - Ana María: Product Manager
# - Luis García: Desarrollador
with_structured_output con TypedDict
Si no necesitas la validación que Pydantic ofrece y prefieres trabajar con diccionarios, puedes usar TypedDict. El resultado será un dict de Python en lugar de un objeto Pydantic.
Ejemplo básico
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from typing import TypedDict, Annotated
class MovieReview(TypedDict):
title: Annotated[str, "Título de la película"]
rating: Annotated[int, "Rating de 1 a 10"]
summary: Annotated[str, "Resumen en una frase"]
model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(MovieReview)
result = structured_model.invoke("Analiza la película The Matrix")
print(type(result)) # <class 'dict'>
print(result["title"]) # The Matrix
print(result["rating"]) # 9
print(result["summary"]) # Un programador descubre que la realidad es una simulación...
Diferencia clave con Pydantic
Con TypedDict el resultado es un diccionario (result["title"]), no un objeto con atributos (result.title). No hay validación automática de tipos — si el modelo retorna un string donde esperabas un int, no se lanzará un error.
¿Cuándo usar TypedDict?
- ✅ Cuando la salida va directamente a un JSON response (API endpoints)
- ✅ Cuando no necesitas validación estricta
- ✅ Cuando prefieres trabajar con diccionarios por familiaridad
- ❌ Cuando necesitas validación de constraints (rangos, patrones, etc.)
- ❌ Cuando quieres métodos custom en el resultado
with_structured_output con JSON Schema
El tercer approach usa un JSON Schema directamente. Esto es útil cuando el schema se genera dinámicamente — por ejemplo, cuando el usuario define la estructura de extracción en runtime.
Ejemplo básico
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
json_schema = {
"title": "MovieReview",
"description": "Análisis de una película",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Título de la película"
},
"rating": {
"type": "integer",
"description": "Rating de 1 a 10"
},
"summary": {
"type": "string",
"description": "Resumen en una frase"
}
},
"required": ["title", "rating", "summary"]
}
model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(json_schema)
result = structured_model.invoke("Analiza la película Interstellar")
print(type(result)) # <class 'dict'>
print(result["title"]) # Interstellar
print(result["rating"]) # 9
print(result["summary"]) # Un grupo de exploradores viaja a través de un agujero de gusano...
¿Cuándo usar JSON Schema?
- ✅ Schemas que se generan en runtime (formularios dinámicos, configuración por usuario)
- ✅ Cuando recibes el schema de una fuente externa (API, base de datos)
- ✅ Cuando necesitas compatibilidad con sistemas que ya usan JSON Schema
- ❌ Cuando el schema es fijo — Pydantic es más ergonómico y seguro
Nested structures
Los schemas simples cubren muchos casos, pero las aplicaciones reales necesitan estructuras jerárquicas. Pydantic permite anidar modelos dentro de modelos.
Ejemplo: reporte con secciones
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Section(BaseModel):
title: str = Field(description="Título de la sección")
content: str = Field(description="Contenido de la sección en 2-3 frases")
class Report(BaseModel):
topic: str = Field(description="Tema del reporte")
executive_summary: str = Field(description="Resumen ejecutivo en una frase")
sections: list[Section] = Field(description="3 secciones del reporte")
conclusion: str = Field(description="Conclusión en una frase")
model = init_chat_model("openai:gpt-4.1-mini")
reporter = model.with_structured_output(Report)
result = reporter.invoke("Genera un reporte breve sobre el impacto de AI en la educación")
print(f"Tema: {result.topic}")
print(f"Resumen: {result.executive_summary}")
print(f"Secciones: {len(result.sections)}")
for section in result.sections:
print(f" - {section.title}: {section.content[:80]}...")
print(f"Conclusión: {result.conclusion}")
# Output:
# Tema: Impacto de la Inteligencia Artificial en la Educación
# Resumen: La AI está transformando la educación mediante personalización...
# Secciones: 3
# - Personalización del aprendizaje: Los sistemas de AI pueden adaptar...
# - Automatización administrativa: Tareas como calificación y reportes...
# - Desafíos y consideraciones éticas: La brecha digital y la privacidad...
# Conclusión: La AI tiene el potencial de democratizar la educación...
La anidación puede ir a múltiples niveles, pero mantén la complejidad razonable. Schemas con más de 3 niveles de profundidad tienden a producir resultados menos confiables.
include_raw: obtener respuesta original
Por defecto, with_structured_output retorna solo el objeto parseado. Pero a veces necesitas acceder a la respuesta original del modelo — para debugging, logging, o para extraer metadata como tokens usados.
Activar include_raw
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Sentiment(BaseModel):
label: str = Field(description="Sentimiento: positivo, negativo o neutro")
confidence: float = Field(description="Confianza entre 0.0 y 1.0")
model = init_chat_model("openai:gpt-4.1-mini")
analyzer = model.with_structured_output(Sentiment, include_raw=True)
result = analyzer.invoke("Este restaurante tiene la peor comida que he probado")
print(type(result)) # <class 'dict'>
print(result.keys()) # dict_keys(['raw', 'parsed', 'parsing_error'])
Con include_raw=True, el resultado cambia: en lugar de recibir directamente el objeto Pydantic, recibes un diccionario con tres claves:
| Clave | Tipo | Contenido |
|---|---|---|
raw | AIMessage | La respuesta original del modelo, con metadata completa |
parsed | Tu modelo Pydantic (o None) | El objeto parseado, igual que sin include_raw |
parsing_error | Exception o None | Error de parsing si lo hubo |
Usar include_raw para debugging
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Sentiment(BaseModel):
label: str = Field(description="Sentimiento: positivo, negativo o neutro")
confidence: float = Field(description="Confianza entre 0.0 y 1.0")
model = init_chat_model("openai:gpt-4.1-mini")
analyzer = model.with_structured_output(Sentiment, include_raw=True)
result = analyzer.invoke("Este restaurante tiene la peor comida que he probado")
if result["parsing_error"]:
print(f"Error: {result['parsing_error']}")
print(f"Raw: {result['raw'].content}")
else:
parsed = result["parsed"]
raw = result["raw"]
print(f"Sentimiento: {parsed.label} (confianza: {parsed.confidence})")
print(f"Token usage: {raw.usage_metadata}")
# Output:
# Sentimiento: negativo (confianza: 0.95)
# Token usage: {'input_tokens': 45, 'output_tokens': 12, 'total_tokens': 57}
¿Cuándo usar include_raw?
- ✅ Debugging durante desarrollo — ver qué respondió realmente el modelo
- ✅ Logging en producción — guardar respuestas raw para auditoría
- ✅ Token tracking — extraer
usage_metadatapara monitorear costos - ✅ Error handling — detectar y manejar errores de parsing sin que crashee tu app
Comparación: Pydantic vs TypedDict vs JSON Schema
| Característica | Pydantic | TypedDict | JSON Schema |
|---|---|---|---|
| Tipo de resultado | Objeto con atributos (result.field) | Diccionario (result["field"]) | Diccionario (result["field"]) |
| Validación de tipos | ✅ Automática | ❌ No | ❌ No |
| Descripciones de campos | ✅ Field(description=...) | ✅ Annotated[type, "desc"] | ✅ "description": "..." |
| Nested structures | ✅ Natural | ⚠️ Limitado | ✅ Con $ref |
| Constraints (min, max, pattern) | ✅ Field(ge=1, le=10) | ❌ No | ✅ "minimum": 1 |
| IDE autocompletado | ✅ Completo | ⚠️ Parcial | ❌ No |
| Schemas dinámicos | ❌ Estático | ❌ Estático | ✅ Generables en runtime |
| Curva de aprendizaje | Media | Baja | Alta |
| Recomendación | Usar por defecto | Prototipos rápidos | Schemas dinámicos |
Regla: Usa Pydantic a menos que tengas una razón específica para no hacerlo. TypedDict para prototipos rápidos donde no quieres instalar Pydantic (aunque ya viene con LangChain). JSON Schema solo cuando el schema se genera dinámicamente.
Conexión con el proyecto
En el Chat Multi-Proveedor con Fallback, usarás Structured Output para dos cosas:
-
Metadata de respuesta: Cada respuesta del chat incluirá metadata structured — qué proveedor la generó, cuánto tardó, y cuántos tokens usó. Esto te permite monitorear el sistema sin parsear logs manualmente.
-
Respuestas formateadas: Cuando el usuario pida análisis o extracción de datos, el chat usará Structured Output para retornar resultados consistentes independientemente del proveedor que los genere.
El schema de metadata se verá así:
from pydantic import BaseModel, Field
class ResponseMetadata(BaseModel):
provider: str = Field(description="Proveedor que generó la respuesta")
model_name: str = Field(description="Nombre del modelo usado")
latency_ms: float = Field(description="Latencia en milisegundos")
tokens_used: int = Field(description="Total de tokens consumidos")
Este patrón — combinar include_raw para extraer metadata del AIMessage con Pydantic para la respuesta estructurada — es exactamente lo que implementarás en la cápsula de proyecto.
Troubleshooting
Problema 1: El modelo no respeta la estructura
Síntoma: Recibes None o un error de parsing en lugar del objeto expected.
Causa: El prompt es ambiguo o contradice la estructura del schema.
Solución: Asegúrate de que el prompt sea compatible con el schema. Las descripciones en Field() ayudan al modelo a entender qué generar.
# Mal — el prompt pide formato libre, pero el schema espera estructura
result = structured_model.invoke("Escribe lo que quieras sobre AI")
# Bien — el prompt es compatible con la estructura
result = structured_model.invoke("Analiza el impacto de AI en la educación")
Problema 2: Error ValidationError de Pydantic
Síntoma: pydantic.ValidationError: 1 validation error for MyModel.
Causa: El modelo generó un valor que no pasa la validación de Pydantic (ejemplo: un string donde esperabas un int).
Solución: Usa include_raw=True para inspeccionar qué generó el modelo realmente, y ajusta las descripciones de tus campos para ser más explícitas.
analyzer = model.with_structured_output(MyModel, include_raw=True)
result = analyzer.invoke("...")
if result["parsing_error"]:
print(f"Error: {result['parsing_error']}")
print(f"Raw: {result['raw'].content}")
Problema 3: with_structured_output no disponible
Síntoma: AttributeError: 'ChatModel' object has no attribute 'with_structured_output'.
Causa: Versión antigua de LangChain o proveedor que no soporta structured output.
Solución:
pip install --upgrade langchain langchain-core langchain-openai
Verifica que estás en LangChain v1.2+. Los proveedores principales (OpenAI, Anthropic, Google) soportan structured output.
Problema 4: Campos Optional siempre retornan None
Síntoma: Los campos opcionales nunca se llenan aunque la información está en el texto. Causa: Las descripciones de los campos opcionales no son lo suficientemente claras. Solución: Mejora las descripciones e indica cuándo el campo debe llenarse:
# Mal
assignee: Optional[str] = Field(description="Persona asignada")
# Bien
assignee: Optional[str] = Field(
description="Nombre de la persona asignada a la tarea. None solo si no se menciona a nadie."
)
Ejercicios
Ejercicio 1: Extracción de contacto (Fácil)
Define un modelo Pydantic ContactInfo con campos para nombre, email y teléfono (los últimos dos opcionales). Usa with_structured_output para extraer información de contacto de un texto en lenguaje natural.
Texto de prueba: "Hola, soy Laura Martínez. Mi correo es laura@ejemplo.com y me puedes llamar al 555-1234."
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
from typing import Optional
class ContactInfo(BaseModel):
name: str = Field(description="Nombre completo de la persona")
email: Optional[str] = Field(description="Dirección de email si se menciona")
phone: Optional[str] = Field(description="Número de teléfono si se menciona")
model = init_chat_model("openai:gpt-4.1-mini")
extractor = model.with_structured_output(ContactInfo)
text = "Hola, soy Laura Martínez. Mi correo es laura@ejemplo.com y me puedes llamar al 555-1234."
result = extractor.invoke(f"Extrae la información de contacto: {text}")
print(f"Nombre: {result.name}") # Laura Martínez
print(f"Email: {result.email}") # laura@ejemplo.com
print(f"Teléfono: {result.phone}") # 555-1234
Explicación: El modelo identifica automáticamente cada pieza de información y la asigna al campo correcto. Los campos Optional se llenan cuando la información está presente y quedan como None cuando no.
Ejercicio 2: Clasificación con Enum (Fácil)
Crea un clasificador de tickets de soporte que categorice el ticket en una de estas categorías: bug, feature_request, question, complaint. Usa un Enum para restringir los valores posibles. Incluye también un campo urgency (1-5) y un summary.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
from enum import Enum
class TicketCategory(str, Enum):
BUG = "bug"
FEATURE_REQUEST = "feature_request"
QUESTION = "question"
COMPLAINT = "complaint"
class TicketClassification(BaseModel):
category: TicketCategory = Field(description="Categoría del ticket")
urgency: int = Field(description="Nivel de urgencia de 1 (bajo) a 5 (crítico)")
summary: str = Field(description="Resumen del ticket en una frase")
model = init_chat_model("openai:gpt-4.1-mini")
classifier = model.with_structured_output(TicketClassification)
tickets = [
"La página de checkout se cae cuando uso Safari. No puedo completar mi compra.",
"¿Podrían agregar modo oscuro? Sería genial para trabajar de noche.",
"¿Cómo exporto mis datos a CSV?",
]
for ticket in tickets:
result = classifier.invoke(f"Clasifica este ticket de soporte: {ticket}")
print(f"Categoría: {result.category.value} | Urgencia: {result.urgency} | {result.summary}")
# Output:
# Categoría: bug | Urgencia: 4 | Error en la página de checkout en Safari...
# Categoría: feature_request | Urgencia: 2 | Solicitud de modo oscuro...
# Categoría: question | Urgencia: 1 | Consulta sobre exportación de datos a CSV...
Explicación: El Enum garantiza que la categoría siempre será uno de los 4 valores válidos. El modelo no puede inventar categorías nuevas. Combinado con el campo urgency de tipo int, obtienes datos listos para procesar programáticamente.
Ejercicio 3: Nested structure con TypedDict (Medio)
Recrea el ejemplo de Report con secciones, pero esta vez usando TypedDict en lugar de Pydantic. Compara la experiencia de desarrollo con ambos approaches.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from typing import TypedDict, Annotated
class Section(TypedDict):
title: Annotated[str, "Título de la sección"]
content: Annotated[str, "Contenido de la sección en 2-3 frases"]
class Report(TypedDict):
topic: Annotated[str, "Tema del reporte"]
executive_summary: Annotated[str, "Resumen ejecutivo en una frase"]
sections: Annotated[list[Section], "3 secciones del reporte"]
conclusion: Annotated[str, "Conclusión en una frase"]
model = init_chat_model("openai:gpt-4.1-mini")
reporter = model.with_structured_output(Report)
result = reporter.invoke("Genera un reporte breve sobre el impacto de AI en la salud")
print(f"Tema: {result['topic']}")
print(f"Resumen: {result['executive_summary']}")
for section in result["sections"]:
print(f" - {section['title']}: {section['content'][:60]}...")
print(f"Conclusión: {result['conclusion']}")
Explicación: Con TypedDict accedes a los campos con result["key"] en lugar de result.key. La estructura funciona igual, pero pierdes validación automática y autocompletado en el IDE. Para schemas anidados, Pydantic suele ser más ergonómico.
Ejercicio 4: Schema dinámico con JSON Schema (Medio)
Crea una función extract_from_text(text, fields) que reciba un texto y un diccionario de campos con sus descripciones, genere un JSON Schema dinámicamente, y extraiga la información. Pruébalo con al menos dos conjuntos de campos diferentes sobre el mismo texto.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
def extract_from_text(text: str, fields: dict[str, str]) -> dict:
"""Extrae información de un texto usando un schema generado dinámicamente."""
schema = {
"title": "Extraction",
"type": "object",
"properties": {
name: {"type": "string", "description": desc}
for name, desc in fields.items()
},
"required": list(fields.keys())
}
model = init_chat_model("openai:gpt-4.1-mini")
extractor = model.with_structured_output(schema)
return extractor.invoke(f"Extrae la siguiente información del texto: {text}")
article = """
Apple presentó el iPhone 16 en septiembre de 2024 con un precio base de $799.
El CEO Tim Cook destacó las nuevas capacidades de AI integradas en el dispositivo.
Las acciones de Apple subieron un 2% después del anuncio.
"""
business_fields = {
"company": "Nombre de la empresa",
"product": "Producto presentado",
"price": "Precio mencionado",
}
print("Extracción de negocio:", extract_from_text(article, business_fields))
# Output: {'company': 'Apple', 'product': 'iPhone 16', 'price': '$799'}
market_fields = {
"stock_movement": "Movimiento de acciones mencionado",
"executive": "Nombre del ejecutivo mencionado",
"date": "Fecha del evento",
}
print("Extracción de mercado:", extract_from_text(article, market_fields))
# Output: {'stock_movement': 'Subieron un 2%', 'executive': 'Tim Cook', 'date': 'Septiembre de 2024'}
Explicación: La misma función extrae información completamente diferente del mismo texto, dependiendo de los campos que le pases. Este patrón es poderoso para aplicaciones donde los usuarios definen qué quieren extraer.
Ejercicio 5: include_raw para monitoreo (Avanzado)
Crea una función analyze_with_metrics que use include_raw=True para retornar tanto el análisis structured como métricas de uso (tokens, modelo). Si hay un error de parsing, debe retornar el error y la respuesta raw en lugar de crashear.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Analysis(BaseModel):
topic: str = Field(description="Tema analizado")
sentiment: str = Field(description="Sentimiento general: positivo, negativo o neutro")
key_points: list[str] = Field(description="3 puntos clave del análisis")
def analyze_with_metrics(text: str) -> dict:
"""Analiza texto y retorna resultado con métricas de uso."""
model = init_chat_model("openai:gpt-4.1-mini")
analyzer = model.with_structured_output(Analysis, include_raw=True)
result = analyzer.invoke(f"Analiza el siguiente texto: {text}")
raw = result["raw"]
parsed = result["parsed"]
error = result["parsing_error"]
metrics = {
"tokens": raw.usage_metadata if hasattr(raw, "usage_metadata") else None,
"model": raw.response_metadata.get("model_name", "unknown"),
}
if error:
return {
"success": False,
"error": str(error),
"raw_content": raw.content,
"metrics": metrics,
}
return {
"success": True,
"analysis": {
"topic": parsed.topic,
"sentiment": parsed.sentiment,
"key_points": parsed.key_points,
},
"metrics": metrics,
}
result = analyze_with_metrics(
"El nuevo framework de Python ha ganado popularidad rápidamente. "
"Los developers lo adoptan por su simplicidad y rendimiento."
)
if result["success"]:
print(f"Tema: {result['analysis']['topic']}")
print(f"Sentimiento: {result['analysis']['sentiment']}")
for point in result["analysis"]["key_points"]:
print(f" - {point}")
print(f"Tokens: {result['metrics']['tokens']}")
print(f"Modelo: {result['metrics']['model']}")
else:
print(f"Error: {result['error']}")
print(f"Raw: {result['raw_content']}")
# Output:
# Tema: Nuevo framework de Python
# Sentimiento: positivo
# - Ganó popularidad rápidamente
# - Los developers lo adoptan
# - Destacado por simplicidad y rendimiento
# Tokens: {'input_tokens': 85, 'output_tokens': 45, 'total_tokens': 130}
# Modelo: gpt-4.1-mini
Explicación: include_raw=True te da acceso a la respuesta completa del modelo, incluyendo metadata de tokens. El patrón de verificar parsing_error antes de acceder a parsed hace tu código robusto ante errores de parsing sin usar try/except.
Resumen
En esta cápsula aprendiste:
- Structured Output elimina el parsing manual de texto libre — defines un schema y recibes objetos tipados
- Pydantic es el approach recomendado: te da validación, descripciones de campos, y objetos con atributos
- TypedDict es la alternativa ligera cuando prefieres diccionarios y no necesitas validación
- JSON Schema es para schemas dinámicos que se generan en runtime
- Las nested structures permiten modelar datos jerárquicos con Pydantic models dentro de models
include_raw=Truete da acceso a la respuesta original para debugging, logging, y token tracking- Las descripciones de campos son instrucciones para el modelo — escríbelas con claridad
Próxima cápsula: Multimodal y Reasoning — cómo procesar imágenes, audio y video con los modelos, y cómo hacer que el modelo muestre sus pasos de razonamiento.
Recursos adicionales
- Structured Output — LangChain Docs — Guía conceptual oficial de structured output
- How to return structured data from a model — Tutorial paso a paso con ejemplos
- Pydantic v2 Documentation — Referencia completa de Pydantic para schemas avanzados
- OpenAI Structured Outputs — Implementación de OpenAI del estándar
- Anthropic Tool Use for Structured Output — Cómo Anthropic implementa structured output via tools
- JSON Schema Specification — Referencia para crear JSON Schemas manuales
- TypedDict — Python Docs — Documentación oficial de TypedDict
- LangChain Chat Models API Reference — Referencia del método
with_structured_output
Módulo 1 — LangChain & LangGraph: From Chains to Agents