Agentes y Tool Use: Arquitectura ReAct y Function Calling

Análisis de la arquitectura de agentes autónomos basada en el paradigma ReAct y la capacidad de Function Calling Se examina la formalización matemática del bucle de razonamiento-acción y la implementación de esquemas JSON para la ejecución determinista de herramientas.

Los Large Language Models (LLMs) son, por diseño, sistemas aislados limitados a su entrenamiento estático. El problema fundamental que aborda el concepto de Agentes es la incapacidad del modelo para interactuar con el entorno externo, obtener datos en tiempo real o realizar acciones computacionales deterministas (cálculos matemáticos, ejecución de código, consultas SQL).

La solución técnica estándar actual se basa en Function Calling (o Tool Use) y patrones de razonamiento como ReAct (Reason + Act). En este esquema, el LLM no solo genera texto conversacional, sino que estructurar salidas (generalmente JSON) que invocan funciones predefinidas. El sistema orquestador ejecuta estas funciones y devuelve el resultado al modelo, cerrando un bucle de retroalimentación. Este enfoque transforma al LLM de un motor de completado de texto a un motor de razonamiento y planificación capaz de orquestar software.


Fundamentos matemáticos

Un agente autónomo puede modelarse como una política de toma de decisiones secuencial sobre una trayectoria de interacciones. A diferencia de un LLM estándar que maximiza la probabilidad $P(y|x)$, un agente opera en un bucle de pasos discretos $t$.

Sea $\mathcal{A}$ el espacio de acciones posibles (herramientas) y $\mathcal{O}$ el espacio de observaciones (salidas de las herramientas). La trayectoria $\tau_t$ en el paso $t$ se define como:

$$\tau_t = (x, a_1, o_1, \dots, a_{t-1}, o_{t-1})$$

Donde $x$ es la instrucción inicial (prompt). El modelo debe predecir la siguiente acción $a_t$ basándose en la trayectoria histórica:

$$a_t \sim P_\theta(a | \tau_t)$$

Una vez seleccionada la acción (por ejemplo, get_weather("London")), el entorno (Environment) ejecuta la acción y produce una observación $o_t$:

$$o_t = \mathcal{E}(a_t)$$

El objetivo del agente es maximizar una función de recompensa o utilidad $R(\tau)$ (completar la tarea) actualizando el contexto:

$$\tau_{t+1} = \tau_t \cup (a_t, o_t)$$

En implementaciones modernas con Function Calling, la distribución $P_\theta$ se ve forzada a seguir una gramática específica (CFG - Context Free Grammar) o un esquema JSON, reduciendo el espacio de búsqueda de tokens válidos a aquellos que conforman una llamada a función sintácticamente correcta.

El siguiente diagrama ilustra el bucle de razonamiento-acción que fundamenta la arquitectura de agentes:


Implementación práctica

La siguiente implementación en Python ilustra un bucle básico de agente sin frameworks abstractos (como LangChain), para exponer la lógica de orquestación y manejo de historial.

Componentes:

  1. Definición de Herramientas: Esquemas JSON.
  2. Bucle de Ejecución: Detección de intención de llamada, ejecución y re-inyección al contexto.
import json
from typing import List, Dict, Any

# 1. Definición de herramientas (Mock)
def get_stock_price(ticker: str) -> str:
    # Simulación de llamada a API
    data = {"AAPL": 150.25, "MSFT": 320.50}
    price = data.get(ticker, "Unknown")
    return json.dumps({"ticker": ticker, "price": price})

tools_schema = [
    {
        "type": "function",
        "function": {
            "name": "get_stock_price",
            "description": "Obtiene el precio actual de una acción",
            "parameters": {
                "type": "object",
                "properties": {
                    "ticker": {"type": "string", "description": "Símbolo bursátil, ej: AAPL"}
                },
                "required": ["ticker"]
            }
        }
    }
]

# 2. Clase Agente Simplificada
class SimpleAgent:
    def __init__(self, client, model="gpt-4-turbo"):
        self.client = client
        self.model = model
        self.messages = []
        self.available_functions = {"get_stock_price": get_stock_price}

    def run(self, user_prompt: str):
        self.messages.append({"role": "user", "content": user_prompt})
        
        while True:
            # Inferencia del modelo
            response = self.client.chat.completions.create(
                model=self.model,
                messages=self.messages,
                tools=tools_schema,
                tool_choice="auto" 
            )
            
            msg = response.choices[0].message
            self.messages.append(msg) # Guardar respuesta en historial

            # Verificar si el modelo decidió usar una herramienta
            if msg.tool_calls:
                for tool_call in msg.tool_calls:
                    fn_name = tool_call.function.name
                    fn_args = json.loads(tool_call.function.arguments)
                    
                    print(f"[DEBUG] Ejecutando {fn_name} con args: {fn_args}")
                    
                    # Ejecución segura
                    if fn_name in self.available_functions:
                        tool_output = self.available_functions[fn_name](**fn_args)
                        
                        # Inyección de la observación (Observation Step)
                        self.messages.append({
                            "tool_call_id": tool_call.id,
                            "role": "tool",
                            "name": fn_name,
                            "content": tool_output
                        })
            else:
                # Si no hay llamadas a herramientas, es la respuesta final
                return msg.content

# Ejemplo de uso (pseudocódigo del cliente)
# agent = SimpleAgent(openai_client)
# result = agent.run("¿Cuánto cuesta Apple comparado con Microsoft?")


Análisis de comportamiento

Al desplegar agentes con capacidad de uso de herramientas en producción, se observan comportamientos críticos:

  • Alucinación de Argumentos: Aunque el modelo entienda qué herramienta usar, frecuentemente alucina los argumentos (e.g., inventar un ID de base de datos que no existe). Esto requiere validación estricta pre-ejecución.
  • Bucles Infinitos: Si una herramienta devuelve un error (e.g., Error 404), el agente tiende a reintentar la misma acción idéntica repetidamente, saturando el context window y aumentando costos, a menos que se implemente un manejo de excepciones explícito en el system prompt o en la lógica de control.
  • Degradación del Contexto: Cada paso del bucle (Pensamiento $\rightarrow$ Acción $\rightarrow$ Observación) consume tokens. En tareas de múltiples pasos, el contexto se llena de JSONs y logs de ejecución, diluyendo la instrucción original y aumentando la latencia linealmente.

Comparativas o referencias técnicas

Comparación del enfoque de Agentes vs. Prompt Engineering tradicional:

Característica Prompt Engineering (RAG Estático) Agentes (ReAct / Tool Use)
Flujo de Control Lineal / Predefinido Dinámico / Cíclico
Determinismo Alto (Retrieval fijo) Bajo (El modelo decide la ruta)
Coste de Inferencia 1 llamada ($O(1)$) $N$ llamadas ($O(N)$)
Capacidad de Resolución Consultas informativas Tareas de múltiples pasos y escritura
Latencia Baja (< 2s) Alta (> 10s dependiendo de los pasos)

Técnicamente, el Native Function Calling (soportado vía fine-tuning en modelos como GPT-4 o Claude 3) supera significativamente a las estrategias basadas en parseo de expresiones regulares (usadas en modelos antiguos como text-davinci-003), reduciendo los errores de sintaxis JSON en un 80-90% según benchmarks internos de proveedores.


Limitaciones y casos donde no conviene usarlo

El uso de agentes autónomos no es una solución universal y presenta riesgos técnicos severos:

  1. Latencia inaceptable: Para aplicaciones en tiempo real (SLA < 500ms), el round-trip de generar argumentos, ejecutar código y volver a generar respuesta es prohibitivo.
  2. Seguridad (Prompt Injection): Si un agente tiene acceso a herramientas de modificación (e.g., delete_user, send_email), un ataque de inyección en el prompt del usuario puede secuestrar el flujo de control y ejecutar acciones destructivas. El aislamiento (sandboxing) es obligatorio.
  3. Ambigüedad en selección de herramientas: Cuando el número de herramientas crece (>15-20), la precisión del modelo para seleccionar la correcta disminuye (recall degradation), requiriendo estrategias de recuperación de herramientas (Tool Retrieval) antes de la inferencia.
  4. Costos impredecibles: Debido a los bucles de reintento automático y cadenas de pensamiento extensas, el consumo de tokens puede dispararse en una sola consulta mal formulada.