Módulo 4: Middleware y Customización
Tu Primer Middleware: Logging y Monitoreo
Descripción de la cápsula
Antes de interceptar y modificar el comportamiento de un agente, necesitas observar lo que hace. ¿Cuántas veces llama al modelo? ¿Qué mensajes le envía? ¿Cuánto tarda cada llamada? ¿Cuántos tokens consume? Sin visibilidad, estás trabajando a ciegas.
En esta cápsula aprenderás los dos hooks más simples del sistema de middleware: before_model y after_model. El primero se ejecuta justo antes de cada llamada al LLM; el segundo, justo después. Juntos te dan visibilidad completa sobre la interacción agente-modelo. Construirás middleware de logging básico, timing, conteo de tokens, y verás cómo se compara con el sistema de callbacks de LangChain.
Al terminar, serás capaz de agregar observabilidad a cualquier agente existente sin modificar una sola línea de su código.
before_model: interceptar antes de la llamada
before_model es una función que se ejecuta cada vez que el agente va a enviar mensajes al modelo. Recibe la lista de mensajes que está a punto de enviarse.
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': LangChain es un framework para LLMs."
def before_model(messages):
print(f"[BEFORE] Enviando {len(messages)} mensajes al modelo")
agent = create_agent(
"openai:gpt-4.1-mini",
tools=[search],
before_model=before_model
)
result = agent.invoke({"messages": [("user", "¿Qué es LangChain?")]})
print(result["messages"][-1].content)
# Output esperado:
# [BEFORE] Enviando 2 mensajes al modelo
# [BEFORE] Enviando 4 mensajes al modelo
# LangChain es un framework para construir aplicaciones con LLMs.
¿Por qué aparece [BEFORE] dos veces? Porque el agente hizo dos rondas del loop ReAct:
- Primera ronda: El agente envía 2 mensajes (system prompt + human message). El modelo decide llamar a
search. - Segunda ronda: El agente envía 4 mensajes (system prompt + human message + AI message con tool_call + ToolMessage con resultado). El modelo genera la respuesta final.
Cada vez que el agente va a llamar al modelo, before_model se ejecuta primero.
Inspeccionar los mensajes
También puedes iterar sobre los mensajes para ver exactamente qué recibe el modelo:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
def before_model(messages):
print(f"\n[BEFORE] {len(messages)} mensajes para el modelo:")
for msg in messages:
msg_type = type(msg).__name__
if hasattr(msg, "tool_calls") and msg.tool_calls:
print(f" {msg_type}: tool_calls={[tc['name'] for tc in msg.tool_calls]}")
else:
print(f" {msg_type}: {str(msg.content)[:80]}")
agent = create_agent("openai:gpt-4.1-mini", tools=[calculator], before_model=before_model)
result = agent.invoke({"messages": [("user", "¿Cuánto es 15 * 37?")]})
print(f"\nRespuesta: {result['messages'][-1].content}")
# Output esperado:
# [BEFORE] 2 mensajes para el modelo:
# SystemMessage: You are a helpful assistant.
# HumanMessage: ¿Cuánto es 15 * 37?
#
# [BEFORE] 4 mensajes para el modelo:
# SystemMessage: You are a helpful assistant.
# HumanMessage: ¿Cuánto es 15 * 37?
# AIMessage: tool_calls=['calculator']
# ToolMessage: 555
#
# Respuesta: 15 × 37 = 555.
Esto es invaluable para debugging: si el agente se comporta de forma inesperada, before_model te muestra exactamente qué información tenía disponible.
after_model: interceptar después de la respuesta
after_model se ejecuta cada vez que el modelo retorna una respuesta, antes de que el agente procese esa respuesta (ejecutar tools o devolver el resultado final).
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': Python fue creado por Guido van Rossum en 1991."
def after_model(response):
print(f"[AFTER] Tipo de respuesta: {type(response).__name__}")
if response.tool_calls:
for tc in response.tool_calls:
print(f" → Tool call: {tc['name']}({tc['args']})")
else:
print(f" → Respuesta final: {str(response.content)[:100]}")
agent = create_agent(
"openai:gpt-4.1-mini",
tools=[search],
after_model=after_model
)
result = agent.invoke({"messages": [("user", "¿Quién creó Python?")]})
# Output esperado:
# [AFTER] Tipo de respuesta: AIMessage
# → Tool call: search({'query': 'quién creó Python'})
# [AFTER] Tipo de respuesta: AIMessage
# → Respuesta final: Python fue creado por Guido van Rossum en 1991.
after_model te permite distinguir entre dos situaciones:
- El modelo decidió llamar una tool →
response.tool_callstiene contenido - El modelo decidió responder directamente →
response.contenttiene la respuesta final
Combinando before_model y after_model
Juntos, before_model y after_model te dan visibilidad completa del ciclo request-response entre el agente y el modelo:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': LangChain fue creado por Harrison Chase en 2022."
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
call_number = 0
def before_model(messages):
global call_number
call_number += 1
last_msg = messages[-1]
print(f"\n--- Llamada #{call_number}: {len(messages)} msgs, "
f"último={type(last_msg).__name__}: {str(last_msg.content)[:60]} ---")
def after_model(response):
if response.tool_calls:
print(f" → tool_calls: {[tc['name'] for tc in response.tool_calls]}")
else:
print(f" → respuesta final ({len(response.content)} chars)")
agent = create_agent(
"openai:gpt-4.1-mini",
tools=[search, calculator],
before_model=before_model,
after_model=after_model
)
result = agent.invoke({
"messages": [("user", "¿Quién creó LangChain y cuánto es 2**16?")]
})
print(f"\nRespuesta: {result['messages'][-1].content}")
# Output esperado:
# --- Llamada #1: 2 msgs, último=HumanMessage: ¿Quién creó LangChain... ---
# → tool_calls: ['search', 'calculator']
# --- Llamada #2: 5 msgs, último=ToolMessage: 65536 ---
# → respuesta final (85 chars)
# Respuesta: LangChain fue creado por Harrison Chase en 2022. Y 2^16 = 65,536.
Ahora puedes rastrear cada interacción: cuántas llamadas hizo el agente, qué recibió cada vez, y qué decidió hacer.
Middleware de timing: medir latencia
Una de las métricas más útiles en producción es cuánto tarda cada llamada al modelo. Con before_model y after_model, puedes medirlo:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
import time
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': FastAPI es un framework web moderno para Python."
call_times = {}
def before_model(messages):
call_times["start"] = time.time()
def after_model(response):
elapsed = time.time() - call_times["start"]
action = "tool_calls" if response.tool_calls else "respuesta final"
print(f"[TIMER] {elapsed:.2f}s → {action}")
agent = create_agent(
"openai:gpt-4.1-mini", tools=[search],
before_model=before_model, after_model=after_model
)
result = agent.invoke({"messages": [("user", "¿Qué es FastAPI?")]})
print(f"Respuesta: {result['messages'][-1].content}")
# Output esperado:
# [TIMER] 0.83s → tool_calls
# [TIMER] 0.65s → respuesta final
# Respuesta: FastAPI es un framework web moderno para Python...
Este patrón es la base del monitoreo en producción. Las métricas que recopilas aquí son las mismas que plataformas como LangSmith o LangFuse rastrean automáticamente. En los ejercicios al final verás una versión con acumulador que reporta totales y promedios.
Middleware de token counting
Si necesitas controlar costos, after_model puede leer usage_metadata de la respuesta para acumular tokens:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': LangGraph permite crear workflows con grafos."
token_usage = {"total_input": 0, "total_output": 0, "calls": 0}
def after_model(response):
token_usage["calls"] += 1
if hasattr(response, "usage_metadata") and response.usage_metadata:
usage = response.usage_metadata
token_usage["total_input"] += usage.get("input_tokens", 0)
token_usage["total_output"] += usage.get("output_tokens", 0)
print(f"[TOKENS] Llamada {token_usage['calls']}: "
f"input={usage.get('input_tokens', 0)}, output={usage.get('output_tokens', 0)}")
agent = create_agent("openai:gpt-4.1-mini", tools=[search], after_model=after_model)
result = agent.invoke({"messages": [("user", "¿Qué es LangGraph?")]})
total = token_usage["total_input"] + token_usage["total_output"]
print(f"\nTotal: {total} tokens en {token_usage['calls']} llamadas")
print(f"Respuesta: {result['messages'][-1].content}")
# Output esperado:
# [TOKENS] Llamada 1: input=85, output=22
# [TOKENS] Llamada 2: input=130, output=45
# Total: 282 tokens en 2 llamadas
⚠️ Nota: usage_metadata depende del proveedor. OpenAI siempre lo incluye. Anthropic lo incluye si habilitas el tracking. Otros proveedores pueden no soportarlo.
El sistema de callbacks: enfoque alternativo
LangChain tiene otro mecanismo para observar lo que pasa internamente: callbacks. Los callbacks existían antes del middleware system y siguen siendo útiles para ciertos casos.
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.callbacks import BaseCallbackHandler
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': Python es el lenguaje más popular de 2025."
class LoggingCallback(BaseCallbackHandler):
def on_llm_start(self, serialized, prompts, **kwargs):
print("[CALLBACK] LLM iniciando...")
def on_llm_end(self, response, **kwargs):
print("[CALLBACK] LLM terminó")
def on_tool_start(self, serialized, input_str, **kwargs):
print(f"[CALLBACK] Tool: {serialized.get('name', '?')}")
def on_tool_end(self, output, **kwargs):
print(f"[CALLBACK] Tool completada")
agent = create_agent("openai:gpt-4.1-mini", tools=[search])
result = agent.invoke(
{"messages": [("user", "¿Cuál es el lenguaje más popular?")]},
config={"callbacks": [LoggingCallback()]}
)
print(f"Respuesta: {result['messages'][-1].content}")
# Output: [CALLBACK] LLM iniciando... → terminó → Tool: search → completada → LLM iniciando... → terminó
Middleware vs callbacks: ¿cuándo usar cada uno?
| Aspecto | Middleware (before_model/after_model) | Callbacks (BaseCallbackHandler) |
|---|---|---|
| Alcance | Específico del agente | Cualquier componente de LangChain |
| Acceso a datos | Mensajes completos y respuesta tipada | Datos serializados y genéricos |
| Modificación | Puede modificar mensajes y respuestas | Solo observación (read-only) |
| Configuración | Parámetro de create_agent | Se pasa en config al invoke |
| Ideal para | Middleware activo (routing, retry, filtering) | Logging pasivo, tracing, integración con plataformas |
Regla práctica: Usa middleware cuando necesites modificar el comportamiento. Usa callbacks cuando solo necesites observar desde fuera. Para logging simple, ambos funcionan — middleware es más directo.
Cómo se pasan los hooks a create_agent
Los middleware hooks se pasan como keyword arguments a create_agent. Puedes usar uno, el otro, o ambos — son opcionales e independientes:
# Solo before_model
agent = create_agent("openai:gpt-4.1-mini", tools=tools, before_model=my_before)
# Solo after_model
agent = create_agent("openai:gpt-4.1-mini", tools=tools, after_model=my_after)
# Ambos
agent = create_agent("openai:gpt-4.1-mini", tools=tools, before_model=my_before, after_model=my_after)
Orden de ejecución
Cuando usas ambos hooks, la secuencia para cada llamada al modelo es:
1. before_model(messages) ← tú interceptas antes
2. model.invoke(messages) ← el modelo procesa
3. after_model(response) ← tú interceptas después
Este ciclo se repite por cada iteración del loop ReAct. Si el agente hace 3 llamadas al modelo, before_model y after_model se ejecutan 3 veces cada uno.
Troubleshooting
Problema 1: "TypeError: before_model() takes 0 positional arguments but 1 was given"
Síntoma: Error al invocar el agente.
Causa: Tu función before_model no acepta el parámetro messages.
Solución: before_model siempre recibe un argumento — la lista de mensajes:
# Mal — no acepta argumentos
def before_model():
print("Antes del modelo")
# Bien — acepta messages
def before_model(messages):
print(f"Antes del modelo: {len(messages)} mensajes")
Problema 2: "TypeError: after_model() takes 0 positional arguments but 1 was given"
Síntoma: Error al invocar el agente.
Causa: Tu función after_model no acepta el parámetro response.
Solución: after_model siempre recibe un argumento — la respuesta del modelo:
# Mal
def after_model():
print("Después del modelo")
# Bien
def after_model(response):
print(f"Después del modelo: {len(response.content)} chars")
Problema 3: El middleware no imprime nada
Síntoma: El agente funciona, pero los print() del middleware no aparecen.
Causa: El middleware no está conectado al agente. Probablemente pasaste los hooks con el nombre incorrecto o no los pasaste.
Solución: Verifica que usas los nombres exactos como keyword arguments:
# Mal — nombres incorrectos
agent = create_agent(model, tools, pre_model=fn, post_model=fn)
# Bien — nombres correctos
agent = create_agent(model, tools, before_model=fn, after_model=fn)
Problema 4: El timing muestra 0.00s
Síntoma: Las mediciones de tiempo siempre muestran 0.00 segundos.
Causa: Estás midiendo el tiempo incorrecto, probablemente creando un nuevo timestamp en after_model en vez de usar el de before_model.
Solución: Guarda el timestamp en una variable compartida entre ambos hooks:
import time
state = {"start": None}
def before_model(messages):
state["start"] = time.time()
def after_model(response):
elapsed = time.time() - state["start"]
print(f"Duración: {elapsed:.2f}s")
Problema 5: usage_metadata es None
Síntoma: response.usage_metadata retorna None o no existe.
Causa: El proveedor no incluye metadata de tokens, o el atributo no existe en tu versión.
Solución: Verifica con hasattr(response, "usage_metadata") and response.usage_metadata antes de acceder. OpenAI siempre lo incluye; otros proveedores pueden no hacerlo.
Ejercicios
Ejercicio 1: Logging básico con before_model (Fácil)
Crea un agente con una tool get_weather y un before_model que imprima cuántos mensajes se envían al modelo en cada llamada. Invoca el agente con "¿Qué clima hace en Tokyo?" y verifica que el log aparece antes de cada respuesta del modelo.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima actual de una ciudad."""
return f"El clima en {city} es lluvioso, 15°C, humedad 80%"
def before_model(messages):
print(f"[LOG] Enviando {len(messages)} mensajes al modelo")
agent = create_agent(
"openai:gpt-4.1-mini",
tools=[get_weather],
before_model=before_model
)
result = agent.invoke({"messages": [("user", "¿Qué clima hace en Tokyo?")]})
print(f"Respuesta: {result['messages'][-1].content}")
# Output esperado:
# [LOG] Enviando 2 mensajes al modelo
# [LOG] Enviando 4 mensajes al modelo
# Respuesta: El clima en Tokyo es lluvioso, con 15°C y 80% de humedad.
Explicación: before_model se ejecuta dos veces: una antes de la llamada que genera el tool call, y otra antes de la llamada que genera la respuesta final con el resultado de la tool.
Ejercicio 2: Detectar tool calls con after_model (Fácil)
Crea un after_model que imprima si el modelo decidió llamar tools o responder directamente. Prueba con dos preguntas: una que necesite tools y una que no.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""Calcula una expresión matemática."""
return str(eval(expression))
def after_model(response):
if response.tool_calls:
tools = [tc["name"] for tc in response.tool_calls]
print(f"[DECISION] Modelo llamó tools: {tools}")
else:
print(f"[DECISION] Modelo respondió directamente ({len(response.content)} chars)")
agent = create_agent(
"openai:gpt-4.1-mini",
tools=[calculator],
after_model=after_model
)
print("--- Pregunta que necesita tool ---")
r1 = agent.invoke({"messages": [("user", "¿Cuánto es 99 * 77?")]})
print(f"R: {r1['messages'][-1].content}\n")
print("--- Pregunta que NO necesita tool ---")
r2 = agent.invoke({"messages": [("user", "¿Qué es una variable?")]})
print(f"R: {r2['messages'][-1].content}")
# Output esperado:
# --- Pregunta que necesita tool ---
# [DECISION] Modelo llamó tools: ['calculator']
# [DECISION] Modelo respondió directamente (18 chars)
# R: 99 × 77 = 7,623.
#
# --- Pregunta que NO necesita tool ---
# [DECISION] Modelo respondió directamente (85 chars)
# R: Una variable es un espacio en memoria que almacena un valor...
Explicación: Con tool, after_model se ejecuta dos veces: primero detecta el tool call, después detecta la respuesta final. Sin tool, se ejecuta solo una vez con la respuesta directa.
Ejercicio 3: Middleware de timing completo (Medio)
Crea un middleware que mida el tiempo de cada llamada al modelo y al final imprima un resumen con tiempo total y promedio por llamada.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
import time
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': Docker es una plataforma de contenedores."
timing = {"start": None, "durations": []}
def before_model(messages):
timing["start"] = time.time()
def after_model(response):
elapsed = time.time() - timing["start"]
timing["durations"].append(elapsed)
action = "tools" if response.tool_calls else "respuesta"
print(f"[TIMER] Llamada {len(timing['durations'])}: {elapsed:.3f}s ({action})")
agent = create_agent(
"openai:gpt-4.1-mini", tools=[search],
before_model=before_model, after_model=after_model
)
result = agent.invoke({"messages": [("user", "¿Qué es Docker?")]})
total = sum(timing["durations"])
avg = total / len(timing["durations"])
print(f"\nTotal: {total:.3f}s | Promedio: {avg:.3f}s | Llamadas: {len(timing['durations'])}")
print(f"Respuesta: {result['messages'][-1].content}")
# Output esperado:
# [TIMER] Llamada 1: 0.872s (tools)
# [TIMER] Llamada 2: 0.641s (respuesta)
# Total: 1.513s | Promedio: 0.757s | Llamadas: 2
Explicación: before_model guarda el timestamp de inicio. after_model calcula el delta y lo acumula. Al final, calculamos totales y promedios.
Ejercicio 4: Contar tokens acumulados (Medio)
Crea un after_model que acumule tokens de input y output a lo largo de toda la conversación. Imprime el acumulado después de cada llamada y el total al final.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': Kubernetes orquesta contenedores en clusters."
tokens = {"input": 0, "output": 0, "calls": 0}
def after_model(response):
tokens["calls"] += 1
if hasattr(response, "usage_metadata") and response.usage_metadata:
inp = response.usage_metadata.get("input_tokens", 0)
out = response.usage_metadata.get("output_tokens", 0)
tokens["input"] += inp
tokens["output"] += out
print(f"[TOKENS] Llamada {tokens['calls']}: +{inp} in, +{out} out "
f"(total: {tokens['input'] + tokens['output']})")
agent = create_agent("openai:gpt-4.1-mini", tools=[search], after_model=after_model)
result = agent.invoke({"messages": [("user", "¿Qué es Kubernetes?")]})
print(f"\nTotal: {tokens['input'] + tokens['output']} tokens en {tokens['calls']} llamadas")
print(f"Respuesta: {result['messages'][-1].content}")
# Output esperado:
# [TOKENS] Llamada 1: +85 in, +20 out (total: 105)
# [TOKENS] Llamada 2: +128 in, +52 out (total: 285)
# Total: 285 tokens en 2 llamadas
Explicación: El input crece en la segunda llamada porque incluye todos los mensajes previos (system prompt + user + AI tool_call + ToolMessage).
Ejercicio 5: Logging con callbacks (Medio)
Implementa logging usando el sistema de callbacks con BaseCallbackHandler. Crea un handler que loggee inicio/fin de cada llamada al LLM y cada ejecución de tool. Pasa el handler vía config={"callbacks": [handler]}.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.callbacks import BaseCallbackHandler
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': Redis es una base de datos in-memory."
class DetailedLogger(BaseCallbackHandler):
def __init__(self):
self.llm_calls = 0
self.tool_calls = 0
def on_llm_start(self, serialized, prompts, **kwargs):
self.llm_calls += 1
print(f"[CB] LLM llamada #{self.llm_calls} iniciada")
def on_llm_end(self, response, **kwargs):
print(f"[CB] LLM llamada #{self.llm_calls} completada")
def on_tool_start(self, serialized, input_str, **kwargs):
self.tool_calls += 1
print(f"[CB] Tool '{serialized.get('name', '?')}' ejecutándose...")
def on_tool_end(self, output, **kwargs):
print(f"[CB] Tool completada: {str(output)[:60]}")
logger = DetailedLogger()
agent = create_agent("openai:gpt-4.1-mini", tools=[search])
result = agent.invoke(
{"messages": [("user", "¿Qué es Redis?")]},
config={"callbacks": [logger]}
)
print(f"\nResumen: {logger.llm_calls} llamadas LLM, {logger.tool_calls} tool calls")
print(f"Respuesta: {result['messages'][-1].content}")
# Output esperado:
# [CB] LLM llamada #1 iniciada → completada → Tool 'search' → completada
# [CB] LLM llamada #2 iniciada → completada
# Resumen: 2 llamadas LLM, 1 tool calls
Explicación: Los callbacks se pasan en config, no como parámetros de create_agent. Capturan eventos de todo el pipeline (LLM + tools).
Ejercicio 6: Factory de middleware configurables (Avanzado)
Crea una función create_monitor(name, verbose) que retorne un par (before_fn, after_fn). Con verbose=True, imprime los últimos 2 mensajes y tool calls. Con verbose=False, solo imprime número de llamada y duración. Crea dos agentes con diferente nivel de detalle.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
import time
@tool
def search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados para '{query}': GraphQL es un lenguaje de consultas para APIs."
def create_monitor(name, verbose=False):
state = {"start": None, "num": 0}
def before_fn(messages):
state["num"] += 1
state["start"] = time.time()
if verbose:
print(f"[{name}] Llamada #{state['num']}: {len(messages)} mensajes")
for msg in messages[-2:]:
print(f" └─ {type(msg).__name__}: {str(msg.content)[:60]}")
else:
print(f"[{name}] #{state['num']}...", end=" ")
def after_fn(response):
elapsed = time.time() - state["start"]
action = "tools" if response.tool_calls else "respuesta"
if verbose:
print(f"[{name}] → {action} ({elapsed:.2f}s)")
else:
print(f"{'T' if response.tool_calls else 'R'} {elapsed:.2f}s")
return before_fn, after_fn
before_v, after_v = create_monitor("VERBOSE", verbose=True)
agent_v = create_agent("openai:gpt-4.1-mini", tools=[search], before_model=before_v, after_model=after_v)
before_q, after_q = create_monitor("QUIET", verbose=False)
agent_q = create_agent("openai:gpt-4.1-mini", tools=[search], before_model=before_q, after_model=after_q)
print("=== Verbose ===")
r1 = agent_v.invoke({"messages": [("user", "¿Qué es GraphQL?")]})
print(f"R: {r1['messages'][-1].content}\n")
print("=== Quiet ===")
r2 = agent_q.invoke({"messages": [("user", "¿Qué es GraphQL?")]})
print(f"R: {r2['messages'][-1].content}")
# Output esperado: Verbose muestra mensajes detallados; Quiet solo "#1... T 0.80s"
Explicación: La función factory encapsula estado y lógica en un closure. Cada instancia tiene su propio contador y configuración, permitiendo reutilizar el patrón en múltiples agentes.
Resumen
En esta cápsula aprendiste:
before_model(messages)se ejecuta antes de cada llamada al LLM — recibe la lista completa de mensajes que el modelo va a procesarafter_model(response)se ejecuta después de cada respuesta del LLM — recibe elAIMessagecon la respuesta o los tool calls- Juntos te dan visibilidad completa del ciclo request-response entre el agente y el modelo
- Se pasan como keyword arguments a
create_agent:before_model=fn,after_model=fn— son opcionales e independientes - El middleware de timing usa
time.time()enbefore_modely calcula el delta enafter_model, guardando el timestamp en una variable compartida - El middleware de token counting lee
response.usage_metadataenafter_modelpara acumular tokens de input y output (depende del proveedor) - El sistema de callbacks (
BaseCallbackHandler) es una alternativa para logging pasivo — captura eventos de todo el pipeline, no solo del modelo - Diferencia clave: middleware puede modificar comportamiento; callbacks solo observan
- Las funciones factory (
create_monitor(name, verbose)) permiten crear middleware configurables y reutilizables
Próxima cápsula: @wrap_model_call: Interceptar Llamadas al Modelo — aprenderás el hook más potente del middleware system, que te permite modificar los requests antes de enviarlos al modelo y transformar las respuestas después.
Recursos adicionales
- create_agent API Reference — Documentación completa de
before_modelyafter_modelcomo parámetros - LangChain Agents Overview — Guía conceptual de agentes y middleware
- LangChain Callbacks Documentation — Sistema de callbacks para logging y tracing
- BaseCallbackHandler API — Referencia de todos los eventos que puedes capturar con callbacks
- Token Usage Tracking — LangChain — Cómo rastrear consumo de tokens por proveedor
- LangSmith Overview — Plataforma de observabilidad que automatiza el logging que construiste manualmente aquí
- Python Closures — Real Python — Referencia para el patrón factory usado en middleware reutilizables
- time.time() vs time.perf_counter() — Para mediciones de timing más precisas, considera
perf_counter
Módulo 4 — LangChain & LangGraph: From Chains to Agents