Arquitecturas de agentes: ReAct, Plan-and-Execute, Reflexion, LATS
Comparación sistemática de los patrones dominantes. ReAct intercala razonamiento y acción; Plan-and-Execute fija un plan y replanifica al desviarse; Reflexion aprende de fallos verbalmente; LATS añade Monte Carlo Tree Search al razonamiento.
01Objetivos de aprendizaje
Al finalizar esta clase, los estudiantes serán capaces de:
- Describir la arquitectura ReAct y explicar por qué intercalar razonamiento con acción mejora el rendimiento del agente.
- Comparar las arquitecturas Plan-and-Execute, Reflexión y LATS, y articular cuándo cada una es más apropiada.
- Implementar un agente ReAct mínimo desde cero en Python (sin frameworks).
- Analizar el bucle observar-pensar-actuar como el patrón común subyacente a todas las arquitecturas de agentes.
- Evaluar arquitecturas híbridas y personalizadas para casos de uso específicos.
- Comparar frameworks de agentes modernos (LangGraph, OpenAI Agents SDK, CrewAI) y comprender cómo implementan los patrones arquitectónicos fundamentales.
- Describir patrones emergentes como Agent-as-a-Service, RAG agéntico y agentes que se automejorán.
271. La necesidad de arquitecturas de agentes
1.1 Más allá del bucle simple
En la Semana 1, construimos un agente mínimo: llamar al LLM, verificar si hay llamadas a herramientas, ejecutarlas, repetir. Esto funciona para tareas simples, pero falla cuando:
- La tarea requiere planificación en múltiples pasos (escribir un informe de investigación con 10 secciones).
- El agente comete errores que necesita detectar y corregir.
- El agente necesita explorar múltiples enfoques y elegir el mejor.
- La tarea requiere razonamiento a largo plazo donde el agente debe mantenerse en el camino correcto a lo largo de muchos pasos.
Consideremos una analogía. El bucle simple del agente es como cocinar mirando los ingredientes que tienes delante y decidiendo qué hacer a continuación. Funciona para huevos revueltos. Pero para una cena de cinco platos para 30 personas, se necesita una receta (plan), un horario (dependencias), pruebas de sabor durante el proceso (evaluación) y la flexibilidad para ajustar cuando algo sale mal (replanificación). Las arquitecturas de agentes proporcionan esta estructura.
Las arquitecturas de agentes proporcionan patrones estructurados para cómo el LLM razona, actúa y aprende de la retroalimentación. Son al desarrollo de agentes lo que los patrones de diseño son a la ingeniería de software: soluciones probadas para problemas recurrentes.
Esta es posiblemente la clase más importante del curso. Las arquitecturas que se aprenden esta semana (ReAct, Plan-and-Execute, Reflexión y LATS) forman el vocabulario del diseño de agentes. Todo agente en producción que se encuentre está usando uno de estos patrones directamente o combinándolos de alguna manera.
1.2 El bucle observar-pensar-actuar
Toda arquitectura de agente es una variación del mismo bucle fundamental:
Interactive · El bucle Observar-Pensar-Actuar
El bucle del agente
El bucle del agente
Toda arquitectura agéntica vive sobre un mismo ciclo: percibir, razonar, actuar, observar. Las arquitecturas que verás más adelante son variaciones sobre este bucle.
El bucle del agente
Percepción
01 / 04
Las arquitecturas difieren en:
- Cuánto pensamiento ocurre antes de actuar.
- Si el agente reflexiona sobre sus acciones después del hecho.
- Cómo el agente explora caminos alternativos.
- Si la planificación se separa de la ejecución.
- Cómo se manejan los errores y se incorporan.
282. ReAct: razonamiento + acción
2.1 El artículo
Yao et al. (2023) introdujeron ReAct (Reasoning + Acting) en "ReAct: Synergizing Reasoning and Acting in Language Models", publicado en ICLR 2023. Este artículo es fundacional: estableció el patrón dominante para los agentes basados en LLM.
2.2 La idea central
Antes de ReAct, había dos líneas de investigación separadas:
- Chain-of-Thought (CoT): Los LLM razonan paso a paso pero no toman acciones.
- Agentes sólo de acción: Los agentes toman acciones basándose en observaciones pero no razonan explícitamente.
La idea de ReAct: intercalar razonamiento (pensamientos) con acciones en una sola generación.
Interactive · The ReAct Architecture
Simulador ReAct
Ejecuta un agente paso a paso
Elige una tarea y mira cómo el agente intercala pensamiento, acción y observación. La memoria y el historial de herramientas se llenan en tiempo real.
Elige una tarea
El agente aún no ha guardado nada.
Ninguna herramienta invocada todavía.
Question: What is the elevation of the birthplace of the inventor of the telephone?
Thought 1: I need to find out who invented the telephone.
Action 1: Search[inventor of the telephone]
Observation 1: Alexander Graham Bell is credited with inventing the telephone.
Thought 2: Now I need to find where Alexander Graham Bell was born.
Action 2: Search[Alexander Graham Bell birthplace]
Observation 2: Alexander Graham Bell was born in Edinburgh, Scotland.
Thought 3: Now I need to find the elevation of Edinburgh, Scotland.
Action 3: Search[elevation of Edinburgh Scotland]
Observation 3: Edinburgh has an average elevation of about 47 meters (154 feet).
Thought 4: I have all the information I need.
Action 4: Finish[47 meters (154 feet)]2.3 Por qué funciona ReAct
La sinergía entre razonamiento y acción no es simplemente aditiva; es multiplicativa. Cada componente mejora al otro:
- El razonamiento fundamenta las acciones: El paso de pensamiento explica por qué el agente toma una acción particular, reduciendo las llamadas a herramientas aleatorias o irrelevantes. Sin razonamiento, un agente podría llamar a una herramienta de búsqueda con una consulta vaga. Con razonamiento, piensa "necesito encontrar el lugar de nacimiento específicamente, así que debería buscar 'Alexander Graham Bell birthplace'" y produce una consulta enfocada y efectiva.
- Las acciones fundamentan el razonamiento: Los resultados de las herramientas proporcionan información factual que mantiene la cadena de razonamiento precisa, reduciendo las alucinaciones. Sin acciones, el modelo podría "razonar" usando hechos fabricados. Con acciones, verifica su razonamiento contra datos reales.
- Transparencia: Los pensamientos intercalados hacen que el proceso de decisión del agente sea inspeccionable y depurable. Cuando un agente comete un error, se pueden leer sus pensamientos y comprender dónde falló el razonamiento.
- Recuperación de errores: Cuando una observación es inesperada, el agente puede razonar sobre qué salió mal y ajustar. "La búsqueda no devolvió resultados. Déjame probar con una consulta diferente."
Idea clave: La magia de ReAct está en el intercalado. Razonamiento puro sin acciones lleva a alucinación. Acción pura sin razonamiento lleva a un uso sin rumbo de herramientas. La combinación mantiene a ambos en el camino correcto.
2.4 ReAct vs. alternativas
Yao et al. (2023) compararon ReAct con varias líneas base en tareas intensivas en conocimiento (HotpotQA, FEVER) y tareas de toma de decisiones (ALFWorld, WebShop):
| Enfoque | Fortalezas | Debilidades |
|---|---|---|
| Solo CoT (sin acciones) | Buen razonamiento en problemas simples | Alucina hechos, no puede acceder a info actual |
| Solo acción (sin razonamiento) | Puede usar herramientas | Hace elecciones aleatorias o subóptimas |
| ReAct (razonamiento + acción) | Razonamiento fundamentado, mejor uso de herramientas | Mayor coste de tokens, puede quedarse en bucles |
| CoT + Self-Consistency | Alta precisión en tareas de razonamiento | No puede tomar acciones, costoso |
ReAct superó tanto los enfoques de razonamiento puro como los de acción pura, demostrando que la sinergia entre pensar y hacer es mayor que cualquiera de los dos por separado.
2.5 Implementación mínima de ReAct (sin frameworks)
"""
Minimal ReAct agent implemented from scratch.
No LangChain, no LangGraph, no frameworks — just the OpenAI API
and Python. This implementation demonstrates the core ReAct pattern.
"""
import json
import re
from openai import OpenAI
client = OpenAI()
# --- Tool Implementations ---
def search(query: str) -> str:
"""Simulated search tool."""
knowledge = {
"python creator": "Python was created by Guido van Rossum, first released in 1991.",
"guido van rossum": "Guido van Rossum is a Dutch programmer, born in Haarlem, Netherlands, on January 31, 1956.",
"haarlem netherlands": "Haarlem is a city in the Netherlands, capital of North Holland province. Population approximately 162,000.",
"haarlem elevation": "Haarlem sits at an elevation of approximately 1 meter above sea level, in the low-lying Netherlands.",
"eiffel tower height": "The Eiffel Tower is 330 meters tall (including antenna) or 300 meters to the roof.",
"transformer paper": "The Transformer was introduced in 'Attention Is All You Need' by Vaswani et al. (2017) at NeurIPS.",
"react paper": "ReAct was published by Yao et al. (2023) at ICLR. It synergizes reasoning and acting in LLMs.",
}
query_lower = query.lower()
for key, value in knowledge.items():
if key in query_lower:
return value
return f"No results found for: {query}"
def calculator(expression: str) -> str:
"""Safe calculator."""
import math
allowed = {"__builtins__": {}, "math": math, "abs": abs, "round": round,
"sqrt": math.sqrt, "pi": math.pi, "e": math.e}
try:
result = eval(expression, allowed)
return str(result)
except Exception as e:
return f"Error: {e}"
def finish(answer: str) -> str:
"""Signal that the agent has reached a final answer."""
return answer
TOOLS = {
"Search": search,
"Calculator": calculator,
"Finish": finish,
}
# --- The ReAct Prompt ---
REACT_SYSTEM_PROMPT = """You are a helpful assistant that answers questions using a Thought-Action-Observation loop.
You have access to the following tools:
- Search[query]: Search for information. Input is a search query string.
- Calculator[expression]: Calculate a mathematical expression. Input is a Python math expression.
- Finish[answer]: Return the final answer. Use this when you have enough information.
You MUST follow this EXACT format for each step:
Thought: <your reasoning about what to do next>
Action: <ToolName>[<input>]
Then you will receive:
Observation: <result of the action>
Important rules:
1. Always start with a Thought before taking an Action.
2. Each response should contain exactly ONE Thought and ONE Action.
3. Use Search when you need factual information.
4. Use Calculator when you need precise computations.
5. Use Finish[answer] when you have enough information to answer the question.
6. If a search returns unhelpful results, try rephrasing the query.
7. Do not make up information — always search for facts you are unsure about.
"""
# --- The ReAct Agent ---
class ReActAgent:
"""A minimal ReAct agent."""
def __init__(self, model: str = "gpt-4o", max_steps: int = 10, verbose: bool = True):
self.model = model
self.max_steps = max_steps
self.verbose = verbose
def run(self, question: str) -> str:
"""Run the ReAct loop to answer a question."""
messages = [
{"role": "system", "content": REACT_SYSTEM_PROMPT},
{"role": "user", "content": f"Question: {question}"},
]
trajectory = [] # For logging
for step in range(self.max_steps):
if self.verbose:
print(f"\n{'='*50}")
print(f"Step {step + 1}")
print(f"{'='*50}")
# Get the next thought and action from the LLM
response = client.chat.completions.create(
model=self.model,
messages=messages,
temperature=0.0,
max_tokens=500,
)
assistant_text = response.choices[0].message.content
messages.append({"role": "assistant", "content": assistant_text})
if self.verbose:
print(assistant_text)
# Parse the action
action_match = re.search(r'Action:\s*(\w+)\[(.+?)\]', assistant_text)
if not action_match:
if self.verbose:
print(" [Warning: Could not parse action, attempting recovery]")
finish_match = re.search(r'(?:answer|final answer)[:\s]+(.+)', assistant_text, re.IGNORECASE)
if finish_match:
return finish_match.group(1).strip()
messages.append({
"role": "user",
"content": "Please follow the required format: Thought: <reasoning>\nAction: <ToolName>[<input>]"
})
continue
tool_name = action_match.group(1)
tool_input = action_match.group(2)
# Check if this is the Finish action
if tool_name == "Finish":
trajectory.append({
"step": step + 1,
"thought": assistant_text.split("Action:")[0].strip(),
"action": f"Finish[{tool_input}]",
"observation": "DONE",
})
if self.verbose:
print(f"\nObservation: Final answer reached.")
return tool_input
# Execute the tool
if tool_name in TOOLS:
observation = TOOLS[tool_name](tool_input)
else:
observation = f"Error: Unknown tool '{tool_name}'. Available tools: {list(TOOLS.keys())}"
if self.verbose:
print(f"\nObservation: {observation}")
# Record the trajectory
trajectory.append({
"step": step + 1,
"thought": assistant_text.split("Action:")[0].strip(),
"action": f"{tool_name}[{tool_input}]",
"observation": observation,
})
# Feed the observation back
messages.append({
"role": "user",
"content": f"Observation: {observation}"
})
return "Agent reached maximum steps without finding an answer."
# --- Usage ---
if __name__ == "__main__":
agent = ReActAgent(verbose=True)
# Example 1: Multi-hop question requiring multiple searches
print("\n" + "=" * 70)
print("QUESTION 1: Multi-hop factual question")
print("=" * 70)
answer = agent.run(
"What is the elevation of the birthplace of the creator of Python?"
)
print(f"\nFINAL ANSWER: {answer}")
# Example 2: Question requiring search + calculation
print("\n" + "=" * 70)
print("QUESTION 2: Search + calculation")
print("=" * 70)
answer = agent.run(
"How many Eiffel Towers stacked on top of each other would it take to reach the cruising altitude of a commercial airplane (35,000 feet)?"
)
print(f"\nFINAL ANSWER: {answer}")2.6 Análisis de la implementación
Decisiones de diseño clave en este agente ReAct:
- Despacho de herramientas basado en texto: En lugar de usar el function calling nativo de la API, usamos un formato de texto (
Action: ToolName[input]) y lo parseamos con regex. Esto es más cercano al artículo original de ReAct y funciona con cualquier LLM, incluídos los de código abierto. - Aplicación estricta del formato: El prompt del sistema específica el formato exacto, y manejamos las violaciones de formato de forma elegante.
- Observación como mensaje del usuario: Los resultados de herramientas se devuelven como mensajes "user" con el prefijo "Observation:". Esto mantiene el ciclo Thought-Action-Observation.
- Máximo de pasos: Un límite duro previene los bucles infinitos.
- Modo verbose: La trayectoria se imprime para depuración y con fines educativos.
2.7 Limitaciones de ReAct
Comprender las limitaciones de ReAct es importante porque motivan las arquitecturas más sofisticadas que siguen:
- Sin planificación explícita: ReAct es reactivo; da un paso a la vez sin un plan global. Es como navegar por una ciudad mirando la siguiente intersección en lugar de consultar un mapa. Funciona para trayectos cortos pero lleva a rutas ineficientes para viajes largos.
- Sin autocorrección: Si el agente toma un camino equivocado, no reflexiona explícitamente sobre qué salió mal. Sigue avanzando, potencialmente construyendo sobre resultados intermedios defectuosos.
- Greedy: Sigue un único camino y no explora alternativas. Si el primer enfoque no funciona, el agente no tiene un mecanismo para probar una estrategia fundamentalmente diferente.
- Crecimiento del contexto: Cada paso añade tokens al contexto, eventualmente alcanzando los límites. Para tareas largas (más de 20 pasos), el contexto temprano puede perderse o degradarse.
Inténtalo tú mismo: Ejecuta la implementación de ReAct anterior con una pregunta multi-salto como "¿Cuál es la población del país donde nació el inventor de la World Wide Web?" Observa: (1) ¿Cuántos pasos toma el agente? (2) ¿Pierde alguna vez el hilo de la pregunta original? (3) Si una búsqueda devuelve resultados inútiles, ¿se recupera? Estas observaciones te ayudarán a entender por qué a veces se necesitan arquitecturas más sofisticadas.
Las arquitecturas del resto de esta clase abordan estas limitaciones.
293. Plan-and-Execute
3.1 La idea
Plan-and-Execute (planificar y ejecutar) separa la planificación de la ejecución en dos fases distintas:
- Fase de planificación: El LLM genera un plan completo (lista de pasos) antes de tomar cualquier acción.
- Fase de ejecución: Cada paso se ejecuta, potencialmente con replanificación si los resultados se desvían de las expectativas.
Interactive · Plan-and-Execute Architecture
Plan-Execute-Replan
Un plan vivo
El agente planifica, ejecuta paso a paso y replanifica en cuanto detecta deriva respecto al objetivo.
La fila roja simula una deriva: el agente debe corregir el plan antes de seguir.
Meta
Entregar un informe ejecutivo con datos verificados.
1 · En curso
Definir alcance
2 · Pendiente
Buscar datos
3 · Pendiente
Analizar
4 · Pendiente
Borrador
5 · Pendiente
Revisar y entregar
3.2 Implementación
"""
Plan-and-Execute agent architecture.
Separates high-level planning from step-by-step execution.
"""
import json
from openai import OpenAI
client = OpenAI()
def create_plan(question: str) -> list[str]:
"""Use the LLM to create a plan for answering the question."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are a planning agent. Given a question, create a "
"step-by-step plan to answer it. Each step should be a "
"clear, actionable instruction.\n\n"
"Available tools:\n"
"- Search the web for information\n"
"- Calculate mathematical expressions\n"
"- Read files from the workspace\n\n"
"Return the plan as a JSON array of strings, where each "
"string is one step. Return ONLY the JSON array."
)
},
{"role": "user", "content": f"Question: {question}"}
],
response_format={"type": "json_object"},
temperature=0.0,
)
result = json.loads(response.choices[0].message.content)
# Handle both {"steps": [...]} and {"plan": [...]} formats
if isinstance(result, dict):
steps = result.get("steps") or result.get("plan") or list(result.values())[0]
else:
steps = result
return steps
def execute_step(step: str, context: str, tools: list[dict]) -> str:
"""Execute a single step of the plan, using tools as needed."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are an execution agent. Complete the given step "
"using the available tools. Provide the result clearly.\n\n"
f"Context from previous steps:\n{context}"
)
},
{"role": "user", "content": f"Execute this step: {step}"}
],
tools=tools,
tool_choice="auto",
temperature=0.0,
)
return response.choices[0].message.content or "[Tool call made]"
def should_replan(original_plan: list[str], completed_steps: list[dict], remaining_steps: list[str]) -> tuple[bool, list[str]]:
"""Check if the plan needs adjustment based on execution results."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are a replanning agent. Given the original plan and "
"the results of completed steps, determine if the remaining "
"steps need to be adjusted.\n\n"
"Respond with a JSON object:\n"
'{"needs_replan": true/false, "new_remaining_steps": [...]}\n\n'
"Only set needs_replan to true if the results significantly "
"deviate from what was expected."
)
},
{
"role": "user",
"content": json.dumps({
"original_plan": original_plan,
"completed": completed_steps,
"remaining": remaining_steps,
})
}
],
response_format={"type": "json_object"},
temperature=0.0,
)
result = json.loads(response.choices[0].message.content)
return result.get("needs_replan", False), result.get("new_remaining_steps", remaining_steps)
def plan_and_execute(question: str, tools: list[dict], verbose: bool = True) -> str:
"""Run the Plan-and-Execute agent."""
# Phase 1: Create the plan
if verbose:
print("PHASE 1: PLANNING")
print("-" * 40)
plan = create_plan(question)
if verbose:
for i, step in enumerate(plan):
print(f" Step {i+1}: {step}")
# Phase 2: Execute each step
if verbose:
print(f"\nPHASE 2: EXECUTION")
print("-" * 40)
completed = []
remaining = list(plan)
context = ""
while remaining:
current_step = remaining.pop(0)
step_num = len(completed) + 1
if verbose:
print(f"\n Executing Step {step_num}: {current_step}")
result = execute_step(current_step, context, tools)
if verbose:
print(f" Result: {result[:200]}...")
completed.append({"step": current_step, "result": result})
context += f"\nStep {step_num}: {current_step}\nResult: {result}\n"
# Check if replanning is needed (every 2 steps)
if remaining and len(completed) % 2 == 0:
needs_replan, new_remaining = should_replan(plan, completed, remaining)
if needs_replan:
if verbose:
print(f"\n [REPLANNING] Adjusting remaining steps")
for i, step in enumerate(new_remaining):
print(f" New Step {len(completed)+i+1}: {step}")
remaining = new_remaining
# Phase 3: Synthesize the final answer
if verbose:
print(f"\nPHASE 3: SYNTHESIS")
print("-" * 40)
synthesis = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "Synthesize the results of the completed steps into a clear, complete answer to the original question."
},
{
"role": "user",
"content": f"Question: {question}\n\nCompleted steps and results:\n{context}"
}
],
temperature=0.0,
)
return synthesis.choices[0].message.content3.3 Cuándo usar Plan-and-Execute
| Escenario | ReAct | Plan-and-Execute |
|---|---|---|
| Preguntas factuales simples | Preferido (menos sobrecarga) | Excesivo |
| Tareas de investigación multi-paso | Funciona pero puede perder el enfoque | Preferido (se mantiene en el objetivo) |
| Tareas con dependencias claras | Adecuado | Preferido (orden explícito) |
| Tareas altamente dinámicas | Preferido (más adaptable) | Puede planificar mal si el entorno cambia |
| Tareas que requieren muchas herramientas | Puede tener dificultades con la selección | Mejor (el plan aclara qué herramientas) |
304. Reflexión: aprendizaje por refuerzo verbal
4.1 El artículo
Shinn et al. (2023) introdujeron Reflexión en "Reflexión: Language Agents with Verbal Reinforcement Learning", publicado en NeurIPS 2023. La idea clave: los agentes pueden mejorar reflexionando sobre sus fallos en lenguaje natural.
4.2 La arquitectura
Reflexión añade un paso de autorreflexión después de completar la tarea:
Interactive · La arquitectura Reflexion
Reflexion
Aprender del fallo, sin reentrenar
Un agente actor genera, un crítico señala el error, y el actor reescribe la siguiente intentona usando una reflexión verbal.
Actor
Crítico
4.3 Cómo funciona Reflexión
- Intentar: El agente intenta completar la tarea.
- Evaluar: Un evaluador verifica si el resultado es correcto.
- Reflexionar: Si el intento falló, el agente genera una reflexión en lenguaje natural analizando qué salió mal y cómo mejorar.
- Reintentar: El agente intenta de nuevo, con las reflexiones anteriores añadidas a su contexto.
4.4 Implementación
"""
Reflexion agent: learns from failures through self-reflection.
"""
import json
from openai import OpenAI
client = OpenAI()
class ReflexionAgent:
"""An agent that improves through verbal self-reflection."""
def __init__(self, model: str = "gpt-4o", max_attempts: int = 3):
self.model = model
self.max_attempts = max_attempts
self.reflections = [] # Persistent memory of past reflections
def attempt(self, task: str, attempt_num: int) -> str:
"""Make one attempt at the task."""
reflection_context = ""
if self.reflections:
reflection_context = "\n\nLessons from previous attempts:\n"
for i, ref in enumerate(self.reflections):
reflection_context += f"\nAttempt {i+1} reflection:\n{ref}\n"
response = client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": (
"You are a problem-solving agent. Solve the given task carefully. "
"Think step by step.\n"
f"{reflection_context}"
"\nIf there are lessons from previous attempts, "
"make sure to incorporate them into your approach."
)
},
{"role": "user", "content": task}
],
temperature=0.0 if attempt_num == 0 else 0.3, # More exploration on retries
)
return response.choices[0].message.content
def evaluate(self, task: str, response: str) -> tuple[bool, str]:
"""Evaluate whether the response correctly addresses the task."""
eval_response = client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": (
"You are a strict evaluator. Assess whether the response "
"correctly and completely answers the task. "
"Respond with JSON: {\"correct\": true/false, \"feedback\": \"explanation\"}"
)
},
{
"role": "user",
"content": f"Task: {task}\n\nResponse:\n{response}"
}
],
response_format={"type": "json_object"},
temperature=0.0,
)
result = json.loads(eval_response.choices[0].message.content)
return result.get("correct", False), result.get("feedback", "")
def reflect(self, task: str, response: str, feedback: str) -> str:
"""Generate a reflection on what went wrong and how to improve."""
reflection_response = client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": (
"You are a self-reflection agent. Analyze the failed attempt "
"and provide specific, actionable insights for improvement.\n\n"
"Your reflection should:\n"
"1. Identify the specific error or gap.\n"
"2. Explain WHY the error occurred.\n"
"3. Provide a concrete strategy for the next attempt.\n"
"Be concise but specific."
)
},
{
"role": "user",
"content": (
f"Task: {task}\n\n"
f"My response:\n{response}\n\n"
f"Evaluation feedback: {feedback}"
)
}
],
temperature=0.0,
)
return reflection_response.choices[0].message.content
def run(self, task: str, verbose: bool = True) -> str:
"""Run the Reflexion loop."""
for attempt_num in range(self.max_attempts):
if verbose:
print(f"\n{'='*50}")
print(f"ATTEMPT {attempt_num + 1}")
print(f"{'='*50}")
# Step 1: Make an attempt
response = self.attempt(task, attempt_num)
if verbose:
print(f"\nResponse:\n{response[:500]}...")
# Step 2: Evaluate
correct, feedback = self.evaluate(task, response)
if verbose:
print(f"\nEvaluation: {'PASS' if correct else 'FAIL'}")
print(f"Feedback: {feedback}")
if correct:
if verbose:
print(f"\nTask completed successfully on attempt {attempt_num + 1}!")
return response
# Step 3: Reflect (only if failed and more attempts remain)
if attempt_num < self.max_attempts - 1:
reflection = self.reflect(task, response, feedback)
self.reflections.append(reflection)
if verbose:
print(f"\nReflection:\n{reflection}")
if verbose:
print(f"\nMax attempts ({self.max_attempts}) reached.")
return response # Return the last attempt
# Usage
if __name__ == "__main__":
agent = ReflexionAgent(max_attempts=3)
task = """
Write a Python function that takes a list of integers and returns
the longest increasing subsequence. The function should handle
edge cases: empty list, single element, all same elements,
and already sorted list. Include type hints and a docstring.
"""
result = agent.run(task)
print(f"\nFinal result:\n{result}")4.5 Idea clave: refuerzo verbal
El RL tradicional usa recompensas escalares (números) para actualizar los pesos del modelo. Reflexión usa retroalimentación verbal (reflexiones en lenguaje natural) almacenada en el contexto del agente. Esta es una idea genuinamente novedosa.
Idea clave: Reflexión "aprende" sin cambiar ningún parámetro del modelo. Los pesos del modelo están congelados; lo que cambia es el contexto en el que opera el modelo. Al añadir reflexiones como "La última vez olvidé manejar el caso límite de entrada vacía" al contexto, el comportamiento efectivo del modelo cambia aunque el modelo en sí no haya cambiado. Esto es análogo a un estudiante que no se vuelve más inteligente entre intentos pero toma mejores notas sobre sus errores.
Este enfoque es potente porque:
- No se necesitan actualizaciones de pesos (funciona con modelos congelados, incluidas APIs de código cerrado).
- Las reflexiones llevan información rica (no sólo "bien" o "mal" sino "incorrecto porque X, intenta Y en su lugar"). Una recompensa escalar de 0.3 dice al modelo casi nada; una reflexión como "La función falló con listas vacías porque usé el índice [0] sin verificar la longitud" le dice exactamente qué corregir.
- Las reflexiones se acumulan entre intentos, creando una base de conocimiento creciente que hace que cada intento posterior esté más informado.
4.6 Limitaciones de Reflexión
- Requiere un evaluador fiable (difícil para tareas abiertas).
- Múltiples intentos multiplican el coste y la latencia.
- La ventana de contexto se llena de reflexiones en tareas largas.
- Funciona mejor cuando el modelo "casi" acierta.
315. Language Agent Tree Search (LATS)
5.1 El artículo
Zhou et al. (2024) introdujeron LATS (Language Agent Tree Search) en "Language Agent Tree Search Unifies Reasoning, Acting, and Planning in Language Models", publicado en ICML 2024. LATS combina la búsqueda en árbol (como Monte Carlo Tree Search usado en AlphaGo) con agentes LLM.
5.2 La idea central
LATS trata la toma de decisiones del agente como un problema de búsqueda en árbol:
Interactive · Búsqueda en árbol de agentes de lenguaje (LATS)
LATS
Búsqueda guiada por evaluación
Language Agent Tree Search expande nodos prometedores según UCB y poda el resto. Pulsa Expandir para ver crecer el árbol.
Los nodos con UCB alto se iluminan: son los caminos que LATS prioriza.
En cada nodo:
- Seleccionar: Elegir el nodo más prometedor para expandir (basándose en UCB1 o criterios similares).
- Expandir: Generar posibles acciones siguientes.
- Evaluar: Evaluar cuán prometedora es cada acción.
- Retropropagar: Actualizar las estimaciones de valor de los nodos padres.
5.3 Cómo LATS difiere de otras arquitecturas
| Característica | ReAct | Tree of Thoughts | LATS |
|---|---|---|---|
| Estrategia de búsqueda | Lineal (greedy) | Breadth-first/DFS | Monte Carlo Tree Search |
| Interacción con entorno | Sí (herramientas) | No (razonamiento puro) | Sí (herramientas + razonamiento) |
| Retroceso | No | Sí | Sí |
| Estimación de valor | Ningúna | Evaluación basada en LLM | Evaluación LLM + feedback del entorno |
| Reflexión | No | No | Sí (en caminos fallidos) |
5.4 LATS simplificado (concepto)
"""
Simplified LATS-inspired agent.
This is a conceptual implementation showing the key ideas.
A full LATS implementation requires more infrastructure
(proper tree data structures, UCB1 selection, etc.).
"""
import json
import math
from dataclasses import dataclass, field
from openai import OpenAI
client = OpenAI()
@dataclass
class Node:
"""A node in the search tree."""
state: str # Description of current state
action: str = "" # Action that led to this state
observation: str = "" # Result of the action
value: float = 0.0 # Estimated value of this node
visits: int = 0 # Number of times this node was visited
children: list = field(default_factory=list)
parent: object = None # Parent node
depth: int = 0
is_terminal: bool = False
def ucb1(self, exploration_weight: float = 1.4) -> float:
"""Upper Confidence Bound for tree selection."""
if self.visits == 0:
return float('inf') # Unexplored nodes have highest priority
exploitation = self.value / self.visits
exploration = exploration_weight * math.sqrt(
math.log(self.parent.visits) / self.visits
)
return exploitation + exploration
class LATSAgent:
"""Language Agent Tree Search."""
def __init__(self, model: str = "gpt-4o", max_iterations: int = 10,
max_depth: int = 5, n_children: int = 3):
self.model = model
self.max_iterations = max_iterations
self.max_depth = max_depth
self.n_children = n_children
def generate_actions(self, node: Node, task: str) -> list[str]:
"""Generate possible next actions from the current state."""
response = client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": (
f"You are solving: {task}\n\n"
f"Current state: {node.state}\n\n"
f"Generate exactly {self.n_children} different possible next actions. "
f"Each should be a distinct approach. "
f"Return as JSON: {{\"actions\": [\"action1\", \"action2\", ...]}}"
)
},
{"role": "user", "content": "What are the possible next actions?"}
],
response_format={"type": "json_object"},
temperature=0.8,
)
result = json.loads(response.choices[0].message.content)
return result.get("actions", [])[:self.n_children]
def evaluate_state(self, node: Node, task: str) -> float:
"""Evaluate how promising the current state is (0-1)."""
response = client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": (
"Evaluate how close this state is to solving the task. "
"Return JSON: {\"score\": 0.0-1.0, \"is_solution\": true/false, \"reasoning\": \"...\"}"
)
},
{
"role": "user",
"content": f"Task: {task}\n\nCurrent state:\n{node.state}"
}
],
response_format={"type": "json_object"},
temperature=0.0,
)
result = json.loads(response.choices[0].message.content)
node.is_terminal = result.get("is_solution", False)
return result.get("score", 0.5)
def select(self, root: Node) -> Node:
"""Select the most promising leaf node using UCB1."""
node = root
while node.children:
node = max(node.children, key=lambda c: c.ucb1())
return node
def backpropagate(self, node: Node, value: float):
"""Update value estimates up the tree."""
current = node
while current is not None:
current.visits += 1
current.value += value
current = current.parent
def run(self, task: str, verbose: bool = True) -> str:
"""Run LATS to solve a task."""
root = Node(state=f"Task: {task}\nNo actions taken yet.", visits=1)
best_solution = None
best_score = 0.0
for iteration in range(self.max_iterations):
if verbose:
print(f"\n--- LATS Iteration {iteration + 1} ---")
# 1. SELECT
leaf = self.select(root)
if leaf.depth >= self.max_depth:
if verbose:
print(f" Max depth reached at this branch.")
self.backpropagate(leaf, 0.0)
continue
# 2. EXPAND
actions = self.generate_actions(leaf, task)
if verbose:
print(f" Generated {len(actions)} actions")
for action in actions:
new_state = f"{leaf.state}\n\nAction: {action}\n"
child = Node(
state=new_state,
action=action,
parent=leaf,
depth=leaf.depth + 1,
)
leaf.children.append(child)
# 3. EVALUATE
for child in leaf.children:
score = self.evaluate_state(child, task)
child.value = score
if verbose:
print(f" Action: {child.action[:60]}... Score: {score:.2f}"
f"{' [SOLUTION]' if child.is_terminal else ''}")
if child.is_terminal and score > best_score:
best_solution = child
best_score = score
# 4. BACKPROPAGATE
self.backpropagate(child, score)
# Early termination if we found a good solution
if best_solution and best_score > 0.9:
if verbose:
print(f"\n Found high-quality solution (score: {best_score:.2f})")
break
if best_solution:
return best_solution.state
else:
best_leaf = self.select(root)
return best_leaf.state5.5 Cuándo usar LATS
LATS destaca cuando:
- La tarea tiene múltiples caminos de solución válidos y algunos son mucho mejores que otros.
- El retroceso es valioso (explorar un camino puede revelar que es un callejón sin salida).
- La función de evaluación es lo suficientemente fiable para guiar la búsqueda.
- Se puede permitir el coste computacional (LATS hace muchas más llamadas al LLM que ReAct).
LATS es excesivo cuando:
- La tarea es directa con un único enfoque obvio.
- Los requisitos de latencia son estrictos.
- El presupuesto es limitado.
326. Comparación de arquitecturas
6.1 Tabla resumen
| Arquitectura | Planificación | Reflexión | Exploración | Uso herram. | Llamadas LLM | Mejor para |
|---|---|---|---|---|---|---|
| ReAct | Implícita (paso a paso) | Ninguna | Camino único | Sí | Bajo (1 por paso) | Tareas multi-paso simples |
| Plan-and-Execute | Explícita por adelantado | Vía replanificación | Camino único (replanificable) | Sí | Medio | Tareas complejas con estructura clara |
| Reflexión | Implícita | Explícita tras fallo | Múltiples intentos | Opcional | Medio-Alto | Tareas con criterios de éxito claros |
| LATS | Implícita por rama | Vía retropropagación | Búsqueda en árbol | Sí | Alto | Tareas complejas con múltiples enfoques |
6.2 Marco de decisión
Este árbol de decisión es una guía práctica para elegir una arquitectura. Imprímelo y ponlo en la pared si construyes agentes regularmente:
En la práctica, la respuesta suele ser "empezar con ReAct y añadir complejidad según sea necesario". La mayoría de las tareas de agentes no requieren toda la potencia de LATS, y el coste adicional es difícil de justificar a menos que los errores sean muy caros de corregir.
6.3 Comparación de costes
Para una tarea que requiere ~5 pasos efectivos:
| Arquitectura | Llamadas LLM apróx. | Coste relativo |
|---|---|---|
| ReAct | 5-10 | 1x |
| Plan-and-Execute | 8-15 | 1,5x |
| Reflexión (2 intentos) | 15-25 | 2,5x |
| LATS (3 iteraciones, 3 ramas) | 30-50 | 5x |
337. Patrones de implementación en frameworks
7.1 LangGraph
LangGraph (de LangChain) modela los flujos de trabajo de agentes como grafos dirigidos, donde los nodos son pasos de procesamiento y las aristas definen las transiciones.
"""
Conceptual LangGraph-style implementation.
(Simplified for educational purposes — actual LangGraph API may differ.)
"""
# LangGraph models agents as state machines with nodes and edges
# Node: A function that processes state and returns updated state
# Edge: A conditional routing function that determines the next node
from typing import TypedDict, Literal
class AgentState(TypedDict):
"""The state that flows through the graph."""
messages: list[dict]
plan: list[str]
current_step: int
results: list[str]
status: str # "planning", "executing", "reflecting", "done"
def planner_node(state: AgentState) -> AgentState:
"""Generate a plan based on the user's query."""
# ... LLM call to create plan ...
state["plan"] = ["step 1", "step 2", "step 3"]
state["status"] = "executing"
state["current_step"] = 0
return state
def executor_node(state: AgentState) -> AgentState:
"""Execute the current step of the plan."""
step = state["plan"][state["current_step"]]
# ... Execute step with tools ...
result = f"Result of: {step}"
state["results"].append(result)
state["current_step"] += 1
return state
def reflector_node(state: AgentState) -> AgentState:
"""Reflect on results and decide whether to continue or replan."""
# ... LLM evaluates progress ...
if state["current_step"] >= len(state["plan"]):
state["status"] = "done"
return state
def router(state: AgentState) -> Literal["executor", "reflector", "done"]:
"""Route to the next node based on current state."""
if state["status"] == "done":
return "done"
elif state["current_step"] < len(state["plan"]):
return "executor"
else:
return "reflector"
# The graph structure:
#
# START → planner → executor ◄──┐
# │ │
# └──▶ reflector ─┘
# │
# └──▶ END (when done)7.2 Por qué arquitecturas basadas en grafos
Las ideas clave:
- Flujo de control explícito: El grafo hace visible y controlable el proceso de decisión del agente.
- Gestión de estado: El estado se pasa explícitamente a través de los nodos, facilitando la depuración.
- Enrutamiento condicional: Diferentes caminos para diferentes situaciones (éxito, fallo, incertidumbre).
- Composabilidad: Los nodos pueden reutilizarse en diferentes flujos de trabajo.
- Humano en el bucle: Nodos específicos pueden pausarse para aprobación humana.
7.3 Patrones de grafo comunes
Pipeline lineal:
Simple, predecible. Bueno para flujos de trabajo bien entendidos.
Bucle con salida:
El patrón ReAct. Bucle hasta que la tarea esté completa.
Ramificación y fusión:
Procesamiento paralelo. Ejecutar pasos independientes concurrentemente.
Jerárquico:
Patrón multi-agente. Un orquestador delega a sub-agentes especializados.
348. Frameworks de agentes modernos y sus arquitecturas
8.1 LangGraph: máquinas de estado basadas en grafos
LangGraph (de LangChain) modela los flujos de trabajo de agentes como grafos dirigidos con estado. Es el framework más explícito respecto al flujo de control.
Conceptos fundamentales:
- Los nodos son funciones Python que reciben y devuelven estado. Cada nodo realiza un paso lógico (llamar al LLM, ejecutar una herramienta, evaluar un resultado).
- Las aristas definen transiciones entre nodos. Pueden ser incondicionales (siempre ir de A a B) o condicionales (ir a B si se cumple la condición X, si no ir a C).
- El estado es un diccionario tipado que fluye a través del grafo y acumula información.
- Los checkpoints permiten guardar y restaurar el estado del grafo, habilitando flujos de trabajo con humano en el bucle y agentes de larga duración.
"""
LangGraph ReAct implementation (conceptual).
This shows how LangGraph maps the ReAct pattern onto a graph structure.
"""
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
from operator import add
class AgentState(TypedDict):
messages: list[dict]
tool_calls: list[dict]
tool_results: Annotated[list[str], add] # Accumulates across steps
def call_model(state: AgentState) -> AgentState:
"""Node: Call the LLM with current messages."""
# ... LLM call that may produce tool calls or a final answer ...
return {"messages": state["messages"] + [response]}
def execute_tools(state: AgentState) -> AgentState:
"""Node: Execute any pending tool calls."""
# ... Execute tools, append results to messages ...
return {"tool_results": [result]}
def should_continue(state: AgentState) -> str:
"""Edge: Decide whether to continue or finish."""
last_message = state["messages"][-1]
if last_message.get("tool_calls"):
return "tools" # Go to execute_tools node
return "end" # Go to END
# Build the graph
graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", execute_tools)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END})
graph.add_edge("tools", "agent") # After tools, always go back to agent
app = graph.compile()Cómo LangGraph implementa los patrones de las Secciones 2-5:
- ReAct: Un bucle de dos nodos (nodo agente y nodo herramientas) con una arista condicional.
- Plan-and-Execute: Tres nodos (planificador, ejecutor, replanificador) con enrutamiento condicional.
- Reflexión: Añadir un nodo reflector que evalúa resultados y enruta al END o de vuelta al agente con contexto de reflexión.
- LATS: Usar la ramificación de LangGraph para explorar múltiples caminos, con un nodo selector para elegir la mejor rama.
La fortaleza de LangGraph es la explicitud: la estructura del grafo hace visible, depurable y testeáble el flujo de control del agente. Su debilidad es la verbosidad: los agentes simples requieren más código repetitivo que otros frameworks.
8.2 OpenAI Agents SDK: agente-como-función con traspasos
El OpenAI Agents SDK (lanzado en marzo de 2025) adopta un enfoque diferente: los agentes son objetos ligeros que pueden traspasar (handoff) el control a otros agentes.
Conceptos centrales:
- Un Agent se define por un nombre, instrucciones (prompt del sistema), un conjunto de herramientas, y opcionalmente una lista de otros agentes a los que puede traspasár.
- Un Handoff es una llamada a herramienta especial donde un agente transfiere la conversación a otro agente más especializado.
- El Runner gestiona el bucle de ejecución, manejando llamadas a herramientas y traspásos de forma transparente.
- Los Guardrails son validadores de entrada/salida que se ejecutan en paralelo con el agente.
"""
OpenAI Agents SDK: Multi-agent handoff pattern (conceptual).
"""
from agents import Agent, Runner
coding_agent = Agent(
name="Coding Assistant",
instructions="You are an expert Python developer. Help with code questions.",
tools=[execute_code, read_file, write_file],
)
research_agent = Agent(
name="Research Assistant",
instructions="You are a research assistant. Search for and synthesize information.",
tools=[web_search, arxiv_search, summarize],
)
triage_agent = Agent(
name="Triage Agent",
instructions=(
"You are a triage agent. Determine whether the user needs help with "
"coding or research, then hand off to the appropriate specialist."
),
handoffs=[coding_agent, research_agent],
)
result = Runner.run_sync(triage_agent, "Help me implement a binary search tree in Python")
# The triage agent hands off to coding_agent, which handles the requestIdea arquitectónica clave: El Agents SDK implementa el patrón Jerárquico (Sección 7.3) como un concepto de primera clase. En lugar de construir un grafo con un nodo orquestador, se definen las relaciones entre agentes de forma declarativa. Esto facilita la construcción de sistemas multi-agente pero proporciona menos control fino sobre el flujo de ejecución que LangGraph.
8.3 CrewAI: equipos de agentes basados en roles
CrewAI organiza agentes como un equipo (crew) donde cada agente tiene un rol, un objetivo y una historia de fondo definidos. Se inspira en metáforas organizacionales en lugar de teoría de grafos.
Conceptos centrales:
- Un Agent tiene un rol (por ejemplo, "Senior Researcher"), un objetivo y una historia de fondo que moldea su comportamiento.
- Una Task es una pieza específica de trabajo asignada a un agente, con un formato de salida esperado.
- Un Crew es una colección de agentes y tareas, con un proceso definido (secuencial o jerárquico).
- En modo secuencial, las tareas se ejecutan una tras otra, con la salida de cada tarea disponible para las siguientes.
- En modo jerárquico, un agente gestor delega tareas a los miembros del equipo dinámicamente.
"""
CrewAI: Role-based agent team (conceptual).
A research crew with complementary roles.
"""
from crewai import Agent, Task, Crew, Process
researcher = Agent(
role="Senior Research Analyst",
goal="Find and analyze the latest papers on agentic AI architectures",
backstory="You are a senior NLP researcher with 10 years of experience.",
tools=[arxiv_search, semantic_scholar_search],
)
writer = Agent(
role="Technical Writer",
goal="Synthesize research findings into a clear, structured report",
backstory="You are a technical writer who specializes in making complex AI topics accessible.",
tools=[],
)
# Tasks
research_task = Task(
description="Find the 5 most influential papers on agent architectures published in 2024-2025.",
expected_output="A list of papers with summaries and key contributions.",
agent=researcher,
)
writing_task = Task(
description="Write a 2-page summary of the research findings.",
expected_output="A structured report in Markdown format.",
agent=writer,
)
# The crew runs tasks sequentially: researcher first, then writer
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
)
result = crew.kickoff()Cómo CrewAI se mapea a los patrones arquitectónicos:
- Proceso secuencial: Esencialmente una arquitectura Plan-and-Execute donde el plan está definido por la lista de tareas.
- Proceso jerárquico: Un agente gestor asigna trabajo dinámicamente, similar al patrón orquestador.
- El diseño basado en roles de CrewAI fomenta la especialización, lo que puede mejorar la calidad de la salida al dar a cada agente instrucciones enfocadas en lugar de que un solo agente maneje todo.
8.4 Comparación de frameworks
| Característica | LangGraph | OpenAI Agents SDK | CrewAI |
|---|---|---|---|
| Paradigma | Máquina de estados basada en grafos | Agente-como-función + traspasos | Equipos basados en roles |
| Flujo de control | Explícito (aristas/condiciones) | Implícito (traspasos) | Definido por tipo de proceso |
| Gestión de estado | Diccionario de estado tipado | Mensajes de conversación | Salidas de tareas |
| Multi-agente | Vía nodos del grafo | Vía traspasos | Vía crew/roles |
| Mejor para | Flujos de trabajo complejos y personalizados | Delegación multi-agente | Flujos de trabajo de equipo estructurados |
| Curva de aprendizaje | Mayor | Menor | Menor |
| Flexibilidad | Muy alta | Moderada | Moderada |
| Proveedor LLM | Cualquiera | OpenAI (principalmente) | Cualquiera |
8.5 Cómo elegir un framework
- Usar LangGraph cuando se necesita control fino sobre el comportamiento del agente, gestión de estado personalizada, checkpoints con humano en el bucle, o flujo de control no estándar.
- Usar OpenAI Agents SDK cuando se construye un sistema de agentes especializados que se delegan entre sí, y se usan principalmente modelos de OpenAI.
- Usar CrewAI cuando la tarea se descompone naturalmente en roles (investigador, escritor, revisor) y se quiere prototipado rápido.
- No usar ningún framework (como en la Sección 2.5) cuando la tarea es lo suficientemente simple para que un framework añada complejidad sin beneficio.
359. Patrones emergentes (2025-2026)
9.1 Agent-as-a-Service
Los agentes tradicionales se ejecutan dentro de una única aplicación. Agent-as-a-Service expone las capacidades del agente a través de APIs, permitiendo que otros sistemas (incluidos otros agentes) los invoquen remotamente.
Características clave:
- Los agentes se despliegan como servicios independientes con interfaces bien definidas.
- La comunicación ocurre vía protocolos como Agent-to-Agent (A2A) de Google o APIs REST/gRPC simples.
- Cada agente puede usar diferentes modelos, herramientas y arquitecturas internamente.
- Los agentes de servicio pueden mantenerse, escalarse y actualizarse de forma independiente.
Este patrón conecta con los protocolos de comunicación multi-agente cubiertos en la Semana 8. La implicación arquitectónica es que las interfaces de los agentes deben diseñarse para la composabilidad: esquemas de entrada/salida claros, manejo de errores y descubrimiento de capacidades.
9.2 RAG agéntico
El RAG estándar sigue un patrón fijo: recuperar documentos, luego generar. El RAG agéntico (Agentic RAG) da al agente control sobre el proceso de recuperación mismo.
Interactive · RAG agéntico: recuperación controlada por el agente
Pipeline RAG
De documento a respuesta
Indexar, recuperar, re-rankear y aumentar la generación. Los dos primeros pasos viven offline; los demás corren cada vez que llega una consulta.
Chunking
Trocear los documentos en piezas semánticamente coherentes.
Capacidades clave del RAG agéntico:
- Planificación de consultas: El agente descompone preguntas complejas en múltiples consultas de recuperación.
- Selección de fuentes: El agente elige qué bases de conocimiento, bases de datos o APIs consultar.
- Evaluación de resultados: El agente evalúa si los documentos recuperados son relevantes y suficientes.
- Refinamiento iterativo: Si los resultados iniciales son deficientes, el agente reformula consultas o prueba fuentes diferentes.
- Recuperación multi-salto: El agente usa información de una recuperación para informar consultas posteriores.
Este patrón conecta con los temas de uso de herramientas y memoria de las Semanas 4 y 7. Arquitectónicamente, el RAG agéntico se implementa típicamente como un agente ReAct donde las herramientas disponibles incluyen múltiples endpoints de recuperación, y el razonamiento del agente determina cuándo y cómo usarlos.
9.3 Agentes que se automejoran
La mayoría de las arquitecturas de agentes tratan cada ejecución de tarea como independiente. Los agentes que se automejoran (self-improving agents) mantienen memoria persistente de sus éxitos y fracasos, usando este historial para mejorar el rendimiento con el tiempo.
Esto difiere de Reflexión en un aspecto importante: Reflexión reflexiona dentro de una sola tarea (entre reintentos), mientras que los agentes que se automejoran aprenden entre tareas a lo largo del tiempo.
Enfoques de implementación:
- Memoria episódica: Almacenar trayectorias completas de tareas (acciones tomadas, resultados, reflexiones) en una base de datos vectorial. Antes de comenzar una nueva tarea, recuperar episodios pasados similares para informar la estrategia.
- Reglas aprendidas: Después de cada tarea, extraer heurísticas generalizables ("Cuando la API devuelve un error 429, esperar y reintentar en lugar de cambiar a un enfoque diferente") y almacenarlas como reglas reutilizables.
- Evolución de prompts: Revisar periódicamente el prompt del sistema o las descripciones de herramientas del agente basándose en datos de rendimiento acumulados.
Desafíos:
- Curación de memoria: Con el tiempo, el almacén de experiencias crece y puede contener lecciones contradictorias. El agente necesita un mecanismo para podar o priorizar experiencias.
- Generalización: Una estrategia que funcionó para una tarea puede no transferirse a otra. Recuperar experiencias irrelevantes puede degradar el rendimiento.
- Fiabilidad de la evaluación: La automejora requiere una evaluación precisa de los resultados. Si el evaluador no es fiable, el agente puede aprender las lecciones equivocadas.
Los agentes que se automejoran son un área activa de investigación. Apuntan hacia un futuro donde los agentes mejoran continuamente en su trabajo, de forma similar a los profesionales humanos que acumulan experiencia a lo largo de años de práctica.
3610. Arquitecturas híbridas y personalizadas
10.1 Combinación de patrones
Los agentes del mundo real a menudo combinan elementos de múltiples arquitecturas:
ReAct + Reflexión:
"""
Hybrid: ReAct agent with Reflexion-style self-correction.
Uses ReAct for step-by-step execution, but adds a reflection
step if the agent detects it is going in circles or making errors.
"""
class HybridAgent:
def __init__(self):
self.react_steps = []
self.reflections = []
self.failed_actions = set()
def run(self, task: str, max_react_steps: int = 10, max_retries: int = 3) -> str:
for retry in range(max_retries):
# Run ReAct
result = self._react_loop(task, max_react_steps)
# Evaluate
success, feedback = self._evaluate(task, result)
if success:
return result
# Reflect and retry
reflection = self._reflect(task, result, feedback)
self.reflections.append(reflection)
self.react_steps = [] # Reset for next attempt
return result # Return best effortPlan-and-Execute + LATS: Usar Plan-and-Execute para la estructura general, pero usar búsqueda en árbol para pasos individuales que son particularmente complejos o inciertos.
10.2 Heurísticas para selección de arquitectura
Al diseñar un agente personalizado, considerar:
- Complejidad de la tarea: Las tareas simples necesitan arquitecturas simples. No sobreingenieres.
- Tolerancia a errores: Si los errores son costosos, añadir pasos de reflexión y verificación.
- Requisitos de latencia: Cada capa arquitectónica añade latencia. Eliminar capas innecesarias.
- Presupuesto: Las arquitecturas más sofisticadas cuestan más. Calcular el coste esperado por tarea.
- Observabilidad: ¿Se puede evaluar el éxito programáticamente? Si sí, Reflexión funciona bien. Si no, se necesita humano en el bucle.
10.3 El principio del "suficientemente bueno"
Un error común en el diseño de agentes es la sobreingeniería. Es tentador implementar LATS porque suena impresionante, pero si la tarea requiere 3 llamadas a herramientas y tiene un criterio de éxito claro, ReAct con lógica de reintentos servirá mejor a 1/10 del coste.
Empezar con la arquitectura más simple que funcione:
- Empezar con ReAct: Implementar el bucle básico pensamiento-acción-observación. Probarlo con los casos de uso.
- Añadir planificación si es necesario: Si el agente pierde frecuentemente el objetivo general, añadir un paso de planificación.
- Añadir reflexión si es necesario: Si el agente comete errores recurrentes, añadir autorreflexión.
- Añadir búsqueda en árbol si es necesario: Si el agente a menudo toma caminos subóptimos, añadir exploración.
Cada adición debería estar motivada por fallos observados en el enfoque más simple.
Idea clave: La mejor arquitectura es la más simple que cumple los requisitos de fiabilidad. Una implementación elegante de LATS que cuesta 0,10 por tarea, a menos que la tarea sea tan crítica que la diferencia de coste de 50x esté justificada por la mejora en fiabilidad.
3711. El bucle observar-pensar-actuar en la práctica
11.1 Trazas de agentes del mundo real
Examinemos cómo un agente de programación del mundo real maneja una tarea:
User: "Fix the failing test in test_auth.py"
OBSERVAR: Leer el archivo de test para entender qué se está probando.
→ read_file("test_auth.py")
→ Encontrado: test_login_valid_credentials está fallando
PENSAR: El test espera un código de estado 200 pero recibe 401.
Esto sugiere que la lógica de autenticación tiene un bug.
Debería mirar el módulo de auth.
ACTUAR: Leer el módulo de autenticación.
→ read_file("auth/login.py")
OBSERVAR: La función de login verifica la contraseña pero tiene un error tipográfico:
Línea 42: if password = stored_hash: (¡asignación, no comparación!)
PENSAR: Encontré el bug — un solo = en lugar de ==.
Necesito corregir esto y verificar que el test pasa.
ACTUAR: Editar el archivo.
→ edit_file("auth/login.py", line=42, new=" if password == stored_hash:")
OBSERVAR: Archivo guardado correctamente.
PENSAR: Ahora debería ejecutar el test para verificar que la corrección funciona.
ACTUAR: Ejecutar el test.
→ execute("python -m pytest test_auth.py::test_login_valid_credentials -v")
OBSERVAR: test_login_valid_credentials PASSED (1 passed in 0.3s)
PENSAR: El test pasa ahora. La corrección es correcta. También debería
ejecutar toda la suite de tests para asegurarme de que no rompí nada.
ACTUAR: Ejecutar todos los tests.
→ execute("python -m pytest test_auth.py -v")
OBSERVAR: Los 12 tests pasaron.
PENSAR: Todos los tests pasan. La corrección está completa.
RESPONDER: "Corregido el bug en auth/login.py línea 42: cambiado un solo = (asignación)
a == (comparación) en la verificación de contraseña. Los 12 tests pasan."11.2 Patrones en la práctica
Obsérvense varios patrones en esta traza:
- Comprensión incremental: El agente construye comprensión paso a paso, no toda de una vez.
- Guiado por hipótesis: El agente forma hipótesis ("el módulo de auth probablemente tiene un bug") y las prueba.
- Verificación: El agente no se detiene después de hacer la corrección; verifica el resultado.
- Comprobación defensiva: Ejecutar toda la suite de tests, no solo el test que fallaba, detecta regresiones.
3812. Preguntas de discusión
-
Arquitectura vs. capacidad del modelo: A medida que los LLM se vuelven más capaces, ¿se volverán innecesarias las arquitecturas complejas como LATS? ¿O seguirán siendo valiosas independientemente de la capacidad del modelo?
-
La paradoja de Reflexión: Si el modelo pudiera reflexionar correctamente sobre qué salió mal, ¿por qué cometió el error en primer lugar? ¿La autorreflexión añade genuínamente información nueva, o simplemente le da al modelo otra oportunidad con una semilla aleatoria diferente?
-
Cuándo dejar de buscar: En LATS, ¿cómo se decide cuándo se ha explorado suficiente y hay que comprometerse con una solución? ¿Cuál es el compromiso entre exploración y explotación en la toma de decisiones del agente?
-
Ubicación del humano en el bucle: En una arquitectura Plan-and-Execute, ¿dónde deberían colocarse los puntos de control humano? ¿Después de la planificación? ¿Después de cada paso? ¿Sólo ante errores?
-
Transferibilidad de arquitecturas: Si se diseña una arquitectura de agente que funciona bien para tareas de programación, ¿qué tan bien se transferiría a otros dominios (diagnóstico médico, investigación legal, escritura creativa)?
3913. Resumen y puntos clave
-
Las arquitecturas de agentes proporcionan patrones estructurados para cómo los LLM razonan, actúan y aprenden de la retroalimentación. Determinan cómo las capacidades del modelo se canalizan hacia un comportamiento efectivo.
-
ReAct (Yao et al., 2023) es el patrón fundacional: intercalar razonamiento (Thought) con acciones (Action) y retroalimentación ambiental (Observation). Es simple, efectivo y el punto de partida para la mayoría de los agentes.
-
Plan-and-Execute separa la planificación de la ejecución, creando un enfoque más estructurado que funciona bien para tareas complejas con sub-objetivos claros.
-
Reflexión (Shinn et al., 2023) añade autocorrección a través de la reflexión verbal sobre los fallos. Permite a los agentes mejorar entre intentos sin actualización de pesos.
-
LATS (Zhou et al., 2024) aplica Monte Carlo Tree Search a la toma de decisiones del agente, permitiendo la exploración sistemática de caminos alternativos. Es potente pero costoso.
-
El bucle observar-pensar-actuar es el hilo común en todas las arquitecturas. Las diferencias residen en cuánto pensamiento, exploración y reflexión ocurre alrededor de cada acción.
-
Empezar simple: Comenzar con ReAct y añadir complejidad solo cuando esté motivada por fallos observados. Sobreingeniería las arquitecturas de agentes desperdicia recursos sin mejorar los resultados.
-
Los frameworks modernos (LangGraph, OpenAI Agents SDK, CrewAI) implementan estos patrones con diferentes paradigmas: máquinas de estado basadas en grafos, agente-como-función con traspasos, y equipos basados en roles. La elección del framework depende de la complejidad del flujo de control y los requisitos multi-agente.
-
Los patrones emergentes están remodelando el diseño de agentes: Agent-as-a-Service expone agentes vía APIs para composición inter-agentes, el RAG agéntico da a los agentes control sobre el proceso de recuperación, y los agentes que se automejorán aprenden de su experiencia entre tareas.
4014. Ejercicio práctico
Implementar y comparar arquitecturas de agentes:
-
Agente ReAct: Usando la implementación de la Sección 2.5, extenderlo con implementaciones reales de herramientas (usar la API de arXiv para búsqueda y
evalde Python para cálculos). -
Agente Reflexión: Añadir autorreflexión al agente ReAct. Después de que el agente complete una tarea, evaluar el resultado y, si es incorrecto, dejar que el agente reflexione y reintente.
-
Experimento de comparación: Seleccionar 5 preguntas multi-salto (se pueden usar preguntas de HotpotQA o crear las propias). Ejecutar cada pregunta a través de ambos agentes y comparar:
- Precisión (¿respuesta final correcta?)
- Eficiencia (número de llamadas al LLM)
- Costé (uso estimado de tokens)
- Recuperación de errores (¿se autocorrigió el agente?)
-
Análisis: Escribir un informe de 2 páginas comparando las arquitecturas.
Entregable: Implementación Python de ambos agentes, resultados de pruebas para 5 preguntas, y el informe comparativo.
41Referencias
- Google (2025). Agent-to-Agent (A2A) protocol specification. github.com/google/A2A.
- Huang, W., Abbeel, P., Pathak, D., & Mordatch, I. (2022). Language models as zero-shot planners: Extracting actionable knowledge for embodied agents. In Proceedings of the International Conference on Machine Learning (ICML).
- LangChain (2024). LangGraph: Build stateful, multi-actor applications with LLMs. langchain-ai.github.io/langgraph/.
- Madaan, A., Tandon, N., Gupta, P., Hallinan, S., Gao, L., Wiegreffe, S., ... & Clark, P. (2023). Self-refine: Iterative refinement with self-feedback. In Advances in Neural Information Processing Systems (NeurIPS).
- Shinn, N., Cassano, F., Gopinath, A., Narasimhan, K., & Yao, S. (2023). Reflexión: Language agents with verbal reinforcement learning. In Advances in Neural Information Processing Systems (NeurIPS).
- Wang, L., Ma, C., Feng, X., Zhang, Z., Yang, H., Zhang, J., ... & Wang, J. (2024). A survey on large language model based autonomous agents. Frontiers of Computer Science, 18(6), 186345.
- Wang, X., Wei, J., Schuurmans, D., Le, Q., Chi, E., Narang, S., ... & Zhou, D. (2023). Self-consistency improves chain of thought reasoning in language models. In Proceedings of the International Conference on Learning Representations (ICLR).
- Wu, Q., Bansal, G., Zhang, J., Wu, Y., Li, B., Zhu, E., ... & Wang, C. (2023). AutoGen: Enabling next-gen LLM applications vía multi-agent conversation. arXiv preprint arXiv:2308.08155.
- Moura, J. (2024). CrewAI: Framework for orchestrating role-playing, autonomous AI agents. docs.crewai.com.
- OpenAI (2025). OpenAI Agents SDK. openai.github.io/openai-agents-python/.
- Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2023). ReAct: Synergizing reasoning and acting in language models. In Proceedings of the International Conference on Learning Representations (ICLR).
- Yao, S., Yu, D., Zhao, J., Shafran, I., Griffiths, T. L., Cao, Y., & Narasimhan, K. (2024). Tree of thoughts: Deliberate problem solving with large language models. In Advances in Neural Information Processing Systems (NeurIPS).
- Zhou, A., Yan, K., Shlapentokh-Rothman, M., Wang, H., & Wang, Y.-X. (2024). Language agent tree search unifies reasoning, acting, and planning in language models. In Proceedings of the International Conference on Machine Learning (ICML).
Parte de "Agentic AI: Foundations, Architectures, and Applications" (CC BY-SA 4.0).