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:
- Definición de Herramientas: Esquemas JSON.
- 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:
- 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.
- 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. - 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.
- 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.