Saltar al contenido

Ingeniería del bucle: cómo diseñar el ciclo de un agente

Un agente es un bucle while. La parte difícil es decidir cuándo debe terminar.

Ingeniería de IA5 min de lectura

Ingeniero de software. Desarrollo extensiones de Chrome, aplicaciones web y herramientas de IA aplicada.

Contenido

Si reduces un agente a lo esencial, quedan unas quince líneas:

let messages = [systemPrompt, userRequest];

while (true) {
  const response = await model(messages, tools);
  messages.push(response);

  if (!response.toolCalls) break;

  for (const call of response.toolCalls) {
    messages.push(await execute(call));
  }
}

Puedes escribirlo en una tarde y conseguir que supere los primeros diez casos de prueba.

Después llegan las entradas reales: relee el mismo archivo durante cuarenta pasos, declara éxito sin hacer nada o consume el presupuesto mensual de tokens en una tarea imposible. Lo difícil es decidir cuándo detenerlo, cómo saber si avanza y qué debe aprender cuando falla un paso.

Ese trabajo es la ingeniería del bucle y requiere algo más que ajustar prompts.

Primer fallo: el bucle no detecta que está atascado

Un fallo caro es el agente cuyas llamadas parecen productivas aunque no lo sean. Una caída, al menos, se hace visible.

El caso clásico: lee un archivo, no encuentra lo que busca, lo relee con otra justificación y repite. Cada llamada es válida y termina correctamente, pero la tarea no avanza. El modelo solo sabe que ya lo intentó si esa información sigue en contexto. Para el paso quince, los primeros intentos pueden haberse compactado o perdido entre el resto.

El bucle necesita su propia señal de progreso:

const seen = new Map<string, number>();

function checkProgress(call: ToolCall): 'ok' | 'repeat' | 'stuck' {
  const key = hash(call.name, call.args);
  const count = (seen.get(key) ?? 0) + 1;
  seen.set(key, count);

  if (count >= 3) return 'stuck';
  if (count === 2) return 'repeat';
  return 'ok';
}

Es barato y útil. Ante repeat, añade una observación al contexto: ya hiciste exactamente esta llamada y recibiste este resultado; necesitas otro enfoque. Ante stuck, detén el ciclo y escala el problema.

Una versión más avanzada registra cambios de estado: ¿cambió algo en el mundo o en el conocimiento del agente después del paso? Cuesta más implementarla, pero detecta más casos. Empieza por identificar las llamadas mediante un hash.

Segundo fallo: no controlas la condición de salida

Un while (true) que solo termina cuando el modelo deja de llamar herramientas delega la finalización al modelo. Puede servir para una tarea bien definida, pero falla en ambas direcciones: termina antes de tiempo o sigue porque cada paso descubre más trabajo.

Un bucle de producción necesita al menos cuatro condiciones de salida. Solo una parte de ellas depende del modelo:

Éxito. El modelo indica que terminó y una verificación lo confirma. «Corregí el error» es una afirmación. Ejecutar la prueba permite comprobarla. Si puedes verificar que la tarea se completó, hazlo.

Presupuesto agotado. Pasos, tokens, tiempo transcurrido y dinero, con límites independientes. Alcanzar un límite es un resultado previsto. El sistema debe devolver el trabajo parcial y explicar qué falta.

Ausencia de progreso. El detector anterior: tres llamadas idénticas o varios pasos sin cambios de estado.

Fallo irrecuperable. Autenticación perdida, un recurso imprescindible inexistente o un permiso denegado de forma permanente. Debe provocar la salida inmediata, sin gastar todo el presupuesto en reintentos.

Escribe ramas explícitas. Cuando el bucle termine, el motivo debe tener un nombre, aparecer en el registro y, cuando corresponda, mostrarse al usuario.

Tercer fallo: errores que no enseñan nada

Cuando falla una herramienta, el mensaje vuelve al contexto y se convierte en la explicación de lo ocurrido. Estás escribiendo un prompt durante la ejecución:

Error: ENOENT

El modelo sabe que algo salió mal, pero no sabe qué ni cómo corregirlo. Adivina y suele repetir la llamada.

Compáralo con esto:

File not found: config/settings.json
The directory 'config/' exists and contains: app.json, database.json
Did you mean 'config/app.json'?

El mismo fallo lleva a un siguiente paso distinto. La segunda respuesta consume más tokens, pero puede ahorrar varias iteraciones.

Los mensajes de error son prompts. Deben explicar qué falló, cuál es el estado real y qué acción razonable puede seguir. Si el manejador puede calcular una sugerencia con poco coste, inclúyela.

Distingue también lo transitorio de lo permanente. Ante un límite de peticiones, el bucle puede esperar y reintentar sin involucrar al modelo. Un argumento mal formado exige que el modelo lo cambie. Una credencial revocada puede hacer imposible continuar en esa sesión. Mezclar estos casos provoca reintentos inútiles o abandonos innecesarios.

type Failure =
  | { kind: 'transient'; retryAfterMs: number }   // loop retries, model unaware
  | { kind: 'invalid'; message: string }          // model sees, corrects
  | { kind: 'fatal'; message: string };           // loop exits

Cuarto fallo: el contexto crece hasta que el ciclo deja de servir

Cada iteración añade salidas, resultados y errores a messages. Hacia el paso veinte puedes estar reenviando un contexto lleno de historia poco útil, pagándolo en cada turno y diluyendo la información necesaria.

La compacción debe formar parte del bucle:

  • Turnos recientes, completos. Conserva los últimos tres a cinco pasos. Son la memoria de trabajo del agente y suelen contener los detalles útiles.
  • Turnos antiguos, resumidos. Reduce los pasos anteriores a un bloque de hallazgos: qué se aprendió, qué se intentó y qué se descartó. Regénéralo periódicamente en vez de seguir acumulando texto.
  • Hechos fijos, siempre presentes. La petición original, las restricciones obligatorias y los invariantes descubiertos, como que una API devuelve snake_case, permanecen completos. Perder la petición original en el paso treinta es un fallo real.
  • Resultados de herramientas, por niveles. Guarda los grandes fuera del contexto y ofrece una referencia para recuperarlos. El agente recibe un resumen y puede volver a consultar. Una respuesta de 200 KB no debería ocupar el contexto sin procesar.

Activa la compacción al alcanzar un umbral, por ejemplo el 60 % del presupuesto de contexto, en lugar de usar un número fijo de pasos. La cantidad de material que produce cada paso varía mucho.

Compactar implica perder información. Registra qué se retiró y aplica controles de conservación adecuados. Una decisión inexplicable en el paso veinticinco puede deberse a un detalle que el resumen descartó en el dieciocho.

Quinto fallo: un solo bucle asume demasiado trabajo

Un ciclo que mantiene una tarea de cincuenta pasos en un único contexto acaba trabajando contra sí mismo. El contexto es enorme, los primeros hallazgos pierden detalle y cada iteración vuelve a razonar sobre el problema entero.

Dividir el trabajo puede ayudar. La unidad útil es un ciclo secundario con contexto y presupuesto propios:

Parent loop: owns the plan, holds ~5 high-level steps
  └─ Sub-loop: owns one step, fresh context, own budget,
               returns a compact result to the parent

El ciclo principal recibe la conclusión en lugar de cuarenta mensajes intermedios. Ese aislamiento del contexto ayuda en tareas largas.

Tiene un coste: se pierde información en la frontera. Si el resumen es pobre, el proceso principal decide sin datos suficientes. Diseña el contrato de retorno: qué encontró el ciclo secundario, qué hizo, qué no pudo hacer y qué necesita saber el principal aunque no lo haya preguntado.

Divide cuando existan fases separables. Si cada parte necesita todo el contexto de las demás, la coordinación puede costar más de lo que ahorras.

Detrás del while hay una máquina de estados

Los cinco fallos tienen algo en común: while (true) solo proporciona la estructura inicial. Necesitas estado explícito: fase actual, presupuesto consumido, señal de progreso e historial de fallos. Evalúalo al principio de cada iteración y define las transiciones en código.

Puedes conservar el bucle y añadir ese control:

while (state.phase !== 'done') {
  if (overBudget(state)) return partial(state, 'budget');
  if (isStuck(state))    return partial(state, 'no-progress');

  const step = await runStep(state);
  state = reduce(state, step);
}

Un agente de producción en el que confiaría se parece a esto. El modelo es una de las llamadas que ocurren dentro.

Qué construir primero

Este orden ayuda a observar el siguiente problema:

  1. Contador de pasos y límite estricto. El límite no demuestra que la tarea terminó, pero te obliga a resolver cómo devolver resultados parciales.
  2. Detección de repeticiones por hash de llamada. Se implementa con poco código y detecta un fallo frecuente y caro.
  3. Tres clases de error: transitorio, inválido y fatal, cada una con una respuesta distinta del bucle.
  4. Compacción con un bloque de hechos fijos. Añádela cuando superes diez pasos de forma habitual.
  5. Ciclos secundarios, al final. Primero consigue que uno sea fiable; de lo contrario solo tendrás varios ciclos poco fiables.

El bucle necesita saber cuándo detenerse para que el agente pueda terminar su trabajo.

Un pequeño voto de confianza

¿Valió la pena leer esta nota?