AI Agent Engineer · Módulo 4
LangChain
El orquestador de código abierto más extendido de la industria: el contrato Runnable, cadenas con LCEL, tool calling, agentes con create_agent, memoria con checkpointers y cómo se traduce todo lo aprendido con Semantic Kernel.
25 min de lectura
Objetivo del módulo
El Módulo 3 cerró con una afirmación que ahora toca demostrar: aprender un orquestador a fondo hace que el segundo se entienda en una tarde, porque los conceptos son los mismos con distinto vocabulario. Este módulo es esa tarde.
LangChain es el framework de orquestación de LLMs más usado del ecosistema de código abierto, y eso lo convierte en conocimiento obligatorio por cuatro razones prácticas. Primera: la mayoría de artículos, ejemplos y repositorios que encontrarás al investigar cualquier técnica — RAG, agentes, evaluación — están escritos en LangChain, y necesitas leerlos con fluidez aunque tu proyecto use otro framework. Segunda: su catálogo de integraciones es el más grande que existe, con conectores mantenidos para cientos de modelos, bases vectoriales y servicios externos. Tercera: sobre sus abstracciones se apoya LangGraph, el runtime de grafos que domina la orquestación multiagente y que el Módulo 8 usa en profundidad. Cuarta: es el vocabulario común de la industria; en cualquier equipo o entrevista de AI Engineering se da por sabido.
El objetivo no es repetir el Módulo 3 con otra sintaxis. Es conseguir dos cosas más valiosas: consolidar los conceptos viéndolos con otra ropa — si algo solo se entendió en la sintaxis de Semantic Kernel, en realidad no se entendió — y construir el criterio para elegir framework por proyecto, en lugar de por inercia.
Advertencia honesta antes de empezar: LangChain arrastra fama de romper su API entre versiones, y durante años fue merecida. La versión 1.0 (octubre de 2025) estabilizó el núcleo y reorganizó el paquete principal alrededor de una sola pieza de alto nivel: create_agent. La consecuencia práctica es que internet está lleno de tutoriales de la era anterior. Si el código que encuentres usa LLMChain, initialize_agent, AgentExecutor o ConversationBufferMemory, describe una API obsoleta: los conceptos siguen valiendo, la sintaxis no. Este módulo está escrito sobre LangChain 1.x; ante cualquier discrepancia de detalle, la referencia es la documentación oficial en python.langchain.com.
El ecosistema: un mapa antes de instalar nada
Tu experiencia con SQL te da la analogía exacta. Cuando conectas una aplicación a una base de datos no escribes código específico del fabricante por toda la aplicación: programas contra un estándar (ODBC, JDBC) y un driver implementa ese estándar para SQL Server, para PostgreSQL o para Oracle. Cambiar de base de datos es cambiar el driver, no reescribir la aplicación.
LangChain está organizado igual, y por eso no es una biblioteca sino una familia de paquetes:
┌────────────────────────────────────────────────────────────┐
│ Tu aplicación │
├────────────────────────────────────────────────────────────┤
│ langchain → la pieza de alto nivel: create_agent │
│ langgraph → runtime de grafos, estado, memoria │
├────────────────────────────────────────────────────────────┤
│ langchain-core → los contratos: Runnable, mensajes, │
│ prompts, tools, callbacks │
├──────────────────┬──────────────────┬──────────────────────┤
│ langchain-openai │ langchain- │ langchain-community, │
│ (OpenAI y Azure) │ anthropic │ cientos más... │
│ drivers de integración: implementan los contratos │
└──────────────────┴──────────────────┴──────────────────────┘
LangSmith → observabilidad, transversal a todoLa capa que da estabilidad al conjunto es langchain-core: define las interfaces — qué es un modelo de chat, qué es un mensaje, qué es una herramienta — y cambia despacio. Los paquetes de integración son los drivers: implementan esas interfaces para cada proveedor y evolucionan deprisa sin arrastrar a tu aplicación. langchain es la capa de alto nivel que une todo, y langgraph es el motor que ejecuta agentes con estado; aparecerá en este módulo por los checkpointers y será protagonista en el Módulo 8.
La instalación para todo el curso:
pip install langchain langchain-openai langgraphEl paquete langchain-openai cubre tanto OpenAI directo como Azure OpenAI. Es el driver que usarás durante todo el curso, contra el deployment que creaste en el Módulo 2.
Pregunta: ¿qué gana el ecosistema con que langchain-core sea un paquete separado de los drivers de integración?
Lo mismo que gana el mundo de las bases de datos con ODBC: tu aplicación programa contra contratos estables (qué es un modelo, un mensaje, una herramienta) y no contra un proveedor concreto. Cambiar de OpenAI a Anthropic, o de Azure a local, es cambiar el driver y una línea de construcción del modelo; las cadenas, herramientas y agentes no se tocan. Además aísla la velocidad de cambio: los drivers pueden publicar versiones cada semana siguiendo las novedades de cada proveedor, mientras los contratos del núcleo se mantienen estables durante años.
Modelos y mensajes
El punto de entrada es el modelo de chat. Con Azure OpenAI, el driver es la clase AzureChatOpenAI:
from langchain_openai import AzureChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
model = AzureChatOpenAI(
azure_deployment="gpt-4o", # el nombre de TU deployment, no del modelo
api_version="2024-10-21",
temperature=0.2,
# Lee AZURE_OPENAI_ENDPOINT y AZURE_OPENAI_API_KEY del entorno
)
respuesta = model.invoke([
SystemMessage(content="Eres un asistente interno de la empresa. Responde en una frase."),
HumanMessage(content="¿Quién aprueba las solicitudes de vacaciones?"),
])
print(respuesta.content)
print(respuesta.usage_metadata)
# {'input_tokens': 31, 'output_tokens': 18, 'total_tokens': 49}Los roles del Módulo 1 — system, user, assistant, tool — aparecen aquí como clases: SystemMessage, HumanMessage, AIMessage y ToolMessage. El modelo devuelve siempre un AIMessage, que además del texto (content) trae metadatos útiles: usage_metadata con el desglose de tokens es el que conviene mirar desde el primer día, porque conecta directamente con la economía de contexto del Módulo 1. Donde Semantic Kernel te daba un objeto ChatHistory que gestionaba los roles por ti, LangChain trabaja con la lista de mensajes desnuda: más explícito, menos envoltorio.
En producción sobre Azure, la recomendación del Módulo 2 sigue vigente: nada de claves de API, autenticación con Entra ID. El driver lo admite directamente:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
token_provider = get_bearer_token_provider(
DefaultAzureCredential(),
"https://cognitiveservices.azure.com/.default",
)
model = AzureChatOpenAI(
azure_deployment="gpt-4o",
api_version="2024-10-21",
azure_ad_token_provider=token_provider, # sin claves, como en el Módulo 2
)Existe también una fábrica agnóstica del proveedor, útil cuando la aplicación debe poder cambiar de modelo por configuración:
from langchain.chat_models import init_chat_model
modelo = init_chat_model("openai:gpt-4o") # el prefijo elige el driver
otro = init_chat_model("anthropic:claude-sonnet-4-5")Es la materialización del argumento ODBC: mismo contrato, distinto driver, una sola línea de diferencia.
Runnables y LCEL: todo tiene el mismo enchufe
En una terminal encadenas comandos que no saben nada unos de otros:
ps aux | grep python | wc -lFunciona porque los tres hablan el mismo contrato — texto entra, texto sale — y la tubería solo conecta salidas con entradas. LangChain aplica la misma idea a las piezas de una aplicación de IA, y al contrato lo llama Runnable: todo objeto que expone las mismas cuatro operaciones.
El contrato Runnable
────────────────────
invoke(entrada) → una ejecución, una salida
batch([entradas]) → varias ejecuciones en paralelo
stream(entrada) → la salida a trozos, según se genera
ainvoke / abatch / astream → las mismas tres, asíncronasModelos, plantillas de prompt, parsers de salida, herramientas y agentes: todos son runnables. Y como todos tienen el mismo enchufe, se componen con el operador de tubería. A esta forma de componer se la llama LCEL (LangChain Expression Language), aunque el nombre es más solemne que la idea:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "Eres el asistente de soporte interno de {empresa}."),
("human", "Resume en una sola línea el siguiente ticket:\n\n{ticket}"),
])
cadena = prompt | model | StrOutputParser()
resumen = cadena.invoke({
"empresa": "Contoso",
"ticket": "No puedo entrar a SharePoint desde ayer. Error 403 al abrir el sitio de Finanzas, ya probé con otro navegador y nada.",
})
print(resumen)La plantilla rellena los huecos y produce mensajes; el modelo produce un AIMessage; el parser extrae el texto plano. Tres piezas, dos tuberías, y la cadena resultante es a su vez un runnable: se puede invocar, encadenar dentro de otra cadena mayor o servir por streaming.
El contrato se paga solo en cuanto hay volumen. Si resumir un ticket tarda 2 segundos, procesar 100 tickets en un bucle secuencial cuesta 200 segundos; con batch, que lanza las llamadas en paralelo (concurrencia por defecto razonable, configurable), el mismo trabajo con concurrencia 10 baja a unos 20 segundos sin cambiar una línea de la cadena:
resumenes = cadena.batch([
{"empresa": "Contoso", "ticket": t} for t in tickets # 100 tickets
])Salida estructurada: el mismo JSON Schema con menos ceremonia
El Módulo 1 presentó las salidas estructuradas como decodificación restringida guiada por un JSON Schema. LangChain empaqueta exactamente eso detrás de una clase Pydantic:
from pydantic import BaseModel, Field
class TicketClasificado(BaseModel):
"""Clasificación de un ticket de soporte interno."""
categoria: str = Field(description="Una de: accesos, hardware, software, rrhh")
urgencia: int = Field(ge=1, le=5, description="1 = puede esperar, 5 = bloquea el trabajo")
resumen: str = Field(description="Una sola línea, máximo 15 palabras")
clasificador = model.with_structured_output(TicketClasificado)
resultado = clasificador.invoke(
"No puedo entrar a SharePoint y tengo que presentar resultados en una hora."
)
print(resultado.urgencia) # un int de verdad, ya validado
print(resultado.categoria) # "accesos"No hay magia nueva: la clase Pydantic se convierte en el JSON Schema del Módulo 1, el modelo genera respetándolo y la respuesta vuelve validada y tipada. La diferencia con hacerlo a mano es la ceremonia que desaparece: ni construir el schema, ni parsear el JSON, ni validar los rangos.
Cuándo basta una cadena y cuándo hace falta un agente
Una cadena es una tubería fija: los pasos y su orden los decides tú en tiempo de diseño, y el modelo solo rellena contenido dentro de cada paso. Un agente invierte el control: el modelo decide en tiempo de ejecución qué paso viene después. El criterio del Módulo 3 no cambia con el framework: si el flujo se puede dibujar de antemano — clasificar, luego enrutar, luego formatear — usa una cadena, que es más barata, más rápida y más predecible; si el camino depende de cada pregunta, toca agente.
Pregunta: tienes que (a) responder en una interfaz de chat donde el usuario espera mirando la pantalla y (b) clasificar cada noche 500 tickets acumulados. ¿Qué operación del contrato Runnable usa cada caso y por qué?
El chat usa stream: el usuario percibe la latencia del primer token, no la del último, así que pintar la respuesta según se genera transforma la experiencia aunque el tiempo total sea el mismo — es la fase de decode del Módulo 1 hecha visible. El trabajo nocturno usa batch: nadie está mirando, lo que importa es el rendimiento total, y el paralelismo divide el tiempo de pared por la concurrencia. invoke queda para el caso simple: una entrada, una salida, sin nadie esperando en vivo.
Tool calling: el bucle del Módulo 1 con otro vocabulario
En el Módulo 3, una función se exponía al modelo con el decorador @kernel_function dentro de un plugin. En LangChain la unidad es más pequeña — la función suelta — y el decorador es @tool:
from langchain_core.tools import tool
@tool
def dias_vacaciones_disponibles(empleado_id: str) -> str:
"""Devuelve los días de vacaciones disponibles de un empleado a partir de su identificador."""
saldos = {"E001": 12, "E002": 3}
if empleado_id not in saldos:
return f"No existe ningún empleado con identificador {empleado_id}."
return f"El empleado {empleado_id} dispone de {saldos[empleado_id]} días."El decorador construye las tres piezas que el modelo necesita y que ya conoces: el nombre (el de la función), la descripción (el docstring) y el esquema de parámetros (deducido de los type hints, con Pydantic por debajo). La lección del Módulo 3 aplica literalmente: el docstring no es documentación para humanos, es prompt para el modelo. Viaja en cada petición, consume tokens y determina si la herramienta se usa bien, mal o nunca. El Módulo 6 dedica una sección entera a escribirlos con oficio.
Puedes inspeccionar lo que el modelo verá:
print(dias_vacaciones_disponibles.name)
print(dias_vacaciones_disponibles.description)
print(dias_vacaciones_disponibles.args)Antes de automatizar nada conviene dar una vuelta al bucle a mano, como en el Módulo 1. Se anuncian las herramientas con bind_tools y se atiende lo que el modelo pida:
from langchain_core.messages import HumanMessage, ToolMessage
modelo_con_tools = model.bind_tools([dias_vacaciones_disponibles])
mensajes = [HumanMessage(content="¿Cuántos días le quedan al empleado E001?")]
ai = modelo_con_tools.invoke(mensajes)
print(ai.tool_calls)
# [{'name': 'dias_vacaciones_disponibles',
# 'args': {'empleado_id': 'E001'},
# 'id': 'call_a1b2c3', 'type': 'tool_call'}]
mensajes.append(ai) # la petición del modelo entra al historial
for llamada in ai.tool_calls:
salida = dias_vacaciones_disponibles.invoke(llamada["args"])
mensajes.append(ToolMessage(content=str(salida), tool_call_id=llamada["id"]))
final = modelo_con_tools.invoke(mensajes)
print(final.content)Es, pieza por pieza, el bucle que escribiste a mano en el Módulo 1 y que Semantic Kernel ejecutaba por dentro en el Módulo 3: el modelo nunca ejecuta nada, solo redacta la petición; tu código ejecuta y devuelve el resultado como ToolMessage. Escribirlo una vez en cada framework vacuna contra la sensación de magia. Mantenerlo a mano en producción, en cambio, es el error que el Módulo 3 ya diagnosticó: fontanería repetitiva que crece con cada herramienta, cada permiso y cada caso de error.
Pregunta: ¿por qué el ToolMessage exige un tool_call_id en lugar de devolver el resultado sin más?
Porque el modelo puede pedir varias herramientas en un mismo turno — buscar dos empleados a la vez, por ejemplo — y los resultados pueden volver en cualquier orden. El identificador empareja cada resultado con la petición exacta que lo originó, de modo que el modelo sepa qué número corresponde a qué llamada. Sin ese emparejamiento, dos resultados numéricos serían ambiguos. Es el mismo mecanismo que Semantic Kernel gestionaba internamente sin enseñarlo.
Agentes con create_agent
En Semantic Kernel bastaba FunctionChoiceBehavior.Auto() para que el kernel se hiciera cargo del bucle. En LangChain 1.x la pieza equivalente es create_agent:
from langchain.agents import create_agent
agente = create_agent(
model=model,
tools=[dias_vacaciones_disponibles],
system_prompt=(
"Eres el asistente de RR. HH. de Contoso. "
"Usa las herramientas para responder con datos reales; "
"si te falta información, pídela antes de actuar. No inventes datos."
),
)
resultado = agente.invoke(
{"messages": [{"role": "user", "content": "¿Cuántos días le quedan a E002?"}]}
)
print(resultado["messages"][-1].content)El agente recibe y devuelve un estado cuya clave principal es messages: la conversación completa, incluidas las peticiones de herramientas y sus resultados. Recorrer resultado["messages"] después de una ejecución es el mejor hábito de depuración de este framework — ahí está, mensaje a mensaje, todo lo que ocurrió, y cada mensaje tiene un método pretty_print() que lo muestra legible.
Por dentro, create_agent no es una clase misteriosa: construye un grafo de LangGraph con dos nodos y una arista condicional.
┌──────────────┐
INICIO ────→│ modelo │──── sin tool_calls ────→ FIN
└──────┬───────┘
│ con tool_calls
▼
┌──────────────┐
│ herramientas │
└──────┬───────┘
│ ToolMessage(s)
└──────────────→ vuelve al modeloDos nodos y un ciclo: eso es un agente. El ciclo es la diferencia estructural con una cadena — una cadena LCEL es un grafo sin ciclos donde tú fijaste el camino; el agente tiene un bucle cuyo número de vueltas decide el modelo en ejecución. El Módulo 8 generaliza este dibujo con más nodos, más aristas y más de un modelo, pero el patrón mental ya está completo aquí.
Dos parámetros más completan el panorama. response_format acepta una clase Pydantic y aplica al final del bucle la misma salida estructurada de la sección anterior: útil cuando el consumidor del agente es otro programa y no una persona. Y middleware acepta una lista de piezas transversales que interceptan el bucle, el pariente directo de los filtros de Semantic Kernel: vienen varias de fábrica — resumir el historial cuando crece demasiado (SummarizationMiddleware), pausar el bucle y pedir aprobación humana antes de ejecutar una herramienta sensible (HumanInTheLoopMiddleware), censurar datos personales antes de que lleguen al modelo — y puedes escribir las tuyas. Los detalles finos de esa API viven en la documentación oficial; el concepto — lógica transversal que intercepta sin ensuciar la lógica de negocio — es exactamente el que ya dominas.
Pregunta: una cadena LCEL y un agente de create_agent son ambos runnables que reciben una entrada y devuelven una salida. ¿Cuál es la diferencia estructural entre los dos?
El control del flujo. En la cadena, el grafo de pasos es fijo y sin ciclos: lo dibujaste tú en tiempo de diseño y el modelo solo genera contenido dentro de cada paso. En el agente, el grafo contiene un ciclo modelo-herramientas y es el modelo quien decide, en cada vuelta y en tiempo de ejecución, si vuelve a iterar o termina. Por eso la cadena es predecible en coste y latencia (n pasos, siempre) y el agente no (el número de vueltas depende de cada pregunta), y por eso la regla práctica es: si puedes dibujar el flujo de antemano, no pagues un agente.
Memoria y estado: checkpointers y thread_id
El Módulo 1 estableció que el modelo es stateless: no recuerda nada entre llamadas, y la memoria de una conversación es siempre una ilusión construida reenviando el historial. En Semantic Kernel esa ilusión la sostenías tú, conservando el objeto ChatHistory entre turnos. Los agentes de LangChain lo resuelven con una pareja de conceptos: el checkpointer y el thread_id.
La analogía es un guardarropa. Entregas el abrigo — el estado de la conversación —, te dan un resguardo — el thread_id — y con ese resguardo cualquier instancia del agente, en cualquier momento posterior, recupera exactamente el mismo abrigo. El guardarropa es el checkpointer, que almacena el estado tras cada paso; el resguardo lo eliges tú, y en una aplicación real suele ser el identificador del usuario o de la conversación.
from langgraph.checkpoint.memory import InMemorySaver
agente = create_agent(
model=model,
tools=[dias_vacaciones_disponibles],
system_prompt="Eres el asistente de RR. HH. de Contoso.",
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "usuario-benja"}}
agente.invoke(
{"messages": [{"role": "user", "content": "¿Cuántos días le quedan a E001?"}]},
config,
)
r = agente.invoke(
{"messages": [{"role": "user", "content": "¿Y si se coge 5, cuántos le quedarían?"}]},
config,
)
print(r["messages"][-1].content)La segunda pregunta no menciona a ningún empleado y aun así se responde bien: el checkpointer recuperó el historial asociado a usuario-benja y lo antepuso a la nueva pregunta. Con otro thread_id, la misma pregunta habría fallado por falta de contexto — cada resguardo es un guardarropa aparte, y esa es justo la propiedad que aísla a un usuario de otro en una aplicación multiusuario.
InMemorySaver vive en la memoria del proceso y muere con él: perfecto para desarrollo, inservible para producción. El ecosistema ofrece checkpointers persistentes con la misma interfaz — SQLite para algo pequeño, PostgreSQL para algo serio — y cambiar de uno a otro es cambiar una línea. El Módulo 10 los retoma al hablar de despliegue.
La memoria, eso sí, no es gratis, y el Módulo 1 ya explicó por qué: como el modelo es stateless, cada turno reenvía el historial completo como prefill. Con turnos de unos 400 tokens, el turno 20 de una conversación ya envía alrededor de 7.600 tokens de historial, y el acumulado de los 20 turnos ronda los 84.000 tokens de prefill (400 multiplicado por la suma de 1 a 20, que es 210). Es el crecimiento cuadrático del Módulo 1 apareciendo en la factura, y es exactamente el problema que SummarizationMiddleware ataca: comprimir lo viejo en un resumen y conservar íntegro solo lo reciente.
Pregunta: en el asistente de empleados del proyecto final, con cientos de usuarios simultáneos, ¿qué usarías como thread_id y qué propiedad garantiza esa elección?
Un identificador estable por usuario y conversación — por ejemplo, el identificador de objeto del usuario en Entra ID combinado con un identificador de conversación que genera la aplicación. La propiedad que garantiza es el aislamiento: dos usuarios jamás comparten resguardo, así que uno no puede ver ni contaminar el historial del otro; y un mismo usuario puede mantener varias conversaciones independientes, cada una con su propio estado recuperable.
Streaming
Todo runnable sabe emitir su salida a trozos, y en una cadena de texto el uso es directo:
for trozo in cadena.stream({"empresa": "Contoso", "ticket": "..."}):
print(trozo, end="", flush=True)En un agente hay más de una cosa que puede interesar ver en directo, y el parámetro stream_mode elige cuál. Con stream_mode="values" se emite el estado completo tras cada paso del grafo — ideal para observar el bucle mientras se aprende o se depura:
for evento in agente.stream(
{"messages": [{"role": "user", "content": "¿Días disponibles de E001?"}]},
config,
stream_mode="values",
):
evento["messages"][-1].pretty_print()Con stream_mode="messages" se emiten los tokens del modelo según se generan, que es lo que una interfaz de chat quiere pintar; con stream_mode="updates", solo el cambio que produce cada paso. La física del Módulo 1 no cambia por el envoltorio: el streaming no acelera el decode, cambia la percepción — el primer token llega en cuanto termina el prefill, y el usuario deja de mirar una pantalla congelada.
Observabilidad: callbacks, el otro pariente de los filtros
Los filtros de Semantic Kernel tienen en LangChain dos parientes. El middleware, ya presentado, intercepta el bucle del agente a nivel de paso. Los callbacks, más antiguos y de grano más fino, escuchan eventos de cualquier runnable: inicio y fin de cada llamada al modelo, de cada herramienta, de cada cadena. Para trazabilidad — la necesidad que el filtro del Módulo 3 cubría — bastan dos métodos:
from langchain_core.callbacks import BaseCallbackHandler
class TrazaHerramientas(BaseCallbackHandler):
def on_tool_start(self, serialized, input_str, **kwargs):
print(f"[TRAZA] herramienta: {serialized.get('name')} | entrada: {input_str}")
def on_tool_end(self, output, **kwargs):
print(f"[TRAZA] resultado: {output}")
config = {
"configurable": {"thread_id": "usuario-benja"},
"callbacks": [TrazaHerramientas()],
}Cada vez que el agente ejecute una herramienta, la traza aparece sin que la lógica de negocio sepa que está siendo observada: el mismo principio transversal del Módulo 3.
Para no reinventar la observabilidad completa, el ecosistema ofrece LangSmith: con dos variables de entorno, cada ejecución queda registrada — cada mensaje, cada llamada, cada token, con tiempos y costes — en una interfaz web donde se puede inspeccionar el árbol entero de una petición.
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="tu-clave"No hace falta usarlo en este módulo, pero conviene saber que existe: el Módulo 9 lo convierte en la base de la evaluación sistemática.
Semantic Kernel y LangChain, frente a frente
Con las dos experiencias completas, el diccionario de traducción cabe en una tabla:
| Concepto | Semantic Kernel | LangChain 1.x |
|---|---|---|
| El modelo | servicio registrado en el kernel | instancia suelta (AzureChatOpenAI) que pasas a quien la necesita |
| Función invocable | @kernel_function dentro de un plugin | @tool sobre una función suelta |
| Descripción para el modelo | parámetro description | docstring de la función |
| Bucle automático | FunctionChoiceBehavior.Auto() | create_agent |
| Historial | objeto ChatHistory que tú conservas | estado messages más checkpointer con thread_id |
| Lógica transversal | filtros del kernel | middleware (bucle del agente) y callbacks (eventos) |
| Plantillas | funciones de prompt | ChatPromptTemplate |
| Salida estructurada | response_format en la configuración de ejecución | with_structured_output y response_format |
| Orquestación avanzada | Agent Framework | LangGraph |
La diferencia estructural más visible no está en la tabla: LangChain no tiene kernel. No existe un contenedor central donde registrar servicios y resolver dependencias; el modelo es un objeto que pasas explícitamente a cadenas y agentes. Menos inversión de control, más tuberías a la vista. Para una aplicación grande al estilo enterprise, el contenedor de Semantic Kernel ordena; para componer rápido y ver todas las piezas, la explicitud de LangChain ayuda. Ninguna de las dos posturas es un error.
El criterio de elección, en frío: Semantic Kernel encaja de forma natural cuando la casa es Microsoft — .NET como primera lengua, integración profunda con Azure y el ecosistema Copilot, equipos acostumbrados a inyección de dependencias. LangChain gana cuando la casa es Python, cuando hacen falta integraciones variadas y abundantes, cuando el proyecto va a necesitar la orquestación en grafo de LangGraph, o cuando pesa el volumen de comunidad, ejemplos y manos disponibles en el mercado.
Para el proyecto final de este curso la respuesta es deliberadamente ambas: vive en el ecosistema Microsoft y usará Semantic Kernel como columna vertebral, pero los Módulos 7 y 8 toman piezas de LangChain y LangGraph donde son más fuertes — la maquinaria de ingesta y recuperación para RAG, los grafos para lo multiagente. Y el Módulo 5 presenta la capa que hace que esta elección duela cada vez menos: MCP, un protocolo con el que una herramienta se escribe una vez y se conecta a cualquiera de los dos frameworks — o a cualquier otro.
Ejercicios
Los ejercicios están pensados para hacerse en orden, cada uno sobre el anterior. Necesitas el deployment de Azure OpenAI del Módulo 2 y las variables de entorno configuradas.
Ejercicio 1. La primera cadena
Objetivo: componer con LCEL y obtener salida estructurada validada.
- Construye el clasificador de tickets de la sección de LCEL: la clase
TicketClasificadoy un modelo conwith_structured_output. - Inventa 5 tickets de soporte interno variados (accesos, hardware, RR. HH.) y clasifícalos con una sola llamada a
batch. - Imprime una tabla simple: categoría, urgencia y resumen de cada ticket.
- Repite la clasificación con
temperature=0y contemperature=1y compara la estabilidad del campourgenciaentre ejecuciones. Es el efecto del Módulo 1 medido sobre un caso real: para clasificar, la temperatura baja no es una preferencia, es un requisito.
Ejercicio 2. El bucle, a mano y en automático
Objetivo: medir con precisión qué automatiza create_agent.
- Define una herramienta
buscar_empleado(nombre: str)sobre un diccionario en memoria con 3 empleados, que devuelva identificador y departamento. - Implementa el bucle manual con
bind_tools: unwhileque invoca al modelo, ejecuta todas lastool_callsque aparezcan, añade losToolMessagey repite hasta que la respuesta llegue sintool_calls. - Pruébalo con una pregunta que exija encadenar dos herramientas: "¿cuántos días de vacaciones tiene Ana García?" obliga a buscar primero el identificador y consultar después el saldo.
- Resuelve la misma pregunta con
create_agenty las dos herramientas. - Cuenta las líneas de cada versión y anota qué desapareció: el while, la ejecución de llamadas, el emparejamiento de identificadores, la gestión del historial. Eso — multiplicado por cada herramienta, permiso y caso de error futuros — es lo que compra un orquestador.
Ejercicio 3. Dos usuarios, dos memorias
Objetivo: comprobar el aislamiento que dan los checkpointers.
- Crea el agente del ejercicio anterior con
checkpointer=InMemorySaver(). - Con
thread_id="ana", pregunta por los días de E001. Conthread_id="luis", pregunta por los de E002. - En cada hilo, pregunta después: "¿de qué empleado estábamos hablando?" y verifica que cada hilo recuerda solo su propia conversación.
- Imprime
len(resultado["messages"])tras cada turno y observa el crecimiento del estado: es la materia prima del coste cuadrático calculado en la sección de memoria.
Proyecto del módulo: el mismo asistente, segundo framework
El proyecto del Módulo 3 fue un asistente de RR. HH. en consola construido con Semantic Kernel. El proyecto de este módulo es, deliberadamente, el mismo asistente reconstruido en LangChain. No es falta de imaginación: reimplementar un sistema conocido en un framework nuevo es la forma más rápida de aislar qué aporta el framework y qué aportabas tú.
Requisitos:
- Herramientas. Tres funciones con
@toolsobre los mismos datos en memoria del Módulo 3:buscar_empleado(nombre), que devuelve identificador y departamento;dias_vacaciones_disponibles(empleado_id); ysolicitar_vacaciones(empleado_id, dias), que valida el saldo y lo descuenta. - Agente.
create_agentcon un system prompt que fije un tono profesional, obligue a confirmar con el usuario antes de registrar una solicitud y prohíba inventar datos de empleados. - Memoria.
InMemorySavercon unthread_idfijo para la sesión de consola. - Trazabilidad. El callback
TrazaHerramientasde la sección de observabilidad, imprimiendo cada herramienta ejecutada con su entrada y su salida: el equivalente del filtro del Módulo 3. - Interfaz. Un bucle de consola que lee la pregunta, invoca al agente, imprime la respuesta y termina cuando el usuario escribe "salir".
Casos de prueba guiados:
- "Hola, ¿qué puedes hacer?" — el agente responde sin ejecutar ninguna herramienta y la traza queda vacía: no toda pregunta merece una llamada.
- "¿Cuántos días de vacaciones tiene Ana García?" — la traza muestra dos herramientas encadenadas (buscar el identificador y consultar el saldo) sin que nadie se lo pida paso a paso.
- "Pídele 5 días." — el agente sabe por la memoria de quién se habla, y confirma antes de ejecutar
solicitar_vacaciones. - "¿Y ahora cuántos le quedan?" — el saldo refleja el descuento: las herramientas comparten estado real, no respuestas memorizadas.
- "¿Cuántos días tiene Carlos Ruiz?" con un empleado que no existe — la herramienta devuelve un mensaje de error controlado y el agente lo comunica con claridad, sin inventar un empleado ni un saldo.
Criterio de éxito adicional, y quizá el más valioso del módulo: pon este código junto al del proyecto del Módulo 3 y escribe tres frases — qué resultó más corto aquí, qué resultó más claro allí, y cuál elegirías para el proyecto final con lo que sabes hasta este punto del curso. No hay respuesta correcta; hay criterio en formación.
Resumen
LangChain organiza sus piezas como el mundo de las bases de datos organiza los drivers: contratos estables en langchain-core, implementaciones por proveedor en paquetes de integración, y encima create_agent como pieza de alto nivel y LangGraph como motor de estado. El contrato central es Runnable — invoke, batch, stream — y hace que plantillas, modelos, parsers, herramientas y agentes se compongan con una tubería. Las herramientas se declaran con @tool y un docstring que es prompt, no documentación; el bucle agéntico que en el Módulo 1 se escribió a mano y en el Módulo 3 ejecutaba el kernel aquí lo industrializa create_agent, que por dentro es un grafo de dos nodos con un ciclo cuya salida decide el modelo. La memoria reaparece como pareja checkpointer y thread_id — el guardarropa y el resguardo — con el coste cuadrático del historial como factura y el middleware de resumen como amortiguador; los filtros del Módulo 3 se traducen a middleware y callbacks.
Sobre todo, el módulo deja demostrada la tesis con la que empezó: los conceptos — modelo, herramienta, descripción, bucle, historial, transversales — son los mismos en ambos frameworks, y quien los domina traduce entre ellos con una tabla. Las piezas que vienen aprietan justo donde este módulo termina: el Módulo 5 saca las herramientas fuera del framework con MCP, para escribirlas una vez y conectarlas a cualquier orquestador; el Módulo 6 enseña a diseñarlas y describirlas con oficio; el Módulo 7 usa la maquinaria de LangChain donde más brilla, la ingesta y recuperación de RAG; el Módulo 8 convierte el grafo de dos nodos de este módulo en grafos multiagente con LangGraph; y los Módulos 9 y 10 retoman LangSmith y los checkpointers persistentes cuando toque evaluar y desplegar.
¿Te sirvió esta lección?
El curso es gratis y así seguirá. Un café ayuda a que salga el siguiente módulo.
Invítame un café¿Quieres llevar esto a tu negocio?
En Burnisoft convertimos estas ideas en apps, web y automatizaciones a la medida. Cuéntanos tu reto, sin compromiso.
Contáctanos