Planificación y descomposición de tareas
Del planning clásico (STRIPS, PDDL) al planning en lenguaje natural con LLMs. Descomposición jerárquica de tareas, validación y refinamiento de planes, replanificación dinámica al desviarse la ejecución. Integración con las arquitecturas de la semana 5.
01Objetivos de aprendizaje
Al finalizar esta clase, los estudiantes serán capaces de:
- Trazar la evolución de la planificación en IA desde los enfoques clásicos (STRIPS, PDDL) hasta la planificación basada en LLM en lenguaje natural.
- Explicar la descomposición jerárquica de tareas y sus ventajas para tareas complejas de agentes.
- Diseñar pipelines de generación, validación y refinamiento de planes para agentes basados en LLM.
- Implementar estrategias de replanificación dinámica que permitan a los agentes adaptarse cuando la ejecución se desvía del plan.
- Construir grafos de tareas con gestión de dependencias para ejecución paralela y secuencial.
- Comparar enfoques modernos de planificación incluyendo Tree of Thoughts y Language Agent Tree Search (LATS).
- Construir un agente de descomposición de tareas en Python que descomponga objetivos complejos en subtareas ejecutables.
021. Planificación clásica en IA
Breve historia
La planificación ha sido un tema central en la IA desde los inicios del campo; es, posiblemente, el problema original de la IA. El reto: dada una descripción del mundo, un conjunto de acciones disponibles y un objetivo, encontrar una secuencia de acciones que transforme el estado actual del mundo en uno que satisfaga el objetivo.
Piensa en lo qué requiere la planificación. Cuando planificas un viaje por carretera, consideras: dónde estás ahora, dónde quieres estar, qué rutas están disponibles, cuánto tiempo tarda cada una y qué restricciones tienes (presupuesto, tiempo, preferencias). Simulas mentalmente diferentes secuencias de acciones y eliges la qué mejor logra tu objetivo. La planificación en IA formaliza este proceso.
Comprender la planificación clásica es valioso incluso al trabajar con sistemas modernos basados en LLM porque revela lo que ganamos (flexibilidad, generalidad) y lo que perdemos (garantías, optimalidad) al pasar de la planificación formal a la planificación en lenguaje natural.
STRIPS (1971)
El Stanford Research Institute Problem Solver (STRIPS), desarrollado por Fikes y Nilsson (1971), fue uno de los primeros sistemas de planificación automatizada. Introdujo una representación formal que sigue siendo influyente:
- Estado: Un conjunto de predicados lógicos que describen el mundo (por ejemplo,
on(A, B),clear(A),on_table(B)). - Acciones: Definidas por precondiciones y efectos.
- Precondiciones: Lo que debe ser verdadero para que la acción sea aplicable.
- Efectos de adición: Lo que se convierte en verdadero después de la acción.
- Efectos de eliminación: Lo que se convierte en falso después de la acción.
- Objetivo: Un conjunto de predicados que deben ser verdaderos en el estado final.
Action: move(Block, From, To)
Preconditions: on(Block, From), clear(Block), clear(To)
Add effects: on(Block, To), clear(From)
Delete effects: on(Block, From), clear(To)La planificación STRIPS funciona buscando a través del espacio de estados posibles, aplicando acciones cuyas precondiciones se cumplen y comprobando si el estado resultante satisface el objetivo.
PDDL (1998)
El Planning Domain Definition Language (PDDL), introducido por McDermott et al. (1998), estandarizó la representación de problemas de planificación. PDDL separa el dominio (qué tipos de objetos existen y qué acciones son posibles) del problema (qué objetos específicos existen, cuál es el estado inicial y cuál es el objetivo).
(define (domain logistics)
(:requirements :strips)
(:types city location package vehicle)
(:action drive
:parameters (?v - vehicle ?from - location ?to - location ?c - city)
:precondition (and (in-city ?from ?c) (in-city ?to ?c) (at ?v ?from))
:effect (and (at ?v ?to) (not (at ?v ?from)))
)
(:action load-package
:parameters (?p - package ?v - vehicle ?l - location)
:precondition (and (at ?v ?l) (at ?p ?l))
:effect (and (in ?p ?v) (not (at ?p ?l)))
)
)PDDL se ha ampliado a lo largo de los años para soportar acciones con duración (temporales), fluentes numéricos, preferencias y efectos probabilísticos. Sigue siendo el lenguaje estándar de la International Planning Competition (IPC).
Fortalezas y limitaciones de la planificación clásica
Fortalezas:
- Corrección demostrable: si se encuentra un plan, está garantizado que logra el objetivo.
- Completitud: si existe un plan, el planificador lo encontrará (dado suficiente tiempo).
- Optimalidad: muchos planificadores encuentran planes que minimizan alguna métrica de coste.
Limitaciones:
- Requiere descripciones de dominio completas y formales (difíciles de escribir para tareas del mundo real). Intenta escribir un dominio PDDL para "planificar una fiesta de cumpleaños" y apreciarás este reto.
- Fragilidad: cualquier discrepancia entre el modelo y la realidad causa el fallo del plan. Si el restaurante en el que planeabas comer está cerrado, todo el plan se rompe.
- No maneja bien la incertidumbre, la observabilidad parcial ni los espacios de estados continuos.
- No puede manejar objetivos abiertos y mal definidos ("escribir un buen ensayo" no es un objetivo PDDL válido).
Idea clave: La planificación clásica proporciona garantías que la planificación basada en LLM no puede igualar (corrección, completitud, optimalidad). Pero requiere formalizar el mundo por completo, lo cual es impracticable para la mayoría de las tareas del mundo real. La planificación basada en LLM intercambia esas garantías por flexibilidad y generalidad. Comprender este compromiso es esencial para elegir el enfoque correcto.
032. Planificación basada en LLM: Planes en lenguaje natural
El cambio de paradigma
La planificación basada en LLM representa una desviación radical de la planificación clásica de IA. En lugar de representaciones formales de estados y esquemas de acciones, se describe la tarea en español o inglés llano y se le pide al LLM que genere un plan. Sin PDDL, sin precondiciones, sin efectos formales; sólo un prompt.
La analogía es la diferencia entre dar direcciones usando coordenadas formales ("Ve a 40.7128N, 74.0060W") versus lenguaje natural ("Ve a la esquina de Broadway con Wall Street, luego gira a la izquierda"). El lenguaje natural es menos preciso pero mucho más práctico para la mayoría de las situaciones.
PLANNING_PROMPT = """You are a planning agent. Given a goal, generate a step-by-step
plan to achieve it. Each step should be:
- Specific and actionable
- Ordered logically (dependencies respected)
- Achievable with the available tools
Goal: {goal}
Available tools:
{tools}
Generate a numbered plan:"""
def generate_plan(goal: str, tools: list[str], llm_call) -> list[str]:
"""Generate a natural language plan for a goal.
Args:
goal: The goal to achieve.
tools: List of available tool descriptions.
llm_call: Function to call the LLM.
Returns:
List of plan steps as strings.
"""
tools_str = "\n".join(f"- {tool}" for tool in tools)
prompt = PLANNING_PROMPT.format(goal=goal, tools=tools_str)
response = llm_call(prompt=prompt)
# Parse numbered steps
steps = []
for line in response.strip().split("\n"):
line = line.strip()
if line and line[0].isdigit():
# Remove the number prefix
step = line.lstrip("0123456789.)")
steps.append(step.strip())
return stepsVentajas de la planificación basada en LLM
- No se necesita descripción formal del dominio: El LLM utiliza su conocimiento entrenado para comprender las acciones y sus efectos.
- Maneja la ambigüedad: Objetivos en lenguaje natural como "mejorar el informe" pueden interpretarse y planificarse.
- Flexible y general: El mismo planificador funciona en diferentes dominios sin reconfiguración.
- Incorpora conocimiento del mundo: El LLM sabe que "reservar un vuelo requiere un pasaporte" sin que se lo digan.
Limitaciones de la planificación basada en LLM
- Sin garantías de corrección: El plan puede contener errores lógicos, pasos imposibles o dependencias faltantes.
- Acciones alucinadas: El LLM puede sugerir acciones que realmente no están disponibles.
- Deficiente en planificación a largo horizonte: A medida que los planes se alargan, los LLM tienen dificultades para mantener la consistencia y rastrear las dependencias.
- Sin optimalidad: El plan generado rara vez es óptimo en términos de pasos, coste o tiempo.
El patrón planificar-y-ejecutar
Un enfoque práctico combina la planificación del LLM con la ejecución paso a paso:
class PlanAndExecuteAgent:
"""An agent that first plans, then executes step by step.
This separates the planning phase (creative, high-level) from the
execution phase (precise, tool-using), allowing each to be optimized
independently.
"""
def __init__(self, planner_llm, executor_llm, tools: dict[str, callable]):
self.planner_llm = planner_llm
self.executor_llm = executor_llm
self.tools = tools
def plan(self, goal: str) -> list[str]:
"""Generate a plan for the goal."""
tools_desc = "\n".join(f"- {name}" for name in self.tools.keys())
prompt = PLANNING_PROMPT.format(goal=goal, tools=tools_desc)
response = self.planner_llm(prompt=prompt)
return self._parse_steps(response)
def execute_step(self, step: str, context: str) -> str:
"""Execute a single step of the plan.
Args:
step: The step to execute.
context: Results from previous steps.
Returns:
The result of executing the step.
"""
prompt = f"""Execute the following step of a plan.
Previous context:
{context}
Current step: {step}
Available tools: {list(self.tools.keys())}
Determine which tool to use (if any) and execute the step.
Respond with the result."""
response = self.executor_llm(prompt=prompt)
# Check if the executor wants to use a tool
for tool_name, tool_fn in self.tools.items():
if tool_name.lower() in response.lower():
# Extract arguments and call the tool
tool_result = tool_fn(step)
return f"Tool '{tool_name}' result: {tool_result}"
return response
def run(self, goal: str) -> dict:
"""Plan and execute a goal.
Returns:
Dict with the plan, execution results, and final outcome.
"""
# Phase 1: Plan
plan = self.plan(goal)
print(f"Plan generated with {len(plan)} steps:")
for i, step in enumerate(plan, 1):
print(f" {i}. {step}")
# Phase 2: Execute
results = []
context = ""
for i, step in enumerate(plan):
print(f"\nExecuting step {i + 1}: {step}")
result = self.execute_step(step, context)
results.append({"step": step, "result": result})
context += f"\nStep {i + 1} ({step}): {result}"
print(f" Result: {result[:200]}")
return {
"goal": goal,
"plan": plan,
"results": results,
"final_context": context,
}
def _parse_steps(self, response: str) -> list[str]:
"""Parse numbered steps from LLM response."""
steps = []
for line in response.strip().split("\n"):
line = line.strip()
if line and (line[0].isdigit() or line.startswith("-")):
step = line.lstrip("0123456789.)- ")
if step:
steps.append(step)
return steps043. Descomposición jerárquica de tareas
¿Por que la descomposición jerárquica?
Las tareas complejas son difíciles de planificar en una sola pasada. Esto es cierto tanto para los humanos como para los agentes de IA. Si le pides a alguien qué "construya una aplicación web para gestionar un restaurante", no empieza inmediatamente a escribir código. Primero piensa a alto nivel (¿qué funcionalidades necesitamos?), luego profundiza en los detalles (¿cómo debería funcionar el sistema de reservas?) y solo entonces llega al nivel de tareas individuales de codificación.
Esto es descomposición jerárquica: dividir un gran problema en problemas más pequeños, y esos problemas más pequeños en otros aún más pequeños, hasta que cada pieza sea lo suficientemente pequeña para ejecutarse directamente. Así es como gestionamos la complejidad en ingeniería de software (módulos, funciones), en organizaciones (departamentos, equipos) y en la vida cotidiana (planificar una boda implica planificar el lugar, la comida, las invitaciones, etc., cada uno de los cuáles se descompone a su vez).
Un objetivo como "construir una aplicación web para gestionar un restaurante" no puede descomponerse de forma significativa en una lista plana de pasos atómicos. Requiere múltiples niveles de abstracción:
- Alto nivel: Diseñar la arquitectura del sistema, construir el frontend, construir el backend, configurar la base de datos, desplegar.
- Nivel medio: Para "construir el frontend", descomponer en: configurar el proyecto, crear el layout, construir la página del menú, construir el sistema de reservas, añadir autenticación.
- Nivel bajo: Para "construir la página del menú", descomponer en: crear el modelo de datos, construir el endpoint de la API, crear el componente React, añadir estilos, escribir pruebas.
Redes de tareas jerárquicas (HTN)
Las Redes de Tareas Jerárquicas son un formalismo clásico de planificación en IA que se ajusta a esta intuición. En la planificación HTN:
- Las tareas primitivas son acciones directamente ejecutables.
- Las tareas compuestas deben descomponerse en subtareas usando métodos.
- Los métodos definen cómo puede descomponerse una tarea compuesta, incluyendo restricciones de ordenación entre subtareas.
Compound Task: prepare_dinner
Method 1: home_cooking
Subtasks: [choose_recipe, buy_ingredients, cook, serve]
Method 2: order_delivery
Subtasks: [choose_restaurant, place_order, wait, receive]Descomposición jerárquica basada en LLM
Los LLM realizan la descomposición jerárquica de forma natural cuando se les da el prompt adecuado:
DECOMPOSITION_PROMPT = """Break down the following task into 3-7 subtasks.
Each subtask should be:
- More specific than the parent task
- Independent enough to be worked on separately
- Small enough to be completed in a focused work session
If a subtask is still complex, mark it with [COMPLEX] so it can be
further decomposed.
Task: {task}
Subtasks:"""
def hierarchical_decompose(
task: str, llm_call, max_depth: int = 3, current_depth: int = 0
) -> dict:
"""Recursively decompose a task into a hierarchy of subtasks.
Args:
task: The task to decompose.
llm_call: Function to call the LLM.
max_depth: Maximum decomposition depth.
current_depth: Current depth in the recursion.
Returns:
A tree structure representing the task hierarchy.
"""
if current_depth >= max_depth:
return {"task": task, "subtasks": [], "leaf": True}
response = llm_call(prompt=DECOMPOSITION_PROMPT.format(task=task))
subtasks = _parse_subtasks(response)
tree = {"task": task, "subtasks": [], "leaf": False}
for subtask_text in subtasks:
is_complex = "[COMPLEX]" in subtask_text
clean_text = subtask_text.replace("[COMPLEX]", "").strip()
if is_complex and current_depth < max_depth - 1:
# Recursively decompose complex subtasks
subtree = hierarchical_decompose(
clean_text, llm_call, max_depth, current_depth + 1
)
tree["subtasks"].append(subtree)
else:
tree["subtasks"].append({
"task": clean_text,
"subtasks": [],
"leaf": True,
})
return tree
def _parse_subtasks(response: str) -> list[str]:
"""Parse subtasks from LLM response."""
subtasks = []
for line in response.strip().split("\n"):
line = line.strip()
if line and (line[0].isdigit() or line.startswith("-")):
subtask = line.lstrip("0123456789.)- ")
if subtask:
subtasks.append(subtask)
return subtasks
def print_task_tree(tree: dict, indent: int = 0) -> None:
"""Pretty-print a task tree."""
prefix = " " * indent
marker = "[leaf]" if tree["leaf"] else "[node]"
print(f"{prefix}{marker} {tree['task']}")
for subtask in tree.get("subtasks", []):
print_task_tree(subtask, indent + 1)054. Validación y refinamiento de planes
Por qué los planes necesitan validación
Los planes generados por LLM no están garantizados como correctos. Problemas comunes incluyen:
- Pasos faltantes: Se omiten prerrequisitos críticos.
- Orden incorrecto: Pasos que dependen entre sí están en el orden equivocado.
- Pasos inviables: Pasos que no pueden ejecutarse con las herramientas disponibles.
- Pasos redundantes: Acciones innecesarias que desperdician recursos.
- Pasos vagos: Pasos demasiado abstractos para ejecutarse directamente.
Estrategias de validación de planes
Auto-validación
Pedir al LLM que critique su propio plan:
VALIDATION_PROMPT = """Review the following plan for achieving the stated goal.
Check for:
1. Missing steps: Are there any prerequisites or necessary actions not included?
2. Ordering errors: Are steps in the right order? Are dependencies respected?
3. Feasibility: Can each step be executed with the available tools?
4. Redundancy: Are there unnecessary or duplicate steps?
5. Specificity: Is each step specific enough to be actionable?
Goal: {goal}
Plan:
{plan}
Available tools: {tools}
Provide your assessment, listing any issues found and suggesting corrections."""
def validate_plan(
goal: str, plan: list[str], tools: list[str], llm_call
) -> dict:
"""Validate a plan and identify issues.
Returns:
Dict with validation results and suggested fixes.
"""
plan_str = "\n".join(f"{i+1}. {step}" for i, step in enumerate(plan))
tools_str = ", ".join(tools)
response = llm_call(
prompt=VALIDATION_PROMPT.format(goal=goal, plan=plan_str, tools=tools_str)
)
return {
"original_plan": plan,
"validation_feedback": response,
"needs_revision": any(
keyword in response.lower()
for keyword in ["missing", "error", "incorrect", "should be", "add"]
),
}Validación cruzada con un agente crítico
Usar un agente separado (o una llamada diferente al LLM con un prompt distinto) para revisar el plan. Esto proporciona una forma de revisión por pares:
CRITIC_PROMPT = """You are a plan reviewer. Your job is to find flaws in plans
before they are executed. Be thorough and skeptical.
Goal: {goal}
Plan:
{plan}
For each step, assess:
- Is this step necessary?
- Is it in the correct position?
- What could go wrong?
- Is it specific enough?
Rate the overall plan quality (1-10) and list all issues."""Validación basada en simulación
Para planes que involucran ejecución de código o llamadas a APIs, validar simulando la ejecución en un entorno controlado:
def simulate_plan_execution(
plan: list[str], tools: dict[str, callable], dry_run: bool = True
) -> list[dict]:
"""Simulate plan execution to catch errors before real execution.
Args:
plan: List of plan steps.
tools: Available tools.
dry_run: If True, tools log what they would do without executing.
Returns:
List of simulation results for each step.
"""
results = []
state = {}
for i, step in enumerate(plan):
result = {
"step": i + 1,
"description": step,
"status": "unknown",
"issues": [],
}
# Check if the step references a known tool
tool_found = False
for tool_name in tools:
if tool_name.lower() in step.lower():
tool_found = True
if dry_run:
result["status"] = "simulated"
result["output"] = f"Would execute: {tool_name}"
else:
try:
output = tools[tool_name](step)
result["status"] = "success"
result["output"] = output
except Exception as e:
result["status"] = "error"
result["issues"].append(str(e))
break
if not tool_found:
result["status"] = "no_tool"
result["issues"].append("No matching tool found for this step.")
results.append(result)
return resultsRefinamiento de planes
Basándose en la retroalimentación de la validación, refinar el plan:
REFINEMENT_PROMPT = """Revise the following plan based on the feedback provided.
Make minimal changes to fix the identified issues while preserving the parts
that are correct.
Original plan:
{plan}
Feedback:
{feedback}
Revised plan:"""
def refine_plan(
plan: list[str], feedback: str, llm_call
) -> list[str]:
"""Refine a plan based on validation feedback."""
plan_str = "\n".join(f"{i+1}. {step}" for i, step in enumerate(plan))
response = llm_call(
prompt=REFINEMENT_PROMPT.format(plan=plan_str, feedback=feedback)
)
return _parse_subtasks(response)El bucle planificar-validar-refinar
Combinando estos componentes en un bucle de mejora iterativa:
def iterative_planning(
goal: str, tools: list[str], llm_call, max_iterations: int = 3
) -> list[str]:
"""Generate and iteratively refine a plan.
Args:
goal: The goal to plan for.
tools: Available tools.
llm_call: Function to call the LLM.
max_iterations: Maximum refinement iterations.
Returns:
The final refined plan.
"""
# Generate initial plan
plan = generate_plan(goal, tools, llm_call)
print(f"Initial plan: {len(plan)} steps")
for iteration in range(max_iterations):
# Validate
validation = validate_plan(goal, plan, tools, llm_call)
if not validation["needs_revision"]:
print(f"Plan validated after {iteration + 1} iteration(s).")
return plan
# Refine
print(f"Iteration {iteration + 1}: Refining plan based on feedback...")
plan = refine_plan(plan, validation["validation_feedback"], llm_call)
print(f"Reached max iterations ({max_iterations}). Returning best plan.")
return plan065. Replanificación dinámica
Cuando los planes se encuentran con la realidad
El estratega militar Helmuth von Moltke dijo la célebre frase: "Ningún plan sobrevive al contacto con el enemigo." Lo mismo es cierto para los agentes de IA: ningún plan sobrevive al contacto con la realidad sin cambios. La capacidad de adaptarse cuando las cosas van mal es lo que distingue a un agente robusto de uno frágil.
Idea clave: La capacidad de planificación es necesaria pero no suficiente para la agencia. La prueba real de un agente no es si puede hacer un plan, sino si puede recuperarse cuando el plan falla. La replanificación dinámica (detectar fallos, diagnosticar causas y generar enfoques alternativos) es lo que hace a los agentes robustos en entornos del mundo real.
Los agentes encuentran:
- Fallos de herramientas: Una API devuelve un error, un archivo no existe.
- Resultados inesperados: Una búsqueda no devuelve resultados, el código produce una salida incorrecta.
- Condiciones cambiantes: Nueva información invalida suposiciones anteriores.
- Pasos bloqueados: Un paso no puede ejecutarse debido a permisos o recursos faltantes.
La replanificación dinámica permite a los agentes adaptarse:
class DynamicPlanExecutor:
"""Execute a plan with dynamic replanning on failure.
When a step fails, the executor asks the LLM to generate an
alternative approach or revise the remaining plan.
"""
def __init__(self, llm_call, tools: dict[str, callable]):
self.llm_call = llm_call
self.tools = tools
def execute_with_replanning(
self, goal: str, initial_plan: list[str], max_replans: int = 3
) -> dict:
"""Execute a plan, replanning when steps fail.
Args:
goal: The original goal.
initial_plan: The initial plan.
max_replans: Maximum number of times to replan.
Returns:
Execution results including any replanning events.
"""
plan = list(initial_plan)
execution_log = []
replans = 0
step_idx = 0
while step_idx < len(plan):
step = plan[step_idx]
print(f"Executing step {step_idx + 1}/{len(plan)}: {step}")
try:
result = self._execute_step(step)
execution_log.append({
"step": step,
"status": "success",
"result": result,
})
print(f" Success: {result[:100]}")
step_idx += 1
except Exception as e:
error_msg = str(e)
print(f" Failed: {error_msg}")
execution_log.append({
"step": step,
"status": "failed",
"error": error_msg,
})
if replans >= max_replans:
print(" Max replans reached. Aborting.")
break
# Replan
print(" Replanning...")
remaining_steps = plan[step_idx:]
completed_context = "\n".join(
f"- {log['step']}: {log.get('result', log.get('error', 'N/A'))[:100]}"
for log in execution_log
)
new_plan = self._replan(
goal, completed_context, remaining_steps, error_msg
)
if new_plan:
plan = plan[:step_idx] + new_plan
replans += 1
print(f" New plan ({len(new_plan)} steps remaining)")
else:
print(" Replanning failed. Skipping step.")
step_idx += 1
return {
"goal": goal,
"execution_log": execution_log,
"replans": replans,
"completed": step_idx >= len(plan),
}
def _execute_step(self, step: str) -> str:
"""Execute a single step using available tools."""
for tool_name, tool_fn in self.tools.items():
if tool_name.lower() in step.lower():
return tool_fn(step)
# If no tool matches, use the LLM to reason about the step
return self.llm_call(prompt=f"Execute this step and report the result: {step}")
def _replan(
self, goal: str, completed: str, failed_steps: list[str], error: str
) -> list[str] | None:
"""Generate a new plan given a failure."""
prompt = f"""A plan step failed during execution. Generate a revised plan
to achieve the goal, taking into account what has already been completed
and the error encountered.
Goal: {goal}
Completed steps:
{completed}
Failed step: {failed_steps[0] if failed_steps else 'N/A'}
Error: {error}
Remaining original steps:
{chr(10).join(f'- {s}' for s in failed_steps[1:])}
Generate a revised plan for the remaining work (avoid repeating completed steps):"""
response = self.llm_call(prompt=prompt)
steps = _parse_subtasks(response)
return steps if steps else NoneEstrategias de replanificación
Diferentes situaciones requieren diferentes estrategias de replanificación:
| Tipo de fallo | Estrategia | Ejemplo |
|---|---|---|
| Herramienta temporalmente no disponible | Reintentar con espera exponencial | Se alcanzó el límite de tasa de la API |
| Herramienta devuelve error | Herramienta alternativa | Cambiar de un motor de búsqueda a otro |
| El paso produce resultado incorrecto | Modificar el paso | Ajustar parámetros de consulta |
| El paso es fundamentalmente inviable | Enfoque alternativo | Reescribir el plan para usar un método diferente |
| El objetivo en sí es poco claro | Solicitar aclaración | Preguntar al usuario por más detalles |
076. Grafos de tareas y gestión de dependencias
Más allá de los planes lineales
Muchas tareas del mundo real no son puramente secuenciales. Consideremos un proyecto de software: el desarrollo del frontend y del backend pueden ocurrir en paralelo, pero las pruebas de integración dependen de que ambos estén completos. Un esquema de base de datos debe diseñarse antes de que el backend pueda implementar consultas, pero el frontend puede construirse en paralelo con el diseño del esquema.
Un plan lineal (paso 1, paso 2, paso 3...) no puede expresar estas relaciones. Un grafo de tareas (un grafo acíclico dirigido, DAG, donde los nodos son tareas y las aristas son dependencias) las captura de forma natural. Es el mismo concepto usado por los sistemas de compilación (Make, Gradle), los orquestadores de flujos de trabajo (Airflow, Prefect) y los pipelines de CI/CD.
Pruébalo tú mismo: Piensa en preparar una cena completa con varios platos. ¿Qué tareas pueden hacerse en paralelo (por ejemplo, hervir la pasta mientras se asan las verduras)? ¿Qué tareas tienen dependencias (por ejemplo, no se puede emplatar antes de cocinar)? Dibuja un grafo de tareas para una cena de tres platos. Verás que un buen grafo de tareas reduce significativamente el tiempo total de cocinado comparado con un enfoque secuencial.
from dataclasses import dataclass, field
from enum import Enum
class TaskStatus(Enum):
PENDING = "pending"
READY = "ready" # All dependencies met
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
SKIPPED = "skipped"
@dataclass
class TaskNode:
"""A node in the task graph."""
id: str
description: str
dependencies: list[str] = field(default_factory=list) # IDs of prerequisite tasks
status: TaskStatus = TaskStatus.PENDING
result: str | None = None
error: str | None = None
def is_ready(self, completed_tasks: set[str]) -> bool:
"""Check if all dependencies are satisfied."""
return all(dep in completed_tasks for dep in self.dependencies)
class TaskGraph:
"""A directed acyclic graph (DAG) of tasks with dependency management.
Supports parallel execution of independent tasks and respects
ordering constraints.
"""
def __init__(self):
self.nodes: dict[str, TaskNode] = {}
def add_task(
self, task_id: str, description: str, dependencies: list[str] | None = None
) -> None:
"""Add a task to the graph."""
deps = dependencies or []
# Validate dependencies exist
for dep in deps:
if dep not in self.nodes:
raise ValueError(f"Dependency '{dep}' not found. Add it first.")
self.nodes[task_id] = TaskNode(
id=task_id, description=description, dependencies=deps
)
def get_ready_tasks(self) -> list[TaskNode]:
"""Get all tasks that are ready to execute (dependencies satisfied)."""
completed = {
node_id for node_id, node in self.nodes.items()
if node.status == TaskStatus.COMPLETED
}
ready = []
for node in self.nodes.values():
if node.status == TaskStatus.PENDING and node.is_ready(completed):
node.status = TaskStatus.READY
ready.append(node)
return ready
def mark_completed(self, task_id: str, result: str) -> None:
"""Mark a task as completed with its result."""
self.nodes[task_id].status = TaskStatus.COMPLETED
self.nodes[task_id].result = result
def mark_failed(self, task_id: str, error: str) -> None:
"""Mark a task as failed."""
self.nodes[task_id].status = TaskStatus.FAILED
self.nodes[task_id].error = error
# Also skip dependent tasks
self._propagate_failure(task_id)
def _propagate_failure(self, failed_id: str) -> None:
"""Skip all tasks that depend on a failed task."""
for node in self.nodes.values():
if failed_id in node.dependencies and node.status == TaskStatus.PENDING:
node.status = TaskStatus.SKIPPED
node.error = f"Skipped because dependency '{failed_id}' failed."
self._propagate_failure(node.id)
def is_complete(self) -> bool:
"""Check if all tasks are completed (or skipped/failed)."""
return all(
node.status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.SKIPPED)
for node in self.nodes.values()
)
def get_execution_order(self) -> list[list[str]]:
"""Get the topological execution order as batches of parallel tasks.
Returns:
List of batches, where each batch contains tasks that can run in parallel.
"""
completed: set[str] = set()
remaining = set(self.nodes.keys())
batches = []
while remaining:
batch = []
for task_id in remaining:
node = self.nodes[task_id]
if all(dep in completed for dep in node.dependencies):
batch.append(task_id)
if not batch:
raise ValueError("Circular dependency detected!")
batches.append(batch)
completed.update(batch)
remaining -= set(batch)
return batches
def visualize(self) -> str:
"""Create a text visualization of the task graph."""
lines = ["Task Graph:"]
batches = self.get_execution_order()
for i, batch in enumerate(batches):
lines.append(f"\n Batch {i + 1} (parallel):")
for task_id in batch:
node = self.nodes[task_id]
deps = f" (depends on: {', '.join(node.dependencies)})" if node.dependencies else ""
status = f" [{node.status.value}]"
lines.append(f" - {task_id}: {node.description}{deps}{status}")
return "\n".join(lines)
# Example usage
def build_example_graph() -> TaskGraph:
"""Build an example task graph for a data analysis project."""
graph = TaskGraph()
# Independent tasks (can run in parallel)
graph.add_task("fetch_data", "Download dataset from API")
graph.add_task("setup_env", "Set up Python virtual environment and install dependencies")
# Depends on fetch_data
graph.add_task("clean_data", "Clean and preprocess the raw data", ["fetch_data"])
graph.add_task("explore_data", "Perform exploratory data analysis", ["fetch_data"])
# Depends on clean_data and setup_env
graph.add_task("train_model", "Train the ML model", ["clean_data", "setup_env"])
# Depends on train_model and explore_data
graph.add_task("evaluate", "Evaluate model on test set", ["train_model"])
graph.add_task("generate_report", "Generate analysis report", ["evaluate", "explore_data"])
return graphGeneración de grafos de tareas a partir de lenguaje natural
Un LLM puede generar grafos de tareas identificando tanto las tareas como sus dependencias:
TASK_GRAPH_PROMPT = """Analyze the following goal and generate a task graph.
For each task, identify:
1. A unique short ID
2. A description
3. Which other tasks it depends on (by ID)
Output format (one task per line):
ID | Description | Dependencies (comma-separated IDs, or "none")
Goal: {goal}
Tasks:"""
def generate_task_graph(goal: str, llm_call) -> TaskGraph:
"""Generate a task graph from a natural language goal."""
response = llm_call(prompt=TASK_GRAPH_PROMPT.format(goal=goal))
graph = TaskGraph()
tasks_to_add = []
# Parse the response
for line in response.strip().split("\n"):
parts = [p.strip() for p in line.split("|")]
if len(parts) >= 3:
task_id = parts[0].lower().replace(" ", "_")
description = parts[1]
deps_str = parts[2]
deps = (
[] if deps_str.lower() == "none"
else [d.strip().lower().replace(" ", "_") for d in deps_str.split(",")]
)
tasks_to_add.append((task_id, description, deps))
# Add tasks in dependency order
added = set()
max_passes = len(tasks_to_add) * 2
passes = 0
while tasks_to_add and passes < max_passes:
remaining = []
for task_id, description, deps in tasks_to_add:
if all(d in added for d in deps):
graph.add_task(task_id, description, deps)
added.add(task_id)
else:
remaining.append((task_id, description, deps))
tasks_to_add = remaining
passes += 1
return graph087. Estrategias de descomposición
Descomposición descendente (top-down)
Empezar con el objetivo de alto nivel y descomponerlo recursivamente en subtareas más pequeñas. Este es el enfoque más natural para los LLM y refleja cómo los humanos planifican proyectos complejos.
Ventajas: Mantiene la coherencia con el objetivo general; asegura que todas las partes sirvan al objetivo principal. Desventajas: Puede pasar por alto ideas ascendentes; las decisiones tempranas de descomposición restringen las opciones posteriores.
Refinamiento iterativo
Empezar con un plan aproximado y refinar progresivamente cada paso:
def iterative_refinement(
goal: str, llm_call, refinement_rounds: int = 3
) -> list[str]:
"""Iteratively refine a plan through multiple passes.
Round 1: Generate a high-level outline (3-5 major phases)
Round 2: Expand each phase into specific steps
Round 3: Add details, error handling, and validation steps
"""
# Round 1: High-level outline
outline = llm_call(
prompt=f"Create a high-level outline (3-5 phases) for: {goal}"
)
# Round 2: Expand each phase
expanded = llm_call(
prompt=(
f"Expand each phase of this outline into 2-4 specific, actionable steps.\n\n"
f"Outline:\n{outline}\n\nExpanded plan:"
)
)
# Round 3: Add details
detailed = llm_call(
prompt=(
f"Review this plan and add any missing steps, error handling, "
f"and validation checkpoints.\n\n"
f"Plan:\n{expanded}\n\nDetailed plan:"
)
)
return _parse_subtasks(detailed)Least-to-Most Prompting
Introducido por Zhou et al. (2023), este enfoque descompone un problema complejo en una serie de subproblemas más simples, resolviéndolos del más simple al más complejo. Cada solución se basa en las anteriores.
Complex problem: "Calculate the total cost of a round trip from Madrid to Tokyo,
including flights, hotels for 5 nights, and daily meals."
Decomposition:
1. What is the average cost of a round-trip flight from Madrid to Tokyo?
2. What is the average cost of a hotel in Tokyo per night?
3. What is the average daily meal budget in Tokyo?
4. Given answers to 1-3, what is the total cost?098. Planificación con incertidumbre e información incompleta
El desafío
La planificación del mundo real a menudo involucra:
- Resultados inciertos: Las acciones pueden tener éxito o fallar de forma probabilística.
- Información incompleta: El agente no conoce todo sobre el estado actual.
- Entornos dinámicos: El mundo cambia mientras el agente ejecuta su plan.
Planificación de contingencia
Generar planes con ramas explícitas para diferentes resultados:
CONTINGENCY_PROMPT = """Generate a plan for the following goal, including
contingency branches for steps that might fail.
For each step, specify:
- The primary action
- What could go wrong
- The fallback action if it fails
Goal: {goal}
Plan with contingencies:"""
@dataclass
class ContingencyStep:
"""A plan step with contingency handling."""
primary_action: str
potential_failure: str
fallback_action: str
def to_text(self) -> str:
return (
f"DO: {self.primary_action}\n"
f" IF FAILS ({self.potential_failure}): {self.fallback_action}"
)Planificación en espacio de creencias
En lugar de planificar sobre estados del mundo, planificar sobre estados de creencia (distribuciones de probabilidad sobre estados posibles). Esto es relevante para agentes que operán con observabilidad parcial:
Belief: "The file is probably in /data/ (80%) or /backup/ (20%)"
Plan:
1. Check /data/ for the file.
2. IF found: proceed with processing.
3. IF not found: check /backup/.
4. IF still not found: ask the user for the file location.109. Comparación: Tree of Thoughts y LATS
Los métodos que hemos discutido hasta ahora (planificar-y-ejecutar, descomposición jerárquica, replanificación) generan todos un único plan e intentan ejecutarlo. Pero, ¿y si el primer plan no es el mejor? ¿Y si hay múltiples enfoques prometedores y queremos explorar varios antes de comprometernos?
Tree of Thoughts y Language Agent Tree Search adoptan un enfoque diferente: exploran sistemáticamente múltiples caminos de planificación, evalúan cada uno y eligen el mejor. Esto intercambia coste computacional por calidad del plan.
Tree of Thoughts (ToT)
Tree of Thoughts (Yao et al., 2023) extiende el prompting de cadena de pensamiento explorando múltiples caminos de razonamiento simultáneamente. En lugar de comprometerse con una cadena de razonamiento, el sistema genera varios candidatos en cada paso y evalúa cuáles son más prometedores, de forma similar a un jugador de ajedrez qué considera varias jugadas posibles antes de elegir una. En lugar de una única cadena lineal de pensamientos, el LLM genera múltiples "pensamientos" candidatos en cada paso y los evalúa para decidir qué caminos seguir.
Interactive · Búsqueda en árbol de pensamientos con poda
Laboratorio de prompting
Cambia la estrategia, mira la salida
El mismo problema con cuatro estrategias de prompting distintas. La diferencia se ve mejor que se explica: pulsa y compara.
Tarea
Estrategia
Sistema
Piensa paso a paso antes de responder.
Usuario
Si una camisa cuesta 24 € y le aplico un 25 % de descuento, ¿cuánto pago?
Pensemos paso a paso. 1. Descuento = 24 × 0,25 = 6 2. Precio final = 24 − 6 = 18 Respuesta: 18 €
Componentes clave:
- Generación de pensamientos: El LLM propone múltiples pasos siguientes candidatos.
- Evaluación de estado: El LLM (o una heurística) evalúa lo prometedora que es cada solución parcial.
- Algoritmo de búsqueda: BFS o DFS con poda basada en evaluaciones.
def tree_of_thoughts(
problem: str,
llm_call,
num_thoughts: int = 3,
max_depth: int = 4,
beam_width: int = 2,
) -> str:
"""Simplified Tree of Thoughts implementation.
Uses beam search over multiple reasoning paths.
Args:
problem: The problem to solve.
llm_call: Function to call the LLM.
num_thoughts: Number of candidate thoughts to generate at each step.
max_depth: Maximum depth of the thought tree.
beam_width: Number of paths to keep at each step.
Returns:
The best solution found.
"""
# Initialize with the problem
beams = [{"path": [], "text": problem, "score": 0.0}]
for depth in range(max_depth):
all_candidates = []
for beam in beams:
# Generate candidate next thoughts
thoughts = llm_call(
prompt=(
f"Problem: {problem}\n\n"
f"Progress so far:\n{beam['text']}\n\n"
f"Generate {num_thoughts} different possible next steps. "
f"Number each option."
)
)
# Parse and evaluate each thought
for thought in _parse_thoughts(thoughts):
# Evaluate this partial solution
evaluation = llm_call(
prompt=(
f"Rate how promising this partial solution is (1-10):\n\n"
f"Problem: {problem}\n"
f"Progress: {beam['text']}\n"
f"Next step: {thought}\n\n"
f"Score (just the number):"
)
)
try:
score = float(evaluation.strip().split()[0])
except (ValueError, IndexError):
score = 5.0
all_candidates.append({
"path": beam["path"] + [thought],
"text": f"{beam['text']}\n{thought}",
"score": score,
})
# Keep top beam_width candidates
all_candidates.sort(key=lambda x: x["score"], reverse=True)
beams = all_candidates[:beam_width]
# Return the best path
return beams[0]["text"] if beams else "No solution found."
def _parse_thoughts(response: str) -> list[str]:
"""Parse numbered thoughts from LLM response."""
thoughts = []
for line in response.strip().split("\n"):
line = line.strip()
if line and line[0].isdigit():
thought = line.lstrip("0123456789.)- ").strip()
if thought:
thoughts.append(thought)
return thoughtsLanguage Agent Tree Search (LATS)
LATS (Zhou et al., 2024) combina la planificación basada en LLM con Monte Carlo Tree Search (MCTS). Trata el proceso de decisión del agente como un problema de búsqueda en árbol:
- Selección: Navegar por el árbol usando UCB1 (Upper Confidence Bound) para equilibrar exploración y explotación.
- Expansión: Usar el LLM para generar nuevos candidatos de acción.
- Simulación: Usar el LLM para simular el resultado de cada acción.
- Retropropagación: Actualizar las puntuaciones de los nodos visitados basándose en los resultados de la simulación.
- Reflexión: Cuando una trayectoria falla, generar una reflexión que informe futuros intentos.
LATS es particularmente potente porque:
- Explora sistemáticamente el espacio de acciones (a diferencia de los enfoques voraces).
- Aprende de los fallos a través de la reflexión.
- Equilibra probar nuevos enfoques con explotar caminos conocidos como buenos.
Comparación
| Característica | Chain of Thought | Tree of Thoughts | LATS |
|---|---|---|---|
| Exploración | Un único camino | Múltiples caminos | Búsqueda sistemática en árbol |
| Evaluación | Ninguna (implícita) | Evaluación del LLM en cada paso | MCTS + evaluación del LLM |
| Retroceso | No | Limitado (beam search) | Completo (retropropagación MCTS) |
| Aprendizaje del fracaso | No | No | Sí (reflexión) |
| Coste | Bajo (1 llamada al LLM) | Medio (muchas evaluaciones) | Alto (muchas simulaciones) |
| Mejor para | Razonamiento simple | Tareas creativas/divergentes | Tareas complejas de múltiples pasos |
1110. Ejemplo práctico: Un agente de descomposición de tareas
Construyamos un agente completo de descomposición de tareas que divide objetivos complejos en subtareas manejables, crea un gráfo de tareas y lo ejecuta.
"""
A task decomposition agent that:
1. Takes a complex goal
2. Decomposes it into subtasks with dependencies
3. Builds a task graph
4. Executes tasks in dependency order
5. Replans on failure
Requirements:
pip install openai (or any LLM client)
"""
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class SubTask:
"""A subtask produced by decomposition."""
id: str
description: str
dependencies: list[str] = field(default_factory=list)
estimated_complexity: str = "medium" # low, medium, high
status: str = "pending"
result: str | None = None
DECOMPOSE_SYSTEM = """You are a task decomposition expert. Given a complex goal,
break it down into concrete, actionable subtasks.
Rules:
1. Each subtask should be completable in a single focused step.
2. Identify dependencies between subtasks explicitly.
3. Rate each subtask's complexity (low/medium/high).
4. Aim for 4-8 subtasks (not too few, not too many).
5. Make subtasks specific enough to execute without further clarification.
Output format (one task per line):
TASK_ID | Description | Dependencies (comma-separated IDs or "none") | Complexity
"""
EXECUTE_SYSTEM = """You are a task execution agent. Given a subtask and context
from previously completed subtasks, execute the task and report the result.
Be specific and thorough in your execution. If you produce any output
(text, code, analysis), include it in your response.
If you cannot complete the task, explain why specifically."""
class TaskDecompositionAgent:
"""An agent that decomposes and executes complex tasks."""
def __init__(self, llm_call):
self.llm_call = llm_call
self.subtasks: dict[str, SubTask] = {}
self.execution_log: list[dict] = []
def decompose(self, goal: str) -> list[SubTask]:
"""Decompose a goal into subtasks."""
response = self.llm_call(
system=DECOMPOSE_SYSTEM,
prompt=f"Decompose this goal into subtasks:\n\n{goal}",
)
subtasks = self._parse_decomposition(response)
for task in subtasks:
self.subtasks[task.id] = task
return subtasks
def execute(self, goal: str, max_replans: int = 2) -> dict:
"""Decompose and execute a goal."""
# Step 1: Decompose
print(f"Goal: {goal}")
print(f"{'='*60}")
subtasks = self.decompose(goal)
print(f"\nDecomposed into {len(subtasks)} subtasks:")
for task in subtasks:
deps = f" (after: {', '.join(task.dependencies)})" if task.dependencies else ""
print(f" [{task.id}] {task.description}{deps} [{task.estimated_complexity}]")
# Step 2: Execute in dependency order
print(f"\n{'='*60}")
print("Executing...\n")
completed = set()
replans = 0
while True:
# Find ready tasks
ready = [
task for task in self.subtasks.values()
if task.status == "pending"
and all(dep in completed for dep in task.dependencies)
]
if not ready:
if all(t.status in ("completed", "failed", "skipped") for t in self.subtasks.values()):
break
else:
print("Deadlock: no tasks are ready but some are still pending.")
break
for task in ready:
print(f"Executing [{task.id}]: {task.description}")
# Build context from completed tasks
context = self._build_context(completed)
try:
result = self._execute_subtask(task, context)
task.status = "completed"
task.result = result
completed.add(task.id)
self.execution_log.append({
"task_id": task.id,
"status": "completed",
"result": result[:200],
"timestamp": datetime.now().isoformat(),
})
print(f" Completed: {result[:150]}...")
except Exception as e:
error = str(e)
print(f" Failed: {error}")
task.status = "failed"
self.execution_log.append({
"task_id": task.id,
"status": "failed",
"error": error,
"timestamp": datetime.now().isoformat(),
})
# Replan if possible
if replans < max_replans:
print(" Attempting to replan...")
new_task = self._replan_task(task, error, context)
if new_task:
self.subtasks[new_task.id] = new_task
replans += 1
print(f" Created replacement task [{new_task.id}]: {new_task.description}")
# Step 3: Generate report
report = self._generate_report(goal)
return report
def _execute_subtask(self, task: SubTask, context: str) -> str:
"""Execute a single subtask."""
prompt = (
f"Execute the following subtask:\n\n"
f"Task: {task.description}\n\n"
f"Context from previous steps:\n{context}\n\n"
f"Execute the task and provide the result:"
)
return self.llm_call(system=EXECUTE_SYSTEM, prompt=prompt)
def _build_context(self, completed_ids: set[str]) -> str:
"""Build context string from completed tasks."""
if not completed_ids:
return "(No previous steps completed yet.)"
lines = []
for task_id in completed_ids:
task = self.subtasks[task_id]
result_preview = task.result[:300] if task.result else "N/A"
lines.append(f"[{task_id}] {task.description}\n Result: {result_preview}")
return "\n".join(lines)
def _replan_task(self, failed_task: SubTask, error: str, context: str) -> SubTask | None:
"""Generate a replacement task for a failed one."""
prompt = (
f"The following task failed:\n"
f"Task: {failed_task.description}\n"
f"Error: {error}\n\n"
f"Context:\n{context}\n\n"
f"Generate an alternative approach to accomplish the same objective. "
f"Respond with a single task description."
)
response = self.llm_call(prompt=prompt)
if response.strip():
new_id = f"{failed_task.id}_retry"
return SubTask(
id=new_id,
description=response.strip(),
dependencies=failed_task.dependencies,
estimated_complexity=failed_task.estimated_complexity,
)
return None
def _parse_decomposition(self, response: str) -> list[SubTask]:
"""Parse the LLM's decomposition into SubTask objects."""
subtasks = []
for line in response.strip().split("\n"):
parts = [p.strip() for p in line.split("|")]
if len(parts) >= 3:
task_id = parts[0].lower().replace(" ", "_")
description = parts[1]
deps_str = parts[2]
deps = (
[] if deps_str.lower() == "none"
else [d.strip().lower().replace(" ", "_") for d in deps_str.split(",")]
)
complexity = parts[3].strip().lower() if len(parts) > 3 else "medium"
subtasks.append(SubTask(
id=task_id,
description=description,
dependencies=deps,
estimated_complexity=complexity,
))
return subtasks
def _generate_report(self, goal: str) -> dict:
"""Generate a final execution report."""
completed = [t for t in self.subtasks.values() if t.status == "completed"]
failed = [t for t in self.subtasks.values() if t.status == "failed"]
skipped = [t for t in self.subtasks.values() if t.status == "skipped"]
return {
"goal": goal,
"total_subtasks": len(self.subtasks),
"completed": len(completed),
"failed": len(failed),
"skipped": len(skipped),
"success_rate": len(completed) / max(len(self.subtasks), 1),
"execution_log": self.execution_log,
"subtask_results": {
task_id: {
"description": task.description,
"status": task.status,
"result_preview": (task.result[:200] if task.result else None),
}
for task_id, task in self.subtasks.items()
},
}
# -- Usage Example ---------------------------------------------------
def main():
"""Demonstrate the task decomposition agent."""
def mock_llm_call(system: str = "", prompt: str = "") -> str:
"""Simulate LLM calls for demonstration."""
if "Decompose" in prompt:
return """t1 | Research existing sentiment analysis approaches | none | low
t2 | Select and download a suitable dataset | none | low
t3 | Set up Python environment with required libraries | none | low
t4 | Implement data preprocessing pipeline | t2, t3 | medium
t5 | Train a baseline sentiment classifier | t4 | medium
t6 | Evaluate model and analyze errors | t5 | medium
t7 | Write up results and conclusions | t6, t1 | low"""
elif "Execute" in prompt or "Execute" in system:
if "research" in prompt.lower():
return (
"Surveyed recent sentiment analysis approaches. Key findings: "
"BERT-based models achieve ~95% accuracy on SST-2. DistilBERT "
"offers a good speed/accuracy trade-off. Recent work explores "
"aspect-based sentiment analysis for finer-grained results."
)
elif "dataset" in prompt.lower():
return (
"Selected the Stanford Sentiment Treebank (SST-2) dataset. "
"Downloaded 67,349 training samples and 872 test samples. "
"Binary sentiment labels (positive/negative)."
)
elif "environment" in prompt.lower():
return (
"Created virtual environment. Installed: torch 2.1, "
"transformers 4.36, datasets 2.16, scikit-learn 1.3."
)
elif "preprocessing" in prompt.lower():
return (
"Implemented preprocessing: tokenization with BertTokenizer, "
"max_length=128, padding and truncation. Created DataLoader "
"with batch_size=32."
)
elif "train" in prompt.lower():
return (
"Trained DistilBERT for 3 epochs. Training loss decreased from "
"0.42 to 0.12. Validation accuracy: 91.2%."
)
elif "evaluate" in prompt.lower():
return (
"Test accuracy: 90.8%. Precision: 0.91, Recall: 0.90, F1: 0.905. "
"Error analysis: model struggles with sarcasm and negation."
)
elif "write" in prompt.lower():
return (
"Report completed. Key findings: DistilBERT achieves 90.8% accuracy "
"on SST-2 with 3 epochs of fine-tuning. Main failure modes are "
"sarcasm detection and complex negation patterns."
)
return "Task completed successfully."
# Create and run the agent
agent = TaskDecompositionAgent(mock_llm_call)
report = agent.execute(
"Build a sentiment analysis model: research approaches, train a model, "
"evaluate it, and write up the results."
)
print(f"\n{'='*60}")
print("EXECUTION REPORT")
print(f"{'='*60}")
print(f"Goal: {report['goal']}")
print(f"Subtasks: {report['total_subtasks']}")
print(f"Completed: {report['completed']}")
print(f"Failed: {report['failed']}")
print(f"Success rate: {report['success_rate']:.0%}")
if __name__ == "__main__":
main()12Preguntas de discusión
-
Horizonte de planificación: ¿Con cuánta anticipación debería planificar un agente? ¿Es mejor planificar toda la tarea por adelantado o planificar incrementalmente (uno o dos pasos a la vez)? ¿Qué factores influyen en esta decisión? Pista: considera el compromiso entre coherencia (la planificación a largo horizonte mantiene la visión general) y adaptabilidad (la planificación a corto horizonte puede reaccionar a resultados inesperados). Piensa en cómo la certeza se degrada cuanto más lejos se mira.
-
Fidelidad al plan: Cuando un LLM genera un plan y luego lo ejecuta, ¿debería seguir el plan exactamente o adaptarse libremente? ¿Cuál es el equilibrio correcto entre adherencia al plan y adaptación oportunista? Pista: esto es análogo al compromiso entre explorar y explotar. Demasiada adherencia conduce a la rigidez; demasiada adaptación conduce a perder el hilo del plan original.
-
Planificar para humanos vs. planificar para agentes: ¿Cómo deberían diferir los planes generados para la ejecución por agentes de los planes generados para la ejecución humana? Considera la granularidad, el manejo de errores y el paralelismo. Pista: los agentes necesitan un manejo de errores más explícito ("si el paso 3 falla, prueba el enfoque alternativo X") porque no pueden usar el sentido común para recuperarse de situaciones inesperadas.
-
El cuello de botella de la planificación: La planificación es a menudo la parte más propensa a errores del pipeline de un agente. ¿Cómo podríamos mejorar la calidad del plan más allá de lo que los LLM actuales pueden producir? Pista: considera la verificación de planes (que otro LLM revise el plan), librerías de planes (reutilizar planes que funcionaron antes) y enfoques híbridos (usar planificadores clásicos para las partes que pueden formalizarse).
-
Lo clásico se encuentra con lo moderno: ¿Podrían combinarse eficazmente los algoritmos de planificación clásica (STRIPS, HTN) con los enfoques basados en LLM? ¿Cómo sería eso? Pista: el LLM podría generar una descripción de dominio PDDL a partir de lenguaje natural, o podría rellenar los detalles de un esqueleto de plan HTN.
-
Planificación bajo restricciones de recursos: ¿Cómo debería planificar un agente de manera diferente cuando tiene un presupuesto limitado (por ejemplo, máximo 10 llamadas a la API, máximo 1$ en costés)? Pista: considera cómo planifican los humanos cuando tienen tiempo limitado: se centran en los pasos más importantes, omiten lo prescindible y dejan menos margen para errores. ¿Cómo podría hacer lo mismo un agente?
13Resumen y conclusiones clave
-
La planificación es fundamental para la agencia. Sin la capacidad de descomponer objetivos en pasos, razonar sobre dependencias y adaptarse a los fallos, los agentes se limitan a interacciones reactivas de un solo paso.
-
La planificación clásica proporciona garantías formales (corrección, completitud, optimalidad) pero requiere especificaciones formales de dominio que son impracticables para muchas tareas del mundo real.
-
La planificación basada en LLM es flexible y general pero no ofrece garantías formales. Los planes pueden contener errores, omitir pasos o alucinar acciones imposibles.
-
La descomposición jerárquica gestiona la complejidad dividiendo grandes objetivos en subtareas progresivamente más pequeñas, cada una a un nivel apropiado de abstracción.
-
La validación y el refinamiento de planes son esenciales. La auto-validación, la validación cruzada con agentes críticos y las pruebas basadas en simulación mejoran la calidad del plan antes de la ejecución.
-
La replanificación dinámica habilita la robustez. Los agentes capaces de detectar fallos y adaptar sus planes son más capaces que los agentes que siguen planes rígidos.
-
Los grafos de tareas con gestión de dependencias permiten la ejecución paralela y proporcionan una estructura clara para tareas complejas de múltiples pasos.
-
Los enfoques avanzados como ToT y LATS exploran sistemáticamente el espacio de planificación, intercambiando coste computacional por mejores soluciones. El uso de la reflexión en LATS es particularmente prometedor.
14Referencias
-
Fikes, R. E., & Nilsson, N. J. (1971). STRIPS: A New Approach to the Application of Theorem Proving to Problem Solving. Artificial Intelligence, 2(3-4), 189-208.
-
McDermott, D., Ghallab, M., Howe, A., Knoblock, C., Ram, A., Veloso, M., Weld, D., & Wilkins, D. (1998). PDDL -- The Planning Domain Definition Language. Technical Report CVC TR-98-003, Yale Center for Computational Visión and Control.
-
Yao, S., Yu, D., Zhao, J., Shafran, I., Griffiths, T. L., Cao, Y., & Narasimhan, K. (2023). Tree of Thoughts: Deliberate Problem Solving with Large Language Models. Advances in Neural Information Processing Systems (NeurIPS), 36.
-
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. International Conference on Machine Learning (ICML).
-
Zhou, D., Scharli, N., Hou, L., Wei, J., Scales, N., Wang, X., Schuurmans, D., Cui, C., Bousquet, O., Le, Q., & Chi, E. (2023). Least-to-Most Prompting Enables Complex Reasoning in Large Language Models. International Conference on Learning Representations (ICLR).
-
Wei, J., Wang, X., Schuurmans, D., Bosma, M., Ichter, B., Xia, F., Chi, E., Le, Q., & Zhou, D. (2022). Chain-of-thought prompting elicits reasoning in large language models. Advances in Neural Information Processing Systems (NeurIPS), 35.
-
Wang, G., Xie, Y., Jiang, Y., Mandlekar, A., Xiao, C., Zhu, Y., Fan, L., & Anandkumar, A. (2023). Voyager: An Open-Ended Embodied Agent with Large Language Models. arXiv preprint arXiv:2305.16291.
-
Huang, W., Abbeel, P., Pathak, D., & Mordatch, I. (2022). Language Models as Zero-Shot Planners: Extracting Actionable Knowledge for Embodied Agents. International Conference on Machine Learning (ICML).
-
Valmeekam, K., Márquez, M., Sreedharan, S., & Kambhampati, S. (2024). On the Planning Abilities of Large Language Models -- A Critical Investigation. Advances in Neural Information Processing Systems (NeurIPS), 36.
Parte de "IA Agentica: Fundamentos, Arquitecturas y Aplicaciones" (CC BY-SA 4.0).