LLMs & Agents
LangGraph
LangChain
Python
agentes de IA
ReAct
LLMs

Agentes de IA con LangGraph para Análisis de Datos

Construye un agente autónomo de análisis de datos con LangGraph: implementa el ciclo ReAct con herramientas y aprende cuándo usar agentes vs RAG.

18 de junio de 2026·7 min read

En un artículo anterior mostré cómo construir un sistema RAG con LangChain: indexas documentos, recuperas los fragmentos más relevantes y se los pasas a un LLM. Eso funciona bien cuando la respuesta ya existe en algún lugar de tus documentos y solo necesitas encontrarla.

Pero ¿qué pasa si la pregunta requiere múltiples pasos? "¿Qué clientes se fueron el mes pasado y qué tenían en común?" — esto requiere cargar datos, filtrar, agregar e interpretar resultados. Un sistema RAG no puede hacer eso. Un agente sí — y en este artículo construirás uno con LangGraph: un agente de análisis de datos que carga CSVs, ejecuta consultas de pandas y reporta sus hallazgos con números.

Agentes vs. RAG: La Diferencia Clave

Un sistema RAG es un pipeline de un solo paso: recuperar → generar.

Un agente es un ciclo:

  1. Razonar: dado el objetivo y el estado actual, ¿qué debo hacer a continuación?
  2. Actuar: llamar a una herramienta (ejecutar SQL, cargar un CSV, graficar)
  3. Observar: leer el resultado de la herramienta
  4. Volver al paso 1 hasta lograr el objetivo

Este es el patrón ReAct (Reason + Act). LangGraph facilita implementarlo como una máquina de estados explícita.

Instalación

bash
pip install langgraph langchain langchain-openai pandas numpy

Necesitarás una clave de API de OpenAI (o puedes cambiar el modelo por cualquier LLM compatible con LangChain):

bash
export OPENAI_API_KEY="sk-..."

Definiendo el Estado del Agente

LangGraph modela el agente como un grafo dirigido donde cada nodo transforma un estado compartido. Empezamos definiendo ese estado:

python
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator

class EstadoAgente(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]
    datos_cargados: bool
    resumen_dataset: str

messages acumula el historial de conversación. operator.add le dice a LangGraph que agregue mensajes nuevos en vez de reemplazar la lista. Los otros campos le dan al agente memoria de trabajo entre turnos.

Construyendo las Herramientas

Las herramientas son las acciones que puede tomar el agente. Definimos tres:

python
import pandas as pd
import numpy as np
from langchain_core.tools import tool

# "Base de datos" en memoria para el demo
_df: pd.DataFrame | None = None

@tool
def cargar_dataset(path: str) -> str:
    """Carga un dataset CSV en memoria. Devuelve un resumen de columnas y forma."""
    global _df
    _df = pd.read_csv(path)
    resumen = (
        f"Dataset cargado: {_df.shape[0]} filas × {_df.shape[1]} columnas.\n"
        f"Columnas: {', '.join(_df.columns)}\n"
        f"Tipos:\n{_df.dtypes.to_string()}"
    )
    return resumen

@tool
def ejecutar_consulta(consulta: str) -> str:
    """
    Ejecuta una consulta pandas sobre el dataset cargado.
    Usa expresiones pandas estándar, por ejemplo:
      - df[df['churn'] == 1]['cargo_mensual'].mean()
      - df.groupby('plan')['churn'].value_counts()
    La variable 'df' se refiere al dataset cargado.
    """
    global _df
    if _df is None:
        return "No hay dataset cargado. Llama a cargar_dataset primero."
    try:
        resultado = eval(consulta, {'df': _df, 'pd': pd, 'np': np})
        return str(resultado)
    except Exception as e:
        return f"Error en consulta: {e}"

@tool
def describir_columna(nombre_columna: str) -> str:
    """Devuelve estadísticas descriptivas para una columna específica del dataset cargado."""
    global _df
    if _df is None:
        return "No hay dataset cargado."
    if nombre_columna not in _df.columns:
        return f"Columna '{nombre_columna}' no encontrada. Disponibles: {list(_df.columns)}"
    stats = _df[nombre_columna].describe().to_string()
    pct_nulos = _df[nombre_columna].isnull().mean() * 100
    return f"{stats}\nValores nulos: {pct_nulos:.1f}%"

herramientas = [cargar_dataset, ejecutar_consulta, describir_columna]

Una advertencia sobre ejecutar_consulta: pasar código generado por el LLM a eval está bien para un demo local, pero nunca lo hagas en producción — una consulta con prompt injection podría ejecutar código arbitrario. Aísla la ejecución (proceso restringido, contenedor) o usa un evaluador seguro de expresiones.

Construyendo el Grafo

Ahora conectamos el agente usando LangGraph:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode

# LLM con herramientas vinculadas
llm = ChatOpenAI(model='gpt-4o-mini', temperature=0)
llm_con_herramientas = llm.bind_tools(herramientas)

PROMPT_SISTEMA = """Eres un asistente de análisis de datos. Tienes acceso a herramientas 
que te permiten cargar datasets CSV, ejecutar consultas pandas y describir columnas.

Cuando el usuario haga una pregunta:
1. Carga el dataset si aún no está cargado.
2. Explora los datos para entender su estructura.
3. Ejecuta las consultas específicas necesarias para responder la pregunta.
4. Resume tus hallazgos claramente, con números.

Siempre verifica que tus consultas funcionaron antes de sacar conclusiones."""

def nodo_agente(state: EstadoAgente) -> EstadoAgente:
    """El nodo de razonamiento: llama al LLM para decidir la siguiente acción."""
    messages = [SystemMessage(content=PROMPT_SISTEMA)] + list(state['messages'])
    respuesta = llm_con_herramientas.invoke(messages)
    return {'messages': [respuesta]}

def debe_continuar(state: EstadoAgente) -> str:
    """Función de enrutamiento: ¿continuar el ciclo o terminar?"""
    ultimo_mensaje = state['messages'][-1]
    if hasattr(ultimo_mensaje, 'tool_calls') and ultimo_mensaje.tool_calls:
        return 'herramientas'
    return END

# Construir el grafo
nodo_herramientas = ToolNode(herramientas)

grafo = StateGraph(EstadoAgente)
grafo.add_node('agente', nodo_agente)
grafo.add_node('herramientas', nodo_herramientas)

grafo.set_entry_point('agente')
grafo.add_conditional_edges('agente', debe_continuar)
grafo.add_edge('herramientas', 'agente')  # Después de la llamada, vuelve al agente

agente = grafo.compile()

El grafo tiene dos nodos — agente y herramientas — conectados en un ciclo. Después de cada llamada a herramienta, el control regresa al nodo agente, que razona sobre el resultado y decide si llamar otra herramienta o detenerse.

Ejecutando el Agente

python
from langchain_core.messages import HumanMessage

def ejecutar_agente(pregunta: str, data_path: str | None = None):
    mensaje_inicial = HumanMessage(
        content=f"Ruta del dataset: {data_path}\n\nPregunta: {pregunta}"
        if data_path else pregunta
    )

    resultado = agente.invoke(
        {'messages': [mensaje_inicial], 'datos_cargados': False, 'resumen_dataset': ''},
        config={'recursion_limit': 15},
    )

    mensaje_final = resultado['messages'][-1]
    return mensaje_final.content

# Ejemplo: analizar un dataset de muestra
respuesta = ejecutar_agente(
    pregunta="¿Cuál es la tasa de churn por número de productos? ¿Qué grupo tiene el mayor churn?",
    data_path="datos/churn.csv",
)
print(respuesta)

Una salida de muestra (la respuesta final del agente después de múltiples llamadas a herramientas):

Cargué el dataset (2000 filas × 6 columnas) y ejecuté el análisis.

Tasa de churn por número de productos:
  1 producto:   42.1%  ← mayor churn
  2 productos:  31.7%
  3 productos:  22.4%
  4 productos:  15.8%
  5 productos:   9.3%  ← menor churn

Los clientes con solo 1 producto tienen la mayor tasa de churn con 42.1%, 
que es 4.5× más alta que los clientes con 5 productos. Esto sugiere que 
la venta cruzada de productos adicionales mejora significativamente la retención.

Inspeccionando el Razonamiento del Agente

Para depurar qué hizo el agente en cada paso:

python
from langchain_core.messages import AIMessage, ToolMessage

resultado = agente.invoke({
    'messages': [HumanMessage(content="Carga datos/churn.csv y dime el cargo mensual promedio para clientes que se fueron vs los que se quedaron.")],
    'datos_cargados': False,
    'resumen_dataset': '',
})

for msg in resultado['messages']:
    if isinstance(msg, AIMessage) and msg.tool_calls:
        for tc in msg.tool_calls:
            print(f"→ Llamada: {tc['name']}({tc['args']})")
    elif isinstance(msg, ToolMessage):
        print(f"← Resultado: {msg.content[:200]}")
    elif isinstance(msg, AIMessage):
        print(f"✓ Respuesta final: {msg.content}")
→ Llamada: cargar_dataset({'path': 'datos/churn.csv'})
← Resultado: Dataset cargado: 2000 filas × 6 columnas...
→ Llamada: ejecutar_consulta({'consulta': "df.groupby('churn')['cargo_mensual'].mean()"})
← Resultado: churn\n0    58.3\n1    79.2
✓ Respuesta final: Los clientes que se fueron pagan significativamente más en promedio ($79.2/mes)...

Agentes vs. RAG: Cuándo Usar Cada Uno

EscenarioUsar
"Resume este contrato de 50 páginas"RAG
"¿Qué cláusula limita la responsabilidad?"RAG
"¿Cuál fue nuestra tasa de churn en Q3?"Agente
"Compara el churn entre los tres niveles de producto"Agente
"Encuentra los clientes más propensos a irse el próximo mes"Agente

La regla general: si la pregunta requiere cómputo o razonamiento multi-paso sobre datos estructurados, usa un agente. Si requiere encontrar y parafrasear información de documentos, usa RAG. Y si lo que necesitas es extraer campos fijos de documentos — facturas, contratos, ofertas de trabajo — no necesitas un agente: la extracción de datos estructurados con LLMs lo resuelve con una sola llamada validada.

Conclusión

LangGraph te da control explícito sobre el ciclo del agente — estado, transiciones y ejecución de herramientas — sin ocultar complejidad detrás de magia. El resultado es un sistema que puedes inspeccionar, depurar y extender. Desde aquí, puedes agregar más herramientas (conectores de base de datos, librerías de graficación, llamadas a API), persistir estado entre sesiones, o construir arquitecturas multi-agente donde agentes especializados colaboran.


Sobre el autor

Rodrigo Arenas es arquitecto de software e ingeniero de machine learning en Medellín, Colombia. Construye productos, plataformas y software open source para IA, Data Engineering y Machine Learning — es el creador de Ciaren, sklearn-genetic-opt y PyWorkforce.



Rodrigo Arenas — ML Engineer & Open Source Maintainer

¿Estás trabajando en algo similar? Puedo ayudarte a llevarlo a producción.

¿Vas a construir una plataforma de IA o de datos?

Productos, plataformas y software open source para IA, Data Engineering y Machine Learning.

Ver proyectos
Visión

Construyendo un ecosistema de productos y software open source para IA, Data Engineering y Machine Learning.

Proyectos
sklearn-genetic-optPyWorkforceCiaren

Iterixs (En construcción)

© 2026 Rodrigo Arenas