En este artículo utilizaremos código Python en una Jupyter Notebook para aprender a usar LangChain con modelos de lenguaje. Repasaremos los bloques básicos para entender cómo funciona el método de “encadenar” prompts y sus ventajas.

Introducción
Para poder aprovechar al máximo este artículo, te recomiendo tener instalado LM Studio con un LLM local como se explica en este artículo ó puedes utilizar tu propio API (de pago) de Open AI u otro modelo de lenguaje como Deepseek o Claude.
La propuesta es entender qué es y cómo funciona LangChain, que nos facilita y ordena el trabajo con LLMs. Lo ideal sería que vayas leyendo el artículo y probando el código en tu ordenador, siguiendo la Jupyter Notebook.
Esta es la serie de Artículos sobre Modelos de Lenguaje:
* ¿Cómo funcionan los Transformers?
* Generación de texto con GPT-2
* ¿Qué son los LLMs?
* Instala un LLM en tu ordenador
* Prompt Engineering
* ¿Qué es LangChain? <– este artículo!
* Crea Agentes Python <– próximamente
* ¿Qué son los RAGs? <– próximamente
En los próximos artículos veremos cómo crear tu propio RAG, “conversar con PDFs” y cómo crear Agentes Avanzados.
¿Qué es LangChain?
LangChain es un framework Open Source que permite a los desarrolladores trabajar con LLMs (Grandes Modelos de Lenguaje) y combinar diversos componentes externos y herramientas para crear Sistemas y Aplicaciones empresariales. Con LangChain podemos enlazar modelos poderosos como ChatGPT, Claude, Llama, Deepseek, Gemini a un conjunto de datasets o fuentes externas y potenciar su sabiduría, limitar las salidas, controlar y/o enriquecer a otros sistemas mediante técnicas de NLP (Procesamiento del lenguaje Natural) ó proporcionar diversa lógica de negocio propia de los casos de uso del proyecto en el cual estemos trabajando.
Desarrolladores, Ingenieros y Científicos de datos con experiencia en lenguajes de programación Python, JavaScript o TypeScript pueden hacer uso de los paquetes ofrecidos por LangChain.
¿Porqué es importante saber usar LangChain?
LangChain es un framework que simplifica el proceso de crear interfaces con Aplicaciones Generativas (GenAI). Ofrece diversas herramientas que permiten crear un “pipeline” (flujo de datos) para poder procesar una entrada y controlar -por ejemplo- el formato de salida, verificar los datos, enriquecerlos. También permite tener control sobre el histórico de mensajes que maneja el LLM, creando una sesión para mantener el hilo de una conversación, dándole capacidad de “Memoria” a nuestra app.
Otro punto importante es la creación de RAGs… Cómo bien sabes, cuando se entrenan los grandes modelos de lenguaje, se utiliza información de internet “temporal”; es decir, imaginemos que entrenamos un modelo con información hasta 2021, pues entonces el modelo entrenado no puede saber quién ganó el mundial de fútbol de 2023. Un RAG (Retrieval Augmented Generation) es un sistema que creamos que inyecta dinámicamente la “nueva información” en el Prompt del modelo para que el LLM pueda responder información actualizada. Otro caso de uso muy común es que tengamos un LLM privado al que proveemos documentos de una empresa, (imaginemos resoluciones legales ó actas) y aprovechamos el RAG para poder hacer consultas sobre esos documentos.
Volviendo a LangChain, posibilita el uso de “templates” que nos facilitan la creación tareas repetitivas, o template de RAGs, por ejemplo con conectores a “bases de datos vectoriales”, necesarios para poder acceder/consultar los documentos privados de manera escalable.
NOTA: Si bien en este artículo veremos la API de Langchain; lo cierto es que en este año (2025) está dando un giro importante sobre su versión “más profesional” llamada LangGraph, sobre la cual hablaremos en un próximo artículo.
Setup inicial – Requerimientos para empezar!
1-Crear un nuevo environment con la version Python 3.11 con Anaconda (o si lo prefieres con Poetry, pyenv, etc).
2-Instalar librerías y versiones. Para esta jupyter notebook utilizaremos:
1 2 3 4 |
pip install langchain==0.3.20 langchain-openai langchain-community # para Ejercicio con agente pip install wikipedia langchain_experimental numexpr duckduckgo-search ddgs |
3-Ejecutar el servidor de LM Studio con un modelo local (gratis!) ó necesitas la suscripción de pago a un proveedor y modelo por ej. GPT4o-mini de OpenAI, para ello, agrega la API_KEY en el ambiente de trabajo.
Modelos, Prompts y Parsers
Veamos algunos conceptos básicos que utilizaremos en LangChain para comenzar. Con “modelos” nos referimos a los modelos de lenguaje; por ejemplo si usamos un modelo “Llama” en Local, o DeepSeek o si usamos gpt-4o mediante API. Los Prompts son inputs para pasar al modelo, la entrada con las instrucciones. Y los parsers son los que toman la salida del modelo y modifican el formato para que sea útil.
Cuando haces una app con LangChain la idea es que tu “motor” sea el LLM; por lo tanto estará “repitiendo tareas” (automatizando) y necesitarás tener control tanto sobre las entradas, como de las salidas, para que la maquinaria funcione a la perfección, tal y como lo esperas.
Para empezar a hacer código creamos un objeto “Chat” de LangChain, desde donde conectamos cualquier modelo que tengamos corriendo en local, en nuestro ejemplo mediante LMStudio. Si utilizas un API utiliza este enlace de LangChain donde te muestra como instanciar el mismo objeto para diversos modelos que soporta: Groq, Mistral, Gemini, Antropic…
En nuestro ejemplo usamos el tipo “ChatOpenAI” que nos sirve de wrapper, aunque realmente conectaremos cualquier modelo local.
1 2 3 4 5 6 |
from langchain.chat_models import ChatOpenAI llm = ChatOpenAI( openai_api_base="http://localhost:1234/v1", openai_api_key="lmstudio", ) |
Usamos el chat, de manera directa llamando al método invoke() enviando un string e imprimimos la salida (atributo content). Aprovechamos en la Jupyter Notebook la función de Markdown para que imprima “bonito” en pantalla.
1 2 3 4 5 |
from IPython.display import Markdown ai_msg = llm.invoke("Dime 5 sitios bonitos para visitar en Galicia") md_text = ai_msg.content Markdown(md_text) |
Ahora, en vez de pasar un string directamente, usaremos un Template, que nos empezará a dar juego por ejemplo para el reemplazo de variables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from langchain.prompts import ChatPromptTemplate template_string = "Dime {cantidad} sitios bonitos para visitar en {lugar}" # Creamos el Template prompt_template = ChatPromptTemplate.from_template(template_string) # Asignamos valor cantidad = "2" lugar = "Roma" message = prompt_template.format_messages( cantidad=cantidad, lugar=lugar) # Ejecutamos el LLM ai_msg = llm.invoke(message) Markdown(ai_msg.content) |
Vamos a crear un Parser de texto para utilizar a continuación; este parser es muy sencillo pero luego veremos que hay parsers más avanzados para formato Json u objetos Pydantic.
1 2 3 |
from langchain_core.output_parsers import StrOutputParser txt_parser = StrOutputParser() |
Cadenas!
Veamos cómo podemos ejecutar el Template en formato cadena. Para ello usamos el carácter especial “pipe” | de la siguiente manera:
1 2 3 4 5 |
chain = prompt_template | llm | txt_parser txt_msg = chain.invoke({"cantidad":"3", "lugar":"Mexico DF"}) Markdown(txt_msg) |
Entonces cuando llamamos al método “invoke()” de la cadena, pasamos las variables de entrada como un diccionario python. En nuestro ejemplo son cantidad y lugar. El método invoke llamará en cadena la implementación de esa función en cada una de las componentes hasta finalizar.
Para hacer la salida con formato JSON, usamos el parser ya existente de LangChain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from langchain.prompts import PromptTemplate from langchain_core.output_parsers import JsonOutputParser json_parser = JsonOutputParser() prompt_template = PromptTemplate( template="Dime {cantidad} sitios bonitos para visitar en {lugar}.\n{format_instructions}", input_variables=["cantidad", "lugar"], partial_variables={"format_instructions": json_parser.get_format_instructions()}, ) chain = prompt_template | llm | json_parser json_msg = chain.invoke({"cantidad": "4", "lugar": "Santiago de Chile"}) json_msg |
Salida Estructurada: Objetos Python con Pydantic
Podemos aprovechar las ventajas de Pydantic para definir objetos Python y tener validación automática. Para ello utilizamos el PydanticOutputParser; vemos un ejemplo:
NOTA: no todos los modelos soportan salida estructurada! Revisa en la tabla de Featured Providers si el modelo que vas a utilizar acepta “Structured Output“. En caso de no soportarlo recibiras un error de tipo “NotImplemented” o similar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
from typing import List from pydantic import BaseModel, Field from langchain.output_parsers import PydanticOutputParser # definimos las clases Pydantic class Punto_de_Interes(BaseModel): """Descripcion de cada punto de interés.""" nombre: str = Field(description="Nombre del punto de interés.") descripcion: str = Field(description="Breve descripción del punto de interés y su relevancia.") class Sitio(BaseModel): """Sitio turístico para visitar.""" nombre: str = Field(description="Nombre del lugar.") puntos_de_interes: List[Punto_de_Interes] = Field(description="Lista que contiene la cantidad de puntos solicitados por el usuario.") pydantic_parser = PydanticOutputParser(pydantic_object=Sitio) prompt_template = PromptTemplate( template="Dime {cantidad} sitios bonitos para visitar en {lugar}.\nDevuelve un objeto JSON que siga exactamente este formato: {format_instructions}", input_variables=["cantidad", "lugar"], partial_variables={"format_instructions": pydantic_parser.get_format_instructions()}, ) chain = prompt_template | llm | pydantic_parser objeto_sitio = chain.invoke({"cantidad":2, "lugar": "Miami"}) objeto_sitio |
El objeto que retorna es un objeto Python y puedes acceder a sus atributos, iterar, etc.
Memoria
Cuando interactúas con un LLM por defecto no recuerdan las conversaciones previas. Cada request es “stateless” (sin estado) e independiente de los anteriores.
Cada vez que hacemos una consulta al LLM, deberemos pasar el historial previo, si queremos que tenga el contexto del pasado. De otro modo, en vez de ser un “chat”, o una charla, serán peticiones aisladas.
LangChain ofrece diversos mecanismos para administrar memoria. En este ejemplo administraremos la memoria manualmente usando una cola de Python que mantendrá los últimos 50 mensajes. Veamos el código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage from langchain_core.prompts import MessagesPlaceholder from collections import deque prompt_template = ChatPromptTemplate.from_messages( [ SystemMessage( content="Eres un asistente que responde preguntas en Español." ), MessagesPlaceholder(variable_name="messages"), ] ) # almacenará los últimos 50 mensajes lista_de_mensajes = deque(maxlen=50) lista_de_mensajes.append(HumanMessage(content="Hola, Mi color favorito es el azul.")) lista_de_mensajes.append(AIMessage(content="Hola ¿En qué puedo ayudarte?.")) lista_de_mensajes.append(HumanMessage(content="De que tamaño es una pelotita de tenis?")) chain = prompt_template | llm ai_msg = chain.invoke( { "messages": list(lista_de_mensajes) } ) print(ai_msg.content) |
Y ahora ponemos a prueba su memoria:
1 2 3 4 5 6 7 8 9 10 11 12 |
# agrego a la memoria el mensaje previo lista_de_mensajes.append(AIMessage(content=ai_msg.content)) # preguntamos lista_de_mensajes.append(HumanMessage(content="¿Cuál es mi color favorito?")) ai_msg = chain.invoke( { "messages": list(lista_de_mensajes) } ) print(ai_msg.content) |
Si todo va bien, deberá recordar que el color favorito era el azul.
Flujo de Datos: Secuencial, Paralelo y Router
Otro aspecto que nos permite realizar LangChain, son diversos flujos de datos, en donde podemos diagramar distintas arquitecturas para nuestro sistema. Los 3 bloques básicos que veremos a continuación, con ejemplos en código, son la ejecución en secuencia, en paralelo y ramificaciones utilizando un router. Estos son los “ladrillos básicos”; esto quiere decir, que podemos seguir extendiendo el pipeline combinando esos mismos 3 bloques y aumentando el tamaño de nuestras cadenas.
Sequential chains
Para ejecutar en secuencia, podemos simplemente extender la cadena que utilizamos e ir alimentando con la salidas “previas” al próximo llamado. En este ejemplo veremos que utilizamos la salida del primer Prompt como entrada en el segundo.
1 2 3 4 5 6 7 8 9 |
prompt_1 = ChatPromptTemplate.from_template(template="Crea tres títulos cortos y atrapantes para animar a quien lea a visitar {lugar}") prompt_2 = ChatPromptTemplate.from_template(template="Crea un parrafo corto en base a estos títulos: {titulos}. Que el texto incluya puntos de interés. Al principio del parrafo; incluye el mejor título y descarta el resto.") chain = prompt_1 | llm | {'titulos' : txt_parser} | prompt_2 | llm | txt_parser articulo = chain.invoke({"lugar": "Sidney, Australia"}) Markdown(articulo) |
Cadenas en Paralelo
Ahora usaremos consultas en paralelo al LLM (realmente al proveedor del servicio). Si ejecutamos en local, dependiendo del poder de nuestro ordenador, podrá manejar en simultáneo las peticiones, o será lento y parecido a un llamado secuencial. Pero en la práctica si utilizamos un servicio API externo como Google Gemini, OpenAi u otro, notaremos un aumento sustancial en la velocidad de procesamiento del pipeline.
En el siguiente ejemplo, realizamos 2 peticiones en simultáneo y unimos el resultado para generar una salida con las 2 respuestas anteriores.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from langchain_core.runnables import RunnableLambda prompt_1 = ChatPromptTemplate.from_template(template="Dame la población de {lugar}. Responde únicamente el número. Sin Markdown.") prompt_2 = ChatPromptTemplate.from_template(template="Dime la comida típica de: {lugar}. Responde con muy pocas palabras. Sin Markdown.") def combinar_salidas(inputs): return f"""Población: {inputs['salida_1']}\n Comida típica: {inputs['salida_2']}""" chain_1 = prompt_1 | llm | txt_parser chain_2 = prompt_2 | llm | txt_parser chain = {'salida_1' : chain_1, 'salida_2' : chain_2 } | RunnableLambda(combinar_salidas) salida = chain.invoke({"lugar": "Bélgica"}) print(salida) |
Cuando invocamos la cadena principal, ésta se encargará de llamar en paralelo las sub-tareas pasando el mismo parámetro de entrada.
Ramificaciones mediante Router
En este caso, decidimos el camino que ejecutará la cadena, dependiendo de alguno de los outputs. Entonces el flujo tomará un camino especializado.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# Creamos los Templates prompt_1 = ChatPromptTemplate.from_template(template="Eres un experto sobre el Espacio y los planetas. Responde detalladamente a la siguiente pregunta: {pregunta}") prompt_2 = ChatPromptTemplate.from_template(template="Eres muy fan del fútbol. Responde la pregunta dando analogías del mundo del fútbol: {pregunta}") prompt_0 = ChatPromptTemplate.from_template(template="Dime si la siguiente pregunta es acerca de un planeta, astronomía o el espacio. Pregunta: {pregunta}. Responde únicamente con SI o NO.") # Creamos las sub-cadenas chain_1 = prompt_1 | llm | txt_parser chain_2 = prompt_2 | llm | txt_parser chain_0 = prompt_0 | llm | txt_parser # creamos el enrutador def router(input): es_planeta = chain_0.invoke({'pregunta': input["pregunta"]}) if "SI" in es_planeta: print("Pregunta sobre planetas") return chain_1 else: print("No es pregunta sobre planetas") return chain_2 # creamos la cadena principal router_chain = RunnableLambda(router) # ejecutamos salida = router_chain.invoke({"pregunta": "¿Cuántas lunas tiene Júpiter?"}) Markdown(salida) |
Una buena idea sería aprovechar las salidas estructuradas (como la de Pydantic que vimos antes) en el Router, y luego continuar el flujo.
Agentes

Por último, veamos el uso de un agente sencillo; dotamos de diversas herramientas al agente y luego preguntamos. El Agente deberá seleccionar la herramienta adecuada y responder.
En este caso le daremos acceso a Wikipedia, al buscador DuckDuckGo y a un motor interno para operaciones matemáticas.
Primero hacemos los imports
1 2 3 |
from langchain.agents import load_tools, initialize_agent, Tool from langchain.agents import AgentType, tool from langchain.utilities import DuckDuckGoSearchAPIWrapper |
Definimos las herramientas
1 2 3 4 5 6 7 8 9 10 |
duck = DuckDuckGoSearchAPIWrapper(region="es-es", max_results=5) tools = load_tools(["llm-math","wikipedia"], llm=llm) ducktool = [Tool( name="duckduckgo", func=duck.run, description="Util para realizar búsquedas en internet.", ),] |
Y creamos al agente utilizando la función initialize_agent que provee Langchain:
1 2 3 4 5 6 |
agent = initialize_agent( tools=tools + ducktool, llm=llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, handle_parsing_errors=True, verbose = True) |
Y ahora hacemos la consulta; en este ejemplo lo resuelve consultando Wikipedia (o a veces buscando con DuckDuckGo).
1 2 |
pregunta = "Mafalda es una historieta de Argentina. ¿Cuándo murió su autor, Quino? Usa las herramientas a tu disposición para responder." result = agent(pregunta) |
Al crear el agente con “verbose = True” nos permite ver “el pensamiento” y herramientas usadas por el agente.

Dale TUS herramientas
También podemos crear herramientas propias y asignarlas al agente para que las use si lo ve necesario:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@tool def creador_de_motes(nombre: str) -> str: """Esta funcion recibe un nombre y devuelve un mote divertido.""" return "Crack" agent= initialize_agent( [creador_de_motes], llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, handle_parsing_errors=True, verbose = True) result = agent("Cual es un buen mote para el nombre Alejandro?.") |
Esto es muy útil para el ámbito empresarial, pues podemos aprovechar funcionalidad que ya tenemos hecha y dar acceso al agente para que le saque provecho.

Conclusión
El uso de LangChain se ha popularizado para el desarrollo de apps que utilizan Modelos de Lenguaje, hay personas que lo aman y otras que lo odian. A mi personalmente me parece que ofrece algunas ventajas para “estandarizar” y poder ordenar un poco el código, ofreciendo algunos patrones de diseño o “marco de trabajo” que podemos aprovechar y reutilizar. Además, permite crear una capa (bastante) independiente para poder usar un LLM y cambiarlo por otro sin conflicto. Esto es importante, porque al momento en el que queramos actualizar de gpt-3.5 a gpt-4 o a DeepSeek porque es más económico, lo podríamos hacer rápidamente; no tiene gran impacto a nivel código y puesta en producción. Inclusive si pasáramos de un modelo “sólo texto” a uno multimodal, con soporte a imágenes, video, sonido.
En el artículo vimos que podemos valernos de templates y parsers para controlar las entradas y salidas y también valernos de los bloques básicos secuencial, paralelo y router y combinarlos para construir flujos de datos complejos.
Finalmente dimos herramientas a un agente sencillo para delegar algunas funciones y darle mayor poder de actuación y decisión.
Tienes todo el código en el Repo de Github, cualquier duda o consulta, déjame tus comentarios.
En los próximos artículos, hablaremos más profundo sobre agentes y distintos tipos de RAGs.