LangChain
El framework más popular para construir agentes tiene un historial caótico y una promesa reciente de estabilidad; pero, en cualquier caso, es un buen punto de entrada para aprender a desarrollar agentes.
1. Introducción
Con la llegada de los grandes modelos de lenguaje, empezó a surgir la necesidad de crear frameworks que permitieran trabajar mejor con la inteligencia artificial generativa. Uno de los más conocidos hoy en día es LangChain, cuyo origen se remonta a finales de 2022, prácticamente al mismo tiempo que el lanzamiento de ChatGPT, lo que ayuda a entender por qué ganó tanta popularidad tan rápido.
La idea original era resolver un problema muy concreto: conectar los LLMs con fuentes externas de datos y herramientas. Es decir, que el modelo no estuviera aislado, sino que pudiera leer documentos, consultar bases de datos, ejecutar código y mucho más.
Se publicó como un proyecto open source casi de forma experimental y con el tiempo fue creciendo hasta contar con un equipo dedicado y una gran comunidad. A excepción de algunas funcionalidades empresariales, mantiene licencia MIT, por lo que es libre y se puede usar sin restricciones en proyectos comerciales.
Lo que antes era una librería con un par de herramientas es ahora una plataforma con cinco productos diferenciados para formar lo que LangChain define como una plataforma para ingeniería de agentes. Esto es lo que incluye:
- Open source
- LangChain: el framework de alto nivel para construir agentes con componentes modulares.
- LangGraph: el runtime de orquestación de bajo nivel sobre el que corre LangChain.
- Deep Agents: es el punto de entrada rápido para agentes, construido sobre los dos anteriores.
- Productos cloud de pago
- LangSmith: observabilidad, trazas, evaluación y despliegue
- LangGraph Platform: infraestructura gestionada para desplegar grafos en producción
- LangSmith Fleet: creación de agentes sin código para perfiles no técnicos
2. ¿Vale la pena?
Antes de seguir, sin embargo, es importante avisar de que el framework se ha caracterizado por cambiar muchísimo cada dos por tres. Cada versión, aunque fuera una minor, presentaba grandes breaking changes, APIs deprecadas sin aviso, documentación desincronizada con el código, y dos formas de hacer lo mismo (la vieja y LCEL) conviviendo sin que quedara claro cuál usar. Muchas personas perdimos horas por esto.
Conscientes de este desastre, en octubre de 2025 anunciaron la versión 1.0 con el compromiso explícito de no volver a introducir breaking changes hasta la v2.0. Es la primera vez que hacen esa promesa formalmente. Puede que la cumplan o no, pero señala que han escuchado el problema.
Ahora bien, es natural sentir desconfianza dada esa inestabilidad histórica y querer mantener cierta distancia hasta que no demuestren un poco de sensatez. Sin embargo, hay que separar dos cosas que se confunden fácilmente: aprender el framework vs aprender los conceptos que el framework implementa.
Dicho de otra forma, hay muchos temas relacionados con el frame que vale la pena aprender. No caducan ni caducarán sea cual sea su evolución o el framework que use el mundo en 2027:
- Cómo gestionar el estado en un agente multi-paso
- Cómo orquestar herramientas y decidir cuándo usarlas
- Cómo hacer RAG correctamente (chunking, embeddings, retrieval)
- Cómo modelar flujos con bifurcaciones y ciclos
- Human-in-the-loop, checkpointing, recuperación de fallos
Estos son conceptos de ingeniería de software aplicados a LLMs. Da igual aprenderlos con LangGraph o con cualquier otro frame. Son conceptos transferibles a cualquier herramienta, al igual que la herencia de clases no es exclusiva de Angular, FastApi o Spring Boot.
Lo que quizás no vale la pena aprender con profundidad son las APIs concretas, los nombres de clases, los parámetros exactos y menos ahora que la IA redacta las líneas de código. Es como aprender SQL vs aprender el ORM de turno. El ORM cambia cada pocos años, SQL lleva 50 años siendo SQL. Pero aprender el ORM te da contexto para entender qué problemas resuelve una base de datos relacional y eso sí vale la pena.
Dicho esto, vamos con un hola mundo : ).
3. Un primer agente
3.1. Preparativos
Antes que nada, lo primero que vamos a necesitar es algún modelo contra el que trabajar. Nos podemos instalar alguno en local o tirar de alguno que exponga un API, en cuyo caso necesitaremos, claro está, un api-key. Por ejemplo:
Como siempre, esta api-key se escribe en un archivo .env en el root del proyecto (archivo que siempre debe estar en el gitignore).
ANTHROPIC_API_KEY=sk-ant-api03...
Luego instalamos LangChain, para lo cual tenemos varias formas. Una opción es usar [uv] (https://docs.astral.sh/uv/):
uv init
uv add langchain
uv sync
Otra alternativa es utilizar pip:
pip install -U langchain
En ambos casos, es recomendable hacerlo dentro de un entorno virtual. Por ejemplo:
source .venv/Scripts/activate
Una vez instalado LangChain, necesitas añadir la integración con el proveedor de modelo que vayas a utilizar. Por ejemplo, con OpenAI
uv add langchain-openai
O con Anthropic:
uv add langchain-anthropic
Y lo mismo aplica para otros proveedores, según el modelo que quieras usar.
Por último, necesitamos algo para recuperar las variables de entorno, como puede ser dotenv.
uv add python-dotenv
Vamos ya con nuestro primer agente. Puedes escribirlo en un archivo .py o, recomendable, en un notebook de jupyter, que se integra muy bien en el Visual Studio Code.
3.2. Hola mundo
Una vez instalado todo, podemos probar un ejemplo muy sencillo para ver cómo funciona. En este caso, creamos un agente con un comportamiento definido mediante un system prompt:
from langchain.agents import create_agent
agent = create_agent(
model="anthropic:claude-haiku-4-5",
system_prompt="Eres un monje budista que responde siempre con haikus incomprensibles de 5-7-5 sílabas.",
)
my_answer = agent.invoke({
"messages": [{"role": "user", "content": "¿Qué es la fotosíntesis?"}]
})
print(my_answer["messages"][-1].content)
La salida podría ser algo así:
Luz toca la hoja, los números bailan solos, silencio que crece.
Aunque apenas son unas líneas, nos permiten ver tres elementos fundamentales:
1. Creamos un agente con **create_agent,** que es una función de LangChain que construye y configura un agente listo para usar a partir de unos pocos parámetros. Dicho de otra forma, es un atajo que te evita tener que montar manualmente toda la lógica que hay detrás de un agente.
2. A create_agent le hemos pasado el modelo contra el que debe trabajar y un **system_prompt,** una instrucción inicial que se le da al modelo para definir cómo debe comportarse desde el principio. Es, básicamente, el contexto base que guía todas sus respuestas. No es una pregunta del usuario, sino una especie de regla o rol que el modelo debe seguir durante la conversación.
3. Y el tercer elemento a destacar es ese invoke(), que es la forma estándar que propone LangChain para ejecutar cualquier componente. Este método no es exclusivo de los agentes. En LangChain, muchos objetos comparten la misma interfaz (lo que llaman Runnable), y todos se ejecutan con .invoke():
- Un modelo.
- Un agente.
- Una cadena (chain).
- Una pipeline más compleja.
Todos funcionan igual:
```python
algo = creamos_algo
resultado = algo.invoke(input)
Es decir, es una manera muy cómoda para unificar la forma de ejecutar cualquier pieza, sin importar lo que haya por debajo.
4. Tools
Con esto ya tendríamos nuestro primer agente, pero la verdad es que es un poco churro. Para el caso, podríamos haber atacado directamente el API del modelo que estemos usando. No sucederá lo mismo con el siguiente, porque vamos a añadir una de las características que definen a los agentes: la posibilidad de usar herramientas.
Un LLM sabe muchísimo, pero tiene dos limitaciones fundamentales que no puede superar por sí solo:
- No tiene acceso al mundo real. No puede consultar el tiempo, leer tu base de datos, buscar en Google, enviar un email ni ejecutar código. Solo sabe lo que había en sus datos de entrenamiento hasta cierta fecha.
- No puede actuar. Puede decirte cómo reservar un vuelo, pero no puede reservarlo él mismo.
Las herramientas resuelven exactamente eso.
Una herramienta es simplemente una función normal que el agente puede decidir llamar durante su ejecución. Por ejemplo, un modelo no puede saber qué hora es sin más, necesita usar una herramienta. Por lo tanto, si quisiéramos construir un agente apasionante que nos diga qué hora es, hay que proporcionársela.
En código.
# La herramienta
from langchain.tools import tool
from datetime import datetime
@tool
def get_current_time() -> str:
"""Returns the current date and time."""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# El agente
from langchain.agents import create_agent
agent = create_agent(
model="anthropic:claude-haiku-4-5",
tools=[get_current_time], # <----- le incoporamos la herramienta
system_prompt="Eres un agente muy divertido que siempre dice la hora acompañada de un chistako",
)
response = agent.invoke({
"messages": [{"role": "user", "content": "¿Qué hora es ahora mismo?"}]
})
print(response["messages"][-1].content)
Vamos a detenernos en dos detalles: El primero es el decorador @tool y el segundo es esa descripción que hay en la docstring ("""Returns…). Aquí está la parte importante, el modelo no ejecuta la función directamente, no puede, es un LLM. Lo que ocurre es que:
- LangChain le explica al modelo qué herramientas tiene disponibles: su nombre, qué hace y qué parámetros acepta. Esta descripción se construye automáticamente a partir del nombre de la función y su docstring.
- El modelo decide si necesita usar una herramienta y devuelve una respuesta estructurada diciendo cuál y con qué argumentos.
- LangChain intercepta esa respuesta, ejecuta la función de verdad, y le devuelve el resultado al modelo.
- El modelo continúa razonando con ese resultado.
Por eso el docstring es crítico. Es lo que el modelo lee para entender qué hace la herramienta; si el docstring es malo, el modelo no sabe cuándo usarla.
5. Un agente chef
Vamos con un ejemplo más complejo: un chef con dos tools, una para revisar la nevera y otra para encontrar recetas en un recetario. Cuando le pidan qué preparar, debe analizar si los ingredientes y las recetas se encuentran entre sus conocimientos y, en caso contrario, responder que no puede proponer nada.
Primero preparamos las herramientas, que recordemos que no son más que meras funciones con el decorador @tool.
from langchain.tools import tool
FRIDGE = {
"huevos": 3,
"tofu": 1,
"pollo": 2,
"ajo": 4,
"jengibre": 1,
"salsa_soja": True,
"aceite_sesamo": True,
"pasta_chili": True,
"cebolleta": 3,
"cacahuetes": True,
"arroz": True,
"fideos": False,
"bok_choy": 2,
}
RECIPES = {
"pollo sichuan": ["pollo", "pasta_chili", "ajo", "jengibre", "salsa_soja", "aceite_sesamo"],
"mapo tofu": ["tofu", "pasta_chili", "ajo", "jengibre", "salsa_soja"],
"kung pao": ["pollo", "cacahuetes", "pasta_chili", "ajo", "salsa_soja", "cebolleta"],
"bok choy con ajo": ["bok_choy", "ajo", "salsa_soja", "aceite_sesamo"],
"arroz frito con huevo": ["huevos", "arroz", "cebolleta", "salsa_soja"]
}
@tool
def check_fridge() -> dict:
"""Devuelve el contenido actual de la nevera con los ingredientes disponibles y sus cantidades."""
return FRIDGE
@tool
def find_recipes(ingredients: list[str]) -> list[str]:
"""Busca recetas chinas que se puedan preparar con los ingredientes dados.
Devuelve una lista de recetas posibles."""
available = [ing for ing, qty in FRIDGE.items() if qty]
possible = []
for recipe, required in RECIPES.items():
if all(ing in available for ing in required):
possible.append(recipe)
return possible if possible else ["No hay recetas posibles con esos ingredientes"]
Y ahora ya las incorporamos al agente.
from langchain.agents import create_agent
SYSTEM_PROMPT = """Eres Wei, un entusiasta chef chino especializado en cocina de Sichuan.
Cuando el usuario pida sugerencias para cenar, SIEMPRE:
1. Revisa la nevera con check_fridge
2. Busca recetas posibles con find_recipes
3. Recomienda la mejor opción con entusiasmo e incluye un consejo de cocina
Nunca sugieras ingredientes que no estén en la nevera."""
agent = create_agent(
model="anthropic:claude-haiku-4-5",
tools=[check_fridge, find_recipes],
system_prompt=SYSTEM_PROMPT,
)
response = agent.invoke({
"messages": [{"role": "user", "content": "Tengo tofu y jengibre, ¿qué puedo cenar esta noche?"}]
})
print(response["messages"][-1].content)
6. Algo de memoria
Otra característica de los agentes es su capacidad de mantener una conversación gracias a la memoria. Ahora mismo, nuestro agente es amnésico: cada invoke es una conversación nueva desde cero. Si le dices “¿y si no tengo pollo?” en un segundo mensaje, no sabe de qué le hablas. Añadir memoria es el salto entre una herramienta que responde preguntas a un asistente con el que puedes mantener una conversación real.
La memoria a corto plazo permite que el agente recuerde interacciones anteriores dentro de una misma conversación o sesión, que es lo que en argot se conoce como thread. Y para que pueda recordar la conversación, el historial de la conversación, LangChain proporciona un mecanismo llamado checkpointer.
Checkpointer viene de checkpoint, punto de control, y es el mismo concepto que en videojuegos: un punto donde se guarda el estado del juego para poder retomarlo después. En LangChain, el agente en cada paso tiene un estado, que es básicamente el historial de mensajes y cualquier otra información que se esté guardando. El checkpointer es el objeto responsable de serializar y persistir ese estado después de cada paso.
Dicho de otra forma.
- El estado es qué se guarda: los mensajes, el contexto, el thread.
- El checkpointer es cómo y dónde se guarda, que puede ser en RAM, en PostgreSQL, en SQLite y en cualquier otro sistema que permita persistir datos.
Y las buenas noticias son que con LangChain es muy fácil añadir memoria a un agente. Tan solo tenemos que incorporar dos cosas:
from langgraph.checkpoint.memory import InMemorySaver
# 1. Crear el checkpointer
checkpointer = InMemorySaver()
# 2. Pasárselo al agente
agent = create_agent(
model="anthropic:claude-haiku-4-5",
tools=[check_fridge, find_recipes],
system_prompt=SYSTEM_PROMPT,
checkpointer=checkpointer, # <---- esto es todo
)
Y además tenemos que crear un thread_id, un id que le permita identificar la conversación que ha guardado.
# En la configuración, definimos un id
config = {"configurable": {"thread_id": "conversacion-1"}}
# Primer mensaje
response = agent.invoke(
{"messages": [{"role": "user", "content": "¡Me muero de hambre! Tengo pollo y tofu ¿Qué puedo cenar?"}]},
config=config,
)
print(response["messages"][-1].content)
# Segundo mensaje — el agente recuerda el primero
response = agent.invoke(
{"messages": [{"role": "user", "content": "¿Qué me acabas de recomendar?"}]},
config=config,
)
print(response["messages"][-1].content)
En este ejemplo, para construir el checkpointer hemos usado InMemorySaver(), que lo guarda en memoria, en la RAM, y solo sirve para desarrollo y tests, entre otras razones, porque se pierde al reiniciar el proceso.
Para producción, hay librerías específicas por base de datos:
- PostgreSQL: langgraph-checkpoint-postgres
- SQLite langgraph-checkpoint-sqlite
- Redis: langgraph-checkpoint-redis
- …
Vamos ahora con otra cuestión clave: darle forma a las respuestas.
7. Structured output
Hasta ahora el agente devuelve texto libre. Wei responde con un párrafo en lenguaje natural y eso está bien para un chatbot, pero si queremos conectar la respuesta a otra parte de una aplicación, como un frontal, una base de datos o una API, necesitamos datos predecibles y deterministas, no un string que parsear.
Esto lo conseguimos mediante la técnica de structured output, que permite que el agente devuelva datos en un formato específico y predecible. En lugar de parsear lenguaje natural, obtenemos objetos estructurados con el sistema que sea (Pydantic, dataclasses, TypedDict), que la aplicación puede usar directamente. Esta respuesta estructurada llega en response[“structured_response”], separada de los mensajes.
En código se entiende bien, así que vamos con otro agente. En este caso, una librera que da recomendaciones siguiendo determinado formato.
from dataclasses import dataclass
from langchain.agents import create_agent
# Definimos un formato de respuesta estructurado para las recomendaciones de libros.
# En este caso usamos dataclasses, pero también podríamos usar Pydantic o cualquier otra librería de definición de datos.
@dataclass
class BookRecommendation:
"""Book recommendation from Sylvia Beach."""
title: str
author: str
year: int
genre: str
why_this_book: str
perfect_moment: str
warning: str | None
# Un poquito de role prompting.
SYSTEM_PROMPT = """Eres Sylvia Beach, la librera americana que fundó Shakespeare and Company en París en 1919.
Conociste a Joyce, Hemingway, Fitzgerald, Gertrude Stein. Publicaste el Ulises cuando nadie se atrevía.
Tienes opiniones fuertes, pasión desbordante por la literatura y un punto de irreverencia.
Cuando alguien te pide una recomendación, escuchas con atención y recomiendas exactamente lo que necesita,
no necesariamente lo que pide. Respondes siempre en español."""
agent = create_agent(
model="anthropic:claude-haiku-4-5",
system_prompt=SYSTEM_PROMPT,
response_format=BookRecommendation, # <----- le decimos al agente que queremos la respuesta en este formato estructurado
)
response = agent.invoke({
"messages": [{"role": "user", "content": "¿Me recomiendas algún libro sobre dinosaurios?"}]
})
rec = response["structured_response"]
print(f" {rec.title} — {rec.author} ({rec.year})")
print(f"Género: {rec.genre}")
print(f"\n{rec.why_this_book}")
print(f"\n Cuándo leerlo: {rec.perfect_moment}")
if rec.warning:
print(f"\n {rec.warning}")
De esta manera, por dentro lo que sucede es que:
- El agente razona normalmente
- Al final, en lugar de generar texto libre, el modelo recibe el schema y genera JSON que lo cumple
- LangChain valida y deserializa ese JSON al dataclass
- El resultado aparece en response[“structured_response”]
Vamos ahora con algo muy chulo: la capacidad de ir recibiendo los datos.
8. Streaming
Cuando llamamos a agent.invoke() el programa se queda esperando, a veces muchísimos segundos, y de repente aparece la respuesta completa de golpe. Para un notebook está bien, pero en una aplicación real es inviable: el usuario enviaría un mensaje, no pasaría nada, no pasaría nada, no pasaría nada… y de repente, plop, aparece todo el texto. La experiencia de usuario sería un desastre.
El streaming resuelve eso: en lugar de esperar a que el agente termine, se recibe la respuesta a medida que se genera, al igual que se ve a alguien escribiendo por WhatsApp, el Teams o cualquier interfaz de IA, como la de DeepSeek. La latencia es la misma, pero la percepción es completamente diferente.
El sistema básico es muy sencillo. En lugar de utilizar el método invoke
response = agent.invoke({
"messages": [{"role": "user", "content": "¿Quién ganaría entre un partido de fútbol entre triceratops y stegosaurios?"}]},
config=config,)
Y luego manejar la respuesta completa…
print(response["messages"][-1].content)
Utilizamos el método stream, añadimos lo de version v2 y manejamos chunks, que es el término para cada trozo de datos que llega en un stream. Algo así.
for chunk in agent.stream(
{"messages": [{"role": "user", "content": "¿Los pterosaurios comían helado de chocolate?"}]},
stream_mode="messages", # <--- queremos el modo strean con los tokens en tiempo real
version="v2", # <--- indicamos la versión 2
):
if chunk["type"] == "messages": # <--- recorremos los que va llegando
token, metadata = chunk["data"]
for block in token.content_blocks:
if block.get("type") == "text":
print(block["text"], end="", flush=True)
Ahora nos van llegando los tokens individuales, cada fragmento de texto a medida que se genera, antes de que el paso esté completo. Es como ver el agente desde dentro: “H… o… l… i… ”. Recibimos el texto carácter a carácter, lo que permite ir mostrando la respuesta en tiempo real al usuario y así reducir la percepción de latencia.
Además, en LangChain el streaming hace algo más que mejorar la UX. También te permite observar el razonamiento interno del agente — ver qué herramientas decide llamar, en qué orden y con qué argumentos, lo que es muy útil para entender qué está pasando y debuggear cuando algo sale mal.
Para ver cómo funciona tenemos que tirar de un agente que use tools, como el entrañable Wei, nuestro cocinero chino. Y en stream_mode, en vez de “messages”, indicamos “updates”.
config = {"configurable": {"thread_id": "conversacion-1"}} # <--- opcional
for chunk in agent.stream(
{"messages": [{"role": "user", "content": "Tengo tofu y jengibre, ¿qué puedo cenar esta noche?"}]},
config=config, # <--- opcional
stream_mode="updates", # <--- OJO con esto
version="v2", # <--- también la v2
):
if chunk["type"] == "updates":
for step, data in chunk["data"].items():
print(f"\n--- paso: {step} ---")
print(data['messages'][-1].content_blocks)
Ahora nos va mostrando todos los pasos que va dando.
--- paso: model ---
[{'type': 'text', 'text': '¡Excelente! Déjame revisar tu nevera y buscar las mejores opciones para ti.'}, {'type': 'tool_call', 'name': 'check_fridge', 'args': {}, 'id': 'toolu_01MA6iTMzgjryXQztzswxAS2', 'extras': {'caller': {'type': 'direct'}}}, {'type': 'tool_call', 'name': 'find_recipes', 'args': {'ingredients': ['tofu', 'jengibre']}, 'id': 'toolu_013uGEak2AowS4R45Juk7ZCp', 'extras': {'caller': {'type': 'direct'}}}]
--- paso: tools ---
[{'type': 'text', 'text': '{"huevos": 3, "tofu": 1, "pollo": 2, "ajo": 4, "jengibre": 1, "salsa_soja": true, "aceite_sesamo": true, "pasta_chili": true, "cebolleta": 3, "cacahuetes": true, "arroz": true, "fideos": false, "bok_choy": 2}'}]
--- paso: tools ---
[{'type': 'text', 'text': '["pollo sichuan", "mapo tofu", "kung pao", "bok choy con ajo", "arroz frito con huevo"]'}]
--- paso: model ---
[{'type': 'text', 'text': '¡Perfecto! Tienes una nevera espléndida. Te recomiendo **Mapo Tofu** -...'}
9. Messages
Terminamos esta introducción a langchain hablando de otro componente fundamental del core, los mensajes, que son la unidad fundamental de contexto para los modelos en LangChain. Representan la entrada y salida del modelo, llevando tanto el contenido como los metadatos necesarios para representar el estado de una conversación.
En realidad, los hemos estado usando desde el principio del tutorial:
agent.invoke({
"messages": [{"role": "user", "content": "¡Me muero de hambre!"}]
})
Ese diccionario con role y content es la forma abreviada. LangChain lo convierte internamente a objetos de mensaje tipados.
Hay cuatro tipos de mensajes:
- SystemMessage para instrucciones de comportamiento del modelo,
- HumanMessage para la entrada del usuario,
- AIMessage para las respuestas generadas por el modelo,
- y ToolMessage para los resultados de las herramientas.
Cada paso del stream es un tipo de mensaje concreto, de ahí que en “paso” de antes se fuera mostrando cuál era:
--- paso: model --- → AIMessage con tool_calls
--- paso: tools --- → ToolMessage con el resultado de check_fridge
--- paso: tools --- → ToolMessage con el resultado de find_recipes
--- paso: model --- → AIMessage con la respuesta final
Se pueden escribir de tres maneras.
- Formato diccionario, es el más corto, el que hemos estado usando.
{"role": "user", "content": "¿Qué puedo cenar?"}
- Objetos tipados, el más explícito
from langchain.messages import HumanMessage, SystemMessage, AIMessage
HumanMessage("¿Qué puedo cenar?")
- Conversación completa con historia. Es lo que hace la memoria por debajo, mantiene esa lista creciendo con cada turno de conversación.
messages = [
SystemMessage("Eres un chef experto"),
HumanMessage("¿Qué puedo cenar?"),
AIMessage("Te recomiendo mapo tofu..."), # respuesta anterior
HumanMessage("¿Y si no tengo tofu?"), # siguiente turno
]
Por cierto, un detalle útil: usage_metadata. El AIMessage lleva metadatos de uso que puedes consultar para controlar costes:
response = agent.invoke({
"messages": [{"role": "user", "content": "¿Qué puedo cenar?"}]
})
ultimo_mensaje = response["messages"][-1]
print(ultimo_mensaje.usage_metadata)
# {'input_tokens': 8, 'output_tokens': 304, 'total_tokens': 312}
Bueno, de momento vamos a dejarlo aquí.