Módulo 1: Modelos y Providers
Modelos Locales, Caching y Rate Limiting
Descripción de la cápsula
Hasta ahora has trabajado exclusivamente con modelos en la nube: cada llamada a invoke() viaja por internet hasta los servidores de OpenAI, Anthropic o Google. Esto funciona, pero tiene tres problemas reales: cuesta dinero por cada token, estás sujeto a límites de velocidad (rate limits) que pueden bloquear tu aplicación, y dependes de una conexión a internet. En esta cápsula aprenderás a resolver los tres.
Primero explorarás Ollama para ejecutar modelos localmente en tu máquina — sin API keys, sin costos, sin conexión a internet. Después verás prompt caching para reducir costos cuando repites los mismos prefijos de prompt en múltiples llamadas. Luego usarás InMemoryRateLimiter para evitar que tus llamadas se bloqueen por exceder cuotas de API. Y aprenderás a monitorear exactamente cuántos tokens consume cada llamada con usage_metadata.
La cápsula cierra con una sección práctica que necesitarás frecuentemente: un mapeo de APIs legacy a APIs modernas de LangChain. Cuando encuentres tutoriales antiguos que usen LLMChain o AgentExecutor, sabrás exactamente con qué reemplazarlos en LangChain v1.2+.
Modelos locales con Ollama
¿Por qué ejecutar modelos localmente?
Cada llamada a un modelo en la nube tiene un costo. Si estás prototipando, experimentando con prompts, o procesando datos sensibles que no puedes enviar a servidores externos, ejecutar un modelo localmente elimina esos tres problemas de un golpe.
Ollama es la forma más sencilla de correr modelos de lenguaje en tu máquina. Actúa como un servidor local que expone una API compatible — y LangChain se conecta a él exactamente igual que a OpenAI o Anthropic.
Instalar y configurar Ollama
# Instalar: macOS
brew install ollama
# Linux: curl -fsSL https://ollama.ai/install.sh | sh
# Windows: descargar desde https://ollama.ai/download
# Descargar un modelo
ollama pull llama3.2 # Modelo ligero (~2GB), bueno para desarrollo
ollama pull llama3.1 # Modelo más capaz (~5GB), mejor calidad
# Verificar modelos disponibles
ollama list
# Verificar que el servidor corre (inicia automáticamente)
ollama serve
# Output: Listening on 127.0.0.1:11434
Usar modelos locales con LangChain
Una vez que Ollama está corriendo, usas init_chat_model con el prefijo ollama: — idéntico a cualquier otro proveedor:
from langchain.chat_models import init_chat_model
model = init_chat_model("ollama:llama3.2")
response = model.invoke("Explica qué es una API REST en una frase")
print(response.content)
# Output esperado: Una API REST es una interfaz que permite a aplicaciones
# comunicarse entre sí usando el protocolo HTTP con operaciones estándar
# como GET, POST, PUT y DELETE.
No necesitas API key. No necesitas .env. No necesitas conexión a internet.
Cloud vs Local: cuándo usar cada uno
| Criterio | Cloud (OpenAI, Anthropic) | Local (Ollama) |
|---|---|---|
| Costo por token | ✅ Pago por uso | ✅ Gratis |
| Calidad de respuesta | ✅ State-of-the-art | ⚠️ Menor (modelos más pequeños) |
| Velocidad | ✅ Rápido (GPUs dedicadas) | ⚠️ Depende de tu hardware |
| Privacidad | ❌ Datos viajan a servidores externos | ✅ Todo queda en tu máquina |
| Conexión a internet | ❌ Requerida | ✅ No necesaria |
| Setup | ✅ Solo API key | ⚠️ Instalar Ollama + descargar modelo |
| Modelos disponibles | ✅ GPT-4.1, Claude, Gemini | ⚠️ Llama, Mistral, Phi (open-source) |
Regla práctica:
- ✅ Usa cloud para producción, tareas complejas, o cuando la calidad es crítica
- ✅ Usa local para desarrollo, prototipado, datos sensibles, o cuando quieres iterar sin costos
Prompt caching
Cómo funciona
Cuando llamas a un modelo, el proveedor procesa todo el prompt desde cero cada vez — incluyendo el system prompt. Si envías el mismo system prompt de 2,000 tokens en 100 llamadas consecutivas, pagas por procesar esos 2,000 tokens 100 veces.
Prompt caching resuelve esto: el proveedor detecta que un prefijo del prompt ya fue procesado recientemente y reutiliza el resultado cacheado. Solo procesa (y cobra) los tokens nuevos.
Sin caching:
Llamada 1: [System prompt: 2000 tokens] + [User: 50 tokens] → Cobra 2050 tokens
Llamada 2: [System prompt: 2000 tokens] + [User: 30 tokens] → Cobra 2030 tokens
Llamada 3: [System prompt: 2000 tokens] + [User: 45 tokens] → Cobra 2045 tokens
Total procesado: 6,125 tokens
Con caching:
Llamada 1: [System prompt: 2000 tokens] + [User: 50 tokens] → Cobra 2050 tokens (cache miss)
Llamada 2: [System prompt: CACHED] + [User: 30 tokens] → Cobra ~530 tokens (cache hit)
Llamada 3: [System prompt: CACHED] + [User: 45 tokens] → Cobra ~545 tokens (cache hit)
Total procesado: ~3,125 tokens (~50% ahorro)
Soporte por proveedor
| Proveedor | Estado | Notas |
|---|---|---|
| Anthropic | ✅ Beta disponible | Se activa automáticamente con system prompts largos (>1024 tokens) |
| OpenAI | ✅ Disponible | Automático en llamadas al mismo endpoint con prefijos repetidos |
| ⚠️ Parcial | Soporte de caching explícito con Context Caching API | |
| Ollama | ❌ No aplica | Modelos locales, no hay costo por token |
Cuándo ayuda
El caching es beneficioso cuando:
- ✅ Usas el mismo system prompt largo en múltiples llamadas
- ✅ Procesas muchos documentos con las mismas instrucciones de análisis
- ✅ Ejecutas un agente que mantiene un prompt base constante entre iteraciones
No vale la pena cuando:
- ❌ Cada llamada tiene un prompt completamente diferente
- ❌ Tu system prompt es corto (< 500 tokens)
- ❌ Haces pocas llamadas (el cache expira rápido)
Nota: Prompt caching es una optimización del proveedor que funciona de forma transparente. No necesitas cambiar tu código — simplemente ocurre cuando el patrón de uso lo permite. Lo importante es entender cuándo te beneficia para diseñar tus aplicaciones aprovechándolo. Profundizaremos en prompt caching con ejemplos prácticos en el Módulo 12 (LangSmith y Producción).
Rate limiting con InMemoryRateLimiter
El problema: rate limit errors
Cada proveedor de API tiene límites de velocidad. Si envías demasiadas solicitudes por minuto, recibes un error 429 Too Many Requests y tu aplicación se detiene.
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
# Esto puede fallar con rate limit error si envías muchas llamadas rápido
for i in range(50):
response = model.invoke(f"Pregunta número {i}")
print(f"{i}: {response.content[:30]}...")
# Error posible:
# openai.RateLimitError: Error code: 429 - Rate limit reached
Puedes usar max_retries para reintentar (cápsula 03), pero eso no resuelve el problema raíz: estás enviando solicitudes más rápido de lo que el API permite.
La solución: InMemoryRateLimiter
InMemoryRateLimiter controla la velocidad de las llamadas antes de que salgan de tu aplicación. En vez de enviar 50 solicitudes instantáneas y esperar que el API las acepte, las espacia automáticamente.
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
rate_limiter = InMemoryRateLimiter(requests_per_second=1)
model = init_chat_model(
"openai:gpt-4.1-mini",
rate_limiter=rate_limiter
)
response = model.invoke("¿Qué es rate limiting?")
print(response.content)
# Output esperado: Rate limiting es una técnica que controla la cantidad
# de solicitudes que un cliente puede hacer a un servicio en un período
# de tiempo determinado.
Puedes ajustar los parámetros: requests_per_second (velocidad máxima), check_every_n_seconds (frecuencia de verificación, default 0.1), y max_bucket_size (burst permitido, default 10).
Ejemplo práctico: procesamiento batch con rate limiting
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time
rate_limiter = InMemoryRateLimiter(requests_per_second=2)
model = init_chat_model(
"openai:gpt-4.1-mini",
rate_limiter=rate_limiter
)
preguntas = [
"¿Qué es Python?",
"¿Qué es JavaScript?",
"¿Qué es Rust?",
"¿Qué es Go?",
"¿Qué es TypeScript?",
]
start = time.time()
for i, pregunta in enumerate(preguntas):
response = model.invoke(pregunta)
elapsed = time.time() - start
print(f"[{elapsed:.1f}s] {pregunta} → {response.content[:50]}...")
# Output esperado (nota el espaciado temporal):
# [0.5s] ¿Qué es Python? → Python es un lenguaje de programación...
# [1.1s] ¿Qué es JavaScript? → JavaScript es un lenguaje de programaci...
# [1.6s] ¿Qué es Rust? → Rust es un lenguaje de programación de sist...
# [2.2s] ¿Qué es Go? → Go es un lenguaje de programación creado por...
# [2.7s] ¿Qué es TypeScript? → TypeScript es un superconjunto tipado...
Las llamadas se espacian automáticamente a ~0.5s entre cada una (2 requests/segundo).
Token usage tracking
Monitorear consumo de tokens
Cada respuesta de un modelo incluye metadata sobre cuántos tokens se consumieron. Accedes a esta información con response.usage_metadata:
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("Hola, ¿cómo estás?")
print(response.content)
print(response.usage_metadata)
# Output esperado:
# ¡Hola! Estoy bien, gracias por preguntar. ¿En qué puedo ayudarte?
# {'input_tokens': 8, 'output_tokens': 15, 'total_tokens': 23}
Calcular costos
Cada proveedor tiene precios por token. Puedes calcular el costo real de cada llamada:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
PRICING = {
"gpt-4.1-mini": {"input": 0.40 / 1_000_000, "output": 1.60 / 1_000_000},
"gpt-4.1": {"input": 2.00 / 1_000_000, "output": 8.00 / 1_000_000},
"gpt-4.1-nano": {"input": 0.10 / 1_000_000, "output": 0.40 / 1_000_000},
}
def invoke_with_cost(model_name: str, question: str) -> dict:
"""Invoca el modelo y calcula el costo de la llamada."""
model = init_chat_model(f"openai:{model_name}")
response = model.invoke(question)
tokens = response.usage_metadata
pricing = PRICING[model_name]
cost_input = tokens["input_tokens"] * pricing["input"]
cost_output = tokens["output_tokens"] * pricing["output"]
total_cost = cost_input + cost_output
return {
"content": response.content,
"input_tokens": tokens["input_tokens"],
"output_tokens": tokens["output_tokens"],
"cost_usd": total_cost,
}
result = invoke_with_cost("gpt-4.1-mini", "¿Qué es Docker en una frase?")
print(f"Respuesta: {result['content']}")
print(f"Tokens: {result['input_tokens']} entrada + {result['output_tokens']} salida")
print(f"Costo: ${result['cost_usd']:.6f} USD")
# Output esperado:
# Respuesta: Docker es una plataforma que permite empaquetar aplicaciones
# en contenedores aislados para ejecutarlos de forma consistente en
# cualquier entorno.
# Tokens: 12 entrada + 25 salida
# Costo: $0.000045 USD
En el Ejercicio 3 de esta cápsula construirás una clase CostTracker que acumula tokens y costos de múltiples llamadas — un patrón esencial para producción.
Mapeo: API Legacy → API Moderna
Por qué necesitas esta tabla
LangChain pasó por una transformación importante con la versión 1.0+ (Octubre 2025). Muchas APIs que encuentras en tutoriales, Stack Overflow, y documentación antigua están deprecadas o reemplazadas. Si copias código de un tutorial de 2024, probablemente no funcione — o funcione con warnings de deprecación.
Esta tabla te da el equivalente moderno de cada API legacy. Úsala como referencia cada vez que encuentres código antiguo.
Tabla de equivalencias
| API Legacy (pre-v1.0) | API Moderna (v1.2+) | Notas |
|---|---|---|
from langchain.llms import OpenAI | init_chat_model("openai:gpt-4.1-mini") | OpenAI era para completion models. Ahora todo usa chat models |
LLMChain(llm=llm, prompt=prompt) | model.invoke(prompt) | LLMChain añadía complejidad innecesaria. invoke() hace lo mismo |
SequentialChain([chain1, chain2]) | chain1 | chain2 (pipe operator) | El operador pipe compone runnables de forma natural |
AgentExecutor(agent, tools) | create_agent(model, tools) | create_agent es más simple y usa LangGraph internamente |
ConversationChain(llm, memory) | create_agent(model, tools) + state | Agents con state management reemplazan conversation chains |
OutputParser | model.with_structured_output(Schema) | Structured output es nativo — no necesitas parsear texto manualmente |
from langchain.chat_models import ChatOpenAI | init_chat_model("openai:...") | init_chat_model es universal, no necesitas clases por proveedor |
CallbackHandler | Sigue válido, pero middleware es preferido | Middleware (@before_model, @after_model) es más potente (Módulo 4) |
ChatPromptTemplate.from_messages | Sigue válido | Los templates de prompt no cambiaron |
Ejemplo: LLMChain → model.invoke()
# ❌ Legacy (pre-v1.0) — NO uses esto
# from langchain.llms import OpenAI
# from langchain.chains import LLMChain
# from langchain.prompts import PromptTemplate
#
# llm = OpenAI(temperature=0.7)
# prompt = PromptTemplate(
# input_variables=["topic"],
# template="Explica {topic} en una frase."
# )
# chain = LLMChain(llm=llm, prompt=prompt)
# result = chain.run(topic="Docker")
# ✅ Moderno (v1.2+) — Usa esto
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini", temperature=0.7)
response = model.invoke("Explica Docker en una frase.")
print(response.content)
# Output esperado: Docker es una plataforma de contenedores que empaqueta
# aplicaciones con todas sus dependencias para ejecutarlas de forma
# consistente en cualquier entorno.
Ejemplo: OutputParser → with_structured_output
# ❌ Legacy — OutputParser manual
# from langchain.output_parsers import PydanticOutputParser
# parser = PydanticOutputParser(pydantic_object=Movie)
# prompt = prompt_template.format(format_instructions=parser.get_format_instructions())
# ✅ Moderno — structured output nativo
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Movie(BaseModel):
title: str = Field(description="Título de la película")
year: int = Field(description="Año de estreno")
genre: str = Field(description="Género principal")
model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(Movie)
result = structured_model.invoke("Dame información sobre Inception")
print(f"{result.title} ({result.year}) - {result.genre}")
# Output esperado: Inception (2010) - Science Fiction
Conexión con el proyecto
En el proyecto de este módulo (Cápsula 08), construirás un chat multi-proveedor con fallback automático. Los conceptos de esta cápsula se aplican directamente:
- Ollama como fallback final: Si OpenAI y Anthropic fallan (API caída, rate limit), el sistema puede caer a un modelo local con Ollama. Sin costo, sin dependencia de internet.
- Rate limiting: Cuando el sistema recibe muchas solicitudes,
InMemoryRateLimiterevita que se bloquee por exceder las cuotas de los proveedores cloud. - Token tracking: El proyecto reporta metadata de cada respuesta, incluyendo tokens consumidos y proveedor utilizado — calculado con
usage_metadata. - APIs modernas: Todo el proyecto usa
init_chat_model,invoke(), ywith_structured_output— las APIs modernas que aprendiste en este módulo.
Troubleshooting
Problema 1: "Connection refused" al usar Ollama
ConnectionError: Connection refused - connect(2) for "127.0.0.1" port 11434
Causa: El servidor de Ollama no está corriendo.
Solución:
# Iniciar el servidor
ollama serve
# Verificar que responde
curl http://localhost:11434
# Debe retornar: "Ollama is running"
Problema 2: Modelo de Ollama no encontrado
Error: model "llama3.1" not found, try pulling it first
Causa: No descargaste el modelo antes de usarlo.
Solución:
# Descargar el modelo primero
ollama pull llama3.1
# Verificar modelos disponibles
ollama list
Problema 3: Rate limit error a pesar de usar InMemoryRateLimiter
Causa: Tu requests_per_second es más alto que el límite real de tu API tier. Solución: Reduce el valor (ej: requests_per_second=0.5) hasta que los errores desaparezcan.
Problema 4: usage_metadata es None
Causa: Algunos proveedores no retornan metadata de uso (Ollama, modelos locales). Solución: Verifica antes de acceder: if response.usage_metadata:.
Problema 5: Respuestas lentas con Ollama
Causa: Hardware insuficiente para el modelo. Solución: Usa un modelo más pequeño (ollama:llama3.2 en vez de llama3.1), verifica GPU con ollama ps, y cierra apps que consuman memoria.
Ejercicios
Ejercicio 1: Tu primer modelo local (Básico)
Instala Ollama, descarga el modelo llama3.2, y haz una llamada con init_chat_model. Imprime la respuesta y verifica que no se requirió API key.
Ver solución
from langchain.chat_models import init_chat_model
model = init_chat_model("ollama:llama3.2")
response = model.invoke("¿Cuál es la diferencia entre una lista y una tupla en Python?")
print(response.content)
print(f"\nTipo de respuesta: {type(response)}")
print(f"Usage metadata: {response.usage_metadata}")
# Output esperado:
# La principal diferencia es que las listas son mutables (puedes modificar
# sus elementos) mientras que las tuplas son inmutables (una vez creadas,
# no puedes cambiar sus elementos). Las listas usan corchetes [] y las
# tuplas usan paréntesis ().
#
# Tipo de respuesta: <class 'langchain_core.messages.ai.AIMessage'>
# Usage metadata: None (o dict con tokens si el modelo lo soporta)
Explicación: init_chat_model("ollama:llama3.2") se conecta al servidor Ollama local. No necesitas load_dotenv() ni API keys. La interfaz es idéntica a cualquier proveedor cloud.
Ejercicio 2: Rate limiter para batch processing (Básico)
Crea un modelo con rate limiter de 1 request/segundo y procesa una lista de 5 preguntas. Mide el tiempo total para verificar que el rate limiting funciona.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time
rate_limiter = InMemoryRateLimiter(requests_per_second=1)
model = init_chat_model("openai:gpt-4.1-mini", rate_limiter=rate_limiter)
preguntas = [
"¿Qué es HTML?",
"¿Qué es CSS?",
"¿Qué es JavaScript?",
"¿Qué es React?",
"¿Qué es Node.js?",
]
start = time.time()
for pregunta in preguntas:
response = model.invoke(pregunta)
elapsed = time.time() - start
print(f"[{elapsed:.1f}s] {pregunta} → {response.content[:40]}...")
total = time.time() - start
print(f"\nTiempo total: {total:.1f}s (esperado: ~5s con 1 req/s)")
# Output esperado:
# [0.8s] ¿Qué es HTML? → HTML es el lenguaje de marcado estánda...
# [1.9s] ¿Qué es CSS? → CSS es un lenguaje de hojas de estilo...
# [2.9s] ¿Qué es JavaScript? → JavaScript es un lenguaje de progra...
# [4.0s] ¿Qué es React? → React es una biblioteca de JavaScript...
# [5.1s] ¿Qué es Node.js? → Node.js es un entorno de ejecución...
#
# Tiempo total: 5.1s (esperado: ~5s con 1 req/s)
Explicación: Con requests_per_second=1, las llamadas se espacian a ~1 segundo entre cada una. El tiempo total refleja el rate limiting activo. Sin rate limiter, las 5 llamadas se enviarían casi simultáneamente.
Ejercicio 3: Tracker de costos por sesión (Medio)
Crea una clase CostTracker que acumule los tokens y costos de múltiples llamadas. Debe tener métodos track(response) y summary().
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
class CostTracker:
PRICING = {
"gpt-4.1-mini": {"input": 0.40 / 1_000_000, "output": 1.60 / 1_000_000},
"gpt-4.1": {"input": 2.00 / 1_000_000, "output": 8.00 / 1_000_000},
}
def __init__(self, model_name: str = "gpt-4.1-mini"):
self.model_name = model_name
self.total_input = 0
self.total_output = 0
self.call_count = 0
def track(self, response) -> None:
"""Registra los tokens de una respuesta."""
if response.usage_metadata:
self.total_input += response.usage_metadata["input_tokens"]
self.total_output += response.usage_metadata["output_tokens"]
self.call_count += 1
def summary(self) -> dict:
"""Retorna resumen de uso y costo."""
pricing = self.PRICING.get(self.model_name, {"input": 0, "output": 0})
cost = (
self.total_input * pricing["input"]
+ self.total_output * pricing["output"]
)
return {
"calls": self.call_count,
"input_tokens": self.total_input,
"output_tokens": self.total_output,
"total_tokens": self.total_input + self.total_output,
"cost_usd": cost,
}
tracker = CostTracker("gpt-4.1-mini")
model = init_chat_model("openai:gpt-4.1-mini")
preguntas = ["¿Qué es Git?", "¿Qué es Docker?", "¿Qué es Kubernetes?"]
for pregunta in preguntas:
response = model.invoke(pregunta)
tracker.track(response)
print(f"{pregunta} → {response.usage_metadata['total_tokens']} tokens")
s = tracker.summary()
print(f"\n--- Resumen ---")
print(f"Llamadas: {s['calls']}")
print(f"Total tokens: {s['total_tokens']}")
print(f"Costo estimado: ${s['cost_usd']:.6f} USD")
# Output esperado:
# ¿Qué es Git? → 48 tokens
# ¿Qué es Docker? → 52 tokens
# ¿Qué es Kubernetes? → 61 tokens
#
# --- Resumen ---
# Llamadas: 3
# Total tokens: 161
# Costo estimado: $0.000212 USD
Explicación: La clase encapsula la lógica de tracking. En producción, podrías persistir estos datos en una base de datos para análisis de costos por usuario, por feature, o por período de tiempo.
Ejercicio 4: Migrar código legacy a API moderna (Medio)
Dado el siguiente código legacy, reescríbelo usando las APIs modernas de LangChain v1.2+.
# Código legacy a migrar:
# from langchain.llms import OpenAI
# from langchain.chains import LLMChain
# from langchain.prompts import PromptTemplate
# from langchain.output_parsers import PydanticOutputParser
#
# llm = OpenAI(temperature=0.5)
# parser = PydanticOutputParser(pydantic_object=Recipe)
# prompt = PromptTemplate(
# template="Dame una receta de {dish}. {format_instructions}",
# input_variables=["dish"],
# partial_variables={"format_instructions": parser.get_format_instructions()}
# )
# chain = LLMChain(llm=llm, prompt=prompt)
# result = chain.run(dish="tacos")
# recipe = parser.parse(result)
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Recipe(BaseModel):
name: str = Field(description="Nombre de la receta")
ingredients: list[str] = Field(description="Lista de ingredientes")
steps: list[str] = Field(description="Pasos de preparación")
prep_time_minutes: int = Field(description="Tiempo de preparación en minutos")
model = init_chat_model("openai:gpt-4.1-mini", temperature=0.5)
structured_model = model.with_structured_output(Recipe)
recipe = structured_model.invoke("Dame una receta de tacos")
print(f"Receta: {recipe.name}")
print(f"Tiempo: {recipe.prep_time_minutes} minutos")
print(f"Ingredientes: {', '.join(recipe.ingredients)}")
for i, step in enumerate(recipe.steps, 1):
print(f" {i}. {step}")
# Output esperado:
# Receta: Tacos de carne asada
# Tiempo: 30 minutos
# Ingredientes: tortillas de maíz, carne de res, cebolla, cilantro, limón, sal
# 1. Marinar la carne con sal y limón por 15 minutos
# 2. Asar la carne a fuego alto por 3-4 minutos por lado
# 3. Picar la cebolla y el cilantro finamente
# 4. Cortar la carne en trozos pequeños
# 5. Calentar las tortillas
# 6. Servir la carne en las tortillas con cebolla, cilantro y limón
Explicación: El código legacy usaba 4 imports, un parser manual, format instructions inyectadas en el prompt, y un chain. El código moderno usa 2 imports, with_structured_output que maneja todo el parsing automáticamente, y invoke() directo. El resultado es tipado (Pydantic), no texto a parsear.
Ejercicio 5: Sistema robusto con rate limiting y tracking (Difícil)
Crea una función robust_batch_process que reciba una lista de preguntas y las procese con: rate limiting (2 req/s), tracking de tokens, y manejo de errores. Debe retornar un resumen con respuestas, tokens totales, y errores encontrados.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time
def robust_batch_process(
questions: list[str],
model_name: str = "openai:gpt-4.1-mini",
requests_per_second: float = 2,
) -> dict:
"""Procesa preguntas en batch con rate limiting, tracking y error handling."""
rate_limiter = InMemoryRateLimiter(requests_per_second=requests_per_second)
model = init_chat_model(model_name, rate_limiter=rate_limiter)
results = []
errors = []
total_input = 0
total_output = 0
start = time.time()
for i, question in enumerate(questions):
try:
response = model.invoke(question)
tokens = response.usage_metadata or {}
total_input += tokens.get("input_tokens", 0)
total_output += tokens.get("output_tokens", 0)
results.append({
"question": question,
"answer": response.content[:100],
"tokens": tokens.get("total_tokens", 0),
})
except Exception as e:
errors.append({
"question": question,
"error": str(e),
})
elapsed = time.time() - start
status = "OK" if not errors or errors[-1]["question"] != question else "ERROR"
print(f"[{elapsed:.1f}s] ({i+1}/{len(questions)}) {status}: {question[:40]}")
return {
"results": results,
"errors": errors,
"total_input_tokens": total_input,
"total_output_tokens": total_output,
"total_tokens": total_input + total_output,
"elapsed_seconds": time.time() - start,
"success_rate": len(results) / len(questions) * 100,
}
questions = [
"¿Qué es Python?",
"¿Qué es FastAPI?",
"¿Qué es SQLAlchemy?",
"¿Qué es Redis?",
"¿Qué es PostgreSQL?",
"¿Qué es Docker Compose?",
]
summary = robust_batch_process(questions)
print(f"\n--- Resumen ---")
print(f"Exitosas: {len(summary['results'])}/{len(questions)}")
print(f"Errores: {len(summary['errors'])}")
print(f"Total tokens: {summary['total_tokens']}")
print(f"Tiempo: {summary['elapsed_seconds']:.1f}s")
print(f"Tasa de éxito: {summary['success_rate']:.0f}%")
# Output esperado:
# [0.6s] (1/6) OK: ¿Qué es Python?
# [1.2s] (2/6) OK: ¿Qué es FastAPI?
# [1.7s] (3/6) OK: ¿Qué es SQLAlchemy?
# [2.3s] (4/6) OK: ¿Qué es Redis?
# [2.8s] (5/6) OK: ¿Qué es PostgreSQL?
# [3.4s] (6/6) OK: ¿Qué es Docker Compose?
#
# --- Resumen ---
# Exitosas: 6/6
# Errores: 0
# Total tokens: 312
# Tiempo: 3.4s
# Tasa de éxito: 100%
Explicación: Esta función combina tres patrones de producción: rate limiting para no exceder cuotas, tracking para monitorear costos, y try/except para que un error en una pregunta no detenga el procesamiento del batch completo. El resumen final da visibilidad total sobre la operación.
Resumen
En esta cápsula aprendiste:
- Ollama permite ejecutar modelos localmente — sin API keys, sin costos, sin internet
init_chat_model("ollama:llama3.2")se usa igual que cualquier proveedor cloud- Prompt caching reduce costos cuando repites el mismo prefijo de prompt en múltiples llamadas
- InMemoryRateLimiter controla la velocidad de llamadas para evitar errores
429 response.usage_metadatareporta tokens consumidos (input, output, total) por llamada- Las APIs legacy (
LLMChain,AgentExecutor,OutputParser) tienen equivalentes modernos en LangChain v1.2+ invoke()reemplaza chains,with_structured_outputreemplaza parsers,create_agentreemplazaAgentExecutor- En producción: siempre configura rate limiting y token tracking
Próxima cápsula: Proyecto — construirás un chat multi-proveedor con fallback automático que integra todo lo aprendido en este módulo: init_chat_model, streaming, structured output, rate limiting, y fallback a Ollama local.
Recursos adicionales
- Ollama Official Site - Instalación, modelos disponibles y documentación
- LangChain + Ollama Integration - Guía oficial de integración con Ollama
- Anthropic Prompt Caching - Documentación de caching en Anthropic
- OpenAI Rate Limits - Límites por tier y estrategias de manejo
- InMemoryRateLimiter API Reference - Referencia de la clase rate limiter
- LangChain Migration Guide - Guía oficial de migración de chains a LCEL
- OpenAI Pricing - Precios actuales por modelo para calcular costos
- Token Usage Tracking - How-to de tracking de tokens en LangChain
Módulo 1 — LangChain & LangGraph: From Chains to Agents