Fran Rodrigo
ArquitecturasW0525 min de lectura

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.

Conceptos núcleoBucle ReActPlan-Execute-ReplanReflexion

01Objetivos de aprendizaje

Al finalizar esta clase, los estudiantes serán capaces de:

  1. Describir la arquitectura ReAct y explicar por qué intercalar razonamiento con acción mejora el rendimiento del agente.
  2. Comparar las arquitecturas Plan-and-Execute, Reflexión y LATS, y articular cuándo cada una es más apropiada.
  3. Implementar un agente ReAct mínimo desde cero en Python (sin frameworks).
  4. Analizar el bucle observar-pensar-actuar como el patrón común subyacente a todas las arquitecturas de agentes.
  5. Evaluar arquitecturas híbridas y personalizadas para casos de uso específicos.
  6. Comparar frameworks de agentes modernos (LangGraph, OpenAI Agents SDK, CrewAI) y comprender cómo implementan los patrones arquitectónicos fundamentales.
  7. 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.

PRAO

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:

  1. Chain-of-Thought (CoT): Los LLM razonan paso a paso pero no toman acciones.
  2. 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

Traza del agente0 / 8
Memoria0

El agente aún no ha guardado nada.

Historial de herramientas0

Ninguna herramienta invocada todavía.

text
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:

  1. 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.
  2. 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.
  3. 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.
  4. 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):

EnfoqueFortalezasDebilidades
Solo CoT (sin acciones)Buen razonamiento en problemas simplesAlucina hechos, no puede acceder a info actual
Solo acción (sin razonamiento)Puede usar herramientasHace elecciones aleatorias o subóptimas
ReAct (razonamiento + acción)Razonamiento fundamentado, mejor uso de herramientasMayor coste de tokens, puede quedarse en bucles
CoT + Self-ConsistencyAlta precisión en tareas de razonamientoNo 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)

python
"""
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:

  1. 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.
  2. Aplicación estricta del formato: El prompt del sistema específica el formato exacto, y manejamos las violaciones de formato de forma elegante.
  3. 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.
  4. Máximo de pasos: Un límite duro previene los bucles infinitos.
  5. 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:

  1. Fase de planificación: El LLM genera un plan completo (lista de pasos) antes de tomar cualquier acción.
  2. 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

python
"""
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.content

3.3 Cuándo usar Plan-and-Execute

EscenarioReActPlan-and-Execute
Preguntas factuales simplesPreferido (menos sobrecarga)Excesivo
Tareas de investigación multi-pasoFunciona pero puede perder el enfoquePreferido (se mantiene en el objetivo)
Tareas con dependencias clarasAdecuadoPreferido (orden explícito)
Tareas altamente dinámicasPreferido (más adaptable)Puede planificar mal si el entorno cambia
Tareas que requieren muchas herramientasPuede tener dificultades con la selecciónMejor (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

Intento Feedback

4.3 Cómo funciona Reflexión

  1. Intentar: El agente intenta completar la tarea.
  2. Evaluar: Un evaluador verifica si el resultado es correcto.
  3. Reflexionar: Si el intento falló, el agente genera una reflexión en lenguaje natural analizando qué salió mal y cómo mejorar.
  4. Reintentar: El agente intenta de nuevo, con las reflexiones anteriores añadidas a su contexto.

4.4 Implementación

python
"""
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.

3 / 7

Los nodos con UCB alto se iluminan: son los caminos que LATS prioriza.

rootucb 1.00aucb 1.40bucb 1.10

En cada nodo:

  1. Seleccionar: Elegir el nodo más prometedor para expandir (basándose en UCB1 o criterios similares).
  2. Expandir: Generar posibles acciones siguientes.
  3. Evaluar: Evaluar cuán prometedora es cada acción.
  4. Retropropagar: Actualizar las estimaciones de valor de los nodos padres.

5.3 Cómo LATS difiere de otras arquitecturas

CaracterísticaReActTree of ThoughtsLATS
Estrategia de búsquedaLineal (greedy)Breadth-first/DFSMonte Carlo Tree Search
Interacción con entornoSí (herramientas)No (razonamiento puro)Sí (herramientas + razonamiento)
RetrocesoNo
Estimación de valorNingúnaEvaluación basada en LLMEvaluación LLM + feedback del entorno
ReflexiónNoNoSí (en caminos fallidos)

5.4 LATS simplificado (concepto)

python
"""
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.state

5.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

ArquitecturaPlanificaciónReflexiónExploraciónUso herram.Llamadas LLMMejor para
ReActImplícita (paso a paso)NingunaCamino únicoBajo (1 por paso)Tareas multi-paso simples
Plan-and-ExecuteExplícita por adelantadoVía replanificaciónCamino único (replanificable)MedioTareas complejas con estructura clara
ReflexiónImplícitaExplícita tras falloMúltiples intentosOpcionalMedio-AltoTareas con criterios de éxito claros
LATSImplícita por ramaVía retropropagaciónBúsqueda en árbolAltoTareas 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:

ArquitecturaLlamadas LLM apróx.Coste relativo
ReAct5-101x
Plan-and-Execute8-151,5x
Reflexión (2 intentos)15-252,5x
LATS (3 iteraciones, 3 ramas)30-505x

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.

python
"""
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:

  1. Flujo de control explícito: El grafo hace visible y controlable el proceso de decisión del agente.
  2. Gestión de estado: El estado se pasa explícitamente a través de los nodos, facilitando la depuración.
  3. Enrutamiento condicional: Diferentes caminos para diferentes situaciones (éxito, fallo, incertidumbre).
  4. Composabilidad: Los nodos pueden reutilizarse en diferentes flujos de trabajo.
  5. 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.
python
"""
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.
python
"""
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 request

Idea 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.
python
"""
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ísticaLangGraphOpenAI Agents SDKCrewAI
ParadigmaMáquina de estados basada en grafosAgente-como-función + traspasosEquipos basados en roles
Flujo de controlExplícito (aristas/condiciones)Implícito (traspasos)Definido por tipo de proceso
Gestión de estadoDiccionario de estado tipadoMensajes de conversaciónSalidas de tareas
Multi-agenteVía nodos del grafoVía traspasosVía crew/roles
Mejor paraFlujos de trabajo complejos y personalizadosDelegación multi-agenteFlujos de trabajo de equipo estructurados
Curva de aprendizajeMayorMenorMenor
FlexibilidadMuy altaModeradaModerada
Proveedor LLMCualquieraOpenAI (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.

Indexación offlineConsulta online
ConsultaRespuesta

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:

python
"""
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 effort

Plan-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:

  1. Complejidad de la tarea: Las tareas simples necesitan arquitecturas simples. No sobreingenieres.
  2. Tolerancia a errores: Si los errores son costosos, añadir pasos de reflexión y verificación.
  3. Requisitos de latencia: Cada capa arquitectónica añade latencia. Eliminar capas innecesarias.
  4. Presupuesto: Las arquitecturas más sofisticadas cuestan más. Calcular el coste esperado por tarea.
  5. 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:

  1. Empezar con ReAct: Implementar el bucle básico pensamiento-acción-observación. Probarlo con los casos de uso.
  2. Añadir planificación si es necesario: Si el agente pierde frecuentemente el objetivo general, añadir un paso de planificación.
  3. Añadir reflexión si es necesario: Si el agente comete errores recurrentes, añadir autorreflexión.
  4. 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 5portareaespeorqueunbucleReActrudimentarioquecuesta5 por tarea es peor que un bucle ReAct rudimentario 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:

text
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:

  1. Comprensión incremental: El agente construye comprensión paso a paso, no toda de una vez.
  2. Guiado por hipótesis: El agente forma hipótesis ("el módulo de auth probablemente tiene un bug") y las prueba.
  3. Verificación: El agente no se detiene después de hacer la corrección; verifica el resultado.
  4. Comprobación defensiva: Ejecutar toda la suite de tests, no solo el test que fallaba, detecta regresiones.

3812. Preguntas de discusión

  1. 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?

  2. 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?

  3. 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?

  4. 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?

  5. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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:

  1. 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 eval de Python para cálculos).

  2. 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.

  3. 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?)
  4. 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).