Registro / artículo

Introducción a LangChain: Agentes, Tools y LLMs en Python

Por Backend Survivor

LangChain es un framework para construir aplicaciones potenciadas por LLMs. Su valor principal es simple: conectar modelos de lenguaje con herramientas del mundo real de forma estructurada.

La palabra framework aquí puede confundir. LangChain no es un modelo ni un motor de inferencia: es una capa de orquestación. Un LLM crudo, por sí solo, es una función que recibe texto y devuelve texto. Eso alcanza para un chatbot de juguete, pero no para una aplicación real, porque en el mundo real necesitas tres cosas más:

  1. Ejecutar acciones. El modelo tiene que poder consultar una API, leer un archivo o escribir en una base de datos. Los LLMs no hacen eso por sí mismos; solo dicen que quieren hacerlo.
  2. Recibir datos tipados. Si el resto de tu sistema es código común, no quieres parsear prosa en lenguaje natural. Quieres un objeto con campos conocidos.
  3. Encadenar pasos. Una pregunta puede requerir varias llamadas al modelo, con decisiones intermedias, antes de llegar a una respuesta.

LangChain pone esas piezas en un formato común y le entrega al modelo un contrato claro sobre qué puede hacer. Este artículo construye el caso mínimo: una tool, un agente, una pregunta. Sobre esa base se entiende todo lo demás.

Configuración previa

Para ejecutar este código necesitas:

uv add langchain langchain-google-genai pydantic python-dotenv requests

Usamos uv como gestor de paquetes. También necesitas un archivo .env con tu API key de Google:

GOOGLE_API_KEY=tu_api_key_aquí

Vamos a desglosar el ejemplo paso a paso.

Cada dependencia cumple un rol concreto y conviene saber cuál, porque en errores de importación casi siempre está la pista:

  • langchain — el paquete principal: create_agent, el decorador @tool y los tipos de mensaje.
  • langchain-google-genai — el conector (integración) que traduce entre la interfaz genérica de LangChain y la API real de Google. LangChain es agnóstico del proveedor; este paquete es el que ata el ejemplo a Gemini.
  • pydantic — validación y modelado de datos. Es la pieza que convierte la respuesta del modelo en un objeto tipado y verificable.
  • python-dotenv — lee el archivo .env y carga GOOGLE_API_KEY en el entorno del proceso.
  • requests — HTTP puro, para llamar a la API del clima desde dentro de la tool.

Sobre el .env: es la forma estándar de no meter secretos en el código. Añádelo siempre a tu .gitignore, porque una API key publicada en un repositorio se puede filtrar en segundos y facturar en tu cuenta. Si alguna vez lo haces, no la borres: rótala en la consola de Google, porque ya está comprometida.

Con uv, uv run main.py ejecuta el script en el entorno del proyecto, sin que tengas que activar un virtualenv a mano.

Estructura básica

from langchain.agents import create_agent
from langchain.tools import tool
from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
from langchain.messages import HumanMessage, SystemMessage
from pydantic import BaseModel, Field
from dotenv import load_dotenv
import requests

load_dotenv()

Los componentes clave son:

  • LLM: El modelo de lenguaje (aquí Gemini via Google)
  • Tools: Funciones que el agente puede ejecutar
  • Agent: El orquestador que decide qué tool usar según la consulta
  • Messages: El formato de comunicación con el LLM

Leídas en orden, esas cuatro líneas son el mapa mental de todo el ejemplo:

  • create_agent es la fábrica del orquestador. En la versión 1 de LangChain está construida sobre LangGraph, es decir, el agente no es un bucle improvisado sino un grafo con estados y transiciones bien definidos.
  • tool es el decorador que convierte una función Python normal en algo que el modelo puede invocar. La diferencia entre una función y una tool no es cosmética: una tool lleva adjunto un esquema con su nombre, su descripción y sus parámetros, y ese esquema es lo que el modelo ve.
  • Las clases de mensaje (SystemMessage, HumanMessage) son el idioma común entre tú y cualquier proveedor. load_dotenv() corre antes que nada porque las integraciones leen la API key del entorno al inicializarse; si lo llamas después de crear el modelo, no la encuentra.

Definiendo estructuras de datos con Pydantic

Antes de crear tools, definimos modelos para datos estructurados:

class WeatherResponse(BaseModel):
    """Schema para la respuesta del clima de una ciudad."""
    weather: str = Field("Clima de la ciudad")
    message: str = Field("Mensaje que se imprimirá al usuario")
    city: str = Field("Ciudad de la petición del usuario")

Pydantic no es opcional aquí: LangChain lo usa para validar inputs/outputs y dar al LLM schemas claros de qué datos esperar.

Vale la pena entender por qué esto importa tanto, porque es el punto donde más gente se salta el paso y luego pelea con el parseo.

Un LLM, por defecto, devuelve texto libre. Si le pides “respóndeme con el clima y la ciudad”, te puede contestar Madrid está nublado, o un JSON, o un párrafo con emojis. Como ser humano lo entiendes; como programa, eso es una pesadilla. WeatherResponse resuelve el problema de raíz: describe tres campos (weather, message, city), cada uno con su tipo, y a partir de esa clase LangChain genera un JSON Schema que se le inyecta al modelo como parte de la petición. Ya no estás rogando por un formato: se lo estás exigiendo de forma estructurada.

Hay dos detalles finos en el código:

  • BaseModel es la clase base de Pydantic. Heredar de ella activa la validación automática: si el modelo devuelve un número donde esperas un str, la validación lo detecta.
  • Field("...") aquí se está usando como descripción, no como valor por defecto (para eso sería Field(default=...)). Esa cadena viaja al LLM como documentación del campo, así que escribirla bien mejora la calidad de las respuestas mucho más de lo que parece.

La consecuencia práctica: result["structured_response"] no es texto que tienes que parsear, es una instancia de WeatherResponse. Puedes hacer result["structured_response"].city con autocompletado y, si algún campo falta, te enteras en ese punto y no tres capas más abajo.

Creando Tools

Las tools son funciones Python decoradas con @tool. El LLM las usa cuando decide que necesita información externa:

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima para una ciudad dada."""
    result = requests.get("https://wttr.in/" + city + "?format=j1")
    json = result.json()
    weather = json["current_condition"][0]["weatherDesc"][0]["value"]
    return weather

Punto clave: el docstring es CRÍTICO. El LLM lo usa para entender CUÁNDO llamar a esa tool. Si el docstring es vago, el agente no sabrá cuándo usarla.

Vale la pena desarmar qué hace el decorador @tool, porque es pura conveniencia sobre algo que puedes hacer a mano. Cuando decoras la función, LangChain inspecciona su firma y genera automáticamente un esquema con:

  • el nombre (get_weather), que es el identificador que el modelo invoca;
  • la descripción, tomada del docstring, que es lo que el modelo lee para decidir si la tool aplica;
  • los parámetros, deducidos de las anotaciones de tipo (city: str), con su obligatoriedad.

Ese esquema es lo único que el modelo ve. Nunca ve el cuerpo de la función. Para el LLM, get_weather no es código: es una capacidad descrita en lenguaje natural junto a una lista de argumentos. Por eso el docstring no es un comentario decorativo; es la interfaz de usuario de tu función, escrita para un lector que no puede leer el código.

De ahí salen dos consecuencias prácticas:

  • Anota los tipos siempre. Sin city: str, el modelo no sabe que el argumento es una cadena, y las llamadas se vuelven una lotería. Las anotaciones no son opcionales cuando la función es una tool.
  • Describe el contrato, no la implementación. Un docstring como """Devuelve el clima actual de una ciudad a partir de su nombre.""" le dice al modelo cuándo usarla. Uno como """Hace una petición a wttr.in""" le dice cómo funciona por dentro, que es justo lo que no necesita.

Dentro del cuerpo, la función es Python corriente: hace la petición HTTP, navega el JSON anidado y devuelve un str limpio. LangChain no toca esa lógica. Su único trabajo es el pegamento: exponer la función y traducir la decisión del modelo en una llamada real de Python.

Configurando el LLM

llm = ChatGoogleGenerativeAI(model="gemini-3.5-flash-lite")

Aquí se instancia el chat model: el conector específico de Google. Lo importante es que cumple una interfaz común. Si mañana quieres cambiar de proveedor —OpenAI, Anthropic, un modelo local— en teoría cambias esta línea y el resto del ejemplo sigue funcionando, porque el agente solo habla el idioma genérico de LangChain.

El nombre del modelo importa más de lo que parece:

  • Los sufijos flash y lite señalan modelos optimizados para latencia y coste. Perfectos para un ejemplo con un agente que puede dar varias vueltas.
  • Un modelo pro daría respuestas más elaboradas pero más lentas y caras, y en una tarea con una tool tan simple no compensa.
  • Para agentes, la capacidad de tool calling es obligatoria. Modelos muy pequeños a veces no soportan el mecanismo de invocar funciones, y ahí el agente falla de formas confusas.

Y un detalle que muerde a quien llega desde el SDK directo: como load_dotenv() ya corrió, este conector encuentra GOOGLE_API_KEY solo. No hay que pasar la clave a mano. Si te da un error de credenciales, el primer sitio donde mirar es el orden de las líneas y el nombre exacto de la variable.

Creando y ejecutando el Agente

tools = [get_weather]
agent = create_agent(model=llm, tools=tools, response_format=WeatherResponse)

result = agent.invoke({
    "messages": [
        SystemMessage(content="Eres un asistente útil que puede responder preguntas sobre el clima."),
        HumanMessage(content="¿Qué tiempo hace en Madrid?")
    ]
})
print(result["structured_response"].message)

create_agent recibe:

  • model: el LLM a usar
  • tools: lista de tools disponibles
  • response_format: esquema de respuesta estructurada (Pydantic)

El agente decide automáticamente qué tool llamar basándose en la pregunta del usuario.

Conviene detenerse en invoke, porque es el corazón del ejemplo. Pasarle {"messages": [...]} es hablar el idioma de LangGraph: el agente no es una función que devuelve un valor, es un grafo que acumula estado. Esa lista de mensajes no es una entrada puntual, es el historial que va creciendo. Por eso se puede encadenar otra llamada y el agente ya recuerda lo anterior.

El bucle que se ejecuta por dentro es, a grandes rasgos, este:

  1. El modelo recibe el historial (el SystemMessage, el HumanMessage) y los esquemas de las tools.
  2. Decide que no puede responder solo, y en lugar de contestar genera una petición de tool: quiero llamar a get_weather con city="Madrid".
  3. LangChain intercepta esa petición, ejecuta la función Python real y añade el resultado al historial como un ToolMessage.
  4. El modelo recibe de nuevo el historial, ahora con el resultado, y esta vez sí responde.
  5. Como se definió response_format=WeatherResponse, esa respuesta final se valida contra el esquema y se adjunta como structured_response.

El SystemMessage merece un párrafo aparte. Es la instrucción permanente: describe el rol y las reglas del asistente, y el modelo la lee antes que nada. No es un truco de prompt, es parte del contrato. Nota que aquí el SystemMessage dice que el asistente responde “preguntas sobre el clima”, lo que ayuda a que el modelo no se vaya por las ramas. Un HumanMessage es, en cambio, la pregunta concreta del usuario.

Y como el bucle puede repetirse varias veces (el modelo podría necesitar encadenar tools), el agente tiene un límite de iteraciones. Si lo supera, corta. Ese límite es una red de seguridad: sin él, un modelo confundido podría dar vueltas indefinidamente quemando tokens y dinero.

Flujo completo

Usuario pregunta → Agent analiza → Decide tool → Ejecuta tool → LLM interpreta resultado → Respuesta estructurada

En nuestro ejemplo:

  1. Preguntamos “¿Qué tiempo hace en Madrid?”
  2. El agent reconoce que necesita el clima
  3. Llama a get_weather("Madrid")
  4. Recibe “fog”
  5. Lo formatea en el esquema WeatherResponse

La clave conceptual es que el modelo nunca ejecuta nada. Solo pide cosas, y el agente es quien las ejecuta. Esa separación es la que hace que un agente sea seguro y controlable: tú decides qué funciones se le ofrecen y, si quisieras, podrías interceptar cada llamada antes de que ocurra para validarla, registrarla o bloquearla. El LLM propone; el código dispone.

Fíjate también en dónde entra el prompt en este flujo. El usuario pregunta en lenguaje natural, pero lo que viaja al modelo es ese input junto a la lista de capacidades disponibles. El modelo elige una tool por su nombre y su descripción, y produce argumentos que LangChain valida contra el esquema antes de llamar a la función. Todo el puente entre “el usuario habla español” y “el código llama a una función Python” lo tiende el framework.

Ejecutando el ejemplo

Para ejecutar el código, en el directorio del proyecto:

uv run main.py

Salida real:

The weather in Madrid is currently fog.

Si esa salida no aparece, el camino del error casi siempre es el mismo y conviene revisarlo en orden:

  • Error de credenciales → revisa GOOGLE_API_KEY en el .env y que load_dotenv() se ejecute antes de crear el modelo.
  • El modelo no llama a la tool → casi siempre es el docstring de get_weather (demasiado vago) o la descripción de los campos del esquema. El modelo decide cuándo usar la tool leyendo texto, así que ahí es donde se arregla.
  • Respuesta sin structured_response → revisa que response_format apunte a la clase correcta y que el modelo haya terminado sin encadenar más tools.
  • La tool revienta a mitad → get_weather navega un JSON anidado de un servicio externo. Si la ciudad no existe o la API cambia el formato, un KeyError o un IndexError es lo esperado. En producción esto se envuelve en try/except y se devuelve al modelo un mensaje de error útil en lugar de una excepción, para que el agente pueda reaccionar.

Limitaciones y siguiente paso

Este ejemplo es simple a propósito: una tool, un agente, una pregunta. Pero ilustra los bloques fundamentales.

Donde LangChain brilla es cuando tienes múltiples tools y el agente debe decidir cuál usar, o cuando encadenas varias llamadas. Pero para muchos casos, una tool + un buen prompt es suficiente.

Vale la pena ser honesto sobre el coste de la abstracción. LangChain añade una capa que, en un caso trivial como este, es más ceremonia que beneficio: casi todo esto lo resolvías con unas pocas líneas del SDK de Google directo. El retorno aparece cuando la complejidad crece y esa ceremonia se convierte en estructura: cambiar de proveedor sin reescribir nada, compartir tools entre agentes, o insertar memoria y persistencia de estado sin montar tu propio sistema.

Con lo que ya tienes montado, los siguientes pasos naturales son:

  • Añadir más tools (por ejemplo, otra que consulte el pronóstico de varios días) y comprobar cómo el agente decide cuál usar sin que tú lo programes explícitamente.
  • Encadenar dos preguntas en el mismo historial y ver cómo el segundo invoke parte del estado anterior.
  • Interceptar la ejecución para registrar qué tool se llamó con qué argumentos: es el primer paso hacia la observabilidad de un agente en producción.

Con eso cubres la mecánica completa de un agente: mensajes, tools, ejecución y respuesta estructurada. Lo difícil no es el framework, sino diseñar qué capacidades le das al modelo y cuáles no.


Tags: langchain, python, ia, llm, agentes