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

CriterioCloud (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

ProveedorEstadoNotas
Anthropic✅ Beta disponibleSe activa automáticamente con system prompts largos (>1024 tokens)
OpenAI✅ DisponibleAutomático en llamadas al mismo endpoint con prefijos repetidos
Google⚠️ ParcialSoporte de caching explícito con Context Caching API
Ollama❌ No aplicaModelos 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 OpenAIinit_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) + stateAgents con state management reemplazan conversation chains
OutputParsermodel.with_structured_output(Schema)Structured output es nativo — no necesitas parsear texto manualmente
from langchain.chat_models import ChatOpenAIinit_chat_model("openai:...")init_chat_model es universal, no necesitas clases por proveedor
CallbackHandlerSigue válido, pero middleware es preferidoMiddleware (@before_model, @after_model) es más potente (Módulo 4)
ChatPromptTemplate.from_messagesSigue válidoLos 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, InMemoryRateLimiter evita 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(), y with_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_metadata reporta 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_output reemplaza parsers, create_agent reemplaza AgentExecutor
  • 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

  1. Ollama Official Site - Instalación, modelos disponibles y documentación
  2. LangChain + Ollama Integration - Guía oficial de integración con Ollama
  3. Anthropic Prompt Caching - Documentación de caching en Anthropic
  4. OpenAI Rate Limits - Límites por tier y estrategias de manejo
  5. InMemoryRateLimiter API Reference - Referencia de la clase rate limiter
  6. LangChain Migration Guide - Guía oficial de migración de chains a LCEL
  7. OpenAI Pricing - Precios actuales por modelo para calcular costos
  8. Token Usage Tracking - How-to de tracking de tokens en LangChain

Módulo 1 — LangChain & LangGraph: From Chains to Agents